# 用防篡改审计轨迹证明 AI 智能体执行过的操作

AI 智能体不需要更好的聊天记录。它们需要在操作离开机器的那一刻生成证据。

这个区别决定了你能否回答这样的事件问题：「这个智能体是否向生产环境推送过内容？由哪个进程执行？谁批准的？远程系统返回了什么？」对话摘要无法承担这个任务。只要清楚它能证明什么、不能证明什么，防篡改日志就可以做到。

我见过一些团队因为终端输出容易保存，就把它当成审计记录。这种做法本末倒置。终端输出在调试时很有用，但作为证据很薄弱，因为写入它的进程可能在产生副作用之前就失败了，可能隐藏了重要参数，也可能后来被完全替换。

这里真正可靠的答案往往很朴素。

操作网关应在执行 HTTP 调用或 SSH 命令时创建记录，把记录封存到有序日志中，并让后续修改变得可见。这样，调查人员拿到的是一个不依赖智能体记忆、提示词或是否愿意承认的证据对象。

## 对话记录和审计轨迹回答的是不同问题

对话记录说明智能体声称自己尝试做了什么，审计轨迹说明操作边界实际接受并返回了什么。把这两种记录混为一谈，会在自主操作变得危险的地方留下缺口。

假设智能体说，它在打开 SSH 会话后运行了 `terraform apply`。这段文字无法证明 SSH 连接成功，也无法证明命令到达了目标主机，更无法证明主机接受了命令。它可能只是执行前打印出的计划命令，也可能在第一行就失败了，还可能在事后被包装器修改过。

一条有用的操作记录会把来自不同层面的事实绑定在一起：

- 发起操作请求的智能体运行任务
- 接收授权的本地进程身份
- 通道和目标地址，例如 SSH 主机或 HTTP 来源
- 规范化的操作摘要和结果状态
- 日志位置以及前一条记录的哈希值

记录不必变成监控数据倾倒。事实上，把每个请求正文和完整响应正文写入永久日志通常很冒险。部署 API 可能返回访问令牌。SSH 命令可能在环境变量赋值中包含临时密钥。只捕获重建安全决策所需的事实，并严格限制敏感载荷的保留范围。

NIST SP 800-92 将日志管理视为涵盖生成、存储、分析和处置的流程，而不是应用程序倾倒文本的文件夹。这份较早的指导原则同样适用于智能体：日志必须在执行器处生成，存储必须能撑过事件响应，分析也不能依赖被调查行为方的可信度。

操作边界会让事实范围变窄。这是好事。

如果智能体从未拿到凭据，而是请求本地网关完成调用，网关就能记录它实际发送的请求。智能体仍然可能被入侵，但它无法在自己的上下文中悄悄改写网关已经完成的操作历史。

## 能检测篡改不等于不可变

能检测篡改，意味着验证者可以发现某些变化；不可变，意味着任何人都无法修改或删除数据。这是两种不同的承诺。供应商经常把它们混在一起，因为「不可变」听起来更容易购买。

哈希链让每条记录都依赖于前一条记录。一个简化的记录可能如下所示：

```text
record_1042 = {
  sequence: 1042,
  run_id: "run_7f3c",
  time: "2026-03-18T14:22:09Z",
  action: "ssh.exec",
  destination: "deploy@release-host",
  result: "exit 0",
  previous_hash: "a4c1...",
  hash: "8d72..."
}
```

实现会以确定性的方式序列化这些字段，根据序列化结果计算哈希，再把前一条记录的哈希放入下一条记录。把 `exit 0` 改成 `exit 1`，修改主机，或调换两条记录的顺序，验证都会从那里开始失败。

NIST 的安全哈希标准规定了经批准的哈希算法，它们会生成固定长度的消息摘要。摘要不是加密，也不是签名。它是一种紧凑的绑定材料，让后续记录依赖于之前完全相同的字节。

它能发现编辑，但不能发现所有形式的不诚实。

如果攻击者可以重写日志并重新计算后续所有哈希，就能生成一条仍然可以通过验证的链。如果攻击者删除最后 40 条记录，就能留下一个有效的早期前缀。如果攻击者维护两条独立的链，就能向不同审核人员展示不同历史。哈希链可以告诉你，当前呈现的序列在内部保持一致。但它本身无法证明序列完整，也无法证明它在全局上唯一。

所以，当有人把本地哈希链称为「不可否认性」时，我会提出异议。它不是。不可否认性需要身份绑定，还需要一套即使被指控方不再可信也能继续验证的机制。本地哈希链依然值得使用，但应该准确描述它：它能有效防止随意编辑和存储损坏。

