# 凭证生命周期与执行需要各自的负责人

密钥管理器应决定哪些凭证存在、何时过期以及如何失效。桌面网关应决定当前进程现在能否执行这项操作，在不把凭证交给进程的前提下执行操作，并记录发生了什么。把这些职责合在一起看似更简单，直到第一次轮换、撤销或安全事件要求你说明究竟由哪个组件负责。

分界线在凭证被使用时由谁持有，而不在凭证存在哪里。HashiCorp Vault 或以 1Password 为后端的工作流可以继续负责生命周期，网关则控制执行，但前提是交接过程能传递生命周期状态和身份信息，并且不会把网关变成一个不受追踪的缓存。如果网关复制了一个值、忘记它的租约并继续使用，那么架构图上虽然有两个框，系统里却有两个密钥管理器。

当自主代理可以访问生产 API 或 SSH 目标时，这种模式值得增加一些连接工作。代理需要范围明确的操作和人工控制，不需要另一种读取凭证的方式。

## 边界位于经过身份验证的操作之前

生命周期系统负责创建、轮换、过期和撤销凭证，执行网关负责放行每一项经过身份验证的操作。这句话就是架构测试。每个字段、缓存、重试和日志条目都应有且只有一方能为它负责。

生命周期责任不只是保存一段加密字符串。对于动态数据库账户，它包括创建账户的角色、租约 ID、TTL、续租规则，以及删除或停用账户的操作。对于静态 API 令牌，它包括上游签发方、当前代次、激活时间、可能的重叠窗口，以及旧代次已停止工作的证明。密码管理器可以保存权威记录，但远程 API 仍然决定是否接受该令牌。

当调用方请求某种结果时，执行责任就开始了，例如 `GET /billing/invoices`、`POST /deployments`，或在指定主机上运行 SSH 命令。网关验证调用方身份、检查本地批准状态、解析已获授权的凭证、把它注入出站协议，然后只返回结果。网关不应向代理提供通用的 `read secret` 操作，否则执行控制又会退化为密钥分发。

因此，清晰的接口应指定操作和凭证引用，而不是凭证值。它还应指定预期的生命周期代次，使延迟的请求不能悄悄跨过轮换边界。一个实用的请求封装如下：

```json
{
  "action_id": "01J...",
  "credential_ref": "vault:database/creds/agent-readonly",
  "expected_generation": "lease:database/creds/agent-readonly/2f6a...",
  "purpose": "read migration status",
  "target": "db-admin.internal",
  "caller": {
    "session_id": "sess_7f2...",
    "process_authority": "signed:TEAMID.example.agent"
  }
}
```

网关可以取得底层值，因为必须有某个组件把字节放入 Authorization 请求头或 SSH 握手中。约束在于，它必须在受信任的执行路径内取得该值，不让它出现在代理可见的参数、环境变量、文件、工具结果和错误文本中，然后按照明确的缓存规则丢弃。对代理无密钥，并不意味着任何组件都从不处理明文。

## 让生命周期权威创建并退役凭证

当 Vault 的密钥引擎能够在目标系统创建和撤销凭证时，应让 Vault 负责生命周期。对于静态凭证，只有当以 1Password 为后端的工作流还会更新签发方、记录新代次并退役旧代次时，才应由该工作流负责。只保存最新值属于清单管理，不是轮换。

HashiCorp 的 Vault 租约文档把这份契约写得很明确。每个动态密钥都有租约，其中包含持续时间和是否可续租的标记。Vault 承诺凭证在这段时间内有效，但过期后，使用方就不能再假设它仍能工作。撤销租约会使密钥失效并阻止续租。对于受支持的引擎，Vault 还会执行底层清理，例如删除生成的云凭证或数据库用户。

这种行为让 Vault 自然成为受支持动态凭证的负责人。网关应请求或接收租约，读取返回的 TTL，并在租约结束前停止使用。它绝不能自行设定更长的本地有效期。HashiCorp 还警告说，请求的续租增量只是建议，客户端必须检查续租响应。网关如果请求延长一小时就假定自己得到了整整一小时，已经破坏了生命周期责任边界。

