# 审批审计轨迹能证明谁批准了代理操作吗？

一条只写着“已批准”的审批记录，无法回答代理接触生产环境后最重要的问题：谁允许了哪项操作，依据什么权限，以及接下来发生了什么？它只记录了用户界面中一个令人安心的瞬间，剩下的内容还得由调查人员自行推断。

审批审计轨迹必须保留从代理进程到人工决定、从这个决定到凭据使用，再从凭据使用到已完成的 HTTP 请求或 SSH 命令的完整链条。如果这些内容只是彼此独立、没有持久关联的记录，审查人员或许能讲出一个合理的故事，却无法证明这个故事是真的。

当操作已经成功，却没人记得批准过它；当代理执行任务到一半重启；或者有人问点击确认和 Touch ID 确认是否具有相同含义时，这种区别就会变得格外棘手。它们并不相同。把两者视为同一种审批，日志在第一次严肃审查前看起来或许完整，之后就会暴露问题。

## 审批事件不能只回答“是”

一条有用的审批事件应说明用户批准了什么、系统为何发出请求、决定是如何作出的，以及授权到哪里为止。可见的按钮点击只是这条事件中的一个字段。

在决定发生时，记录以下事实：

- 唯一的审批标识符，以及带时区偏移的事件时间戳。
- 审批方式，例如 `click` 或 `touch_id`。
- 本地账户或其他已知审批人身份，以及用来建立这一归属的证据。
- 授权范围，是一个会话，还是一次调用中的一次凭据使用。
- 触发提示的请求，并用稳定的请求或调用标识符标记。

不要仅仅因为机器上有一个名为 Alex 的账户，就写入 `user=alex`。这可能是现有条件下最好的归属信息，仍然值得记录，但应准确称为本地账户上下文。如果生物识别提示通过了，应记录“该设备上登记的生物特征授权了这次事件”。这样的表述比“某个叫 Alex 的人批准了它”更严谨，因为它没有假装审计日志知道超出实际范围的事实。

NIST SP 800-171 Rev. 3 为审计内容提供了一个合理起点：时间戳、源地址和目标地址、用户或进程标识符、事件描述、适用的访问控制以及结果。它还指出，详细记录可以包含特权命令，以及共享账户背后的个人身份。这是代理操作应达到的基础线，并不是完整设计。代理审批流程还需要把决定、凭据和调用之间的关系作为一等数据保存。

一个常见错误，是把审批记录成最终操作的属性，例如 `approved=true`。这样会把一个事件压扁成一个标签。请求和决定之间的时间、决定来源、同意范围以及之后的撤销都会丢失。你也无法区分用户审批、默认允许、缓存授权和自动化规则。

即使答案是否定的，决定也应拥有自己的记录。被拒绝的提示可以解释代理为何部署失败。过期的提示可以解释代理为何重试。保险库锁定可以解释系统为何根本没有发起网络调用。这些是性质不同的事件，之后的审查人员不应从缺少成功记录这一点反向猜测它们的区别。

## 五种身份让时间线保持真实

完整的时间线需要五种彼此分开的身份。把它们合并起来，虽然能节省表格中的几列，却会破坏调查中的含义。

第一种是**代理进程**。记录进程或运行标识符、启动它的可执行文件或代码签名方、开始时间和结束时间。用户应该能回答：“哪个正在运行的程序发出了这个请求？”项目名称或聊天记录不够用。两个相同的编程代理可能同时运行，其中一个正常，另一个却指向不同的代码仓库。

第二种是**会话**。会话是一个代理进程与网关之间有边界的关系。它必须拥有自己的标识符，因为一个进程可能发起多次调用，而且会话授权通常适用于多次调用。进程退出时，会话也应结束。如果之后启动了新进程，即使它使用相同的可执行文件、本地账户和任务描述，也应创建新会话。

