Skip to content
SecretArchitectureDesign

Secret Lease 设计原理:凭据如何永不进入模型上下文

三条泄漏路径

在生产环境中,凭据泄露通常不是被"黑客攻破",而是被系统自己记录和暴露:

  1. Prompt 泄漏:用户把 API Key 贴进 ChatGPT,OpenAI 的日志里就有了
  2. 日志泄漏:应用框架记录完整 HTTP 请求,敏感 header 原样落盘
  3. 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 不是万能药,但它在"安全"与"可用"之间找到了本地优先的最优解。