Vault 的 KV 引擎则不同。Vault 文档说明，即使响应里包含租约时长，KV 也不会签发租约。把 API 令牌放进 KV 不会让它变成动态令牌，删除旧的 KV 版本也不一定会在签发方撤销令牌。轮换工作流必须调用提供方、验证替代凭证、更新权威记录并停用旧令牌。

同样的限定也适用于 1Password。它的 CLI 文档介绍了密钥引用和 `op run`、`op read`、`op inject` 等命令。这些机制会在运行时取回已保存的密钥，但它们本身不会在第三方签发方轮换通用 API 密钥。团队仍可让 1Password 保存权威记录，但围绕它的自动化必须负责远程更新和退役流程。

不要按照供应商类别分配生命周期责任。要看谁能实际控制签发方，并产生可观察的代次变化。

## 安全交接要传递引用和租约

网关需要可解析的引用、有效期边界和撤销路径。只传引用解决了命名问题，却丢失了时间信息。只传过期时间会丢失真正能够撤销凭证的权威。只传密钥值则两者都没有。

对于 Vault 动态密钥，最自然的代次标识就是租约 ID。`vault read database/creds/my-role` 的响应结构包含 `lease_id`、`lease_duration`、`lease_renewable`、`username` 和 `password`。网关必须把凭证字节和这些元数据绑定为一个对象，不能把密码放在一个缓存中、租约放在另一个可能发生偏移的缓存中。

对于 1Password 中的静态记录，应在轮换工作流里创建不可变的代次标记。它可以是所选集成公开的项目版本标识，也可以是与引用一起保存的轮换事件 ID。不要用 `prod-api-key` 这样的可变项目名称作为代次。名称告诉网关去哪里解析，却不能证明它收到了哪个值。

交接契约应包含这些属性：

- `credential_ref` 标识来源，但不包含密钥材料。
- `generation` 在可用凭证发生变化时随之变化。
- `not_after` 在存在截止时间时，为网关提供硬性的本地停止时刻。
- `revocation_ref` 告诉事件处理工具要撤销或退役哪个对象。
- `issued_for` 把凭证绑定到预期角色、目标和环境。

网关响应应原样返回这些非敏感部分，让调用方和审计管道能够关联一次操作，而不会得知凭证：

```json
{
  "action_id": "01J...",
  "status": 200,
  "credential_ref": "vault:database/creds/agent-readonly",
  "generation": "lease:database/creds/agent-readonly/2f6a...",
  "executed_at": "2026-07-27T14:03:12Z",
  "result_digest": "sha256:9b0..."
}
```

这份契约还揭示了一个不太好处理的事实：桌面网关需要自己的机器身份，才能从生命周期系统取回凭证。这个引导凭证也有生命周期。Vault 令牌应使用范围狭窄的策略，并有自己的 TTL 或续租行为。1Password 服务账户令牌应只能访问所需的保险库。把引导令牌藏进本地设置文件，只是把原来的问题向下移动了一层。

## 过期必须优先于缓存和重试

只有每条执行路径都把当前时间与生命周期权威返回的边界进行比较，过期语义才能在交接后继续成立。如果剩余有效期不足以覆盖解析、批准、建立连接、执行和少量时钟余量，网关就应拒绝新操作。

假设 Vault 签发一个有效期十分钟的数据库凭证。代理在第九分钟开始导出，用户花四十秒阅读批准卡，网关在网络超时后又重试两次。缓存如果只在取回凭证时检查 TTL，最后一次重试可能在凭证过期后才开始。目标随后返回身份验证错误，但审计记录可能把它误标为网络故障或用户拒绝。

应根据权威元数据一次性设定可用截止时间：

```text
usable_until = min(authority_not_after, fetched_at + local_cache_cap)
latest_start = usable_until - approval_budget - connect_budget - clock_margin
```

这些预算是运行选择，不会延长凭证寿命。如果 `now` 晚于 `latest_start`，应取回新代次；如果获批操作会发生实质变化，还要显示新的批准请求。绝不要为了挽救在本地队列中等待的请求而续租 Vault 租约。续租属于生命周期决定，可能会扩大暴露窗口。

重试也必须遵守同样的纪律。只有代次仍然匹配、凭证仍在可用期限内，并且远程操作可以安全重复时，重试才能复用凭证。HTTP 幂等性和凭证有效性是两项独立检查。令牌有效并不会让重复的 `POST` 变得安全。

对于没有签发方规定过期时间的静态凭证，可以用本地缓存上限减少旧副本的存活时间，但应把它称为缓存策略，而不是过期。生命周期工作流仍然需要发送代次通知，或者在轮换后强制重新解析。否则，网关可能在重叠窗口内持续使用仍有效的旧令牌，让退役测试看似成功，直到提供方最终停用它。

## 撤销是一个端到端事件

只有生命周期权威在签发方停用凭证，并且网关停止以后所有对缓存代次的使用，撤销才算完成。只清理一边并不完整。

Vault 为运维人员提供了具体机制。`vault lease revoke` 会使一个租约失效，按前缀撤销则可以让某个路径下的租约失效。HashiCorp 的命令文档还区分正常撤销和强制移除。即使密钥引擎未能撤销目标凭证，强制移除也可能让 Vault 忘记该租约，导致 Vault 与目标不同步。应把这项警告当作安全事件状态，而不是成功清理的消息。

如果生命周期系统能发送撤销信号，桌面网关应消费该信号，但它还需要主动拉取当前状态的防线。在敏感使用前，网关可以重新验证代次或再次解析。对于短期 Vault 租约，严格的 TTL 加短缓存可能已经足够。对于静态 API 令牌，轮换协调器应在切换时让网关缓存失效，然后用一个无害端点直接测试已退役令牌。

对于已经运行的操作，需要明确规则。撤销可以可靠地阻止尚未开始的操作，却不一定能撤回远程 API 已接受的请求、已提交的数据库事务，或已交给 shell 的 SSH 命令。网关应把操作状态标记为 `authorized`、`dispatched`、`acknowledged` 或 `unknown`，而不是声称撤销已经抹去了结果。

用受控测试工具保留的旧代次测试完整路径：

1. 通过网关执行一次无害读取并记录代次。
2. 撤销租约，或在签发方轮换并停用静态凭证。
3. 通过仍有热缓存的网关尝试同一操作。
4. 从测试工具直接尝试使用已退役凭证。
5. 确认两次尝试均失败，并将它们与生命周期和执行记录关联。

如果第三步成功，说明网关忽略了撤销。如果第四步成功，说明生命周期工作流没有在签发方撤销凭证。如果两步都失败，但记录无法指出同一代次，事件响应人员仍然无法证明发生了什么。

## 归因需要签发方身份和调用方身份

生命周期日志回答谁创建、续租、轮换或撤销了凭证。网关日志回答哪个本地进程请求了什么操作、谁批准了它、哪个目标收到操作，以及返回了什么结果。任何一种日志都不能替代另一种。

当每个租约都生成不同的远程身份时，动态凭证可以改善签发方的归因。HashiCorp 的数据库密钥引擎文档指出，唯一的生成用户名可让运维人员把数据库访问追踪到特定服务实例。这很有用，但数据库用户名仍然标识网关租用的身份，不一定标识要求网关运行查询的代理进程。

因此，网关需要稳定的会话身份和可信的进程身份。仅有 PID 很弱，因为操作系统会复用 PID，进程也可以启动子进程。应记录平台能够提供的可执行文件身份、存在时的签名权威、父进程、会话起止时间，以及不可猜测的会话 ID。把每条操作记录绑定到该会话。

连接字段就是凭证代次。把 Vault 租约 ID 或静态轮换事件 ID 同时写入生命周期记录和网关操作记录。如果 API 返回远程请求 ID，也要记录。在安全事件中，调查人员应能沿着这条链路检查：

```text
agent session -> gateway action -> credential generation -> issuer event -> remote request
```

不要把密钥值、Authorization 请求头、私钥或解析后的环境块写入这些日志。事后脱敏并不可靠，因为异常、调试转储和追踪导出器可能先复制数据。应从安全字段白名单构建结构化记录。

如果所有操作共用一个寿命很长的服务账户，并且没有保留网关记录，归因同样会失败。轮换会缩短该账户的寿命，却不能识别调用方。反过来，如果网关遗漏租约或版本，再完善的进程日志也无法证明哪个凭证代次到达了目标。两个维度都要保留。

## 环境变量注入会跨过边界

把密钥注入代理环境的工作流，会让代理持有该密钥，因此桌面网关不再控制每次使用。对 1Password CLI 模式来说，这一区别尤其重要，因为取回方便很容易被误认为执行控制。

1Password 文档说明，`op run` 会启动子进程，并把密钥作为环境变量提供给该进程。这样做可以避免把明文放在提交到版本库的 `.env` 文件中，但子进程可以读取变量、打印变量、把它传给另一个进程，或用于未经批准的请求。`op inject` 会把引用解析到配置流中，`op read` 则向调用方返回解析后的值。这些路径都不等同于把密钥留在代理之外的网关。

对于需要广泛使用原生 SDK 行为、由人操作的脚本，环境变量注入可能可以接受。对于网络和 SSH 操作都需要逐次控制的自主代理，它会破坏预期边界。应把受限的取回身份交给网关，并向代理公开按操作设计的工具。

引导凭证仍然需要审视。1Password 建议使用服务账户来落实最小权限，并允许把访问限制在特定保险库。这样可以缩小网关能取回的范围，却不能限制网关取回之后能做什么。因此，网关的操作表面、批准机制和出站目标验证仍不可少。

不要采用一种常见的变通方法：在包装器中解析密钥，把凭证作为参数传给网关，然后承诺网关会脱敏。代理或包装器已经持有该值，shell 历史和进程检查可能会暴露它，而且网关无法证明它没有被再次使用。应跨边界传递引用，并在执行操作的组件内部解析。

## 批准与轮换相互独立

刚轮换的凭证仍能授权不当操作，经过认真批准的操作也可能使用过期凭证。轮换和批准降低的是不同风险，任何一方都不能悄悄代替另一方。

生命周期权威回答：“这一凭证代次有效吗？”网关回答：“这个调用方现在可以产生这项结果吗？”远程服务回答：“这个已经验证的身份有权限吗？”三个答案都要可见。除非网关已做检查，绿色批准卡不应暗示凭证是最新的。当前租约也不应绕过破坏性调用所需的人工批准。

复用批准需要明确范围。如果网关批准一个代理会话，应把批准绑定到进程会话，并在进程退出或用户撤销时终止。如果某个凭证被标记为每次调用都要批准，那么新代次出现后仍应保留这项要求。新值不能把敏感凭证重置为更弱的默认设置。

批准文本应描述操作、目标和调用方，而不是密钥。“允许已签名进程 X 对生产环境运行 `POST /deployments`”给人提供了可判断的信息。“允许使用 API 密钥 prod-3”则迫使用户从清单名称猜测意图，并训练他们批准含糊的提示。

Sallyport 在 macOS 上为 HTTP API 和 SSH 操作实现了执行这一侧：代理通过它的 MCP shim 连接，凭证留在它的进程内加密保险库中，由应用执行操作。它的固定控制把锁定的保险库、进程会话批准和可选的逐次批准分开；当另一个系统负责创建或轮换凭证时，这种模型仍然需要外部生命周期负责人。

## 双重轮换会产生两个表面权威

不要让生命周期系统和网关独立轮换同一个凭证。团队有时把这称为纵深防御，但两个写入方会造成代次含糊、回滚不可靠和撤销竞态。

假设某个静态提供方允许两个 API 密钥同时有效。1Password 工作流创建密钥 B、测试它并更新权威项目，同时让密钥 A 在短暂切换期内继续有效。与此同时，网关的本地调度器发现自己的密钥 A 副本达到设定时长，于是创建密钥 C。有些进程解析到 B，热缓存仍然保存 A，网关则开始使用 C。此时停用 A 几乎证明不了什么，因为没人知道应该保留 B 还是 C。

必须由一个协调器负责轮换状态机。对于 Vault 动态密钥，Vault 已经通过角色和租约机制充当协调器，网关只消费租约，不轮换远程账户。对于通过 1Password 管理的静态记录，轮换任务可以协调提供方和存储项目，网关只观察代次变化并让缓存失效。网关的本地加密可以保护已存副本，但重新加密存储并不等于轮换签发方凭证。

可行的静态轮换应有明确状态，而不是单一的 `rotated` 标记：

```text
prepared -> activated -> distributed -> old_disabled -> verified
                |             |
                +-> rollback <-+
```

`prepared` 表示签发方已经创建候选代次。`activated` 表示用它进行的无害身份验证请求已成功。`distributed` 表示权威引用已经解析到候选项，网关也确认了新代次。`old_disabled` 表示签发方拒绝前一代。`verified` 表示热缓存网关和直接测试工具都无法再使用旧值。只有在前一代仍被有意保持有效时，才应允许回滚。

网关接收的状态变化应只包含引用和代次 ID，绝不能同时包含两个密钥值。失效事件可以指定 `credential_ref`、`old_generation`、`new_generation` 和 `effective_at`。网关收到后丢弃旧缓存项，取消绑定到它的排队操作，并在下一项已批准操作开始时重新解析。如果事件始终没有到达，网关的缓存上限和代次检查仍必须最终收敛到权威值。

回滚也需要同样严格。在提供方已经停用密钥 A 后，把 1Password 项目重新指向它并不能恢复工作。用旧显示名称重新签发一个新值，也不会恢复旧代次。协调器只能按照提供方支持的方式创建或重新激活凭证，分配新的代次 ID，然后再次完成正常的分发和验证阶段。

责任记录应足够简短，让人员在安全事件中可以检查。对每个凭证引用，指定一个轮换协调器、一个签发方、一项网关缓存策略、一条撤销命令，以及一个有权开始紧急退役的人或服务。如果两行都声称自己能创建下一代，就应停止。这不是冗余，而是一场涉及密钥材料的竞态。

## 测试接缝，而不只是各个组件

Vault 测试通过、网关测试通过，并不能证明二者的交接正常。要在边界上测试旧缓存、重叠代次、延迟批准、撤销失败、进程重启和缺失的连接字段。

使用一次性目标身份，在进入生产环境前运行这份验收矩阵：

| 条件 | 网关的预期决定 | 所需证据 |
| --- | --- | --- |
| 当前代次，TTL 足够 | 执行 | 会话、操作、代次、远程请求 |
| 当前代次，TTL 太短 | 重新解析或拒绝 | 返回的 TTL 和本地计时决定 |
| 记录已轮换，旧缓存仍热 | 拒绝旧代次 | 缓存失效和签发方拒绝 |
| Vault 租约已撤销 | 拒绝，不重试旧租约 | 撤销记录和目标身份验证失败 |
| 等待期间批准过期 | 拒绝或请求新批准 | 批准截止时间，且没有派发事件 |
| 批准后网关重启 | 要求执行配置好的会话决定 | 新会话身份和旧会话结束记录 |
| 签发方撤销失败 | 标记事件并保留证据 | 提供方错误和未解决的代次 |
| 目标接受已退役令牌 | 判定生命周期测试失败 | 直接探测结果和回滚决定 |

应针对代理实际使用的同一路径运行矩阵。按指令返回 `401` 的模拟无法揭示真实数据库插件是否删除了用户、提供方是否允许密钥重叠，也无法说明凭证撤销后 SSH 连接是否仍然存活。

设定明确的通过标准。任何操作都不能在 `not_after` 之后开始。已撤销或退役的代次必须通过热缓存测试失败。每条执行记录都要能连接到唯一的生命周期代次，并且不含密钥材料。签发方撤销失败时，不能声称退役成功。批准记录要识别进程会话，并按配置的控制规则过期。

只有故障能局限在各自范围内，这条边界才有意义。Vault 或 1Password 轮换工作流可以替换凭证，而不让代理知道密钥值。网关可以改变批准行为，而不用变成签发方。事件响应人员可以撤销一个代次、停止一个会话，并查看哪些远程结果可能已经发生。如果接缝测试不能独立证明这三项操作，就先修正契约，再添加另一个密钥后端。
