Secret Lease 设计原理:凭据如何永不进入模型上下文
三条泄漏路径
在生产环境中,凭据泄露通常不是被"黑客攻破",而是被系统自己记录和暴露:
- Prompt 泄漏:用户把 API Key 贴进 ChatGPT,OpenAI 的日志里就有了
- 日志泄漏:应用框架记录完整 HTTP 请求,敏感 header 原样落盘
- UI/Trace 泄漏:错误提示、调试面板、分布式追踪把 token 当普通字符串打印
Vigils 的设计目标是:让真实 secret 值在任何时候都不出现在 Agent 可读取的内存区域之外。
Lease 抽象
传统方案是"存储 + 检索":把 secret 存在某个 vault,需要时取出来用。问题是"取出来"的那一瞬间,secret 就暴露给了请求方。
Vigils 引入 Lease 抽象,核心思想是引用绑定 + 进程内短期注入:
- 长期存储:OS Keychain(KeyringSecretStore)只存引用别名,如 "github/read-only"
- 短期授权:Lease Broker 为单次操作签发 lease,绑定 scope 和 TTL(默认 300s)
- 执行注入:沙箱进程启动前,通过环境变量把 lease 值注入,env_clear 保证不泄漏
- 自动销毁:操作完成或 TTL 到期,lease 立即撤销,进程内缓存清空
Agent 和模型上下文永远只看到 "secret://github/read-only" 或 "[REDACTED github_token]"。
生命周期详解
1. Mint(签发)
当 Policy Engine 判定某操作需要凭证时,向 Lease Broker 申请:
lease.mint {
id: lse_9m4p,
secret_ref: "github/read-only",
scope: ["repo:read"],
ttl: 300s,
bound_to: "sess_9x2k"
}
Broker 从 Keyring 取出真实值,生成 lease_id → value 的进程内映射。value 不进入任何 struct 的持久字段。
2. Bind(绑定)
Lease 与具体操作绑定。如果操作被防火墙拦截或审批拒绝,lease 在 mint 阶段就被丢弃,不会进入注入流程。
3. Inject(注入)
Wasmtime 沙箱启动时,通过 pre_exec 钩子设置环境变量。这是 lease 值唯一一次离开 Vigils 进程内存。
4. Revoke(撤销)
三种触发条件:
- 操作正常完成 → 立即 revoke
- TTL 到期 → 后台定时器 revoke
- 进程异常退出 → atexit 钩子 revoke
revoke 后,lease_id → value 映射被显式 zeroize,不依赖 GC。
审计链中的秘密
审计负载走 Vigils redaction 模块。DecisionRecord 中存储的是:
secret_injected: {
alias: "github/read-only",
lease_id: "lse_9m4p",
// 没有 value 字段
}
即使审计数据库被拖库,攻击者也只能拿到 alias,无法反查真实值(需要同时攻破 OS Keychain 和 Vigils 进程内存)。
威胁模型:进程被攻破时
假设攻击者在 lease 有效期内控制了 Vigils 进程:
- 最坏情况:能读取当前活跃 lease 的 value(内存中的 HashMap 条目)
- 无法做到:读取历史 lease(已 revoke 并 zeroize)、读取未授权 secret(scope 隔离)、从审计日志还原 value
这就是"短期"的意义——攻击窗口被 TTL 严格限制。
与替代方案对比
| 方案 | 持久化风险 | 作用域控制 | 审计友好 |
|---|---|---|---|
| 环境变量注入 | 高(env 可被 dump) | 无 | 差 |
| Docker Secret | 中(文件系统残留) | 容器级 | 中 |
| Vault 动态凭证 | 低 | API 级 | 好 |
| Vigils Lease | 极低(进程内+zeroize) | 操作级 | 极好(alias 审计) |
Lease 不是万能药,但它在"安全"与"可用"之间找到了本地优先的最优解。