# 哈希链式审计日志：它能证明什么，又遗漏什么

哈希链可以提供有力证据，证明一组审计记录在链创建后没有被编辑、重新排序，也没有被悄悄插入中间。但它不能证明应用记录了所有相关事件，不能证明时间戳与真实时间一致，也不能证明某个名叫的人亲自执行了操作。把它当成这些事情的全部证明，团队就会构建出看起来很漂亮、却经不起第一个严肃问题的证据。

我见过事故复盘因为人们要求日志回答它根本没有设计来回答的问题而停滞。一条链验证无误，但没人能确定服务是否记录了那次被拒绝的请求，操作员身份是否真实，或者系统时钟是否发生了漂移。密码学没有问题，证据包却不完整。

对于运行可访问 API 和 SSH 的智能体的开发者来说，这种区别比平时更重要。智能体可以快速执行许多操作，而事后真正有用的问题往往非常具体：哪个进程请求了这次调用，凭什么权限，执行器发送的确切请求是什么，返回了什么，以及是否有人批准了它？哈希链只能保护这段故事的一部分，其余部分不会自动出现。

## 有效的链证明记录连续性，而不是现实

有效的哈希链可以证明每条现有记录都提交了前一条记录，并且从选定的起点开始，整个序列一直保持内部一致。如果有人修改旧事件、交换两个事件，或在已有记录之间插入一条记录，验证就会失败，除非此人能够重新计算后面的每个链接，并替换所有受信任的检查点。

这是有意义的证据。调查人员可以说：“这些记录构成了生成这个已知链头的同一个序列。”措辞很重要。这个判断依赖已知链头或其他可信参考。如果链的唯一副本和最终摘要都放在攻击者控制的同一台机器上，攻击者可能同时改写两者。

哈希链不能证明以下说法：

- 应用观察到了所有本应记录的事件。
- 事件载荷准确描述了记录器之外发生的事情。
- 事件时间与可靠时钟一致。
- 执行操作的是人，而不是使用其凭据的受损进程。
- 链在攻击者控制系统之前就已经开始。

这些是不同的命题，需要不同的证据。不要把日志称为“绝对防篡改”。软件不会因为使用 SHA-256 就获得这种地位。在明确假设的前提下，哈希链可以让修改变得可检测，这才是有用且站得住脚的属性。

**完整性**和**完整性范围**的区别需要特别注意。完整性关注的是你拥有的记录是否被改动。完整性范围关注的是是否有记录缺失。哈希链很好地回答了第一个问题。只有在外部证据同时确定了预期检查点，以及记录器必须发出哪些事件时，它才能回答第二个问题。

## 记录格式决定哈希实际覆盖了什么

哈希链只能保护进入摘要的字节。在讨论算法之前，先定义规范的事件格式，并纳入调查人员理解这项操作所需的全部字段。

最小记录可以如下所示：

```json
{
  "sequence": 1842,
  "event_id": "7b2ea6de-9c3f-4bb4-b1d7-8b13fbb1c5b9",
  "recorded_at": "2025-03-08T17:14:22.481Z",
  "actor": {"kind": "agent_process", "process_id": "p-91f"},
  "action": "http.request",
  "target": "api.example.internal/v1/releases",
  "request_digest": "sha256:...",
  "result": {"status": 201, "response_digest": "sha256:..."},
  "previous_hash": "sha256:..."
}
```

写入方以一种预先定义的形式序列化记录，对这些字节进行哈希，然后把生成的摘要作为下一条记录中的前置引用。概念上可以写成：

```text
record_hash[n] = SHA-256(canonical_record[n])
canonical_record[n+1].previous_hash = record_hash[n]
```

`previous_hash` 字段本身必须位于 `record_hash[n]` 覆盖的字节中。遗漏它是一个尴尬但确实存在的实现错误。在这种设计下，记录只是带有摘要外观的装饰，并没有真正绑定序列。

规范化不是细节。两个 JSON 序列化器可能以不同顺序排列对象字段，以不同方式转义 Unicode，或以不同格式表示数字。如果验证者重新构建 JSON，而不是验证实际存储的字节，就可能拒绝真实记录，甚至导致人们对记录含义产生分歧。保存原始字节表示，明确编码方式，并测试不同独立实现之间的验证结果。

哈希链应该覆盖上下文，而不只是请求正文。一条写着“已部署”的记录，证据很薄。若记录绑定了执行器版本、操作类型、目标标识符、经过身份验证的进程身份、授权决定、请求摘要、响应摘要、序列号和记录时间，审查人员才有内容可评估。它仍然不能证明每个字段都是真的，但可以阻止后来的编辑者逐字段改写整个故事。