## 哈希链需要一个写入者无法控制的外部锚点

如果有人保存了日志写入者无法悄悄替换的检查点，哈希链就会更难被重写。没有外部引用，控制写入者的攻击者也控制着你的故事开头。

为日志设置锚点有多种方式，各自的成本和运维适配性不同。

- 将签名检查点导出到权限范围严格受限的独立账户。
- 将定期生成的日志根发送到智能体主机无法管理的安全事件存储中。
- 将加密日志分段复制到离线或仅追加存储中。
- 让独立监控器保存观测到的日志根，并报告不一致之处。

不要以为目标位置越多，证据就越可靠。由同一管理员控制的云存储桶，即使有五份副本，也可能同时失效。权限分离比复制数量更重要。

Certificate Transparency 提供了一个很有用的思维模型。RFC 9162 使用 Merkle 树支持仅追加日志、包含证明，以及已发布树头之间的一致性证明。该 RFC 也直截了当地指出了难点：不诚实的日志可能会尝试向不同客户端展示不一致的视图。

简单的哈希链不是 Merkle 树。它提供高效的顺序验证，却不能为任意条目提供紧凑证明，也没有公共一致性协议。对于一台执行中等数量智能体操作的 Mac，这通常是合理的取舍。除非你有独立监控器、许多验证者，或需要证明某个条目属于大型共享日志，否则仅仅因为透明度论文中出现了 Merkle 这个词，就添加一项 Merkle 服务，属于不必要的复杂化。

设置锚点确实有成本。你必须管理另一个存储边界，决定检查点频率，处理故障，并保留足够的元数据，把检查点与本地日志关联起来。每次操作都创建检查点会带来大量通信和更多故障情况。每天创建一次检查点，则会留下很长的窗口，截断操作可能难以暴露。检查点间隔应与有害操作变得代价高昂的速度相匹配。

对于生产部署凭据，与其争论每天导出在技术上是否足够，我更愿意为每个完成的特权操作设置锚点。对于低风险的只读 API 调用，定期保存日志根可能就够了。

## 加密隐藏的是记录内容，不是历史

加密日志可以防止普通读者看到敏感操作详情，但加密本身无法说明记录是否发生过变化。团队经常构建了一种属性，却把它描述成两种属性，直到调查时才发现差异。

假设一条日志记录包含 HTTP 路径、授权凭据标签、请求元数据和返回的错误。加密这条记录，可以防止偷走磁盘的人知道智能体访问了哪个客户端点。但它无法阻止拥有特权的本地进程用另一段密文替换原来的密文。你需要对每条记录使用认证加密，并在密文记录之间，或在它们经过认证的表示之间建立哈希链。

更清晰的设计是，让完整性验证可以在不暴露内容的情况下进行。验证者读取加密记录序列，检查封装格式和前置记录关系，重新计算哈希链，并报告当前字节是否仍构成预期历史。它不需要解锁凭据保险库，就能判断记录 1042 是否仍然接在记录 1041 后面。

这种分离在遏制事件期间很有价值。调查人员可以复制日志、运行验证并保存结果，然后再请求任何人解锁敏感材料。负责第一轮事件调查的人，不应该为了判断证据是否被修改，就必须广泛访问部署凭据。

加密也迫使你做出保留决策。如果设备迁移后没人能解密旧记录，你的完整性证明可能仍然有效，但日志已经失去实际意义。恢复流程和保留流程应与审计格式分开。不要把解密密钥放在加密归档旁边，然后认为问题已经解决。

在 macOS 上，硬件支持的保护可以缩小本地密钥的暴露范围。Apple 的安全文档将 Secure Enclave 描述为一个隔离的硬件安全处理器，Apple 的开发者文档也指出，其中描述的 Secure Enclave 机制无法将明文密钥材料传入或传出。这有助于构建本地保险库，但不会自动认证 Mac 上的每个进程。

日志仍然必须标识哪个进程获得了批准，以及哪个操作越过了网关。硬件保护只能守住一个边界。审计证据需要守住多个边界。

## 失败通常在危险命令之前就开始了

一次看似合理的智能体事件，很少从一条明显恶意的命令开始。它通常始于一个平常的请求，却继承了过多权限。

设想一个编程智能体收到工单，要求它诊断一次失败的发布。它读取代码库，看到了部署脚本，然后向发布主机打开 SSH 通道。第一条命令没有危险：

```text
systemctl status web.service
```

输出中包含一个临时配置文件的路径。智能体随后读取该文件，找到部署凭据引用，并运行第二条命令修改环境变量。远程命令返回退出码 0。二十分钟后，一位客户报告请求失败。

控制措施可以在哪里阻止或暴露这次操作？

