Skip to content

askalf/strongroom

Repository files navigation

strongroom

ci CodeQL OpenSSF Scorecard License: MIT

strongroom — own your agent secrets. An encrypted vault that hands agents scoped, short-lived, single-use leases instead of raw keys. Part of Own Your Stack — own your AI infrastructure instead of renting it by the token.

Formerly keeper. Renamed to strongroom for the npm release; the GitHub repo redirects and the legacy keeper CLI alias keeps working. KEEPER_* env vars and the ~/.keeper home directory are unchanged for compatibility.

Agents need credentials — API keys, tokens, passwords — to do anything useful. Today they get them the worst possible way: a long-lived key stuffed into an environment variable or, worse, into the prompt. OpenClaw leaked the keys of ~135k exposed instances exactly this way. A key in the model's context is a key in every log, every trace, and every place a poisoned tool can read.

strongroom holds the keys so the agent doesn't. The raw secret stays encrypted in the vault; the agent only ever holds a lease — a scoped, short-lived, use-limited handle — and the real key is revealed only at the egress point, only while the lease is valid:

  • vault — secrets encrypted at rest (AES-256-GCM, key in ~/.keeper, 0600). Never a plaintext env var, never in a prompt.
  • leasegrant mints an opaque handle bound to a TTL, a use count, and (optionally) a destination host. The agent's context holds the lease, not the secret.
  • redeem — exchange a lease for the secret at the point of use, iff it's still valid (not expired, uses remaining, host in scope). A denial is audited and never burns a use.
  • audit — every grant / redeem / deny / revoke is hash-chained (shared with redstamp) — editing or deleting a past access breaks strongroom audit --verify.

Completes the agent-security stack: redstamp contains the call · truecopy vets the tool · strongroom holds the keys.

Quick start

Install: npm i -g @askalf/strongroom (or run any command below with npx -y @askalf/strongroom). Also installable straight from GitHub: npm i -g github:askalf/strongroom.

echo "sk-live-…" | strongroom add OPENAI_API_KEY          # stored encrypted

LEASE=$(strongroom grant OPENAI_API_KEY --ttl 300 --uses 1 --host api.openai.com)
# → the agent gets $LEASE — not the key

# at the egress point, run the call with the key in the child's env only:
strongroom exec "$LEASE" --as OPENAI_API_KEY -- \
  curl https://api.openai.com/v1/models -H "Authorization: Bearer $OPENAI_API_KEY"

strongroom audit --verify                                 # tamper-evident access log

The agent dispatched strongroom exec <lease> …; the key was decrypted inside strongroom and handed to the subprocess's environment — it never entered the agent's context, stdout, or logs. Run the whole story: npm run demo.

Egress broker — the agent just swaps a base URL

Run the broker and the agent needs no key, no exec, no redeem — only a base-URL swap:

# bind a lease to ONE upstream, how to inject, which endpoints, and a rate cap
LEASE=$(strongroom grant OPENAI_API_KEY \
  --upstream https://api.openai.com --inject bearer \
  --paths "/v1/chat/*,/v1/models" --rate 60 --concurrency 4 --ttl 600 --uses 100)
strongroom broker --port 8771 &

Point the agent's client at the broker:

const openai = new OpenAI({ baseURL: `http://127.0.0.1:8771/${LEASE}`, apiKey: 'unused' });
await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [/* … */] });

For each call the broker redeems the lease (atomic + audited), makes the real upstream request itself with the secret injected (Authorization: Bearer …), and streams the response back. The key is injected at the network boundary — it never enters the agent's context, env, or logs. And because the lease is bound to one upstream, the secret can only ever go to that host; the agent can't redirect it. --inject: bearer (default) · x-api-key (Anthropic) · Header-Name (custom).