NIST 特别出版物 800-92《计算机安全日志管理指南》也以更直白的运营语言表达了同一点：日志记录需要包含足够的事件、来源、用户、状态和时间信息，以支持分析；组织还需要保护日志数据。给模糊记录套上一条完美的链，只会把模糊记录完美地保存下来。这不是审计设计。

## 第一条记录和缺失的尾部仍然暴露在外

每条链都有第一条记录，通常称为创世记录。它的前置值由约定固定，例如空字节串的摘要，或者引用更早的检查点。从这一点之后，链可以建立连续性，但它无法说明为什么历史从这里开始。

设想一个服务把第 1 条到第 10,000 条记录写入本地存储。攻击者取得完全控制权，删除第 1 条到第 7,000 条记录，修改保留下来的记录中的序列字段，再从原来的第 7,001 条记录开始构建一条新链。伪造的链可以通过验证。没有更早检查点的审查人员看不到任何密码学缺陷。

尾部也有同样的问题。崩溃、断电或攻击者都可能阻止最后几条事件写入持久存储。即使几秒后发生了操作，最后保留的记录仍可能验证无误。哈希链能证明保留下来的末端在事后没有被修改，却不能证明它就是实际发生的最后一个事件。

可以通过在写入方控制范围之外发布检查点来缩小这两个缺口。检查点至少应包含链标识符、序列号、记录摘要和检查点时间。把它发送到独立账户、一次写入存储、外部时间戳服务或由独立团队管理的收集器。签名检查点比未签名检查点更好，因为它能把声明绑定到签名身份。

RFC 3161 描述了一种时间戳协议。在该协议中，时间戳机构会签名确认：它在指定时间观察到了某个消息印记。这可以支持一个范围明确且有用的判断：该机构在那个时间之前已经获得了这个摘要。但它不能告诉你事件载荷是否诚实，也不能说明提交检查点之前是否遗漏了早期事件，更不能证明执行者获得了授权。应把它用于时间和存在性证据，而不要用它取代运营记录。

检查点频率是风险决策。频繁检查点可以缩短有人删除未锚定尾部的时间窗口，也会产生更多需要保留和核对的外部记录。不要假装每小时一次的检查点能保护分钟级别的事故重建。在运营流程中明确最长未锚定间隔。

## 哈希不会把时间戳变成可信时间

日志时间戳记录的是记录器创建记录时，它的时钟显示了什么。这对普通调试可能已经足够。但涉及截止时间、交易窗口、终止权限后的访问，或跨机器判断事件顺序时，它就是薄弱证据。

管理员可以修改本地时钟。虚拟机恢复运行时可能带着过时的时间。网络时间同步可能失败。即使主机同步正确，应用写入事件的时间也可能不同于远程服务收到请求或提交变更的时间。

在这种区别重要时，应分别保留以下时间：

- `observed_at`：源组件观察到事件的时间。
- `recorded_at`：审计写入方创建记录的时间。
- `remote_at`：远程系统返回的时间，如果该系统提供了时间。
- `checkpoint_at`：独立见证方接受链头的时间。

不要把它们覆盖到一个看起来轻松明确的 `timestamp` 字段里。每个时间都有不同的来源和故障方式。这样，调查人员可以推断一个时间范围，而不是依赖虚假的精确度。

如果需要时间证据，就记录主机如何同步时间，保留同步健康状态告警，并保存带签名的检查点回执。对于后果严重的操作，还应把本地记录与远程服务自己的审计记录进行比较。一条在 10:02:01 记录的请求和一条在 10:02:05 记录的远程变更，可能描述的是同一项操作。哈希链保护你的记录不被编辑，交叉印证则把它与外部系统联系起来。

常见建议是：“使用只追加数据库和时间戳，审计问题就解决了。”它很受欢迎，因为听起来简单易行。但它是错的，因为单个服务内部的追加行为，几乎不能说明时钟是否可信、事件是否缺失、身份是否真实，或外部影响是否发生。只要适合你的场景，就使用只追加存储，但要明确你仍然缺少哪些证明。

## 归因需要摘要之外的身份轨迹

哈希链可以保留类似 `actor = alice@example.com` 这样的归因声明，但它无法证明是 Alice 提供了凭据，无法证明身份提供商正确地验证了她，也无法证明攻击者没有使用她仍然有效的会话。摘要保护的是这句话，而不是这句话的真实性。

对于智能体操作，进程身份通常比含糊的用户字段更有用。记录智能体进程标识符、可用时记录父进程、可执行文件的签名机构、启动时间、会话标识符，以及批准该会话的人或服务账户。只有进程名称的证据很弱，任何程序都可以选择一个熟悉的名称。

代码签名能支持一个范围有限的判断：操作系统可以识别某个可执行文件带有与签名机构相关的签名，并且该签名按照平台规则验证有效。但它不能证明可执行文件的操作者怀有善意。它确实有助于把已知构建版本与同名的任意二进制文件区分开来。

身份验证和授权也需要分别记录。身份验证说明系统接受了哪个凭据或主体。授权说明系统为什么在那个时间允许这项操作。批准对话框、角色分配、令牌范围或变更工单都可能成为授权证据。把该决定的稳定引用或摘要放入操作记录，再在访问控制下保留底层决定记录。

正确使用数字签名可以加强这一层。如果审计写入方对周期性链头进行签名，验证者就能检查这些签名是否由私钥持有者生成。这比无密钥哈希多了一项来源声明，但仍然依赖私钥保管、证书状态、密钥轮换记录，以及证书主体与真实运营身份之间的映射。

不要把所有内容塞进一个 `user` 字符串。事故期间，人们需要区分批准运行的人、请求操作的进程、执行操作的服务，以及接受该凭据的凭据机构。这可能是四个不同的参与者。

## 授权证据回答的问题不同于活动记录

活动记录回答“执行器做了什么？”。授权记录回答“它为什么被允许这么做？”。团队经常把两者合并，因为它们会在同一个请求期间出现。合并反而让调查变得更困难。

假设智能体请求执行一条 SSH 命令。执行器记录请求的主机、命令摘要、结果、进程身份和时间。授权层则单独记录：哪个新进程获得了批准、谁批准了它、批准覆盖什么范围，以及批准何时失效。如果 SSH 命令后来运行，活动事件应该引用适用的授权记录。

这种设计让审查人员可以提出正确的问题。命令是否发出？查看活动记录。会话是否拥有权限？查看授权记录。批准界面显示的身份是否准确？查看界面和进程身份记录。远程主机是否执行了命令？查看服务器日志或结果状态。

批准整个会话与批准每次敏感凭据使用，提供的是不同证据。会话批准证明某人允许特定进程在其整个生命周期内运行。逐项操作批准则证明了更接近具体操作的一次决定。两者没有自动的优劣之分，选择取决于操作频率、后果，以及人是否有能力实际评估反复出现的提示。

批准疲劳是设计失败，不是停止记录批准的理由。如果一个人看到数百个无法区分的请求，最后的点击几乎不能证明他认真考虑过授权。可以把低风险工作归入一个有明确范围的会话决定；对凭据或人能够判断目标和后果的操作，保留重复确认；并用通俗语言记录授权范围。

批准事件本身也不能证明知情同意。它只能证明批准机制记录了一次决定。向用户展示的进程详情、该展示与实际执行进程之间的绑定，以及审计记录，共同决定了这项决定日后能否作为有分量的证据。

## 完整性范围取决于操作从哪里可以绕过记录器

如果智能体已经持有原始凭据，仅仅在日志文件上添加哈希链，无法得到完整的智能体操作记录。智能体一旦获得 API 令牌或 SSH 私钥，就可以调用另一个客户端、复制秘密，或不经过受审计路径直接发出请求。哈希链可能忠实地保存它看到的请求，却漏掉真正重要的那些请求。

完整性范围始于对操作边界的控制。持有凭据的组件应该自己执行网络请求或 SSH 操作，并在返回结果前记录决定和结果。智能体应该收到结果，而不是秘密。这样，声明就从“我们要求智能体记录自己的工作”变成了“凭据持有方通过这条通道观察到了每次使用”。

即使如此，范围仍必须明确。如果开发者还可以在终端中使用同一个凭据，审计轨迹覆盖的是经智能体介导的使用，而不是全部凭据使用。如果智能体能通过第二条网络路径绕过执行器，审计轨迹也不覆盖那条路径。完整性范围声明必须说明覆盖的通道、主体和时间段。

对于受支持的 HTTP 和 SSH 通道，Sallyport 遵循这一边界：加密保管库让 API 和 SSH 凭据远离智能体，由应用执行操作并记录下来。与智能体侧活动日志相比，这让它的审计轨迹具有更清晰的范围，但它不会对通过无关凭据路径执行的操作作出保证。

一个有用的故障测试方法，是画出从智能体到外部影响的每一条路径。包括直接 HTTP 客户端、shell 工具、包含令牌的本地文件、浏览器会话、云元数据服务、CI 变量和 SSH 配置。所有绕过已记录执行器的路径，都是完整性范围声明的例外。关闭它、隔离它，或者如实记录这个例外。

## 机密性、访问和保留需要各自的控制措施