第三种是**审批人上下文**。它包括设备账户、经过身份验证的应用用户和审批方式。不要让审批人字段承载它无法支持的事实。`local_account=maya`、`method=touch_id` 和 `device_id=...` 都很清楚，`human=maya` 则提出了更强的身份断言。在某些环境中这一断言合理，在另一些环境中，共享工作站或已解锁的桌面就会立即推翻它。

第四种是**凭据引用**。它标识网关使用的权限，而不是秘密本身。稳定的不透明凭据 ID、便于人理解的标签、通道和凭据类型，通常足以支持活动审查。承载令牌不是审计字段。SSH 私钥指纹本身也可能成为敏感上下文，因此在把它传播到普通日志之前，应先确定调查人员是否确实需要它。

第五种是**已执行的操作**。对于 HTTP，它包括解析后的目标身份、请求方法、规范化路径、选定的非敏感请求信息、响应状态和耗时。对于 SSH，它包括主机身份、远程账户、命令或已批准命令的摘要、退出状态和耗时。事件必须说明实际运行了什么，而不仅仅是请求了什么。

这些身份构成的是一张图，而不是一行扁平记录：

```text
agent_process
  -> session
    -> approval_decision
      -> credential_use
        -> executed_call
```

当用户需要快速查看时，扁平的活动界面可以把这张图显示成一行。但底层关联仍应保留。界面是给人浏览一天的工作用的，标识符则是给六周后必须解释某一次调用的人用的。

## 点击和 Touch ID 是不同的证据

点击记录的是用户在当前界面中与审批控件的交互。Touch ID 记录的是操作系统完成了一次成功的生物识别授权，同时也记录了触发它的交互。两者都可以授权操作，但不应共享一个含糊的值，例如 `approved_manually`。

使用明确的方式字段，并限定可用值。例如：

```json
{
  "approval_id": "apr_01J8K4VY5Q",
  "occurred_at": "2026-07-22T14:18:06.184Z",
  "decision": "approved",
  "method": "touch_id",
  "approver": {
    "local_account": "maya",
    "identity_assurance": "device_account_and_biometric"
  },
  "scope": "credential_use",
  "session_id": "ses_01J8K4TE0M",
  "requested_call_id": "call_01J8K4VPM2"
}
```

字段名称本身不是关键，分离才是。`method` 告诉你审批是如何完成的。`identity_assurance` 告诉你系统可以负责任地对这个人作出什么断言。`scope` 告诉你决定授权了什么。`requested_call_id` 将审批关联到一个在人看到提示之前就已经存在的请求。

对于低摩擦确认，点击可以是合适的选择，尤其是用户已经在观察代理运行时。Touch ID 会为敏感操作增加更强的本地确认步骤，但它不会自动提供企业身份、决定原因，也不会代表用户认可之后的每一次调用。如果团队需要通过外部身份提供商确认具体员工，就需要一个能记录该身份提供商断言的流程。不要把企业身份保证悄悄借自本地生物识别事件。

反过来的错误同样严重：把 Touch ID 当成装饰。如果某项操作要求生物识别审批，而日志只剩下 `approved=true`，记录就无法证明更严格的控制确实运行过。审查人员会失去一项重要证据，也无法区分用户有意确认和误点了范围很大的会话提示。

谨慎记录失败的生物识别尝试。审计轨迹通常需要知道请求的操作没有获得批准，但很少需要记录操作系统级别的每一次身份验证失败。一个有用的事件可以是 `decision=denied_or_cancelled`、`method=touch_id`，并在平台提供这一信息时记录 `user_cancelled` 等原因。不要把操作网关变成生物识别遥测收集器。

## 会话同意和单次调用同意的范围不同

会话授权允许一个有边界的代理进程在用户审查进程身份后继续运行。单次调用审批则针对一次凭据使用和一项操作授予同意。如果不记录范围，却把两者都叫作“审批”，之后的时间线就会产生误导。

设想一个在 09:00 启动的代理进程。网关显示一张授权卡，说明该进程的代码签名方。开发者点击批准。09:20，代理使用一个不要求每次调用确认的凭据发起 HTTP 请求。由于会话仍处于授权状态，这次调用可能被允许。正确的时间线应显示两个独立事实：