Scope it down further:

  • --paths "/v1/chat/*,/v1/models" — restrict the lease to specific endpoints (glob; a chat lease can't reach billing or admin).
  • --rate 60 — cap it at 60 requests/min.
  • --concurrency 4 — cap simultaneous in-flight requests (a runaway or hijacked agent can't hold N parallel streams open through one lease).

All three are enforced before the secret is redeemed — an out-of-scope, over-rate, or over-concurrency request gets 403 / 429, consumes no use, and is audited.

And the upstream call itself is bounded. The broker's upstream request times out after 30 s to first response headers (strongroom broker --timeout <ms>, or KEEPER_BROKER_TIMEOUT_MS). A black-hole upstream gets a 504 (audited as deny/timeout) instead of hanging the request forever — which also means a hung upstream can't pin a --concurrency slot and wedge the lease. The bound is to headers: a healthy streaming response is never cut off mid-stream.

And the response is sanitized on the way back. If the upstream ever reflects the injected secret — an echo/debug endpoint, a verbose error, a misconfigured proxy — the broker redacts it from the relayed headers and body ([strongroom:redacted]) and records a sanitize audit event. The scan is streaming-safe: SSE passes through event-by-event, and a secret split across chunk boundaries is still caught. Without this, a reflecting upstream would hand the raw key straight back into the agent's context, defeating the injection boundary.

Windows / Git Bash: MSYS auto-rewrites an argument that looks like a Unix absolute path, so a bare --paths "/v1/models" reaches strongroom as C:/Program Files/Git/v1/models and silently never matches (every call then 403s on path). A comma-list like "/v1/chat/*,/v1/models" is left alone, which is why it works. Prefix the run with MSYS_NO_PATHCONV=1 (use drive-letter paths for any file args), or call strongroom from PowerShell/cmd. Not a strongroom bug — it mangles the arg before strongroom sees it.

Redeem-daemon — no master key on the redeeming side

The broker covers HTTP APIs. For credentials a tool consumes directly — git over GIT_ASKPASS, a CLI that reads a token — the redeem happens in the agent's own process tree, and a local strongroom redeem would need the master key there. The redeem-daemon removes that requirement:

strongroom serve &                          # long-lived local process — HOLDS the master key
KEEPER_DAEMON=1 strongroom redeem "$LEASE"  # this side holds NO key, NO passphrase

With KEEPER_DAEMON=1, strongroom redeem / strongroom exec route lease→secret over a local socket (unix domain socket / Windows named pipe — token-gated, owner-only 0600, never TCP) instead of opening the vault. Same-user callers need zero config — both sides share the default socket path, and the client reads the capability token from the daemon's 0600 info file; a sandboxed worker is instead handed only KEEPER_SOCKET + KEEPER_DAEMON_TOKEN (pin one via env before serve) and never reads strongroom's home at all. Either way the redeeming process never holds the master key: compromise it and you get its leases — scoped, expiring, revocable — not the vault. This is how a control plane hands git credentials to sandboxed workers: a GIT_ASKPASS helper that runs strongroom redeem, with zero token bytes on disk and zero key material in the worker.

Examples — real SDKs, zero keys in the agent

Three end-to-end examples, each running a genuine client with a credential that never enters the agent's context:

Example Shows
examples/anthropic-sdk-strongroom the Anthropic SDK (@anthropic-ai/sdk) making a real messages.create call through the broker — x-api-key injected at egress
examples/openai-agents-strongroom a real OpenAI Agents SDK agent run loop with its model calls brokered through a lease
examples/mcp-strongroom an MCP server whose tools return leases, not keys — the "where does the key live?" answer for every credentialed MCP server

Why a lease, not the key

a raw key in env / prompt a strongroom lease
in the model's context yes — leaks to logs, traces, poisoned tools no — only an opaque handle
lifetime until you rotate it seconds (TTL)
blast radius every call, every host one use, one host
revocable rotate everywhere strongroom revoke <lease>
audited no every access, tamper-evident

Dispatching to a fleet

A platform that runs agents on remote devices shouldn't ship a long-lived key to each one — that's how OpenClaw leaked ~135k of them. Ship a lease instead:

  • the control plane stores the secret in strongroom and grants a scoped, short-lived lease per task (--upstream, --paths, --rate, --concurrency, --ttl, --uses);
  • the device receives only the lease id and runs through strongroom broker — the key is injected at egress, never written to the device;
  • a compromised device yields a lease (scoped, expiring, revocable), not a key. strongroom revoke <lease> kills it instantly — no production-key rotation.

The control plane never scrapes human output. grant, leases, ls, and audit take --json and put exactly one JSON value on stdout — no ANSI, no prose, no stderr summary:

strongroom grant TASK_API_KEY --ttl 300 --uses 50 --upstream https://api.example.com --json
# → {"id":"lease_…","secret":"TASK_API_KEY","usesLeft":50,"expiresAt":1720000000000,"ttlS":300,
#    "host":null,"upstream":"https://api.example.com","inject":null,"rate":null,"paths":null,"concurrency":null}
strongroom leases --json          # → array of secret-safe lease records (fingerprints, never raw ids)
strongroom ls --json              # → ["TASK_API_KEY", …]
strongroom audit --json           # → the parsed event array
strongroom audit --verify --json  # → {"ok":true,"entries":n} | {"ok":false,"reason":"audit-tip-forged"} — exit code 0/1 preserved

grant --json returns the same one-time id + metadata the human path already returns — just machine-readable. Without --json, output is unchanged.

See it end to end: npm run demo:platform.

Security model

strongroom is a vault, so its own security is the point:

  • Encrypted at rest — AES-256-GCM, with the secret name bound in as AAD, so a ciphertext can't be swapped between names.

  • Master key — three options, in priority order:

    • KEEPER_PASSPHRASE — derived with scrypt; never on disk (only a salt is).
    • KEEPER_KEYCHAIN=1 — held by the OS keychain: macOS Keychain · Linux Secret Service · Windows DPAPI (user scope). Never plaintext on disk, and it fails closed if no keychain is available (no silent downgrade). strongroom keychain shows the active backend.
    • else — a random key file in ~/.keeper (0600 + a restrictive ACL on Windows).

    Use the passphrase or the keychain for anything that matters.

  • Rotation is built instrongroom rekey re-encrypts every secret under a fresh master key, optionally switching key stores (--to passphrase|keychain|file; a passphrase target reads KEEPER_NEW_PASSPHRASE). It's atomic and fail-closed: a wrong current passphrase aborts with nothing changed, an interrupted swap is completed or discarded safely on the next run, retired key material (old salt / key file / keychain entry) is removed, and the audit's authenticated tip is re-MACed under the new key. Restart a running daemon/broker afterwards — they hold the old key and fail closed.

  • Operator ceiling on grants — set KEEPER_MAX_TTL (seconds) and/or KEEPER_MAX_USES and no lease minted from this vault — CLI or library — may exceed them. An over-cap grant is rejected with an error naming the cap (never silently clamped) and audited as a deny/policy event, so "leases stay small" is vault policy, not caller discipline. Unset = no ceiling (unchanged behavior). Zero, negative, or non-numeric --ttl/--uses are always rejected — a NaN would otherwise mint a lease that never expires.

  • Leases are bearer tokens — only sha256(id) is stored; the raw id is returned once, to you. Reading leases.json therefore can't redeem anything.

  • Single-use is atomic — redeem is a check-and-consume under a cross-process lock, so concurrent redeems can't double-spend a one-use lease.

  • Fail-closed — a tampered, swapped, or wrong-key entry returns null and denies; it never throws or leaks garbage.

  • Tamper-evident audit — every access is hash-chained (shared with redstamp) and logged by lease fingerprint, never the raw id. An authenticated tip (HMAC under a subkey of the master key) commits to the chain's length and last hash, so truncating or splicing the log is caught — not just editing an entry.

  • Reflected secrets can't ride back in — the broker redacts any occurrence of the injected secret from relayed response headers and bodies and audits it (sanitize), so an echoing or misconfigured upstream can't hand the raw key back into the agent's context.

What it is not: a defense against an attacker who already has your passphrase / master key or full process memory — at that point they have the vault. strongroom shrinks the agent's exposure (a lease, not the key; short-lived; scoped; audited); it doesn't replace OS-level isolation.

Commands

strongroom add <name>                  store a secret (stdin, or --value=)
strongroom ls [--json]                 list secret names (never values)
strongroom grant <name> [--ttl --uses --host]                        mint a lease
              [--upstream --inject --paths --rate --concurrency]  (broker scoping)
              (KEEPER_MAX_TTL / KEEPER_MAX_USES, if set, cap every grant — over-cap is rejected + audited)
              [--json]                 one machine-readable JSON object on stdout
strongroom redeem <lease> [--host]     exchange a valid lease for the secret (egress side)
strongroom exec <lease> --as <ENV> -- <cmd...>  redeem + run <cmd> with the secret in its env only
strongroom broker [--port 8771]        egress-injection proxy (base-URL swap, zero key in the agent)
strongroom serve [--socket <path>]     redeem-daemon: holds the master key, answers lease→secret
                                   over a local socket (KEEPER_DAEMON=1 on the keyless side)
strongroom leases [--json] · strongroom revoke <lease> · strongroom rm <name>
strongroom audit [--verify] [--json]   the access log, optionally chain-verified
strongroom rekey [--to passphrase|keychain|file]   rotate the master key (re-encrypts the vault)
strongroom keychain                    master-key backend status (KEEPER_KEYCHAIN=1 to use the OS keychain)

Library

import { addSecret, grant, redeem } from '@askalf/strongroom';

addSecret('STRIPE_KEY', process.env.STRIPE_KEY);
const lease = grant('STRIPE_KEY', { ttlS: 60, uses: 1, host: 'api.stripe.com' });
// hand `lease.id` to the agent; at egress:
const { ok, value } = redeem(lease.id, { host: 'api.stripe.com' });

The agent-security stack

Three composable layers, one defense: redstamp contains the call · truecopy vets the tool · strongroom holds the keys (you are here). Run all three together → agent-security-stack.


Part of Own Your Stack — own your AI infrastructure instead of renting it. Built by Thomas Sprayberry.

About

Own your agent secrets — your AI agent holds a scoped, single-use lease, never the raw API key. strongroom injects the real secret only at egress, so a leaked prompt or poisoned tool can't leak a credential. Encrypted vault · tamper-evident audit · zero deps. Part of the agent-security stack (redstamp · truecopy · strongroom).

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages