# 代理审计日志：会话记录与调用记录

代理审计日志在把两个不同问题塞进同一条记录时就会失效。发生事件时，你需要同时确认谁拥有执行权限，以及到底有什么操作触达了外部世界。运行记录不能代替调用记录，请求列表也无法解释为什么那个进程拥有权限。

团队常常只保留其中一种视图，因为在演示时，一种视图看起来已经够用了。可一旦代理创建工单、更新生产设置或运行 SSH 命令，调查就会沦为猜测时间戳。这完全可以避免。用会话日志记录权限和生命周期，用活动日志记录每一次尝试执行的外部操作。再用一个由系统分配、不能由人或代理随意编造的标识符把两者关联起来。

## 运行记录回答谁拥有权限

会话记录应回答：某个特定代理进程是否有权执行操作，权限持续了多久，以及背后依据的是哪项人工决定。当有人问「哪个代理实例做了这件事，为什么我们允许它这样做？」时，你就要查这条记录。

有用的会话记录应从进程第一次请求权限时开始，而不是从用户打开编辑器时开始，也不是从项目目录出现时开始。代理名称的证据价值很弱。两个进程可以使用同一个名称，恶意二进制文件也可以借用一个熟悉的名称。应记录主机能够提供的进程身份、操作系统支持时的代码签名权限、必要时的父进程、生成的会话 ID、开始和结束时间，以及授权结果。

会话记录还需要生命周期事件。批准是一项事件，撤销是一项事件，进程退出也是一项事件。如果锁定凭据库会阻止活动，那么锁定也值得记录并关联。没有这些边界，调查人员就无法判断某次调用发生在批准期间，还是发生在所谓的关闭之后。

不要把会话记录变成每个模型思路、每一行终端命令和每次文件编辑的日记。这样会堆积大量敏感材料，却仍然漏掉真正重要的边界：代理进程获得权限，可以请求网关执行外部操作。应清楚地记录能够确立这条边界的证据。

下面是一条简化的会话记录：

```json
{
  "type": "session.authorized",
  "session_id": "ses_7f4c2",
  "observed_at": "2025-04-18T14:03:11Z",
  "process": {
    "pid": 8124,
    "signing_authority": "Example Development Team",
    "parent_pid": 8090
  },
  "decision": "approved",
  "approved_by": "local_operator"
}
```

会话记录不需要员工姓名也能发挥作用。在共享工作站上，`local_operator` 可能就是诚实的归因粒度。假装掌握更多信息，只会制造自信的虚构。如果你的环境可以把批准绑定到经过身份验证的人员，就记录这种绑定，以及建立绑定的方式。

会话记录绝不能声称整个运行过程都是安全的。批准授予的是权限，并不是预先批准进程可能产生的每一种影响。这个区别听起来很细，但当编程代理让会话持续数小时并在一次批准下发出数百次调用时，它就非常重要。

## 调用记录回答什么触达了外部世界

调用记录应描述一次操作尝试及其结果。当有人问「代理是否发出了这个请求，目标是什么，结果如何？」时，你需要的就是这条证据。

记录尝试，而不只是记录成功。被拒绝的请求可能表明代理正在探测凭据。失败的请求可能说明主机名称拼写错误、机密已过期，或远程端拒绝了不安全的命令。取消的请求可以解释为什么一次会话看起来在部署中途停止。只记录成功的日志讲述的是一个好听却不完整的故事。

对于 HTTP 操作，应保留目标身份、HTTP 方法、路径或受控的路径表示、凭据引用而不是凭据值、请求时间、完成时间、状态结果、会话 ID 和调用 ID。对于 SSH，应保留预期的主机身份、命令或安全的命令表示、连接结果、可用时的远程退出状态、会话 ID 和调用 ID。

应谨慎决定保留哪些请求数据。习惯性记录完整请求头和正文，迟早会引发事件。授权请求头、Cookie、签名 URL、访问令牌、客户隐私数据和密码经常藏在其中。好的记录应保留操作含义，同时删去机密材料。例如，`POST /v1/users/123/disable` 可能已经足够还原一次管理变更，而复制完整 JSON 正文可能会暴露远超调查所需的信息。

记录目标身份，不要只记录原始 URL 字符串。`https://api.example.test` 和 `https://api.example.test:443` 可能代表同一个目标，而仿冒主机名可能只差一个字符。对于 SSH，在系统能够获取时，记录用于验证的主机身份。仅凭主机名，无法确定连接是否到达了预期机器。

下面这组配对记录展示了两者的区别：

```json
{
  "type": "call.completed",
  "call_id": "call_b91d",
  "session_id": "ses_7f4c2",
  "observed_at": "2025-04-18T14:09:27Z",
  "channel": "http",
  "operation": "POST",
  "target": "api.example.test/v1/deployments/42/cancel",
  "credential_ref": "deployment-service",
  "authorization": "session_approved",
  "outcome": "completed",
  "response_status": 202
}
```

会话 ID 说明这次调用使用了谁的权限。目标和结果说明发生了什么。如果把它们合并成 `agent performed task` 这样的模糊事件，两个问题都没有得到很好的回答。

## 批准和执行是两件不同的事实

团队常常把批准事件误认为请求实际运行的证据。两者是不同的事实，审计轨迹必须同时保留。

操作员可能批准了一个新的代理进程，然后离开。进程可能因为任务在本地完成而没有发出任何调用，也可能发出一次失败的 API 请求，或者发出五十次成功请求。三种情况下，批准记录都不变。只有单独的调用记录才能显示实际影响。

反过来的混淆也会发生。有人在网络日志中看到一个出站请求，就认定它是由获批代理发出的。网络日志可以证明连接或请求片段，具体取决于采集位置。它通常无法显示批准决定、实际进程身份，或网关是否代表代理注入了凭据。不要强迫网络日志回答它从未被设计来回答的问题。

NIST Special Publication 800-92《Guide to Computer Security Log Management》区分了事件源、日志基础设施和分析流程。对代理系统来说，它的实际启示很直接：在掌握事实的那一层采集事件。授权层知道会话是否获得了权限，操作网关知道它使用受保护凭据尝试了哪项操作，防火墙知道它观测到的流量。每种记录的证据范围都不同。

在每次调用上记录准确的授权状态很有帮助。`session_approved` 表示会话拥有持续有效的批准。`per_call_approved` 表示操作员批准了这次使用。`denied_locked` 表示凭据库锁定时拒绝了尝试。`denied_user` 表示操作员拒绝了请求。这些不是装饰性标签，它们能告诉调查人员操作是否到达执行器，以及是否由人工干预将其阻止。

不要把每次成功调用都标成「已批准」。这个词会掩盖会话开始时授予的权限，与实际使用时授予的权限之间的区别。当事件复核人员问「有人批准删除操作吗？」时，记录应当让人一眼就能得到答案。

逐次调用审批有其适用场景，但应把它作为范围有限的控制措施。对于每次使用都影响重大，或会改变目标状态且操作员应当看到的凭据，可以启用它。如果连日常读取和无害的构建调用也要求人工点击，人们很快会习惯性批准一片提示。纸面上仍然有批准，但它已经不再代表知情同意。

## 失败时间线能揭示摘要掩盖的缺口

只要建立一条事件时间线，就能明显看出两种视图的价值。假设一个自主编程代理接到任务，要清理过期的预览环境。操作员批准了它在本次会话中运行。代理发现一个旧的部署 API 凭据，并请求网关使用它发出调用。

14:03，会话日志记录一个获批进程，并分配 `ses_7f4c2`。14:07，活动日志记录一条列出部署的 `GET` 请求。14:09，记录了上面展示的取消调用。14:10，第二次取消尝试收到 403 响应。14:12，操作员发现代理选择了错误的环境组，于是撤销会话。

现在假设你只保留了会话记录。你可以说某个进程被批准，之后又被撤销，但无法确定它取消了一个、多个还是零个环境。你也无法区分第二次尝试是被阻止还是成功了。你只能去询问部署服务，而它的日志保留时间或请求细节可能无法满足需求。

再假设你只保留调用记录。你可以看到两次取消请求，但无法确定是哪一个本地进程发起的，也无法确定操作员是否批准了该进程、批准在当时是否仍然有效，或操作员是否在撤销后及时采取了行动。

时间线应保留事件顺序，但不要假装墙上时钟绝对准确。机器会发生时钟漂移，远程服务也会报告自己的时间戳。网关观测到请求时写入网关时间戳，并在相关情况下单独保留远程结果时间戳。如果审计日志有内部排序序列，也应使用它。不要仅仅因为两个事件在时钟上共享同一秒，就宣称它们存在因果关系。

