Secret Manager
The Secret Manager provides secure storage for sensitive data using native credential stores on each platform. All secrets are stored in a single keychain entry (vault) to minimize permission prompts—one “Always Allow” grants access to all secrets.
Platform Support
Section titled “Platform Support”| Platform | Backend | Persistence | Notes |
|---|---|---|---|
| macOS | Keychain | Persistent | Integrated with system security |
| Windows | Credential Manager | Persistent | Protected by user account |
| Linux | Secret Service | Persistent | GNOME Keyring, KWallet, etc. |
| Linux | keyutils (fallback) | Session-only | Used when Secret Service unavailable |
Linux Notes
Section titled “Linux Notes”On Linux, the Secret Manager first attempts to use the Secret Service API (D-Bus based). This works with:
- GNOME Keyring
- KWallet
- Other Secret Service implementations
If Secret Service is unavailable (e.g., headless servers, minimal containers), it falls back to Linux keyutils. Important: keyutils stores secrets in the kernel keyring which is session-scoped—secrets do not persist across reboots.
Environment Variables
Section titled “Environment Variables”| Variable | Description |
|---|---|
SIDESEAT_SECRET_BACKEND | Force a specific backend (see below) |
Valid backend values:
keychain- Force macOS Keychaincredential-manager- Force Windows Credential Managersecret-service- Force Linux Secret Servicekeyutils- Force Linux keyutils
Secret Structure
Section titled “Secret Structure”Secrets are stored with metadata for tracking and management:
pub struct Secret { pub value: String, // The actual secret pub metadata: SecretMetadata,}
pub struct SecretMetadata { pub provider: Option<String>, // e.g., "openai", "anthropic" pub scope: Option<String>, // e.g., "api", "oauth" pub expires_at: Option<DateTime>, // Optional expiration pub created_at: DateTime, pub updated_at: DateTime,}API Reference
Section titled “API Reference”Initialization
Section titled “Initialization”use sideseat::core::SecretManager;
// Initialize (auto-detects platform backend)let secrets = SecretManager::init().await?;
// Check which backend is activeprintln!("Using: {}", secrets.backend().name());
// Check if storage persists across rebootsif !secrets.is_persistent() { println!("Warning: Secrets will not persist after reboot");}Storing Secrets
Section titled “Storing Secrets”use sideseat::core::{SecretManager, Secret, SecretKey, SecretMetadata};use chrono::Utc;
let secrets = SecretManager::init().await?;
// Simple API key storagesecrets.set_api_key("OPENAI_API_KEY", "sk-xxx...", Some("openai")).await?;
// Full secret with metadatalet secret = Secret { value: "github_pat_xxx...".to_string(), metadata: SecretMetadata { provider: Some("github".to_string()), scope: Some("repo,read:user".to_string()), expires_at: Some(Utc::now() + chrono::Duration::days(90)), created_at: Utc::now(), updated_at: Utc::now(), },};
let key = SecretKey::new("GITHUB_TOKEN");secrets.set(&key, &secret).await?;Retrieving Secrets
Section titled “Retrieving Secrets”// Get just the valueif let Some(api_key) = secrets.get_value("OPENAI_API_KEY").await? { println!("Got API key");}
// Get full secret with metadatalet key = SecretKey::new("GITHUB_TOKEN");if let Some(secret) = secrets.get(&key).await? { println!("Provider: {:?}", secret.metadata.provider); println!("Created: {}", secret.metadata.created_at);}Updating Secrets
Section titled “Updating Secrets”// Update value, preserving metadatasecrets.update_value("OPENAI_API_KEY", "sk-new-key...").await?;
// Or replace entirelylet new_secret = Secret::with_provider("new-token", "github");secrets.set(&SecretKey::new("GITHUB_TOKEN"), &new_secret).await?;Deleting Secrets
Section titled “Deleting Secrets”secrets.delete(&SecretKey::new("OLD_API_KEY")).await?;Checking Existence
Section titled “Checking Existence”if secrets.exists(&SecretKey::new("OPENAI_API_KEY")).await? { println!("API key is configured");}Secret Keys
Section titled “Secret Keys”Secrets are identified by a name and optional target:
// Simple keylet key = SecretKey::new("MY_API_KEY");
// Key with target (for disambiguation)let key = SecretKey::with_target("API_KEY", "production");The target is useful when you have multiple secrets with the same name for different environments or purposes.
Expiration Handling
Section titled “Expiration Handling”Secrets with an expires_at timestamp are automatically checked when retrieved:
let mut metadata = SecretMetadata::new();metadata.expires_at = Some(Utc::now() + chrono::Duration::hours(1));
let secret = Secret { value: "temporary-token".to_string(), metadata,};
secrets.set(&SecretKey::new("TEMP_TOKEN"), &secret).await?;
// Later, if expired, get() returns an errormatch secrets.get(&SecretKey::new("TEMP_TOKEN")).await { Err(e) => println!("Token expired: {}", e), Ok(Some(s)) => println!("Token valid"), Ok(None) => println!("Token not found"),}Vault Architecture
Section titled “Vault Architecture”All secrets are stored in a single keychain entry called “vault”. This design provides:
- Single permission prompt - One “Always Allow” grants access to all secrets
- In-memory caching - Vault loaded once at startup, reads are instant
- Atomic updates - All secrets saved together when any secret changes
Keychain Access Pattern
Section titled “Keychain Access Pattern”| Operation | Keychain Access |
|---|---|
SecretManager::init() | 1 READ (loads vault) |
get() / get_value() | 0 (in-memory cache) |
set() / set_api_key() | 1 WRITE (saves vault) |
exists() | 0 (in-memory cache) |
macOS Keychain Prompts
Section titled “macOS Keychain Prompts”On macOS, you’ll see a keychain prompt on first access. Click “Always Allow” to grant permanent access. If prompted for both read and write, allow both for uninterrupted access.
Security Considerations
Section titled “Security Considerations”- Secrets are never logged - Only key names appear in logs, never values
- OS-level encryption - All backends use platform-native encryption
- Memory safety - Consider using
zeroizefor sensitive in-memory data - No file fallback - Secrets are never stored in plain text files
- Session warning - A warning is logged when using non-persistent backends
- Vault consolidation - All secrets in one entry reduces attack surface
Error Handling
Section titled “Error Handling”The Secret Manager returns Error::Secret for all secret-related errors:
use sideseat::Error;
match secrets.get_value("API_KEY").await { Ok(Some(value)) => { /* use value */ } Ok(None) => println!("Secret not found"), Err(Error::Secret(msg)) => println!("Secret error: {}", msg), Err(e) => println!("Other error: {}", e),}Common error scenarios:
- Secret not found (returns
Ok(None)for get, error for delete/update) - Secret expired (returns error)
- Backend unavailable (returns error during init or operations)
- Serialization failure (corrupted secret data)
Best Practices
Section titled “Best Practices”- Initialize once - Create one
SecretManagerand share it - Check persistence - Warn users if using non-persistent backend
- Use providers - Tag secrets with provider names for organization
- Set expiration - Use
expires_atfor temporary tokens - Handle missing secrets - Always check for
Nonereturns - Don’t store in config - Use Secret Manager instead of config files for credentials