1. 09:00，开发者批准了会话 `ses_...`，授权期限为该进程的生命周期。
2. 09:20，该会话使用凭据 `cred_...` 发起了调用 `call_...`。

不应虚构一条 09:20 的用户审批。开发者没有看到并批准这一次具体调用，早先的授权已经覆盖了它。

现在改变一个设置：该凭据要求每次使用都审批。09:20，网关再次发出请求，开发者使用 Touch ID 批准。新的事件必须指向 `call_...`，标明 `scope=credential_use`，并包含 `method=touch_id`。会话审批仍然重要，因为它解释了代理为何能访问凭据请求，但它不能替代第二个决定。

当代理在会话后段发起一项令人意外的调用时，这个区别尤其重要。如果调用旁边只写着“已批准”，审查人员需要知道它代表的是半小时前有人批准了这个可执行文件，还是三秒前有人批准了这次具体的凭据使用。两种情况对提示设计、凭据设置和事件响应的影响完全不同。

不要通过让每次调用都需要审批来解决含糊问题。这个建议听起来很安全，也会产生大量令人安心的记录，但它同样会训练用户不阅读内容就批准重复提示，之后也无法分辨异常调用和日常调用。应让使用时需要最新人工确认的凭据启用单次调用审批，默认保留会话授权，把代理进程绑定到明确、可追责的审批边界。

撤销也需要范围。如果操作员撤销一个会话，应针对该会话写入撤销事件，并记录生效时间。不要覆盖旧的审批。如果用户停用或删除凭据，应单独记录这一变化。审计时间线应解释后续调用为何被拒绝，而不是改写历史，让之前的授权凭空消失。

## 凭据记录必须标识权限，但不能暴露权限本身

凭据使用记录是许多团队做出危险取舍的地方：为了让调查更方便，把秘密材料加入日志。这是一笔糟糕的交易。日志会被复制、索引、导出，保存时间也往往比生成它的进程更长。活动日志中的秘密会让每个能读取日志的人都成为凭据持有者。

为每个存储的凭据分配一个不透明且不可变的标识符，例如 `cred_01J8K...`。再配上便于人理解用途的标签，例如 `payments-readonly` 或 `staging-deploy`。记录通道和注入方式，例如 `http_bearer`、`http_custom_header` 或 `ssh_key`。这样调查人员就有足够上下文提出正确问题，而不必把密钥复制进记录。

实际的凭据使用事件可以如下：

```json
{
  "credential_use_id": "use_01J8K4WHD7",
  "occurred_at": "2026-07-22T14:18:06.221Z",
  "credential": {
    "id": "cred_01J7ZB7F8P",
    "label": "inventory-production",
    "channel": "http",
    "injection": "bearer"
  },
  "session_id": "ses_01J8K4TE0M",
  "approval_id": "apr_01J8K4VY5Q",
  "call_id": "call_01J8K4VPM2",
  "secret_exposed_to_agent": false
}
```

当网关设计已经保证这一点时，`secret_exposed_to_agent` 字段看起来似乎多余。如果时间线可能包含多条执行路径或迁移路径，保留它仍有价值。它让安全属性和操作记录出现在同一条可检查的记录中。如果所有受支持的路径都具备相同保证，也可以把这一字段作为系统设计中的隐含属性，并在其他地方统一说明。

把凭据选择和凭据使用分开。代理可以按标签请求凭据，但只有网关开始执行出站操作时，凭据才算真正被使用。这对拒绝事件很重要。如果用户在请求离开设备前取消了 Touch ID，应写入一次尝试调用和一条被拒绝的审批事件，但不要写入成功的凭据使用事件。否则审计统计会声称生产凭据已被使用，实际却并非如此。