一个棘手的问题是，撤销会话后，调用是否仍可能完成。答案是可能，具体取决于撤销何时到达执行器，以及请求是否已经离开机器。记录应让这一点清晰可见。记录撤销时间，然后记录之后完成的任何调用，同时注明调用的开始时间和完成时间。简单删除会话的系统会让这种分析变得不可能。

## 关联 ID 需要严格归属

只有在网关分配并控制会话 ID 时，它才有意义。不要让代理提供会话标识符，再把它当作安全证据。

代理可以在工具调用之间携带任意文本。它可能在重启后重复使用旧 ID、输入错误的 ID，或者在接口允许时故意冒用另一个会话的 ID。网关必须根据经过身份验证的本地连接或进程关系推导关联，然后自行把会话 ID 附加到每次调用上。

调用 ID 也需要同样处理。在请求离开前、位于操作边界处生成调用 ID。如果 HTTP 请求发生重试，应记录这是一个与原调用关联的新尝试，还是同一个调用中的多次传输尝试。两种模型都可以，但混用会在中断期间破坏计数准确性。

使用小而一致的关联模型：

- 会话 ID 将一个代理进程的权限和生命周期事件归为一组。
- 调用 ID 标识一次请求的外部操作。
- 尝试 ID 在重试重要时标识一次传输尝试。
- 凭据引用标识已配置的机密，但不暴露其值。
- 目标引用标识主机、服务或命令目标。

不要要求每个标识符对人类都具有全局意义。它们的任务是可靠地关联记录。可读标签可以与它们并列，但标签会变化、冲突，也容易被随意编辑。

对于并发代理，关联机制可以避免一种常见错误。工程师看到 16:21 的破坏性请求，又找到一个 16:21 的代理终端记录，于是认定两者对应。与此同时，另一个代理进程可能也在同一账户下运行。操作记录中的会话 ID 可以消除这种猜测。如果不存在稳定的关联方式，就在事件报告中明确说明这一限制，不要用自信填补空白。

## 审计日志必须同时显示拒绝和使用

被拒绝的操作有时比已完成的操作更重要，因为它们能揭示代理在控制措施阻止之前试图做什么。应记录足够的上下文来解释决定，同时避免让拒绝日志变成新的机密泄露源。

锁定的凭据库应拒绝所有需要凭据的操作。生成的调用记录应说明操作在外部执行前就被拒绝，标明请求的会话和目标，以及原因类别。记录中不应出现伪造令牌、部分私钥或复制来的授权请求头。

逐次调用拒绝也需要同样谨慎。如果操作员拒绝了一条 SSH 命令，应记录命令表示、目标、会话、决定时间和决定结果。此时缺少远程退出状态就有了明确含义：执行器从未启动该命令。这与命令已经启动但返回非零状态不同。

Sallyport 使用三项固定控制：绝对凭据库闸门、默认的每会话授权，以及可选的单凭据逐次调用审批。这种边界清晰的模型让审计解释更容易，因为每条记录都可以指出是哪项决定阻止或允许了操作。

不要为每个代理操作都采用一个庞大的策略语言。这种做法很流行，因为它承诺完全自动化。实际上，策略引擎会增加第二套程序，团队必须在事件期间对它进行审查、测试、更新和解释。如果网络或服务治理需要策略，就在相关层使用它们。不要假装一组无法读懂的规则可以替代清晰的会话和调用证据。

记录应区分以下结果：

- 代理从未拥有经过授权的会话。
- 会话拥有权限，但凭据库处于锁定状态。
- 网关请求逐次调用决定，操作员拒绝了它。
- 网关执行了操作，但远程目标拒绝或执行失败。
- 网关执行了操作，并获得成功结果。

这些情况需要不同的后续处理。会话请求被拒绝可能指向不受信任的进程，远程 403 可能指向凭据权限范围问题，而成功但不符合预期的调用，可能需要复核任务指令、会话批准决定，以及目标凭据允许的用途。

## 篡改证据保护事件之后的记录

普通应用日志很容易在有人控制主机后被编辑、截断或替换。这不代表它们毫无用处，但会限制它们能够证明的内容。采用哈希链的审计日志，可以让验证者将链与收到的记录进行比对，从而发现后续修改。

这个区别很重要。哈希链可以表明保留下来的链中某条记录被修改，或中间有记录消失。它不能证明系统记录了所有本应存在的事件，也无法挽救在创建事件之前就已被入侵的主机。它还不能告诉你操作员是否理解了一张审批卡片。超出这些范围的说法，都是安全表演。