首先，保险库网关可以在锁定状态下阻止所有外部操作。它无法判断命令是否明智，但能防止后台智能体在用户关闭安全边界后继续使用凭据。

其次，会话授权可以在第一次操作前显示智能体进程的身份。这正是代码签名授权重要的地方。一个熟悉的编辑器进程和一个从 `/tmp` 启动的未签名辅助程序，即使它们都使用 MCP，也不应仅因此获得同等对待。

第三，对发布主机凭据设置审批要求，可以在第二次操作时中断流程。提示应展示足够的目标和操作上下文，让人看出这已经不再是诊断。一个笼统的「允许 SSH」按钮达不到这个标准。

最后，日志必须将两条命令记录为同一运行任务下两个独立的已完成操作。如果事件记录只有「智能体调查了部署故障」，就会掩盖运行从观察转向修改的那个时刻。

对于能够改变生产状态的凭据，我倾向于每次使用都要求审批。这会更慢，但也会迫使人面对诊断任务越过边界、变成实际操作的具体时刻。

不要为每个无害请求都要求点击。人们会在一长串相同提示面前不加阅读地全部批准，毕竟界面会教会他们这样做。应把阻力放在凭据使用上，因为每次调用的影响范围都可能不同。

## 进程身份必须写入记录

一条只写着「Claude Code 使用了 SSH」的审计轨迹，太模糊，无法解决争议。你需要知道哪个本地进程启动了运行任务，操作系统报告了什么签名授权，以及审批是否覆盖了这个进程实例。

这里还有一个常被混淆的定义：应用身份不等于进程身份。品牌名称可以指向一个合法产品，但被入侵的扩展、复制出来的二进制文件或 shell 包装器，也可能从不同的可执行文件上下文调用同一个协议。审批决定应绑定到实际请求操作的进程，并在该运行任务退出时失效。

日志应保存稳定的运行标识符以及足够的进程来源信息，以便之后回答这些问题：

- 第一个请求和最后一个请求是否由同一个进程发出？
- 用户是否在调用发生前批准了这次运行？
- 在后续请求到达之前，这次运行是否已被撤销？
- 是否有无关的本地进程试图复用该通道？

不要把可变的显示名称当成最重要的身份字段。名称会改变，路径也可能被替换。签名授权或同等的平台身份更有用，因为它能把审批界面、会话日志和之后的调查连接起来。

这需要付出代价。合法的开发构建、本地分支和未签名工具会带来更多审核工作。这不是审计模型的缺陷，而是允许实验性软件访问真实凭据所必须承担的成本。应考虑让这些工具使用单独的低权限凭据，而不是教会审核人员忽略陌生的身份详情。

记录还需要区分被拒绝的尝试和已完成的操作。被拒绝的 SSH 请求说明控制措施正在发挥作用。连接失败说明有人尝试过这条路径，但不能证明远程执行发生过。带有返回结果的已完成请求，证据强度更高。不要把这些结果都压缩成一个名为 `success` 的布尔值。

## 用一份日志记录运行和调用，时间线会更清晰

会话事件和操作事件应该从同一个有序事实源中生成，尽管运维人员阅读它们的目的不同。在压力下，由不同组件写入的独立日志文件很容易彼此偏离。

会话视图回答智能体生命周期相关的问题：运行何时出现、属于哪个进程、是否得到用户授权，以及是否有人撤销了它。活动视图回答单个操作相关的问题：选择了哪个凭据标签、使用了哪个目标地址、调用是否获准，以及返回了什么结果。

这两种视图不应成为彼此独立的来源。如果某个操作没有对应会话，或已撤销的运行任务看起来仍在继续发出调用，调查人员需要通过同一个序列判断这是数据不一致，还是系统行为本身有问题。

这正是一个写入盲的加密日志具有实际优势的地方。各组件可以追加它们有权报告的信息，同时日志格式又能防止它们随意浏览不相关的记录。安全收益不在于让日志变得神秘，而在于减少可以读取、编辑和重新解释历史记录的代码路径。

Sallyport 会从一份加密、哈希链式审计日志中生成 Sessions 日志和 Activity 日志，而 `sp audit verify` 可以在不解锁保险库的情况下，离线对密文执行哈希链验证。这正是本地智能体证据应该具备的形态，因为在遏制事件期间，验证路径仍然可用。

对于后果严重的环境，我仍然会导出检查点。本地验证只能告诉你手上的副本在内部是否完整。外部检查点则有助于发现有人交给你的是更旧、更短的历史。