对于 SSH，不要把主机别名视为完整的目标身份。`prod-db` 便于阅读，但别名可能发生变化。应记录配置中的目标，以及连接流程验证过的主机身份证据。如果代理请求的是 `prod-db`，但解析后的目标不同，这个差异必须进入执行记录。错误部署发生后，这类细节往往非常关键。

## 已执行的调用才是操作发生的证据

审批证明了同意，凭据选择证明了预期使用的权限。只有执行记录能告诉你网关是否尝试执行了面向外部世界的操作，以及返回了什么结果。

对于 HTTP 调用，以规范化形式记录操作。保留请求方法、目标源或服务身份、规范化路径、必要时选定的查询字段名称、响应状态、开始和结束时间戳，以及结果引用。应有意识地决定哪些请求和响应字段可以安全保留。授权请求头、Cookie、类似令牌的值、完整请求正文和原始响应正文，都不应进入通用活动时间线。

请求摘要有助于证明已批准的请求载荷与实际执行的载荷一致，但前提是必须精确定义摘要的输入。对 JSON 正文进行哈希时，如果不先规范化字段顺序，就会产生错误不匹配。即使正文中只有一个短小且可预测的值，哈希也可能帮助攻击者确认猜测。应在载荷已由其他机制保护时使用摘要进行完整性关联，不要把它当成处理内容的通用替代方案。

对于 SSH，记录远程账户、目标身份、命令表示、退出状态以及开始和结束时间。完整命令行可能在环境变量、临时 URL 或参数中包含秘密。一种合理的折中方案是为日常审查保存安全的命令呈现，为调查保存受保护的完整表示或摘要。不要声称摘要是可读证据，它只能告诉你两个值是否一致，不能告诉审查人员命令做了什么。

RFC 5424 将时间戳和消息身份与结构化数据分开，因为解析器需要可靠字段，而不是必须从自然语言中猜测的信息。它的时间戳格式还携带时区偏移，并允许小数秒。你不必输出 syslog，但其中的设计经验仍然适用：让事件类型和关联字段保持结构化，把说明留给人类可读文本。

使用不同的事件类型。`call.requested`、`call.dispatched`、`call.completed` 和 `call.failed_before_dispatch` 比一个含义随状态变化的 `call` 事件更清楚。额外的记录可以回答网络超时是否发生在凭据注入之后、本地验证是否先拦截了请求，以及远程服务是否返回了响应。

仅凭时间无法确定不同机器之间的顺序。使用带偏移的 UTC 时间戳，并在每份本地审计日志中保留单调递增的序列号。如果远程 API 返回自己的请求 ID，将它作为远程关联值保存。这样调查人员可以把本地时间线与供应商记录进行比较，而不必假设两边时钟完全一致。

## 普通的成功日志也可能隐藏断裂的时间线

设想一个部署代理在 10:02 获得会话审批。它读取代码仓库、准备发布，然后在 10:17 调用生产部署端点。端点接受了请求。10:18，开发者发现选错了环境。

一份薄弱的日志可能是这样：

```text
10:02 approved agent
10:17 deployment API call succeeded
```

这份日志几乎没有回答任何问题。10:17 的调用是否受到 10:02 审批的覆盖？凭据是否要求第二次提示？是哪一个进程发起了调用？代理使用的是预期的部署凭据，还是权限更广的令牌？请求究竟发往生产环境，还是因为重定向或配置错误才到达了那里？用户是点击批准、使用 Touch ID，还是根本没有看到针对这项操作的提示？

有用的时间线应当这样记录：

```text
10:02:11  session.opened       ses_71  process=proc_44 signer=known_authority
10:02:14  approval.approved    apr_02  method=click scope=session session=ses_71 account=maya
10:17:03  call.requested       call_88 POST deploy.example/release target=production session=ses_71
10:17:04  credential.selected  use_53  credential=cred_prod_deploy call=call_88
10:17:04  call.dispatched      call_88 destination=deploy.example
10:17:06  call.completed       call_88 status=202 remote_request=req_914
```