哈希链本身不会泄露任何内容，但审计记录经常包含不适合写入普通应用日志的信息。请求 URL 可能暴露客户标识符。命令行可能含有秘密。响应可能包含个人数据。让记录更难修改的哈希链，也会让粗心造成的泄露更难挽回。

加密审计存储，并限制可以读取它的人。在可能的情况下，把验证完整性的能力与解密内容的能力分开。验证者可以对加密后的记录字节重新计算哈希，并比较前置引用，而不必看到受保护的载荷。当审查人员需要先确认封存档案保持完整，再由获授权调查人员打开它时，这种方式很有用。

脱敏必须在记录进入哈希链之前完成。写入后再替换秘密会改变字节并破坏哈希链。应改为记录敏感请求正文的摘要、稳定的令牌引用、适当情况下的长度信息，或说明某字段被保留的结构化声明。同时记录脱敏规则，否则两条看起来相似的记录可能是由不同逻辑脱敏得到的。

保留策略也会影响证据价值。如果保留任务删除旧记录，它应该生成自己的记录，说明删除的范围、适用的保留规则、删除时间，以及删除前的最后一个检查点。把检查点保存在单独的档案中。哈希链不能让已删除的材料重新出现，但保留下来的证据可以表明删除是在明确流程下发生的，而不是悄悄进行的。

访问日志同样重要。记录谁导出了审计档案、谁解密了它，以及谁修改了保留配置。事故期间审计日志很容易成为目标。哈希链保护过去的记录不被悄悄修改，但读者范围过大，仍然可能导致敏感内容被复制，或让操作员受到压力而停止未来的记录。

## 在真正需要之前验证档案

验证应该是带有保存结果的日常操作，而不是有人在中断服务时第一次尝试的命令。流程应明确具体档案范围、验证器版本、预期检查点来源、验证时间、操作员和结果。

验证器至少需要发现四类失败：记录格式错误、前置引用不正确、某条记录的摘要与其存储字节不匹配，以及计算出的链头与预期检查点不匹配。输出应当易于保存。例如：

```text
$ audit verify --archive agent-actions-2025-03-08.log --checkpoint checkpoints/2025-03-08.json
chain_id: agent-actions-prod-a
records_checked: 1842
first_sequence: 1
last_sequence: 1842
computed_head: sha256:4e7c...a912
checkpoint_head: sha256:4e7c...a912
result: VALID
```

`VALID` 表示验证器根据所提供的检查点检查了所提供的字节。它不表示“所有生产操作都已记录”，也不表示“这些事件都是真的”。应当把这个限制直接写入流程。在压力之下看到绿色结果的人，否则会赋予它超过实际范围的含义。

在非生产档案中测试破坏性场景。修改记录正文中的一个字节，移动两条记录，删除中间记录，替换检查点链头，截断尾部。除了没有预期最终检查点时普通的尾部截断，验证器应拒绝所有这些情况。对于后一种情况，它可能报告一个有效前缀。这一报告是正确的，但你应该因为正确的原因感到担忧。

Sallyport 通过 `sp audit verify` 提供离线链验证，验证器无需保管库密钥即可检查加密审计链。在调查迫使你处理这些工作之前，应在导出后运行检查，并练习把结果与独立的会话和活动视图进行核对。

## 证据包需要能够彼此形成有意义对照的记录

站得住脚的调查会使用多种故障方式不同的记录。独立来源之间相互印证，比一份不断重复自身假设的日志更有分量。

对于一次敏感的智能体操作，应保留哈希链式活动记录、会话或批准记录、经过身份验证的进程身份、相关配置版本、外部检查点回执，以及目标系统自己的记录或可观察结果。普通请求不需要每个来源，但操作出错的后果越严重，就越需要足够的来源。

应当预料到来源有时会不一致。本地活动记录可能早于远程服务的时间戳，因为网络传输需要时间。请求可能返回成功，但远程系统稍后又回滚了操作。会话记录可能显示已批准，而活动记录却显示获准权限没有被使用。这些差异是需要调查的证据，不是应该被抹平的缺陷。

写下系统能够支持的确切声明。例如：“这个档案包含截至检查点 X 的、未被修改的执行器记录序列。”然后写下没有其他记录就无法支持的声明：“仅凭这个档案，无法证明没有发生直接凭据使用。”清楚说明边界，反而能让强有力的声明更可信。

最实际的第一步，是选取一项后果严重的智能体操作，从头到尾重建它。确认操作边界、执行前写入的字段、执行后写入的字段、批准证据、检查点目的地、远程印证以及每条绕过路径。如果任何一部分依赖某个人凭记忆回想发生了什么，你还没有审计轨迹，只有一段可能经不起事故检验的故事。
