Secrets broker¶
A short-lived-token broker that replaces dotfile-in-workspace and process-env credential patterns for agent spawns. The agent process receives only a minted token; the raw backing secret never appears in the spawned environment.
Lifecycle¶
- Operator declares a
security.secretsblock inbernstein.yamlwith a chosen backend. - Orchestrator builds the broker once at startup.
- Each task asks the broker to mint a token for a named secret with a TTL. The broker reads the raw value from the backend, generates a new opaque token, and registers the mapping in-process.
- The minted token value is plumbed into the agent's env in place of the real credential.
- On task exit (success or failure) the broker revokes every token owned by that task id; tokens also auto-expire at their TTL.
- The redactor scrubs both the raw backing value and the minted token value from any persisted transcript.
Config¶
security:
secrets:
backend: file_encrypted # vault | aws_secretsmanager | gcp_secret_manager
# | macos_keychain | linux_keyring | file_encrypted
mint:
ttl_seconds_default: 900 # default token lifetime
ttl_overrides:
ANTHROPIC_API_KEY: 1800
SHORT_LIVED_TOKEN: 60
backend_settings: # forwarded to the backend constructor
path: /var/lib/bernstein/secrets.enc
grants: # scoped per-task credential grants (optional)
require_grant: false # refuse to mint without a verifiable grant
identity_mode: ed25519 # ed25519 (default) | spiffe
When grants.require_grant is true, the orchestrator must supply a verifiable, chain-anchored grant to mint(); the broker fails closed otherwise. See Scoped per-task grants below.
Backend setup¶
vault¶
Reads VAULT_ADDR and VAULT_TOKEN from env. Pass mount in backend_settings to override the default KV mount (secret). Secret payloads must either contain a value field or a single field; the broker reads exactly one scalar per name.
aws_secretsmanager¶
Requires boto3. The standard AWS credential resolution chain applies (env vars, profile, instance role). secret_name is the secret name or ARN. JSON payloads with a value field are unwrapped automatically; plain-string payloads pass through unchanged.
gcp_secret_manager¶
Requires google-cloud-secret-manager. Reads GOOGLE_CLOUD_PROJECT from env or project from backend_settings. Always reads the latest version by default; override via version.
macos_keychain¶
Shells out to the system security CLI. Pass service in backend_settings to use a non-default service name (default: bernstein). Store an item with:
linux_keyring¶
Uses the keyring Python package, which brokers between freedesktop Secret Service, KWallet, and the other supported backends. Store an item with:
file_encrypted¶
Zero-dependency fallback: a Fernet-encrypted JSON object on disk. Requires cryptography. The encryption key is supplied via BERNSTEIN_BROKER_KEY (urlsafe base64) or a file path in backend_settings.key_path. Build the store by encrypting a JSON object that maps secret names to values.
CLI¶
bernstein secrets list # enumerate backend secrets where supported
bernstein secrets mint --task t-42 --secret ANTHROPIC_API_KEY --ttl 900
bernstein secrets mint --task t-42 --secret K --reveal # print the raw token value
mint prints a JSON object with the token id, secret name, task id, TTL, expiry, and a masked token value. Pass --reveal to print the raw token value when piping into an out-of-band agent invocation.
Audit log¶
Every mint, resolve, revoke, and expiry event flows through:
- The module logger at INFO level. Only non-secret identifiers (token id, secret name, task id, TTL) are logged, never the raw value.
- An optional in-process
AuditSinkcallback that the orchestrator can wire to the lineage subsystem or any other audit store.
Redactor coupling¶
bernstein.core.security.redactor.redact_text consults the broker's in-process registry and scrubs every minted token value and raw backing value from the text it processes. The registry is updated automatically on mint and revoke; tests can clear it via clear_redaction_registry().
Scoped per-task grants¶
By default the broker hands any token holder the full backing secret. In grant-enforcing mode the credential becomes a projection of a chain-recorded grant, so a worker holds only a scoped, expiring token and post-incident forensics can prove the exact credential window per task from the chain alone.
The grant record¶
Before a worker spawn the orchestrator issues a scoped grant with bernstein.core.identity.grants.GrantLedger.issue_grant: task id, secret name, audience, expiry, and capability ceiling. Each grant record carries two independent tamper anchors:
- an Ed25519 signature by the manager identity over the grant body (proves who authorized it), and
- an HMAC over
prev_hmac+ the canonical record body, keyed by the install audit key (the same construction as the delegation receipts indocs/security/manager-auth.md), so a mutated, deleted, or reordered record breaks the chain.
Records land under <audit>/grants/<run>.jsonl and reconstruct the full grant_issued -> grant_exchanged -> grant_revoked lifecycle offline.
Broker exchange¶
SecretsBroker.mint(..., grant=grant) refuses to mint unless the grant verifies against the chain and matches the (task_id, secret_name) pair; the refusal is itself a grant_refused chain record. The minted token inherits the grant's task id, audience, and expiry, and the token id is recorded via a grant_exchanged record so token-to-grant resolution works offline. resolve(token, audience=...) refuses a token presented outside its granted audience, recording the refusal as a chain event. revoke() and task-exit auto-revoke append a signed grant_revoked record referencing the grant.
Verifying offline¶
bernstein secrets grants list run-42 # reconstructed lifecycle
bernstein secrets grants verify run-42 # HMAC + Ed25519 offline
bernstein secrets grants verify run-42 --json # byte-identical report
bernstein audit verify # includes all grant chains
grants verify exits non-zero and names the first failing record on any mutation, deletion, or reordering. Two independent verifiers over the same chain slice produce byte-identical --json reports.
SPIFFE identity mode¶
With grants.identity_mode: spiffe and the optional spiffe extra installed against a reachable SPIRE Workload API socket, new grants carry the workload's SPIFFE ID as the issuer identity, binding grants to the workload identity already checkable via bernstein spiffe verify-binding. bernstein doctor preflights the extra and socket. With the extra absent the default Ed25519 identity path is unchanged.
Migration¶
Existing broker configs keep working unchanged: grants.require_grant defaults to false and identity_mode to ed25519, so a broker built without a grant ledger behaves exactly as before. To adopt scoped grants, add the grants block, construct the broker with a GrantLedger, and issue a grant per task before the spawn.