这份记录或许能证明代理拥有有效的会话授权，但没有获得针对该调用的单独审批。这并不能证明部署符合用户意图，它只能说明控制机制是如何运行的。团队可以据此决定生产凭据是否应要求单次调用审批、提示是否应更清楚地显示目标环境，或者代理是否根本不应访问该凭据。

现在加入单次调用确认。正确的新增记录不是另一行笼统的 `approved`，而应写明它批准的调用和范围：

```text
10:17:04  approval.approved    apr_03  method=touch_id scope=credential_use
          session=ses_71 call=call_88 credential=cred_prod_deploy account=maya
```

如果代理在超时后重试，应为重试分配新的调用 ID。它可以复用已有的会话授权，但如果凭据策略要求单次调用审批，重试就应产生新的审批要求。把重试记录成原始调用，会错误地让人以为一次确认覆盖了两次外部操作。

## 审计完整性需要单独声明

审计日志可以足以解释一段流程，却仍然很容易被修改。它可以在设计上是只追加的，但拥有本地访问权限的管理员或恶意软件仍可能删除不利记录。应把内容完整性和记录完整性视为两种不同属性。

哈希链式日志通过加密摘要把每条记录与前一条记录关联起来。修改旧记录后，后续链就无法通过验证。这很有用，因为导出的活动界面可以与底层日志进行比对，而不是只能被动相信。它不能证明原始系统记录了每一个事件，也不能证明被攻破的写入方没有伪造记录，更不能证明一次有效审批就是正确决定。这些是不同的断言，需要不同的控制措施。

应在证据离开系统的边界进行验证。调查人员应能取得加密记录流，离线运行完整性检查，并在不暴露凭据的情况下确认序列是否完整。验证输出应标识检查范围、链状态，以及验证失败时第一个失败的序列号。

Sallyport 从同一份不可写、加密、哈希链式审计日志生成 Sessions 和 Activity 日志，`sp audit verify` 可以在没有保险库密钥的情况下，对密文离线验证链条。这种安排很重要，因为会话决定和单次操作仍然是同一份证据的不同视图，而不是两套可以分别修改的故事。

不要因为能够验证完整性，就无限期保留过量数据。保留期限、访问控制和脱敏仍然重要。一份充满秘密、保存完好的日志，只是在等待某个方便的搜索查询触发事故。应明确谁可以查看原始记录、谁可以导出记录、记录保留多久，以及哪些字段可以安全地出现在日常视图中。

## 围绕关联构建时间线，然后测试最棘手的情况

审查数据结构时，应先问一个直接的问题：调查人员能否从任意一项已执行操作开始，不靠猜测就回溯到审批？如果不能，在美化活动界面之前先补上缺失的标识符。

针对实现运行一组小型测试矩阵。不需要大规模模拟，只需要能暴露范围和顺序错误的案例：

- 启动一个新的代理进程，用点击批准其会话，然后发起一次低风险调用。
- 使用要求单次调用审批的凭据，通过 Touch ID 授权，并确认调用指向这次审批。
- 取消生物识别提示，确认不会出现成功的凭据使用记录。
- 结束代理进程，再次启动它，确认新进程不能继承旧的会话授权。
- 在调用发出后强制远程操作失败，检查时间线是否区分了发送和完成。

从两个方向检查结果。先从审批开始，列出所有依赖它的操作。再从已执行的调用开始，追踪进程、会话、决定和凭据。第一种视角能找出范围过大或有效时间过长的授权，第二种视角能找出证据断裂或含义不清的调用。

界面用语也应和数据模型一样严格。“通过点击批准的会话”很清楚。“使用 Touch ID 批准了这次调用的生产部署凭据”也很清楚。“已批准”只是一个装饰性状态词，让读者自行补出最重要的细节。

第一次有人问“谁批准了这项代理操作”时，不要给他一张带绿色徽章的截图。给他一条时间线，显示进程、决定方式、范围、凭据权限、具体调用和结果。少了任何一项，在演示时或许很方便，但在真正重要的操作面前经不起审查。
