Secret Management
Encrypted vault using AES-256-GCM with per-entry HKDF derivation, plus leak detection, placeholder resolution, and secret-usage approval RPC.
The secrets module provides encrypted storage for sensitive credentials (API keys, tokens, passwords). It uses AES-256-GCM with per-entry HKDF-SHA256 key derivation, with SharedTokenManager as the master key; supports 1Password; and scans outbound text and the {{secret:NAME}} resolution path for accidental secret leaks.
Design Philosophy
- Per-entry encryption — Each entry has its own derived key and random nonce / salt
- Zeroize on drop — Sensitive data is held by
secrecy::SecretStringand zeroized on drop - Leak detection — Outbound text and LLM echo text are scanned; detected secrets trigger Block / Redact
- Use requires approval — Runtime retrieval goes through an explicit JSON-RPC approval flow
Core Components
SecretVault
File-based encrypted storage:
pub struct SecretVault {
data: VaultData,
path: PathBuf,
}Location: <config_dir>/secrets.vault (default ~/.aleph/secrets.vault).
Atomic write: VaultIo uses a temp file plus an fs2 lock on secrets.vault.lock, then atomic rename — safe under concurrency and crash.
Corruption recovery: SecretVault::open_or_backup renames an unreadable-but-present file to <path>.corrupt-<unix_ts> before opening a fresh vault, so open().unwrap_or_else(empty) cannot overwrite the original on next save().
Encryption
let key = hkdf_derive(master_key, salt, info);
let ciphertext = aes_256_gcm_encrypt(plaintext, key, nonce);Master key: the 32-byte secret held by SharedTokenManager (the HMAC shared-token secret) acts as the AES-GCM master key.
DecryptedSecret
pub struct DecryptedSecret {
value: SecretString,
}SecretString comes from the secrecy crate:
- Zeroize on drop — memory is cleared when the value is dropped
Debug/Displayredact the value — debug print and logging never expose plaintextexpose(&self) -> &str— explicit access; minimize the surface that touches plaintext
Name Validation
validate_secret_name(name) enforces an [A-Za-z0-9_.-:] character set and a 128-character maximum — unifying the syntax for {{secret:NAME}} placeholders, recoverable keys, and Hub namespaced keys.
Secret Providers
#[async_trait]
pub trait SecretProvider: Send + Sync {
fn provider_type(&self) -> &str;
async fn get(&self, reference: &str) -> Result<DecryptedSecret, SecretError>;
async fn health_check(&self) -> Result<ProviderStatus, SecretError>;
async fn list(&self) -> Result<Vec<SecretMetadata>, SecretError>;
}Local Vault
SharedTokenManager exposes store_secret / get_secret / delete_secret / list_secret_names as the local vault provider, with provider_type() returning "local_vault".
1Password
pub struct OnePasswordProvider { /* ... */ }Retrieves secrets through the 1Password CLI (op). health_check returns classified error states (sign-in required / item missing / other) with fixed, credential-safe messages.
Template Injection and Placeholder Resolution
Templates can reference secrets through placeholder syntax:
pub fn render_with_secrets(
template: &str,
resolver: &dyn AsyncSecretResolver,
) -> Result<String>;
pub fn extract_secret_refs(text: &str) -> Result<Vec<SecretRef>, SecretError>;Placeholder syntax: {{secret:NAME}}
The resolver fetches the named secret from the vault and substitutes it into the template, with this order:
- Extract —
extract_secret_refsruns before any resolution - Resolve — fetch each
namethroughAsyncSecretResolver - Register — resolved secrets are registered into
secret_leak_detector - Substitute — longest-first ordering, so
{{secret:api_key}}is replaced before{{secret:api}}
Hub-vault namespaced keys (ext.{kind}.{id}.{field}) are produced by src/hub/secrets.rs::field_key, share the same character set, and round-trip through the placeholder parser as {{secret:ext.mcp.…}}.
Leak Detection
src/secrets/leak_detector.rs::LeakDetector scans text for accidental secrets:
pub struct LeakDetector { patterns: Vec<Regex> }
pub enum LeakDecision {
Clean,
Warn,
Redact,
Block { reason: String, redacted_content: String },
}Clean— no secret detectedWarn— record onlyRedact— replace the hit with a placeholder (used forBearertokens and similar)Block { reason, redacted_content }— confirm and block; the returned content has been run throughSecretMasker::mask, never raw
The RuntimeSecurityGuard orchestrates the calls: outbound runs leak detection between secret resolution and substitution, inbound runs it before the LLM's response reaches the host. Every hit enters the AuditEventType::LeakWarning / ExecBlocked audit stream. SecretMasker is applied a second time on the display side (approval card, Guardian payload, log).
Safety Properties
- Lock recovery —
unwrap_or_else(|e| e.into_inner())for everyMutex - No stderr leakage — error messages use fixed safe text, raw stderr logged through
tracing::debug - JSON parse failures logged —
serde_jsonerrors are debug-logged, never silently swallowed - Zeroize —
SecretStringclears memory on drop - Atomic write —
VaultIofile lock + temp file + rename
secrets.* RPC
src/gateway/handlers/secrets.rs provides vault CRUD:
| RPC | Params | Behavior |
|---|---|---|
secrets.list | — | returns the registered secret names (sorted; never values) |
secrets.set | { key, value } | stores a new entry |
secrets.delete | { key } | deletes an entry; not found ⇒ -32004 |
secrets.verify | { key } | reports { key, bytes, present } (length, never plaintext) |
secrets.providers | — | reports the configured providers as { key, type, account? } |
secrets.list and secrets.verify return only names and byte lengths — plaintext never crosses the RPC boundary. The local aleph-server secret CLI (LockOnly path) is the surface that retrieves the raw value.
Secret-Usage Approval
src/gateway/handlers/secret_approvals.rs::SecretApprovalManager provides:
| RPC | Purpose |
|---|---|
secret.approval.request | Agent requests permission to use a named secret |
secret.approval.resolve | Client approves or denies the request |
secret.approvals.pending | List pending approval requests |
SecretApprovalRequest carries secret_name / usage / agent_id / session_key / created_at / timeout_ms. SecretApprovalManager holds pending entries in a HashMap<id, ApprovalRecord> keyed by id, with timeout on the request.
Startup Secret Migration
src/gateway/handlers/secret_migration.rs::migrate_all_secrets_to_vault runs at server startup, moves any plaintext key in config.toml into the vault, and strips the original field. Coverage:
| Source section | Vault key prefix |
|---|---|
providers.{name}.api_key | ai:{name} |
generation.{type}_providers.{name}.api_key | gen:{name} |
memory.embedding.providers[].api_key | embed:{id} |
memory.rerank.api_key | rerank:{provider} |
search.backends.{name}.api_key | search:{name} |
channels.{id}.bot_token and similar | channel:{id}:{field} |
voice.streaming.api_key | voice_streaming:api_key |
Only the affected sections get re-saved; the file is touched only when at least one migration occurred.
Guardian Judge Masks Secrets
GuardianApprovalRequester runs its verdict payload through src/exec/masker.rs::SecretMasker — both summary and per-command-segment raw text are masked before being concatenated, so bearer tokens, URL basic-auth credentials, and generic password assignments never enter the LLM-context payload or the audit stream.
Code Location
src/secrets/mod.rs— module entry andvalidate_secret_namesrc/secrets/vault.rs—SecretVault(withopen_or_backup)src/secrets/crypto.rs— HKDF + AES-256-GCMsrc/secrets/types.rs—DecryptedSecret/EncryptedEntrysrc/secrets/injection.rs— template resolution +AsyncSecretResolversrc/secrets/placeholder.rs—{{secret:NAME}}extractionsrc/secrets/leak_detector.rs— outbound and inbound leak scanningsrc/secrets/vendor_patterns.rs— vendor formats (cloud-provider keys, etc.)src/secrets/vault_resolver.rs— wraps the local vault as anAsyncSecretResolversrc/secrets/virtual_key_resolver.rs— reference resolutionsrc/secrets/provider/mod.rs—SecretProvidertraitsrc/secrets/provider/onepassword.rs— 1Password CLI integrationsrc/utils/vault_io.rs—VaultIo(file lock + atomic write)src/gateway/security/shared_token.rs— master-key holdersrc/gateway/handlers/secrets.rs—secrets.*RPCsrc/gateway/handlers/secret_approvals.rs—SecretApprovalManager+secret.approval.*RPCsrc/gateway/handlers/secret_migration.rs— startup plaintext migrationsrc/hub/secrets.rs— Hub namespacingsrc/exec/masker.rs—SecretMasker(display-side secret masking)
See Also
- Security Primitives — runtime guard and audit
- PII Protection — privacy filtering
- Approval — Guardian Judge upstream
PII Protection
Gateway-level PII filtering engine that detects and redacts personally identifiable information before it reaches LLM API providers, with per-category and per-platform policy.
Process Supervisor
PTY-based process control for external CLI tools, and the Turn-level verifier chain plus shell stop hooks for the agent loop.