RFC 5848《Signed Syslog Messages》讨论了相关问题：日志消息在系统之间传递时，可能失去完整性和来源保证。即使使用的是本地加密日志而不是 syslog，它的启示仍然适用。在事件发生点附近保护日志，保留顺序证据，并进行验证，而不是只相信一个漂亮的界面。

Sallyport 将 Sessions 日志和 Activity 日志都写入同一个不可写入、加密并采用哈希链的审计日志。其 `sp audit verify` 命令可以在离线状态下对密文验证链，无需凭据库密钥。当复核人员需要验证记录完整性，却不应获得执行操作所用的机密时，这项功能很有用。

在筛选、导出或为事件添加注释前，先运行验证。先保留原始加密证据，再复制一份工作副本进行分析。如果验证失败，应记录失败情况和检查过的确切工件。不要悄悄继续使用清理后的导出文件，因为完整性问题本身已经成为事件的一部分。

篡改证据也会改变日常纪律。如果团队知道后续编辑会留下痕迹，就不会再把审计日志当作部署失败后改写历史的方便位置。这本身不能阻止错误，但能保留从错误中学习所需的证据。

## 保留期限需要边界，而不是不加区分地采集

会话和调用记录应保留足够长的时间，以调查延迟发现的问题、凭据滥用和访问复核。但不要因为存储便宜，就永远保留所有正文。最危险的日志往往是那些没人分类的日志。

先从团队必须回答的事件问题开始。代理运行后多久，服务负责人可能发现不应有的变更？员工离职后，需要追踪凭据使用多久？哪些法规或合同规定了保留期限？这些答案决定保留时间，但并不意味着你应收集不需要的原始提示、完整响应或机密。

把运营可见性与取证保存分开。操作员可能需要查看活动会话和近期调用的简洁当前视图，调查人员则可能需要完整且不可变的序列，包括拒绝和详细时间信息。让每个开发者都能无限制访问后者，会把审计轨迹变成另一份敏感数据集。

对复核设置角色边界，但不要把访问控制当作向事件负责人隐藏材料的理由。服务负责人可能需要知道某次调用修改了自己的服务，但不需要承载令牌或无关客户正文。

当记录指向存放在其他位置的敏感内容时，应保存受控引用和检索流程。例如，保留一个请求 ID，让目标服务可以按照自身访问规则定位受保护的正文。这样可以让操作日志保持有用，又不会把敏感业务数据复制到每个审计系统。

删除也需要自己的记录。如果保留期限导致一批记录过期，应在删除数据前记录保留事件、范围和触发删除的权限。否则，后续验证者无法区分经过授权的过期删除和无法解释的缺失。保留期限政策应足够易懂，让事件复核人员无需咨询原系统编写者就能执行。

## 从调用向外关联，建立事件视图

在进行中的调查中，应从可疑调用开始向外追查。调用通常是最具体的证据：它包含目标、操作、时间和结果。利用其中的会话 ID 找到权限记录，然后检查该会话附近的其他调用，以及会话的撤销或退出事件。

按以下顺序进行：

1. 在编辑或导出原始审计记录前，先保留并验证它们。
2. 按目标、调用 ID、操作或事件时间窗口定位调用记录。
3. 找到关联的会话记录，确认进程身份、批准时间和生命周期状态。
4. 查看该会话在事件前后的每次调用，包括拒绝和重试。
5. 将操作时间线与目标服务自己的记录进行比较，并记录缺口，不要猜测填补。

这种方法既能发现明显错误，也能发现更隐蔽的问题。明显错误是本不该发生的调用，隐蔽错误则可能是任务结束后会话仍然保持授权，或服务超时后重试导致操作重复。

不要因为轮换所有凭据看起来果断，就先这么做。如果凭据库一直没有把凭据交给代理，而记录显示网关只向一个已知目标发出调用，那么大范围轮换可能只会制造不必要的中断。如果仍可能发生滥用，应立即撤销有效会话。然后利用调用记录决定具体需要处理的是哪个凭据、目标或访问范围。

两种视图确实会产生更多记录，但它们也能消除事件报告中代价最高的一句话：「我们无法确定获批代理是否真的做出了这项变更。」在会话边界让权限可见，在调用边界让影响可见。少了其中任何一项，你的团队都只能从零散痕迹中重建安全事件。
