askalf/strongroom
GitHub: askalf/strongroom
一个为 AI Agent 设计的加密密钥保险库,通过作用域受限的单次 lease 替代原始密钥,仅在出口点注入凭证,防止泄露。
Stars: 1 | Forks: 0
# 保险库
[](https://github.com/askalf/strongroom/actions/workflows/ci.yml)
[](https://github.com/askalf/strongroom/actions/workflows/codeql.yml)
[](https://scorecard.dev/viewer/?uri=github.com/askalf/strongroom)
[](LICENSE)
Agent 需要凭证 —— API key、token、密码 —— 才能执行有用的操作。如今它们获取凭证的方式极其不安全:将一个长期有效的 key 硬塞进环境变量中,甚至更糟,塞进 prompt 中。OpenClaw 正是通过这种方式泄露了约 13.5 万个暴露实例的 key。模型上下文中的 key,就等于存在于每一份日志、每一次 trace 以及每一个被投毒的工具可以读取的角落。
**strongroom 替 agent 保管 key,因此 agent 无需亲自持有。** 原始 secret 在 vault 中保持加密状态;agent 仅持有一个 **lease** —— 一个作用域受限、生命周期短、使用次数有限的 handle —— 而真实的 key **仅在出口点** 且仅在 lease 有效时才会被揭示:
- **vault** —— 静态加密的 secret(AES-256-GCM,key 存放在 `~/.keeper` 中,权限为 `0600`)。绝不会作为明文环境变量出现,也绝不会进入 prompt。
- **lease** —— `grant` 会生成一个不透明的 handle,绑定到 **TTL**、**使用次数** 以及(可选的)**目标主机**。Agent 的上下文持有的是 lease,而不是 secret。
- **redeem** —— 在使用点将 lease 兑换为 secret,*当且仅当* 它仍然有效时(未过期、仍有使用次数、主机在限定范围内)。拒绝操作会被审计,且绝不会消耗使用次数。
- **audit** —— 每一次 grant / redeem / deny / revoke 都是 **hash-chained**(与 [redstamp](https://github.com/askalf/redstamp) 共享)—— 编辑或删除任何过去的访问记录都会导致 `strongroom audit --verify` 校验失败。
补全了 Agent 安全技术栈:**redstamp** 容纳调用 · **truecopy** 审查工具 · **strongroom** 保管 key。
## 快速开始
```
echo "sk-live-…" | strongroom add OPENAI_API_KEY # stored encrypted
LEASE=$(strongroom grant OPENAI_API_KEY --ttl 300 --uses 1 --host api.openai.com)
# → agent 获取 $LEASE — 而不是 key
# 在 egress point,仅在 child 的 env 中使用 key 运行调用:
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
```
Agent 发送了 `strongroom exec …` 指令;key 在 strongroom 内部解密并传递给子进程的环境变量 —— 它从未进入 agent 的上下文、stdout 或日志中。运行完整流程:`npm run demo`。
## 出口代理 —— Agent 只需替换 base URL
运行代理后,agent 不再需要 key、不需要 `exec`、也不需要 redeem —— 只需替换 base-URL:
```
# 将 lease 绑定到一个 upstream,如何 inject,哪些 endpoints,以及 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 &
```
将 agent 的客户端指向代理:
```
const openai = new OpenAI({ baseURL: `http://127.0.0.1:8771/${LEASE}`, apiKey: 'unused' });
await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [/* … */] });
```
对于每次调用,代理都会兑换 lease(原子操作且经过审计),使用注入的 secret(`Authorization: Bearer …`)自行发起**真正的**上游请求,并将响应流传回。key 在网络边界处注入 —— 它永远不会进入 agent 的上下文、环境变量或日志中。而且由于 lease **绑定到了单一上游**,secret 只会发送到该主机;agent 无法重定向它。`--inject`:`bearer`(默认)· `x-api-key`(Anthropic)· `Header-Name`(自定义)。
**进一步缩小作用域:**
- `--paths "/v1/chat/*,/v1/models"` —— 将 lease 限制在特定 endpoint(支持 glob 匹配;chat 类 lease 无法触及计费或管理接口)。
- `--rate 60` —— 限制为每分钟 60 次请求。
- `--concurrency 4` —— 限制同时进行的在途请求数量(失控或被劫持的 agent 无法通过一个 lease 保持 N 个并行流处于打开状态)。
这三项都在 **兑换** secret **之前** 强制执行 —— 超出范围、超频或超并发的请求将收到 `403` / `429`,不消耗任何使用次数,并会被审计。
**同时,上游调用本身也是有边界的。** 代理的上游请求在等待首个响应 header 超过 **30 秒** 后将超时(`strongroom broker --timeout `,或 `KEEPER_BROKER_TIMEOUT_MS`)。黑洞上游会收到 `504`(被审计为 `deny`/`timeout`),而不是让请求永远挂起 —— 这也意味着卡住的上游无法占用 `--concurrency` 插槽并阻塞 lease。该边界仅针对 *header*:健康的流式响应绝不会在流传输过程中被切断。
**并且响应在传回时经过了过滤。** 如果上游*反射*了注入的 secret —— 例如 echo/debug endpoint、详细的错误信息、配置错误的 proxy —— 代理会从转发的 header 和 body 中将其涂黑(`[strongroom:redacted]`),并记录一个 `sanitize` 审计事件。该扫描是流安全的:SSE 会逐个事件通过,即使是跨 chunk 边界拆分的 secret 也能被捕获。如果没有这项机制,反射上游就会将原始 key 直接传回 agent 的上下文中,从而破坏注入边界。
## redeem-daemon —— 兑换端无需 master key
代理涵盖了 HTTP API。对于工具*直接*消费的凭证 —— 例如通过 `GIT_ASKPASS` 的 git、读取 token 的 CLI —— 兑换过程发生在 agent 自己的进程树中,而本地的 `strongroom redeem` 在那里会需要 master key。**redeem-daemon** 移除了这一要求:
```
strongroom serve & # long-lived local process — HOLDS the master key
KEEPER_DAEMON=1 strongroom redeem "$LEASE" # this side holds NO key, NO passphrase
```
设置 `KEEPER_DAEMON=1` 后,`strongroom redeem` / `strongroom exec` 会通过**本地 socket**(unix domain socket / Windows named pipe —— 受 token 保护,仅限所有者访问权限 `0600`,绝不使用 TCP)进行 lease 到 secret 的路由,而不是直接打开 vault。同一用户的调用者无需任何配置 —— 双方共享默认的 socket 路径,客户端从守护进程的 `0600` 信息文件中读取 capability token;而**沙箱化 worker** 则只接收 `KEEPER_SOCKET` + `KEEPER_DAEMON_TOKEN`(在 `serve` 之前通过环境变量固定一个),从不读取 strongroom 的主目录。无论哪种方式,执行兑换的进程都不会持有 master key:即使它被攻破,攻击者得到的也只是它的 lease —— 作用域受限、会过期、可撤销 —— 而不是整个 vault。这就是控制平面向沙箱化 worker 传递 git 凭证的方式:一个运行 `strongroom redeem` 的 `GIT_ASKPASS` 助手,在磁盘上不留任何 token 字节,worker 内也不留任何关键密钥材料。
## 示例 —— 真实的 SDK,agent 中零 key
三个端到端示例,每个都运行一个真实的客户端,且凭证永远不会进入 agent 的上下文:
| 示例 | 展示内容 |
|---|---|
| [`examples/anthropic-sdk-strongroom`](examples/anthropic-sdk-strongroom) | **Anthropic SDK**(`@anthropic-ai/sdk`)通过代理发起真实的 `messages.create` 调用 —— 在出口处注入 `x-api-key` |
| [`examples/openai-agents-strongroom`](examples/openai-agents-strongroom) | 一个真实的 **OpenAI Agents SDK** agent 运行循环,其模型调用通过 lease 进行代理 |
| [`examples/mcp-strongroom`](examples/mcp-strongroom) | 一个 **MCP server**,其工具返回的是 *lease,而非 key* —— 为所有需要凭证的 MCP server 回答了“key 该存放在哪里?” |
## 为什么用 lease,而不是 key
| | 环境变量 / prompt 中的原始 key | 一个 strongroom lease |
|---|---|---|
| 是否存在于模型上下文中 | **是** —— 会泄露到日志、trace 和被投毒的工具中 | 否 —— 仅是一个不透明的 handle |
| 生命周期 | 直到您轮换它为止 | 几秒钟(TTL) |
| 影响范围 | 每次调用、每个主机 | 一次使用、一个主机 |
| 可撤销 | 需要在各处轮换 | `strongroom revoke ` |
| 可审计 | 否 | 每次访问均可审计,防篡改 |
## 调度到集群
在远程设备上运行 agent 的平台,不应该向每台设备发送一个长期有效的 key —— 这正是 OpenClaw 泄露约 13.5 万个 key 的原因。请改为发送一个 **lease**:
- **控制平面** 将 secret 存储在 strongroom 中,并为每个任务授予一个作用域受限、生命周期短的 lease(`--upstream`、`--paths`、`--rate`、`--concurrency`、`--ttl`、`--uses`);
- **设备** 仅接收 lease id 并通过 `strongroom broker` 运行 —— key 在出口处注入,绝不写入设备;
- 被攻破的设备泄露的只是一个 *lease*(作用域受限、会过期、可撤销),而不是 key。`strongroom revoke ` 会立即使其失效 —— 无需轮换生产环境的 key。
**控制平面绝不抓取人类可读的输出。** `grant`、`leases`、`ls` 和 `audit` 接受 `--json` 参数,并在 stdout 上仅输出**一个 JSON 值** —— 没有 ANSI 转义、没有多余的文本、没有 stderr 摘要:
```
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` 返回与人类可读路径相同的一次性 id 及元数据 —— 只不过是机器可读的格式。如果不加 `--json`,输出保持原样。
查看完整的端到端演示:`npm run demo:platform`。
## 委派 lease —— Agent 间的最小权限
在多 agent 树中,父 agent 可以向**子 agent** 传递其自身访问权限中一个*更小*的切片,而无需触及 vault。`grant --from-lease` 会将父 agent 持有的 lease **衰减** 为一个子 lease,其每一个作用域范围都 `≤` 父 lease —— TTL 更短、使用次数更少、主机/upstream/paths/rate/concurrency 更严格。**绝不会扩大。**
```
# Parent 持有宽泛的 lease:1小时,100 次使用,所有 /v1/*
PARENT=$(strongroom grant OPENAI_API_KEY --upstream https://api.openai.com \
--paths "/v1/*" --ttl 3600 --uses 100)
# 向 summarizer sub-agent 委托严格的 sub-lease:5 分钟,3 次使用,仅限 chat
CHILD=$(strongroom grant --from-lease "$PARENT" \
--paths "/v1/chat/completions" --ttl 300 --uses 3)
```
- **仅限衰减。** 子 lease 可以**缩小**任何维度的范围,或者**继承**它(未设置 = 继承父级设置),但绝不会扩大:更长的 TTL、更多的使用次数、更宽泛的 `--paths` glob、不同的 `--host`/`--upstream`,或更高的 `--rate`/`--concurrency` 都会被**拒绝**,并返回一个指明维度的错误。`--paths` 必须是父级路径的**子集**(使用与代理执行的相同的 segment-glob 语义进行检查)。如果父级的某个维度保持*无限*,则可以由子级进行*设限*。
- **记录溯源。** 子 lease 带有**父 lease 指纹**,并且子级的 `grant` 审计事件将其作为 `from` 带入 —— 因此,委派操作会作为一个父→子的链接出现在经过哈希链处理且带有 authenticated tip 的审计记录中,并且仍然可以通过校验。`strongroom leases` 和 `strongroom audit` 会将其渲染为 `⤷ from `。
- **基于能力的保证。** 子级是一个独立的 lease,受限于 `child scope ⊆ parent scope`,因此子 agent 永远无法将其父级无法访问的任何内容进行兑换 —— 并且委派操作**不会**消耗父级的使用次数。
## 安全模型
strongroom 是一个 vault,因此其自身的安全性至关重要:
- **静态加密** —— 采用 AES-256-GCM,并将 secret *名称* 作为 AAD 绑定其中,因此密文不能在不同名称之间互换。
- **Master key** —— 有三个选项,按优先级顺序排列:
- `KEEPER_PASSPHRASE` —— 通过 **scrypt** 派生;绝不触碰磁盘(仅存有 salt)。
- `KEEPER_KEYCHAIN=1` —— 由 **OS keychain** 托管:macOS Keychain · Linux Secret Service · Windows DPAPI(用户作用域)。绝不以明文形式存在于磁盘上,并且如果不可用 keychain,它会**安全失败**(不会默默降级)。`strongroom keychain` 可显示当前活动的后端。
- 其他情况 —— 位于 `~/.keeper` 中的一个随机 key 文件(在 Windows 上具有 `0600` 权限 + 严格的 ACL)。
请务必使用 passphrase 或 keychain 来保护任何重要的内容。
- **内置轮换功能** —— `strongroom rekey` 会在新的 master key 下重新加密所有 secret,并可选择切换 key 存储(`--to passphrase|keychain|file`;如果是 passphrase 目标,则会读取 `KEEPER_NEW_PASSPHRASE`)。该操作是原子性且安全失败的:输入了错误的当前 passphrase 会直接中止,不做任何更改;中断的切换操作会在下次运行时被安全地完成或丢弃;废弃的 key 材料(旧的 salt / key 文件 / keychain 条目)将被移除;审计记录的 authenticated tip 会在新 key 下重新进行 MAC 计算。之后需重启正在运行的 daemon/broker —— 它们持有的是旧 key 并且会安全失败。
- **针对 grant 的操作员上限** —— 设置 `KEEPER_MAX_TTL`(秒)和/或 `KEEPER_MAX_USES`,从此 vault(无论是通过 CLI **还是**库)生成的任何 lease 都不得超过此限制。超出上限的 grant 会被**拒绝**,并返回一个指明该上限的错误(绝不默默截断),同时作为 `deny`/`policy` 事件被审计,因此“保持 lease 简短有效”是 vault 的策略强制要求,而非依赖调用者的自觉。未设置 = 没有上限(行为不变)。零、负数或非数字的 `--ttl`/`--uses` 始终会被拒绝 —— 否则一个 NaN 会生成一个永不过期的 lease。
- **Lease 属于 bearer token** —— 仅存储 `sha256(id)`;原始 id 仅向您返回一次。因此,读取 `leases.json` 无法兑换任何内容。
- **单次使用是原子性的** —— redeem 是在跨进程锁下进行检查并消费,因此并发的兑换操作不会导致一次性 lease 被重复消费。
- **安全失败** —— 被篡改、替换或使用了错误 key 的条目会返回 null 并被拒绝;它绝对不会抛出异常或泄露垃圾数据。
- **防篡改审计** —— 每一次访问都是哈希链接的(与 redstamp 共享),并且仅记录 lease 的*指纹*,而不是原始 id。一个**authenticated tip**(在 master key 的子密钥下进行 HMAC)会承诺链的长度和最后一个哈希值,因此*截断*或*拼接*日志都会被发现 —— 而不仅仅是编辑条目。
- **反射的 secret 无法趁虚而入** —— 代理会从转发的响应 header 和 body 中涂黑任何出现的注入 secret 并进行审计(`sanitize`),因此产生回显或配置错误的上游无法将原始 key 传回 agent 的上下文中。
它**不是**什么:它无法防御已经获取了您的 passphrase / master key 或完整进程内存的攻击者 —— 到了那个地步,他们已经掌握了整个 vault。strongroom 只是缩小了 *agent* 的暴露面(使用 lease,而不是 key;生命周期短;作用域受限;可审计);它并不能替代操作系统级别的隔离。
## 命令
```
strongroom add store a secret (stdin, or --value=)
strongroom ls [--json] list secret names (never values)
strongroom grant [--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 grant --from-lease [tighter opts] DELEGATE: attenuate a lease you hold into a
narrower sub-lease for a sub-agent (shorter --ttl, fewer --uses, tighter
--host/--upstream/--paths/--rate/--concurrency; NEVER wider; unset scopes inherit)
strongroom redeem [--host] exchange a valid lease for the secret (egress side)
strongroom exec --as -- redeem + run 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 ] 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 · strongroom rm
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)
```
## 库
```
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' });
// Delegate a narrower sub-lease to a sub-agent (attenuate-only; child ⊆ parent):
import { grantFromLease } from '@askalf/strongroom';
const sub = grantFromLease(lease.id, { ttlS: 30, uses: 1 }); // sub.parent = parent fingerprint
```
## Agent 安全技术栈
三个可组合的层,构成同一种防御:**[redstamp](https://github.com/askalf/redstamp)** 容纳调用 · **[truecopy](https://github.com/askalf/truecopy)** 审查工具 · **[strongroom](https://github.com/askalf/strongroom)** 保管 key *(您正在浏览此处)*。同时运行这三者 → **[agent-security-stack](https://github.com/askalf/agent-security-stack)**。
属于 **[Own Your Stack](https://github.com/askalf)** 项目 —— 拥有你自己的 AI 基础设施,而不是去租用。由 Thomas Sprayberry 构建。
标签:MCP服务, MITM代理, Streamlit, StruQ, 加密存储, 自定义脚本, 访问控制, 防篡改审计, 零依赖