让日志展示与日志事实保持分离。日常使用的便捷界面可以筛选、分组和编辑记录。底层验证器必须依据稳定字节和确定性顺序工作，而不能依赖当前界面碰巧展示的内容。

## 验证应该成为日常流程，而不是仪式

只有事件发生后才运行的验证命令，是一个没人测试过的功能。应把它纳入日常维护，并让失败处理变得清晰可预期。

对于 Sallyport 安装，第一项具体检查是：

```bash
sp audit verify
```

在重大升级前、抹掉开发 Mac 前，以及怀疑本地状态发生异常变化时，对复制出的日志运行验证。保存你实际验证过的那份日志副本。之后在另一份副本上重新运行命令，无法证明事件发生时原本存在什么。

单靠命令还不够。应配合一套简单的操作流程：

1. 在修复之前，保留日志副本及其检查点引用。
2. 记录受影响的运行标识符和正在调查的时间范围。
3. 将已完成的 HTTP 和 SSH 操作与远程服务提供商自己的日志进行比对。
4. 在让智能体执行清理之前，先撤销正在运行的任务。
5. 导出或传输后再次验证，确认复制出的证据没有损坏。

第三项很重要，因为本地完整性和外部事实互为补充。有效日志可以显示网关发出了 `POST /deployments`，部署服务提供商则可以显示它接受、排队还是拒绝了这项操作。SSH 退出状态可以说明 shell 命令已经完成，主机服务日志则可以显示之后实际运行的进程。

我对只能在仪表板中展示事件的审计系统没有耐心。如果不依赖仪表板就无法验证存储的日志，那么事件响应就依赖于同一套可能正在被调查的应用程序栈。

对于缺失分段、无效序列号、格式错误的密文封装，或前置记录不匹配，验证都应该明确失败。一个为了生成更漂亮的时间线而跳过坏记录的工具，只是一个态度礼貌的证据粉碎机。

## 审批记录需要和操作记录一样严谨

一次审批点击属于安全事件的一部分，而不是装饰性的界面细节。如果系统只记录某项操作被允许，就无法在之后确认用户批准的是某个特定运行、某类凭据，还是完全不同的提示。

保存当事人实际看到的决策上下文：请求进程的身份、审批范围、相关凭据标签和时间。对于会话级决定，记录会话开始的时间，以及结束或被撤销的时间。对于每次使用的决定，将审批绑定到一次具体的操作尝试，避免它悄悄授权后续的其他目标地址。

良好的审批边界包含一个直接的取舍。更宽泛的会话授权可以减少打扰，让自主循环更易用。更窄的逐次调用授权给人更多机会阻止错误方向，但如果用于日常读取操作，就会让人疲惫。没有哪种设置绝对更安全，合理范围取决于凭据的实际影响。

通常，三项控制就够了：在用户打开保险库之前保持锁定，为一个会话授权一个已识别的进程，并对指定凭据的每次使用要求明确确认。规则语言看起来可能更复杂，但每个条件都会变成发布时必须测试的另一项声明。对于桌面操作网关，我会选择一组小而固定的控制，而不是一套凌晨两点没人能解释清楚的策略引擎。

这个选择会排除一些工作流程。固定的决策阶梯无法表达所有环境例外或基于时间的策略。拥有服务器集群和复杂委派需求的团队，可能需要另一种系统。假装一个本地菜单栏应用应该变成通用授权服务器，结果就是让原本专注的安全边界变得脆弱。

## 在授予访问权限前，先决定你需要证明什么

真正有用的问题不是「我们有日志吗？」而是「这次运行出错时，我们需要证明哪项事实？」

针对智能体可能使用的每个凭据，用简单语言写出要证明的事实。例如：这个经过签名的进程获得了这次运行的授权；这次运行调用了这个 API 端点；这个 SSH 命令越过了本地网关；被拒绝请求发生时该凭据处于锁定状态；自保留的检查点以来，这些记录没有被修改。

然后用棘手的场景测试日志。删除最后 50 行，修改一个目标地址字段，把日志复制到另一台 Mac，在锁定保险库后尝试验证，在两次调用之间撤销一次运行。如果你无法说清楚哪个控制应该阻止事件，以及哪条记录应该显示它，那么你拥有的是可观测性，而不是证据。

哈希链不会让不安全的凭据变得安全。加密无法证明完整性。审批提示也无法挽救一个漫不经心的审核人员。每项控制都有更狭窄的职责。

在操作发生的位置生成记录，保护记录内容，按顺序建立哈希链，在写入者之外保留锚点，并在智能体接触任何可能造成伤害的权限之前反复演练验证。这套流程不如庞大的策略引擎耀眼，但当有人问「实际运行了什么」时，它更容易解释，也更容易守住。
