Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

12 治理:审计、最小权限、human-in-the-loop

本章回答

一个 agent 在凌晨发起 1.25 万元退款时,谁有权批准、批准依据是什么、事后凭什么复原?审计事件要记哪几个字段,哈希链到底能证明什么、不能证明什么?最小权限为什么要同时管工具、数据、时间和环境四层?审批为什么会在大量低风险请求里失效,超时之后应该降级还是放行?策略为什么要和代码一起版本化,合规条款又该落到哪些可执行产物上?

读完你会

为一个高风险动作画出「请求 → 策略 → 审批 → 执行 → 审计」的决策链;写出可重建动作的审计事件与保留策略;为工具设计带 TTL 的权限租约与 break-glass 流程;把审批点、四眼、超时和审批卡落到状态机;用 OPA/Cedar 的思路组织策略测试与决策日志;把 NIST AI RMF、ISO 42001、EU AI Act 的条款映射成工程证据;跑通一个策略引擎与审批工作流。

第 11 章把安全边界压到「不可信数据不得获得执行权」。边界回答的是「什么不允许发生」;治理回答的是另一半问题:如果某件事必须发生,谁在什么条件下批准它,留下什么证据,事后谁来复原。一个系统可以没有漏洞,却仍然不可治理:权限是永久的管理员,审批是聊天窗口里的一句「可以」,日志只存在应用 stdout 里,出了事只能靠当事人回忆。这种系统在演示里很安全,在事故里很脆弱。

治理常被写成一叠制度文档:安全委员会、AI 伦理准则、责任声明。文档不是没有价值,但它不改变任何一次工具调用的结果。本章只讨论可执行的部分:一个请求如何被策略判定,一个批准如何被身份约束,一个权限如何到期,一条证据如何被验证。制度只有落到这四件事上,才在系统里存在。

这一章在全书路径的末端:第 9 章给了评估,第 10 章给了 trace,第 11 章给了安全边界。治理把三者接起来——评估告诉我们风险在哪,trace 提供事实,安全边界提供拒绝能力,治理决定「例外由谁授权、证据留多久」。第 13、14 章的两个项目会把这里的策略引擎、审批流与审计日志接进真实执行器。

12.1 治理对象:谁批准、谁负责、留什么证

先看一次具体动作。退款 agent 在凌晨 02:17 发起一笔 1,250,000 分的退款,收款方是三天前刚更换的账户,订单来自生产租户 acme。系统有四个问题必须能回答,而且答案不能依赖任何人的记忆:

  1. 谁批准:这笔退款经过了谁?是自动规则、单个值班人,还是两个不同的人?
  2. 谁负责:如果这笔退款是错的,责任落在模型提供方、agent 产品方、审批人,还是平台运维?
  3. 留什么证:事后能否拿出请求参数、策略版本、审批记录、执行回执,证明「当时确实批准过」?
  4. 多久:这些证据保留多久?权限多久回收?审批多久超时?

四个问题对应治理的四个对象。它们不是四份文档,而是四组必须能相互校验的系统状态。

治理对象要回答的问题没有它的症状最小产物
决策权谁在什么条件下可以批准哪个动作?谁都能点「同意」;或没人敢点策略表 + 审批角色 + 授权边界
责任动作出错时,哪个角色对哪个决定负责?事故复盘变成「模型抽风」责任矩阵 + 值班表 + 升级路径
证据事后凭什么重建这次动作?只有最终答案,没有中间记录审计事件 + 执行回执 + 证据链
时效批准、权限、证据各自活多久?临时权限变永久;日志过期才发现要查TTL + 保留期 + 复核期限

这张表说明什么:四者缺一不可。只有决策权没有证据,批准无法被证明;只有证据没有时效,日志会无限膨胀,临时权限会永久留存;只有责任没有决策权,复盘只能找人背锅,改不了系统。

定义 12.1.1 · 治理对象四元组

一次 agent 动作的治理配置可写成

G=(D, R, E, T), \mathcal{G} = (D,\ R,\ E,\ T),

其中:

  • DD 是决策权映射:对主体 ss、动作 aa、资源 rr 与上下文 cc,给出允许、拒绝或需要谁批准;
  • RR 是责任分配:把 DD 中的每个决定绑定到一个可联系的角色与升级路径;
  • EE 是证据模式:定义一次动作要记录哪些事件、事件之间的关联键、完整性与保留要求;
  • TT 是时效约束:审批截止时间、权限租约到期时间、证据保留期与事后复核期限。

称 G\mathcal{G} 是可执行的,当且仅当 DD 能被策略引擎求值、EE 能被写入不可变或可验证的存储、TT 能被执行器检查,且 RR 中的每个角色都有明确的接管人。四元组中任何一项只存在于文档里,都不构成可执行治理。

这个定义故意把「责任」放在「决策权」旁边。很多系统只实现了一半:策略引擎能拒绝,审批流能放行,但没有人对「为什么这条规则是这样」负责。规则漂移、阈值被临时调高、审批人长期离线,这些问题不会让引擎报错,却会让治理慢慢失效。责任分配的作用,是给每个自动决定配一个能改它、也能为它解释的人。

治理不是文档,是「例外怎么走」

正常路径谁都能写进代码:读文件、查订单、发通知。治理真正要设计的是例外:这笔退款为什么可以绕过限额,这个服务为什么可以临时拿到生产库权限,这次删除为什么没有走常规审批。例外如果没有固定入口,就会走聊天窗口;走聊天窗口的例外没有证据,也没有时限。治理的质量,几乎等于「例外路径的质量」。

  请求                        策略                        审批
  ┌──────────────────┐      ┌──────────────────┐      ┌──────────────────┐
  │ request_id        │      │ policy_version    │      │ approval_id       │
  │ actor / tenant    │─────▶│ rule_id           │─────▶│ approver(s)       │
  │ action / resource │      │ allow / approve / │      │ four_eyes?        │
  │ amount / env      │      │ deny + obligations│      │ deadline          │
  └──────────────────┘      └──────────────────┘      └──────────────────┘
           │                         │                         │
           │                         │  require_approval        │ approved
           │                         ▼                         ▼
           │                  ┌──────────────────────────────────────┐
           │                  │ 执行:scoped credential + 幂等键      │
           │                  │ execution_id / receipt / result       │
           │                  └──────────────────────────────────────┘
           │                                      │
           ▼                                      ▼
  ┌─────────────────────────────────────────────────────────────────────┐
  │ 审计:request → policy_decision → approval → execution → review     │
  │ 每条记录带 prev_hash/hash;链头定期锚定到 WORM 或外部时间戳          │
  └─────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
                          证据链:可重建 / 可验证 / 可保留

读图:一条高风险动作至少产生五条独立记录——请求、策略决策、审批、执行回执、事后复核。它们由不同组件写入,通过 request_id / approval_id / execution_id 关联。五条记录齐全时,审计员可以在不知道当事人记忆的情况下回答「谁在什么时候、依据哪版策略、批准了什么参数、执行结果是什么」。缺任何一条,结论就会降级:缺审批记录,无法区分「自动放行」与「人工批准」;缺策略版本,无法复现当时的判定;缺执行回执,无法证明批准的动作真的执行了,或执行的是不是同一个动作。

12.1.1 决策权:不是「谁能用」,而是「谁能批」

决策权经常被写成角色权限:admin 可以退款,support 只能查询。这不够。退款动作至少有三个不同的权力:发起、批准、执行。把它们混在一个角色里,等于让同一个人写申请、批申请、再执行,四眼原则从一开始就不存在。

一个可执行的决策权表至少要有五列:

动作类发起者批准者执行凭证约束
只读查询任意已认证 agent无需批准短期只读 token数据范围按租户过滤
小额退款客服 agent自动规则(限额内)支付工具 scope单笔与日累计上限
大额退款客服 agent财务 + 风控(两人)一次性支付凭证收款账户变更需额外复核
生产删除SRE agentSRE 负责人 + 安全break-glass 或变更单必须先有备份与回滚方案
跨租户导出数据 agent数据所有者 + 安全一次性导出凭证含 PII 时必须脱敏与水印

这张表说明什么:批准者与发起者必须是不同主体;执行凭证必须是这次动作专用的,不能是发起者手里长期持有的密钥。很多越权事故不是因为策略写错,而是因为批准之后,执行器仍然用 agent 的长期凭证去调用下游——批准只改了工单状态,没有改凭证。

12.1.2 责任:模型是组件,不是责任主体

出事后最常见的归因是「模型自己决定这么做」。这句话在工程上不成立。模型输出的是 token;把它变成工具调用、给它凭证、让它访问生产数据、允许它在没有人复核的情况下执行的,都是宿主系统。模型没有法律人格,不理解组织政策,也不能持有或归还权限。能绕过审批的从来不是模型,而是宿主给模型留的那条路径。

模型行为是系统行为的一个输入,不是并列的责任主体。责任要按「谁能改变这个决定」来分,而不是按「谁离故障最近」来分:

行为/决定直接产生者负责角色常见错误归因
生成退款意图与参数模型agent 产品方:提示词、工具契约、参数校验「模型幻觉」
选择可用的工具与 scope宿主/工具注册表平台方:工具目录、scope 审批「模型调用了不该调的工具」
批准高风险动作审批人审批人 + 其所属业务线「系统自动批的」
签发执行凭证策略引擎/凭证服务平台安全:策略版本、凭证 TTL「模型拿到了 token」
记录审计事件执行器/日志管道平台运维:完整性、保留、访问控制「日志没开」
事后复核复核人风控/审计:抽样、结论、整改「没人想到要看」

这张表说明什么:模型只占第一行。后面五行的每一个「是」,都是组织里某个人可以改的。把责任推给模型,实际效果是让真正能修复的环节免于被检查。模型提供方仍有责任:能力声明、已知局限、版本变更与安全公告;但授权、执行与证据属于宿主和组织,不能由提供方代为决定。

关于治理的五个误解
  1. 「治理就是写制度」——制度只有落到策略、凭证、日志、复核四个机制上才会改变行为。没有求值器的制度,只是一份愿望清单。
  2. 「审批就是多一个人点同意」——审批的有效性取决于:审批人是否独立、是否看得懂证据、是否有时间看、拒绝是否真的能阻止执行。缺一条,审批就是橡皮章。
  3. 「审计就是多打日志」——日志回答「发生了什么」,审计要回答「凭什么相信这条记录」。没有 schema、关联键、完整性与保留策略的日志,事故时不可用。
  4. 「最小权限就是给低权限账号」——低权限账号仍然可能持有过宽的 scope、永久凭证或跨租户数据访问。最小权限是四层约束的组合,不是账号等级。
  5. 「合规就是拿证/过审」——证书证明某个时点的管理体系存在,不证明今天的这次高风险动作被正确批准。合规检查的是证据,不是口号。

12.1.3 时效:治理有四个时钟

治理失败经常不是「没有规则」,而是「规则过期了」。四个时钟要分别设计,不能用一个「定期 review」糊过去:

时钟约束什么典型值(示例,按风险定)超时行为
审批时钟一次待批请求活多久15 分钟–24 小时fail-closed,拒绝并通知
权限时钟一次授权活多久5 分钟–8 小时自动过期,重新申请
证据时钟一类事件保留多久90 天–7 年到期归档/删除,保留法律冻结
复核时钟break-glass 后多久复核24 小时–5 个工作日升级到安全负责人

这张表说明什么:四个时钟的默认方向不同。审批和权限要短,短到能限制暴露窗口;证据要长,长到能覆盖调查与追责周期;复核要紧,紧到当事人还记得上下文。把它们都设成「一年」或都设成「一天」,都会在某一边出问题。

12.1.4 最小可运行治理清单

治理不需要一次建完。一个团队可以先落地八件事,覆盖「决策、证据、时效」的最小闭环:

#产物最小内容负责人没有它的直接后果
1策略包一份带版本号的规则文件 + 默认拒绝平台/安全放行靠口头,无法复现
2审批矩阵动作 × 风险 × 审批人/人数业务负责人谁都能批,或没人敢批
3审计 schema六问字段 + 关联 id + 完整性平台运维事后无法重建
4保留策略按数据类分层 + 删除/冻结规则安全/法务日志要么过期,要么成 PII 仓库
5租约签发TTL、scope、数据谓词、撤销接口平台安全临时权限永久化
6break-glass事件号、通知、24h 复核值班负责人例外走聊天窗口
7漂移查询授予 vs 使用,日更IAM/平台权限慢慢腐烂
8复核节奏抽样、指标、整改跟踪风控/审计审批退化成盖章

这张表说明什么:八件事里,第 1、3、5 件是系统能力,其余五件是流程与责任。很多团队先买日志平台,却没有人定义 schema 与保留策略,结果只是把不可用的日志存得更久。顺序应该反过来:先定事件与决策,再选存储。

12.1.5 先做哪三件

如果只能先做三件,按这个顺序:

  1. 给高风险动作加审批与审计:选一个不可逆、金额大的动作,写清谁批、批什么、留什么。它立刻降低最坏损失。
  2. 给临时权限加 TTL:所有非长期授权必须有到期时间;没有 TTL 的授权视为缺陷。它降低暴露窗口。
  3. 建立每周一次的漂移对账:授予与使用的差集,先手工跑,再自动化。它让前两件不会慢慢失效。

三件都做完,再考虑策略语言、WORM、合规映射。反过来,先上策略引擎却没有审批人与证据,只会把「没有控制」变成「有代码但没有控制」。

12.2 审计:事件、不可篡改、保留期与证据链

审计的第一性原理不是「多记」,而是事后能否在不知道当事人记忆的情况下重建一次动作。这要求每条记录回答六个问题:谁(who)、做了什么(what)、何时(when)、为什么(why)、依据哪版策略(policy_version)、结论是什么(decision),并留下能指向原始证据的引用(evidence)。少一个问题,重建就会断。

定义 12.2.1 · 审计事件

一条审计事件是一个八元组

e=(who, what, when, why, v, d, x, h), e = (\mathrm{who},\ \mathrm{what},\ \mathrm{when},\ \mathrm{why},\ v,\ d,\ x,\ h),

其中:

  • who\mathrm{who} 是主体标识、角色与认证方式,不是自由文本姓名;
  • what\mathrm{what} 是动作、资源标识与参数摘要(含哈希,敏感字段脱敏);
  • when\mathrm{when} 是墙钟时间与单调时钟,二者都要有;
  • why\mathrm{why} 是请求理由、工单/事件号与策略命中的规则;
  • vv 是策略版本与策略包哈希;
  • dd 是决策(allow / require_approval / deny)与附带的义务(obligations);
  • xx 是证据引用:trace_id、审批 id、执行回执 id、diff 或快照地址;
  • hh 是完整性字段:前一条的哈希与本条哈希。

一条事件只描述一个决定或一次状态转移;不要把「提交、批准、执行」塞进同一条记录。拆分后才能定位是哪一步缺失。

字段不能凭感觉定。下面这张表给出一组最小字段与缺失后的直接后果:

字段示例必填缺失后果
event_id / seqevt-000042 / 42是无法稳定引用;重放时顺序不确定
recorded_at2026-10-19T02:17:03Z是无法建立时间线;跨系统因果断裂
monotonic_ns884219003311是墙钟回拨时无法判断先后
event_typepolicy_decision是无法区分决策、批准与执行
actor / rolessupport-3 / support是无法判断是否四眼、是否越权
action / resourcerefund / invoice:INV-2091是不知道发生了什么
params_digestsha256:9f2c...是无法证明执行的是批准的那组参数
policy_version2026.10.1是无法复现当时的判定
decision / rule_idrequire_approval / require-approval-high-value-refund是不知道为什么被拦或放行
reason / ticket_id大额退款复核 / CHG-1042是不知道为什么发起、对应哪张工单
evidence_reftrace:8f3a、approval:apr-003是无法跳到原始证据
prev_hash / hash... / ...是无法发现篡改或删除

这张表说明什么:policy_version 与 params_digest 是最容易被忽略、却最影响复现的两列。没有版本,你只能知道「当时被批准了」,不知道「当时的规则是否允许」;没有参数摘要,你无法证明执行的回款账户与批准时看到的是同一个。

一条真实形态的事件如下(来自本章实验的 JSONL,字段略有删节):

{
  "seq": 2,
  "event_id": "evt-000002",
  "recorded_at": "2026-10-19T09:00:01Z",
  "event_type": "policy_decision",
  "actor": "support-3",
  "action": "refund",
  "resource": "invoice:INV-2091",
  "resource_tenant": "acme",
  "policy_version": "2026.10.1",
  "decision": "require_approval",
  "obligations": ["four_eyes", "finance_ticket"],
  "evidence": {
    "request_id": "req-002",
    "reason": "退款金额超过 500000 分,需要人工审批",
    "matched_rule": "require-approval-high-value-refund",
    "context": {"amount_cents": 1250000, "environment": "prod", "reversible": false}
  },
  "prev_hash": "6a1f...",
  "hash": "b73c..."
}

这条事件能回答「谁、做了什么、为什么需要审批、按哪版策略」,但它不能单独证明「谁批准了」。批准是另一条事件,带自己的 actor、approval_id 与 wait_s。把两者混在一条记录里,四眼原则就无法被机械校验。

12.2.1 append-only、WORM 与哈希链:三层不是一回事

「不可篡改」经常被当成一个开关:买个 WORM 桶,或者加个哈希链,就宣称审计不可篡改。实际上这是三层不同的保证,成本与威胁模型都不同:

层机制能防什么防不了什么成本
追加写应用只 append;文件/表禁止 update/delete误删、覆盖、应用 bug有存储写权限的人重写低
WORM对象锁、合规保留、删除需等保留期管理员误删、内部人删证据保留期外的删除;写错内容中
哈希链 + 外部锚定链式哈希;链头定期写入独立存储/时间戳静默改单条、删中间记录重算整条链并改写链头中高

这张表说明什么:三层要叠加。只有追加写,内部人可以直接改文件;只有 WORM,写进去的错误内容仍然错误;只有哈希链,能重写整条链的人可以重算所有哈希。外部锚定是关键:把链头哈希定期写到另一个信任域(独立账号、时间戳服务、纸质归档),攻击者要同时攻破两个域才能自洽。

定理 12.2.1 · 哈希链的篡改可见性

设事件序列为 e1,…,ene_1,\ldots,e_n,链定义为

h0=IV,hi=H(hi−1∥ci),i=1,…,n, h_0 = \text{IV},\qquad h_i = H(h_{i-1} \parallel c_i),\quad i=1,\ldots,n,

其中 cic_i 是 eie_i 的规范编码,HH 是抗第二原像的哈希函数。若验证方持有可信的 hnh_n(例如外部锚定的链头),则任何对某个 eje_j(j≤nj \le n)的修改,只要不同时改变 hnh_n,都会以压倒性概率被验证发现。

证明

设被修改的最早事件下标为 jj。由于 cjc_j 改变,且 hj−1h_{j-1} 未变,hj=H(hj−1∥cj)h_j = H(h_{j-1} \parallel c_j) 以压倒性概率不等于原 hjh_j(否则找到 HH 的第二原像)。对 i=j+1,…,ni = j+1,\ldots,n 归纳:若 hi−1h_{i-1} 已不同,则 hi=H(hi−1∥ci)h_i = H(h_{i-1} \parallel c_i) 也不同。故重算得到的 hnh_n 与可信链头不同,验证失败。若攻击者能同时改写可信链头,则本定理的前提不成立;这正是需要外部锚定的原因。∎

∎

定理的边界比结论更重要。它假设:哈希函数抗第二原像;验证方拿到的链头确实可信;链没有在尾部被截断。截断是哈希链的常见盲点:攻击者删掉最后几条记录,剩下的链仍然自洽。对策是记录「预期条数/最后序号」并一起锚定,或者用外部系统记录每个时间窗口的链头。另一个盲点是密钥与写权限:如果签名密钥和执行器在同一台机器上,攻击者可以签一条新的历史。

哈希链不是 WORM,WORM 也不是审计

哈希链证明「记录之间一致」,不证明「记录完整」。WORM 证明「这段时间内删不掉」,不证明「写进去的是真话」。审计的可信度来自三件事的组合:写入端可信(执行器不能伪造结果)、存储端不可改(WORM + 锚定)、读取端可验证(独立验证工具)。三者缺一,审计就只是「一份比较难改的日志」。

12.2.2 保留期与 PII 最小化:记多久与记多细

审计日志本身也是数据,也会包含个人数据。保留期太长,违反最小化;太短,事故发生时已经过期。可行的做法是按证据价值分层,而不是按「全部保留七年」一刀切:

数据类内容保留期(示例)到期动作
决策元数据谁、何时、动作、决策、策略版本1–7 年,按适用要求归档;保留聚合统计
审批记录审批人、时间、意见、证据快照与决策同级归档;不可单独删除
执行回执下游 id、幂等键、结果码1–3 年归档;保留引用
调试日志完整请求/响应、堆栈7–90 天删除或降采样
敏感原文证件号、账户、自由文本默认不写写哈希/令牌化;原文另存受控库

这张表说明什么:审计事件里不要放自由文本和完整参数。需要原文时,存引用而不是副本:evidence_ref 指向一个访问受控、保留期更短的 trace 存储。这样审计日志可以长期保留而不成为 PII 仓库;调查时再按权限取原文,并记录「谁在何时看了哪条证据」。

PII 最小化的三个具体动作:

  1. 字段白名单:审计写入器只接受预定义字段,拒绝任意 dict。实验里的 sanitize_context() 只保留金额、环境、是否可逆等策略字段,就是这一条的最小实现。
  2. 令牌化:主体 id 用内部 pseudonym,资源 id 用不可逆哈希或代理 id;需要映射时走独立权限。
  3. 访问审计:读取审计日志本身也要记事件。否则「谁看过这笔退款的证据」永远无法回答。

12.2.3 不可抵赖:哈希链不证明「是他签的」

审计的最后一个要求是不可抵赖(non-repudiation):当事人事后不能否认自己做过这个决定。哈希链只证明「这条记录没有被改」,不证明「这条记录确实由声称的人写下」。两者之间隔着身份、签名与意图。

条件问题工程做法伪实现
身份强度怎么证明是这个人?强认证、硬件密钥、短时会话共享账号、代签、截图
签名记录能否被伪造?审批人私钥签名,或由独立服务签收应用自己写 actor 字段
意图他批准的是这组参数吗?对冻结的参数摘要签名批准一段可后续修改的文本
时间他何时批准的?可信时间戳 + 单调时钟客户端本地时间

这张表说明什么:actor 字段只是声称,不是证据。没有强认证与签名时,最坏情况是「有人用共享账号点了同意,事后所有人都否认」。高风险动作的审批记录应该由审批服务签名,签名覆盖 approval_id、参数摘要、审批人、时间与决定;验证方用公钥独立验证,而不是相信数据库里的 approved_by 列。

不可抵赖不是所有动作都需要的成本。低风险自动批准只需可追溯;高风险、对外、涉钱的审批才值得上签名与强认证。判断标准与 12.4 的审批点一致:越不可逆、越跨边界,越需要「无法否认」而不是「大概记得」。

12.2.4 证据链:重建一次高风险动作

例题 12.2.1 · 用五条记录重建一笔退款

某退款在事后被投诉「未经本人同意」。审计员拿到五条记录:

req-8842    request      support-3  refund invoice:INV-2091  amount=1250000  env=prod
dec-8842    policy       policy-engine  require_approval  rule=high-value-refund  v=2026.10.1
apr-8842    approval     finance-2 -> finance-4  two distinct approvers  wait=150s
exe-8842    execution    payments.refund  idempotency=refund-INV-2091-v1  receipt=pay_7f31
rev-8842    review       risk-1  sampled=true  conclusion=valid  evidence=trace:8f3a

重建过程:请求记录确定「谁发起、对哪个订单、多少钱」;策略记录确定「为什么需要审批、按哪版规则」;审批记录确定「两个人分别是谁、间隔多久」;执行记录用幂等键与回执确定「实际打款的是哪一笔」;复核记录确定「事后有没有人看过」。

如果缺 apr-8842:无法证明这是人工批准而不是自动放行,投诉无法反驳。 如果缺 dec-8842:无法证明 1.25 万在当时确实超过阈值;策略可能已经改过。 如果缺 exe-8842:无法证明批准的动作与实际执行的是同一笔;参数可能在批准后被替换。 如果只有应用日志:字段不全、可能被轮转删除、没有哈希链,任何一条都无法独立验证。

结论:证据链不是「日志很多」,而是「五条记录、至少三个独立系统、一个可验证的链头」。缺一条,重建就退化成推测。

方法 12.2.1 · 设计一条能重建动作的审计事件
  1. 先写问题,不写字段:列出事后必须回答的五个问题(谁发起、谁批准、依据什么、执行了什么、谁复核)。
  2. 给每个问题配一个标识:request_id、approval_id、execution_id、policy_version、trace_id;确保跨系统可关联。
  3. 定义参数摘要:对会影响结果的参数做规范化 JSON 后哈希;敏感字段先脱敏再入日志。
  4. 定义完整性:prev_hash/hash、链头锚定周期、预期条数;把验证脚本放进 CI。
  5. 定义保留与访问:按数据类分表;原文放受控存储;读取审计日志也记事件。
  6. 做一次重建演练:随机抽一条历史动作,只给审计记录,要求在 10 分钟内回答五个问题;失败就补字段。

Q1 为什么不把完整 prompt 和工具返回都写进审计事件?

答案

因为审计事件要长期保留,而 prompt/返回可能含 PII、密钥、第三方内容和注入文本。正确做法是分层:审计事件存 trace_id、参数摘要与决策理由;完整原文存在访问受控、保留期更短的 trace 存储里;调查时按权限取用,并记录读取行为。这样既保留可重建性,又不把审计日志变成 PII 仓库。

Q2 有了哈希链,是否还需要 WORM?

答案

需要。哈希链只保证「记录之间一致」,不能阻止有写权限的人重写整条链并重算哈希。WORM 提供保留期内的删除保护;外部锚定提供链头的独立可信副本。三者解决不同威胁:哈希链发现单点修改,WORM 阻止删除,锚定发现整链重写。少了任何一个,攻击面都会扩大。

12.3 最小权限:工具级、数据级、时间级与 JIT

第 4 章讲的是内核机制:uid、capability、seccomp、landlock。它们回答「进程能做什么」。这一节讲的是授权生命周期:一个 agent 在什么条件下、对哪部分数据、在多长时间内、在哪个环境里,能调用哪个工具。内核边界再严,如果执行器给 agent 一个永久的生产管理员 token,最小权限仍然是空话。

最小权限要同时收四层。只收一层,另外三层会把它抵消:

层级约束什么具体做法失效模式
工具级能调用哪些动作一个工具一个 scope;禁止 payments:* 这种通配工具复用导致权限连坐
数据级能碰哪些行/字段/租户行级谓词、字段掩码、租户隔离同一工具读到全部租户
时间级权限活多久JIT 租约、短时凭证、到期自动回收临时权限永不回收
环境级在哪个环境生效prod/staging 凭证分离、数据脱敏开发凭证能打生产

这张表说明什么:四层是乘法关系,不是加法。工具级收了,但数据级没过滤,agent 仍可读全量;数据级过滤了,但凭证永久有效,攻击者可以慢慢利用;环境级分离了,但 staging 用生产数据副本,风险又回来了。

12.3.1 权限租约:把「授予」变成「有期限的借用」

角色(role)适合描述长期职责,不适合描述一次动作。agent 的一次退款、一次导出、一次排障,需要的是参数化、短时、可审计的授权。这就是权限租约(lease)。

定义 12.3.1 · 权限租约

一条权限租约是一个八元组

ℓ=(s, τ, δ, ϵ, t0, t1, i, o), \ell = (s,\ \tau,\ \delta,\ \epsilon,\ t_0,\ t_1,\ i,\ o),

其中:

  • ss 是主体(agent 实例或工作负载身份);
  • τ\tau 是工具/动作 scope;
  • δ\delta 是数据谓词(租户、行范围、字段掩码);
  • ϵ\epsilon 是环境(prod/staging);
  • [t0,t1)[t_0, t_1) 是生效窗口,t1−t0t_1 - t_0 是 TTL;
  • ii 是签发者与策略版本;
  • oo 是义务(最大金额、幂等键、必须四眼、必须水印等)。

一次调用被授权,当且仅当当前时间在窗口内、数据谓词为真、环境匹配、租约未撤销,且调用参数满足全部义务。租约过期后,执行器必须拒绝,不得用缓存或旧凭证续期。

租约与角色的区别,用一张表最清楚:

维度角色租约
生命周期月/年分钟/小时
参数粗粒度(finance)细粒度(租户、金额上限、单据)
审批入职/转岗时每次高风险动作
回收依赖流程到期自动
证据角色分配表租约 id + 每次使用记录

这张表说明什么:角色不是没用,它是租约的上限。租约不能超过角色允许的 scope;角色决定「你能申请什么」,租约决定「这次你能做什么」。两层都要有,缺一层就会出现「角色很大但没人用」或「租约很细但可以无限续」。

一个租约的 JSON 形态如下:

{
  "lease_id": "lease-7f3a91",
  "subject": "agent-refund-3",
  "tool": "payments.refund",
  "data": {"tenant": "acme", "invoice_prefix": "INV-2"},
  "environment": "prod",
  "not_before": 1792376100,
  "expires_at": 1792377000,
  "issued_by": "policy-engine@2026.10.1",
  "obligations": {"max_amount_cents": 500000, "idempotency_key_required": true}
}

租约里的时间统一用 Unix 秒,避免时区与字符串比较。执行器侧的检查必须是 fail-closed 的。下面这段是示意,不是本章实验的一部分;重点是检查顺序与默认值:

def lease_allows(lease: dict, request: dict, now: int, revoked: set[str]) -> bool:
    if lease["lease_id"] in revoked:
        return False
    if not (lease["not_before"] <= now < lease["expires_at"]):
        return False
    if lease["tool"] != request["tool"] or lease["environment"] != request["environment"]:
        return False
    if lease["data"]["tenant"] != request["tenant"]:
        return False
    if request["amount_cents"] > int(lease["obligations"].get("max_amount_cents", 0)):
        return False
    return True

四个细节:时间用同一时钟源;revoked 集合要能被策略引擎写入;义务检查失败必须拒绝而不是降级;每次检查都要把 lease_id 写进审计事件。少了最后一条,事后无法证明「这次调用用的是哪条租约」。

12.3.2 JIT:权限在需要时出现,在不需要时消失

JIT(just-in-time)不是「申请更快」,而是「默认没有,用时才发,到期就收」。一个可执行的 JIT 流程有六步:

  1. 请求:agent 提交动作、资源、理由、预计时长;不接受「给我 admin」这种无参数请求。
  2. 求值:策略引擎按主体、动作、资源、上下文给出 allow / require_approval / deny。
  3. 批准:高风险动作走 12.4 的审批;低风险可按规则自动。
  4. 签发:凭证服务签发短时租约;TTL 由风险等级决定,不由申请人决定。
  5. 使用:执行器每次调用检查租约与义务,并写使用记录。
  6. 回收:到期自动失效;提前完成则主动撤销;异常使用触发撤销与复核。

JIT 的关键指标不是「签发速度」,而是暴露窗口:

暴露窗口=∑ℓ∈L(t1(ℓ)−t0(ℓ))⋅1[ℓ 被使用]. \text{暴露窗口} = \sum_{\ell \in L} (t_1(\ell) - t_0(\ell)) \cdot \mathbf{1}[\ell \text{ 被使用}].

窗口越短,凭证泄露后可利用的时间越短。但窗口不能短到正常任务无法完成,否则会诱导用户申请更长的 TTL,反而扩大风险。合理的做法是按任务类型设定上限,并在任务完成时主动撤销,而不是让租约自然到期。

定理 12.3.1 · 租约到期封闭性

假设:(1) 执行器在每次调用前检查租约窗口与撤销状态;(2) 租约不能在没有新策略决策的情况下延长;(3) 时钟单调且执行器与签发者使用同一时间基准。则对任意租约 ℓ\ell,在 t≥t1(ℓ)t \ge t_1(\ell) 且没有新租约的情况下,任何依赖 ℓ\ell 的调用都会被拒绝。

证明

由 (1),调用被允许蕴含当前时间 t∈[t0,t1)t \in [t_0, t_1) 且 ℓ\ell 未撤销。由 (3),t≥t1t \ge t_1 时窗口条件为假,检查返回拒绝。由 (2),ℓ\ell 的 t1t_1 不会被执行器之外的路径改写;任何延长都必须产生新的策略决策与新租约。因此旧租约在 t1t_1 之后不再授权任何调用。若执行器缓存了授权结果或跳过检查,则前提 (1) 不成立,结论不成立——这正是「租约存在但执行器不查」的故障模式。∎

∎

这个定理看起来近乎同义反复,但它的三个前提正是事故现场最常见的缺口:执行器缓存了「已授权」的布尔值;续期接口只要一个 refresh token;容器时钟漂移导致过期判断晚了几分钟。定理的价值不在证明,而在把这三个前提变成检查项。

12.3.3 break-glass:把例外变成可复核的流程

break-glass 是治理里最危险、也最容易被滥用的机制。它的目的不是「绕过审批」,而是在事故中把审批挪到事后,同时提高证据等级。一个可执行的 break-glass 至少要满足六条:

要求机制反例
有事件号必须关联 incident_id,无号拒绝「先开了再说,事后补单」
范围最小只开当前故障需要的 scope 与资源直接给 prod:admin
TTL 短15–30 分钟,到期自动失效开到「问题解决为止」
强制通知签发即通知安全/值班/负责人只在本地日志留一行
事后复核24 小时内由独立角色复核并结论事故结束就没人再看
不可静默使用时打标,指标与告警可见与普通调用无区别

这张表说明什么:break-glass 的成本要高于正常审批,而不是低于。如果 break-glass 比走审批更快、更省事,它就会成为默认路径;那时它不再是例外,而是绕过的后门。

12.3.4 权限漂移:实际使用与授予的差集

权限不会一次性配错,它会慢慢漂移:临时授权忘了收、转岗后角色没换、工具升级后 scope 变宽、共享账号被多人使用。检测漂移不需要等审计,只要定期对比两个集合:

  • GG:当前授予的权限集合(按主体、scope、资源);
  • UU:窗口期内实际使用的权限集合。

两个差集分别对应两类问题:

未使用授予=G∖U,未授予使用=U∖G. \text{未使用授予} = G \setminus U,\qquad \text{未授予使用} = U \setminus G.

前者是回收候选:授予了但从未使用,说明权限过宽或主体已不需要。后者是事故候选:使用了但从未授予,说明越权、旁路、共享凭证或日志缺口。两类都要查,但优先级不同:未授予使用优先,因为它可能正在发生。

漂移类型证据根因处置
过度授予unused 持续 30 天按角色批量授权,未按任务收紧回收;改为 JIT
孤儿授权主体已离职/下线,G 仍有记录生命周期事件没触发回收自动禁用;责任人确认
影子访问U \setminus G 非空共享密钥、旁路脚本、日志缺失立即调查;轮换凭证
scope 蔓延同一工具 scope 逐月变宽为个别需求改全局策略拆分工具;按租户/动作细分

这张表说明什么:漂移检测的输出不是一张报表,而是工单。每条 unused 要有回收期限与责任人;每条 ungranted 要有调查结论。没有处置的检测,只是让报表变长。

实验里的 detect_permission_drift() 就是这两个差集的最小实现:

python3 - <<'PY'
import sys
sys.path.insert(0, "experiments/ch12")
from policy_engine import detect_permission_drift
report = detect_permission_drift(
    {"agent-1": {"repo:read", "prod:deploy"}},
    {"agent-1": {"repo:read"}, "agent-2": {"secret:read"}},
)
print(report.to_dict())
PY
{'clean': False, 'unused': {'agent-1': ['prod:deploy']}, 'ungranted': {'agent-2': ['secret:read']}}

真实系统里,G 来自 IAM/租约库,U 来自执行器的使用日志;两边都要有稳定的主体 id 与 scope 命名,否则差集会充满假阳性。漂移检测的第一项工程任务不是写查询,而是统一命名。

12.3.5 权限评审:从定期 review 到持续对账

季度权限 review 的典型失败是「看表格点确认」:表格列的是角色与授予,不列实际使用;评审人没有上下文,只能全部保留。把 review 改成持续对账,能同时降低风险和评审成本:

节奏动作输入输出
每日差集计算授予快照 + 使用日志unused / ungranted 工单
每周高风险租约复核本周 break-glass、超时、异常放行复核结论与整改项
每月孤儿主体清理离职/下线事件 + 授予自动禁用清单
每季角色与策略重审命中率、漂移趋势、事故策略变更单
事件驱动异常使用新 IP、非工作时间、跨租户立即调查或撤销

这张表说明什么:review 的对象要从「权限清单」变成「差异与事件」。前者只会得到「都还需要」;后者会得到具体的回收、调查与策略变更。评审人不需要看几百行授予,只需要看几十条有差异的记录。

12.4 Human-in-the-loop:审批点、四眼原则与超时

Human-in-the-loop 最常见的失败不是「没有人」,而是「人出现在错误的位置」。如果审批发生在动作已经执行之后,人只能事后追认;如果审批请求里只有一句「是否同意」,人只能凭感觉点;如果每个低风险动作都要审批,人会在第 200 次点击时失去注意力。审批点的选择,比审批界面的设计重要一个数量级。

12.4.1 在哪里放审批点

审批点应该放在不可逆、影响面大、信息不对称的地方。四个判据最常用:动作是否可逆、金额/影响是否超过阈值、是否跨租户或跨边界、是否首次执行。任一为真,就值得一个人看一眼;多个为真,就需要两个人。

场景典型例子默认策略审批强度
不可逆删除、打款、发送对外邮件、物理动作必须审批1–2 人,含业务负责人
高金额/高影响退款、折扣、资源扩容、批量导出超过阈值审批财务/数据所有者
跨租户/跨边界跨 tenant 查询、跨 region 复制默认拒绝数据所有者 + 安全
首次执行新工具、新收款方、新部署目标首次审批,之后可自动工具 owner + 业务
低风险高频只读查询、状态轮询、格式化自动抽样审计,不逐次审批
已有回滚可幂等重试的 staging 变更自动 + 观察异常时人工

这张表说明什么:审批强度要和可逆性成反比。可逆动作可以自动,因为错了能回滚;不可逆动作必须审批,因为错了只能赔偿。把审批均匀撒在所有动作上,等于把最需要注意力的人耗在不需要看的事情上。

12.4.2 审批请求:给证据,不给一大段日志

审批人不是调试者。他在 30 秒内要判断的是「这个动作是否应该发生」,而不是「这个 agent 的内部状态是什么」。因此审批请求必须是一张证据卡,而不是日志导出。

定义 12.4.1 · 审批请求

一条可执行的审批请求是一个九元组

q=(a, Δ, b, ρ, ϵ, v, ℓ, t, r), q = (a,\ \Delta,\ b,\ \rho,\ \epsilon,\ v,\ \ell,\ t,\ r),

其中:

  • aa 是待执行动作与资源标识;
  • Δ\Delta 是动作将改变的具体差异(金额、目标账户、删除范围、部署版本);
  • bb 是影响面(blast radius):涉及多少记录、哪些租户、是否对外;
  • ρ\rho 是可逆性与回滚方案;无法回滚时明确写「不可逆」;
  • ϵ\epsilon 是环境(prod/staging)与执行窗口;
  • vv 是策略版本、命中的规则与风险等级;
  • ℓ\ell 是证据链接:trace、diff、测试结果、历史同类动作;
  • tt 是截止时间与超时行为;
  • rr 是请求者、替代方案与「不做会怎样」。

审批请求必须绑定一个冻结的参数摘要。批准的是摘要,不是一段可被后续修改的自然语言。

九项里最容易被删掉的是 Δ\Delta 与 ρ\rho。只写「申请退款 1.25 万」,审批人无法判断收款账户是否变更;只写「回滚服务」,审批人无法判断回滚到哪个版本、是否会丢数据。一个实用的审批卡长这样:

动作      refund  invoice:INV-2091  (acme / prod)
金额      1,250,000 分(¥12,500.00)
差异      收款账户 2026-10-16 由 A 改为 B;B 为新账户(3 天)
影响      单笔;该客户本月第 1 次退款;不可逆
回滚      不可回滚;可发起追回工单,预计 3–5 个工作日
依据      政策 v2026.10.1 / high-value-refund;风控分 0.82(高)
证据      trace:8f3a  diff:acct-change  history:INV-1880 同类已退
超时      15 分钟未批 → 拒绝;拒绝后可补充材料重新提交
请求者    support-3;替代方案:先冻结账户,等待 24h 人工核验

这张卡的信息量大约是一屏。它不包含 prompt、不包含模型自述、不包含几十行工具日志;它包含审批人真正需要的六个判断:做什么、改什么、影响多大、能不能回滚、凭什么、来不及怎么办。

审批不是盖章,是一次有成本的决定

盖章的特征是:不读、不问、不承担。审批的特征是:读了差异、问了疑点、签字后要能解释。让审批有效,不是加一个「同意」按钮,而是让「拒绝」有明确后果、让「同意」有明确依据、让「不处理」有默认结果。三者缺一,审批就退化成盖章。

12.4.3 四眼原则:两个独立的人,而不是两个会话

四眼原则(four-eyes principle)要求至少两个独立主体参与批准。工程上,「独立」至少包含四层:

独立性要求常见伪实现
身份独立两个不同的认证主体同一人用两个账号;共享账号
职责独立发起者不能批准;最好不同团队同一人先发起再「转交」自己
信息独立两人看到同一份冻结证据第二人只看到第一人的结论
时间独立两次批准有最小间隔一键「全部批准」

这张表说明什么:两个会话不等于两个人。共享账号、代点、批量批准都会让四眼退化成一眼。技术上的最小校验是:approver != requester、approver 去重、审批对象是参数摘要的哈希、两次批准记录各自带时间与身份。更强的做法是要求不同认证器(密码 + 硬件密钥),或不同团队的角色。

四眼不是越多越好。三个人审批一个低风险变更,不会提高安全性,只会提高「找两个人一起点」的社交成本。审批人数应与风险等级挂钩:中风险 1 人,高风险 2 人,关键动作 2 人且其中一人来自安全/风控。人数之外,还要规定谁不能批:发起者、受益人、直接下属、与被审批动作有利益冲突的人。

12.4.4 超时与降级:默认拒绝,而不是默认放行

审批一定会遇到「没人看」。超时行为必须在设计时写死,不能留给值班人临场决定。三种可选策略:

策略行为适用风险
fail-closed超时即拒绝,需重新申请高风险、不可逆业务延迟;可能促使绕过
escalate超时升级到下一级/值班主管中风险、有时限升级链断掉时仍会卡住
degrade降级为只读/预览/排队,不执行副作用可分解动作实现复杂;需保证降级路径真的无副作用

这张表说明什么:不能自动放行高风险动作。超时的默认结果必须是「没有发生」,而不是「视为同意」。如果业务无法接受拒绝,正确做法是提前扩容审批人、缩短审批链、把动作拆成可逆的小步,而不是把超时改成同意。

一个审批状态机至少要有五个状态:pending、approved、denied、expired、cancelled。每个状态转移都要写一条审计事件,带 wait_s 与 degraded_to。超时事件要记录「当时还差几个人」「最后一次提醒在何时」「升级给了谁」,否则事后无法区分「没人看」与「系统没通知」。

12.4.5 审批疲劳:注意力是有限预算

审批疲劳不是态度问题,是负载问题。当一个审批人每小时收到 40 条请求,每条只有 7 秒处理时间,他的最优策略就是点「同意」——不是因为不负责,而是因为拒绝需要调查,而调查没有时间预算。疲劳的根因通常不是人少,而是审批点放得太多、请求信息太差、批量操作太方便。

可观测的疲劳指标:

指标定义危险信号
审批负载每人每小时待批数持续 > 20
停留时间打开到决定的秒数中位数 < 10 秒
批准率approved / decided> 95% 且高风险也如此
批量比例批量批准 / 总批准高风险出现批量
非工作时间夜间/周末审批占比持续上升
推翻率事后复核推翻 / 抽样> 5%

这张表说明什么:批准率高不一定是好事。如果高风险请求也以 98% 通过、中位停留 6 秒,说明审批没有产生信息,只是在产生记录。此时应该做的不是加人,而是把低风险请求移出人工队列:提高自动批准上限、按风险路由、对低风险做抽样而不是逐条审批。

批量批准要单独限制。低风险的批量操作可以保留,但高风险动作必须逐条批准,且每条都绑定自己的参数摘要。一个常见的反模式是「全部批准」按钮把 30 条请求一起通过,其中一条是跨租户导出;审计记录上看似有 30 个批准,实际上只有一次注意力。

方法 12.4.1 · 审一张审批卡
  1. 看差异:金额、账户、范围、版本是否与请求描述一致;不接受「详见附件」。
  2. 看影响面:涉及多少记录/租户/用户;是否对外;是否触及敏感数据。
  3. 看可逆性:能否回滚;回滚需要多久;最坏情况损失是多少。
  4. 看依据:策略版本、风险分、历史同类动作;风险分高在哪里。
  5. 看替代方案:不做会怎样;有没有更小的动作能达到目的。
  6. 做决定:批准、拒绝、要求补充证据;拒绝要写理由,批准要确认参数摘要。
  7. 留记录:审批意见进入审计事件;高风险动作标记事后复核。

Q1 一张审批卡写了「退款 1.25 万,已通过风控」,但没写收款账户变更。审批人应该怎么做?

答案

拒绝或要求补充,而不是批准。收款账户变更是退款欺诈最常见的信号之一;「已通过风控」是结论,不是证据。审批人需要看到账户变更时间、新账户历史、与请求者的关系,以及是否有回拨机制。缺少这些信息时,正确动作是「暂停 + 补充材料」,并记录拒绝理由,而不是用自己的经验替系统补证据。

Q2 团队把高风险请求也接入了「全部批准」,理由是「每天只有三五条」。这样做的问题是什么?

答案

问题不在数量,而在注意力与证据的绑定。批量批准把多条请求压缩成一次点击,审批人实际上没有逐条检查差异;一旦其中一条被替换参数或跨租户,记录上仍然显示「已批准」。高风险动作必须逐条批准,每条绑定独立的参数摘要与证据;批量只适用于低风险、同质、可抽样的请求。

12.5 策略即代码:OPA/Cedar 思路与决策日志

当策略只存在于文档或代码的 if 分支里,它会有三个问题:无法独立测试、无法解释一次决定、无法安全地灰度。策略即代码(policy as code)要解决的就是这三件事:把策略从业务代码里抽出来,变成有版本、有测试、有决策日志的构件。

12.5.1 三种形态:硬编码、外部策略引擎、授权语言

形态例子优点缺点
硬编码if amount > 500_000: require_approval()简单、快改策略要发版;难以审计;易散落
外部策略引擎OPA/Rego、Cedar、自研 DSL策略独立版本、可测试、可灰度多一个服务;输入契约要稳定
授权库/语言Cedar、OpenFGA 等类型化、可分析学习成本;表达能力有边界

这张表说明什么:不是所有系统都需要 OPA。策略少、变更慢时,硬编码加上良好的测试也能工作;但一旦出现「不同租户不同限额」「不同环境不同审批人」「监管要求按地区变化」,策略就会从几个 if 变成一张矩阵,此时外部化才划算。

12.5.2 Rego 与 Cedar:两种思路,不写语言教程

OPA(Open Policy Agent)用 Rego 描述「输入文档是否满足条件」。它的典型形态是:宿主把请求打包成 JSON,交给 OPA 求值,得到 allow/deny 或结构化决策。下面是一段示意,不是完整策略:

package agent.authz

default allow := false

deny if {
    input.subject.tenant != input.resource.tenant
}

require_approval if {
    input.action == "refund"
    input.context.amount_cents > 500000
    input.subject.tenant == input.resource.tenant
}

allow if {
    input.action == "read"
    input.subject.tenant == input.resource.tenant
}

三段规则可以同时为真。Rego 只负责求值,优先级必须由调用方显式定义;本章实验用 deny > require_approval > allow 的保守顺序。若把优先级交给规则书写顺序,策略行为会随文件重排而变。Rego 的强项是通用:输入是任意 JSON,策略可以遍历数组、引用数据文档、组合多个包。它适合基础设施授权、Kubernetes 准入、跨服务统一策略。代价是:输入 schema 靠约定,复杂策略的调试成本不低,性能取决于求值方式。

Cedar 走另一条路:实体(principal/resource)、动作、上下文都是类型化的,策略是 permit/forbid 规则,forbid 默认覆盖 permit。它的典型形态:

permit (
    principal,
    action == Action::"read",
    resource
) when {
    principal.tenant == resource.tenant
};

forbid (
    principal,
    action,
    resource
) when {
    principal.tenant != resource.tenant
};

Cedar 的强项是可分析:类型化让策略可以静态检查,forbid 覆盖让「默认拒绝」更容易表达。它适合应用内的细粒度授权,尤其是多租户 SaaS。代价是:实体模型要先设计好,跨系统的通用策略不如 Rego 自然。

维度Rego / OPACedar
输入任意 JSON 文档类型化实体 + 动作 + 上下文
默认策略作者显式写 default无 permit 即拒绝;forbid 覆盖
强项通用、生态广、适合基础设施类型安全、可分析、适合应用授权
测试opa test + 表驱动用例策略测试 + 授权请求批量验证
决策日志宿主记录 input/result/版本宿主记录 request/decision/版本

这张表说明什么:两者都能表达 allow 与 deny;本章的 require_approval 需要引擎把「审批」作为结构化输出(例如 obligations 或单独的 decision 实体),不能只返回一个布尔。选型时先看输入是否稳定、是否需要跨系统复用、团队是否能维护,而不是看语法是否优雅。语言不是治理的难点,输入契约与版本管理才是。

策略不是 if 语句,是可回放的决定

一段 if 只告诉你「现在走哪个分支」。一条策略要能回答:当时输入是什么、命中哪条规则、依据哪版策略、谁改过这版策略、如果改成新版本会发生什么。把策略当代码,不是为了写得漂亮,而是为了让它像代码一样被测试、审查、回滚和追责。

12.5.3 策略生命周期:测试、影子、灰度、回滚

一个可执行的策略生命周期有六步:

  1. 建模输入:定义主体、动作、资源、上下文的 schema;拒绝自由文本进入策略。
  2. 写规则与测试:每条规则至少一个 allow、一个 deny、一个边界用例;测试和策略同仓库。
  3. 影子模式(dry-run):新策略只计算、不执行,把「新旧决策差异」写进日志;差异率高就先查输入。
  4. 灰度:按租户/动作/比例启用;高风险动作先小范围,保留快速回滚开关。
  5. 强制:全量启用;决策日志带策略版本与包哈希;异常决策告警。
  6. 复盘与下线:定期清理不再命中的规则;删除前确认没有流量与审计依赖。

每条规则至少配三行测试,测试与策略同仓库、同一次代码评审:

用例输入期望
同租户只读action=read, tenant 相同allow
跨租户读取action=read, tenant 不同deny
大额退款action=refund, amount=1,250,000require_approval
边界金额action=refund, amount=500,000allow(阈值是严格大于)
规则异常条件抛异常deny(fail-closed)

这张表说明什么:测试要覆盖「允许、拒绝、审批、边界、异常」五类,而不是只测 happy path。边界用例尤其重要:> 与 >= 的一字之差,会让审批阈值在真实金额上失效。

影子模式是这里最重要的工程实践。它把「策略变更」从一次不可逆的生产切换,变成一次可比较的实验:

旧策略: allow         新策略: require_approval   diff: 12 条/小时
旧策略: deny          新策略: allow              diff: 3 条/小时   ← 优先审查

第二行最危险:新策略放宽了原本被拒绝的请求。每一条差异都要有人解释:是修 bug,还是放宽了边界?没有解释的放宽,不应该上线。

12.5.4 决策日志:策略的「执行回执」

策略引擎每次求值都要留下决策日志,字段与 12.2 的审计事件对齐:input_digest、policy_version、bundle_hash、decision、rule_id、obligations、latency_ms。日志要能回答三个问题:这次为什么是这个结果、当时用的是哪版策略、如果换成新策略会怎样。

{
  "request_id": "req-002",
  "input_digest": "sha256:5c1e...",
  "policy_version": "2026.10.1",
  "bundle_hash": "sha256:9a0d...",
  "decision": "require_approval",
  "rule_id": "require-approval-high-value-refund",
  "obligations": ["four_eyes", "finance_ticket"],
  "latency_ms": 2
}

四个常见坑:

  • 策略求值与执行之间的 TOCTOU:策略看到的是旧属性,执行时属性已变。对策是把决策绑定到输入摘要,执行器拒绝摘要不匹配的请求。
  • 义务不执行:策略返回 four_eyes,执行器只检查了 allow。对策是把 obligations 变成执行器的强制参数,而不是注释。
  • 版本漂移:决策日志记了版本,但策略包没有哈希;同名版本被覆盖后无法复现。对策是版本 + 内容哈希双记录。
  • 策略蔓延:规则越加越多,没人知道哪条还在生效。对策是命中率统计与定期清理,删除前跑影子模式确认无差异。

12.6 合规映射:NIST AI RMF、ISO 42001 与 EU AI Act

合规不是把法条抄进代码,而是把外部要求翻译成可检查的工程产物。同一件事通常有三种说法:框架说「建立治理职能」,标准说「形成文件化信息」,法条说「实施风险管理体系」。工程师需要的是第四种说法:哪个字段、哪条日志、哪个测试、谁签字。这一节只做这种翻译,不解释法律条文,也不构成法律意见。

12.6.1 NIST AI RMF:四个函数与生成式 AI Profile

NIST AI Risk Management Framework(AI RMF 1.0,2023)是自愿性框架,不是认证;它把 AI 风险管理组织成四个函数:Govern、Map、Measure、Manage。它们不是四个阶段,而是持续循环的四类活动:

函数核心问题工程产物本书落点
Govern谁负责、什么政策、如何问责?治理四元组、责任矩阵、策略版本12.1、12.5
Map系统在什么场景、影响谁、有什么风险?用途清单、数据流、风险登记册11.1、12.6
Measure风险如何度量、证据是什么?评估集、红队结果、审计指标9.4–9.6、11.7
Manage风险如何处置、残余风险由谁接受?处置计划、审批记录、复核结论12.4、12.7

这张表说明什么:Govern 不是「写一份治理文档」,而是把另外三个函数需要的决策权、证据与时效定下来。没有 Govern,Map 会变成一次性清单,Measure 会变成没有阈值的报表,Manage 会变成出了事再开会。

NIST 的 Generative AI Profile(NIST AI 600-1,2024)在 AI RMF 之上补充了生成式 AI 特有的风险,例如 confabulation、信息完整性、知识产权、数据隐私、信息安全和有害内容。对 agent 系统,最直接的可执行项是:

  • confabulation:把「模型声称完成」与「系统验证完成」分开;工具结果与验收测试进入 trace。
  • 数据隐私:审计事件最小化;训练/检索数据来源可追溯;删除请求有执行路径。
  • 信息安全:prompt injection 的边界在能力隔离与出口约束,见第 11 章。
  • 信息完整性:对外发布的内容要有来源与人工复核点,尤其是高影响领域。

12.6.2 ISO/IEC 42001:管理体系落到文件与记录

ISO/IEC 42001:2023 是 AI 管理体系(AIMS)标准,结构沿用 ISO 管理体系的高层结构:组织环境、领导作用、策划、支持、运行、绩效评价、改进。它要求组织建立 AI 政策、角色与职责、风险评估与处置、生命周期管理、供应商管理、事件管理、监视与内部审计。

工程上可以把它压成一张「文件 → 记录 → 证据」表:

管理体系要求可执行产物证据在哪里
AI 政策与职责决策权表、责任矩阵、审批角色策略仓库、组织架构、审批日志
风险评估与处置风险登记册、阈值、残余风险接受人风险评审记录、策略变更单
生命周期管理需求、测试、发布、退役的门禁CI 记录、评估报告、发布单
供应商与第三方工具/模型清单、合同条款、评估结果工具注册表、供应商评估
事件管理事故分级、上报路径、复盘事件工单、审计事件、整改项
监视与内部审计指标、抽样、审计计划仪表盘、审计报告、抽样记录

这张表说明什么:认证审核看的是「你说有,能不能拿出来」。如果审批日志只有 7 天、策略版本没有哈希、事故复盘没有整改项,管理体系文件再完整也补不上证据缺口。反过来,证据齐全但没人定期看,也不构成「监视」。

12.6.3 EU AI Act:时间表与工程可执行项

EU AI Act 的适用是分阶段的。下表按公开时间线整理,具体适用日期以欧盟官方最新发布为准:

节点日期内容状态
生效2024-08-01法规生效已生效
禁止性条款2025-02-02禁止特定 AI 实践已适用
通用目的 AI(GPAI)2025-08-02GPAI 模型义务已适用
高风险(原定)2026-08-02附件 III 高风险系统义务可能调整
高风险(附件 I)2027-08-02嵌入受监管产品的系统原定

2025 年 11 月,欧盟委员会提出 Digital Omnibus,其中包含对 AI Act 部分义务适用时间的调整建议。截至本书写作时,高风险系统的最终适用时间仍可能变化;不要把它当成已经钉死的日期。 工程上应做的是:按当前官方时间表规划,但把「合规日历」做成可配置项,而不是写死在代码里。

对 agent 系统,EU AI Act 中最直接的工程可执行项是:

义务方向工程动作证据
风险管理风险登记册 + 每次高风险动作的风险分级风险记录、策略决策
日志与可追溯审计事件、策略版本、执行回执append-only 日志、哈希链
人类监督审批点、四眼、超时、可中止审批记录、人工中止事件
技术文档架构、数据流、工具清单、评估结果文档仓库、版本标签
事件报告严重事件分级、时限、上报路径事件工单、通知记录
上市后监测指标、抽样、漂移检测、回归仪表盘、评估报告

这张表说明什么:合规条款最终都要落到「一条记录 + 一个负责人 + 一个时限」。如果某个条款找不到对应的记录,它就没有被工程化;如果找到了记录但没人负责,它就没有被治理。

合规时间表会变,代码不要把日期写死

监管时间表、指南与豁免会更新。把「2026-08-02」写进策略常量,会让一次法规调整变成一次紧急发版;正确做法是把适用日期、适用地区、适用角色做成配置,并由合规负责人维护。代码只负责在配置生效时执行对应的控制项。另:本节不是法律意见,具体适用请咨询法务。

关于合规的四个误解
  1. 「过了 ISO 42001 认证就等于合规」——认证证明管理体系符合标准,不证明具体系统符合 AI Act 或行业法规;两者范围不同。
  2. 「有审计日志就等于可追溯」——没有策略版本、参数摘要与执行回执的日志,无法重建动作;可追溯性看的是证据链,不是日志量。
  3. 「高风险系统只要加人工审批就行」——人工监督只是义务之一;风险管理、技术文档、日志、事件报告、上市后监测都要有对应产物。
  4. 「法规日期是固定的」——适用时间可能因修订或过渡条款调整;以官方最新时间表为准,系统设计要能配置。

合规映射的终点不是一张对照表,而是证据清单:每个控制项都能指向一条可验证的记录,每条记录都有负责人和保留期。第 12.8 节的实验把其中最小的一环——策略决策与审批记录——跑成可执行的代码;其余环节(风险登记、技术文档、事件报告)需要在组织流程里落地。

12.7 失败案例:审批疲劳、日志缺失与权限漂移

三个案例分别对应治理四元组的不同缺口:审批疲劳是决策权与注意力的问题,日志缺失是证据的问题,权限漂移是时效的问题。案例中的数据经过脱敏与取整,但触发条件、观测证据与修复路径都来自可复现的工程模式。

例题 12.7.1 · 审批疲劳:96% 的通过率掩埋了一次高风险退款

触发条件。 某客服平台把所有退款请求都送进同一个人工队列,包括 500 元以下的小额退款。12 名值班人每月处理约 1,847 条请求,人均每个工作日约 7 条,看起来不高;但请求集中在两个高峰时段,峰值时每人队列里有 30–40 条待批。

观测证据。 审计日志显示:当月批准率 96.2%,中位停留时间 7.4 秒,92% 的批准没有填写意见。一条 1,250,000 分的退款在 02:41 进入队列,审批卡只写了「退款 1.25 万,已通过风控」,没有收款账户变更信息;值班人在 4 秒内批准,当时他的队列里有 38 条待批。三天后客户投诉收款账户不是本人,资金已转出。

根因。

  1. 低风险请求没有自动批准,占用了 80% 的注意力;
  2. 审批卡只给结论(「已通过风控」),不给差异(账户变更)与证据;
  3. 队列没有按风险分级,高风险请求和 50 元退款混在一起;
  4. 系统没有审批负载指标,管理层只看到「96% 通过、处理很快」;
  5. 批量批准按钮让「一次点击通过 20 条」成为常规操作。

修复。

  • 50,000 分以下、同租户、无账户变更的退款自动批准,并做 5% 抽样;
  • 高风险队列独立,审批卡强制包含金额、账户变更、影响面、回滚方案;
  • 每人每小时待批上限 15 条,超过自动升级到第二值班组;
  • 高风险禁止批量批准,批量只保留给低风险同质请求;
  • 新增指标:中位停留、批准率、批量比例、事后推翻率;当「高风险中位停留 < 15 秒」时告警。

结果。 三个月后,71% 的退款自动完成;高风险请求的中位停留升到 96 秒,拒绝率 6.1%;抽样推翻率从 0.3% 升到 1.8%——不是变差了,而是以前根本没在看。

例题 12.7.2 · 日志缺失:一次生产删除没有证据链

触发条件。 某数据平台的运维 agent 在 02:14 调用迁移工具删除了一张生产表。应用日志写 stdout,保留 7 天;策略判定在服务内存里完成,没有落盘;删除操作使用平台管理员账号,没有单独审批。

观测证据。 事故发生后,团队能找到的只找到 3 行有用的应用日志:

02:14:01 INFO  tool=db.migrate action=delete target=orders_v2
02:14:01 INFO  result=ok rows=0
02:14:02 INFO  task finished

没有 request_id,没有策略版本,没有审批记录,没有执行回执,也没有「谁发起了这次调用」。SIEM 里有网络日志,但只能看到服务账号访问了数据库,无法区分是哪个 agent、哪次任务。调查花了三天,最后靠值班人回忆才确认是「清理测试表」的脚本跑错了环境。

根因。

  1. 日志是为调试写的,不是为审计写的:字段不全、无关联键;
  2. 保留期 7 天,等到要查时已经轮转;
  3. 危险动作没有审批,也没有 break-glass 事件号;
  4. 执行器没有返回可核对的回执(删了哪张表、多少行、哪个环境);
  5. 没有哈希链或 WORM,无法证明日志未被修改。

修复。

  • 定义审计事件 schema:request_id、actor、action、resource、environment、policy_version、decision、params_digest、evidence_ref;
  • 审计日志写入独立存储,启用对象锁,保留 1 年;链头每日锚定到独立账号;
  • 生产删除必须走审批或 break-glass,执行器返回回执并写审计;
  • 每季度做一次「只给审计记录,重建一次删除」的演练,10 分钟内答不出五个问题就补字段;
  • 把 params_digest 与执行回执比对纳入发布门禁。

结果。 下一次类似操作在 6 分钟内完成重建:谁发起、什么策略、谁批准、删了哪张表、影响多少行。日志量下降了 40%,因为不再把完整请求体写进审计事件,而是存摘要与引用。

例题 12.7.3 · 权限漂移:一次 30 分钟的临时权限活了 94 天

触发条件。 一次线上故障中,on-call agent 需要临时读取生产数据库的慢查询日志。值班人通过工单系统给它加了 prod:admin,计划「故障结束后回收」。故障在 40 分钟后解除,工单关闭,权限没有回收。

观测证据。 94 天后做权限盘点时发现:

  • IAM 里 agent-oncall 仍持有 prod:admin,签发时间 94 天前,无过期时间;
  • 使用日志显示这条权限在 94 天里只被用过 2 次,都是故障当天;
  • 同期授予的 37 条临时权限中,有 14 条从未使用,未使用率 38%;
  • 其中一条凭证在非工作时间从新的出口 IP 使用过一次,无法解释;
  • 季度 review 的表格只列了「角色是否仍然存在」,没有列「实际是否使用」。

根因。

  1. 临时权限绑定工单,不绑定 TTL;工单关闭不触发回收;
  2. 权限粒度太粗:只需要读慢查询日志,却给了 prod:admin;
  3. review 以「授予」为对象,不以「使用」为对象,漂移不可见;
  4. 没有 break-glass 流程,临时授权走的是普通工单;
  5. 没有「未授予使用」告警,异常调用无人发现。

修复。

  • 所有临时权限改为 JIT 租约:TTL 30 分钟、scope 限定到 db:slowlog:read、到期自动失效;
  • 故障场景走 break-glass:必须带 incident_id、通知安全、24 小时内复核;
  • 每日对比授予与使用:unused 超过 30 天自动进入回收工单,ungranted 立即告警;
  • 季度 review 改为「使用记录 + 租约记录」双表,不再只看角色;
  • 新出口 IP 的凭证使用触发二次认证或拒绝。

结果。 临时权限的平均存活时间从 94 天降到 27 分钟;未使用授予从 38% 降到 6%;ungranted 事件从每月 11 条降到 1 条,且每条都有调查结论。

三个案例的共同模式可以压成一张表:

案例缺失的治理对象第一份可用证据最小修复
审批疲劳决策权(风险路由)+ 责任(负载)审批停留时间、队列长度、批准率低风险自动 + 高风险独立队列
日志缺失证据(schema/完整性)执行回执、策略版本、参数摘要审计事件 + WORM + 锚定
权限漂移时效(TTL/回收)授予时间、使用记录、差集JIT 租约 + 漂移检测

这张表说明什么:三个案例都不是「模型变坏了」。它们分别是路由、证据、时效的工程缺口;修复也都不需要换模型,只需要改策略、日志与权限生命周期。

12.8 实验:策略引擎 + 审批工作流

实验把本章的三条主线压成可运行的最小件:策略求值与版本、审批状态机与四眼/超时/break-glass、审计哈希链与权限漂移。全部只依赖 Python 3.11+ 标准库,时钟与输入都是确定的,输出可逐字复现。

实验目录是 experiments/ch12/,四个文件分工如下。

12.8.1 文件与运行

文件作用关键实现
policy_engine.py主体/动作/资源/上下文 → allow / require_approval / deny优先级、默认拒绝、策略版本、dry-run、JSONL 哈希链
approval_flow.py风险分级、四眼、超时、break-glass状态机、审批人角色、审计事件、统计
test_governance.py16 条回归测试优先级、默认拒绝、超时、break-glass、漂移
README.md运行方式、语义、限制与正文一致
cd agent-systems
python3 experiments/ch12/policy_engine.py demo --log /tmp/ch12-decisions.jsonl

真实输出(macOS,Python 3.13.13):

[           allow] req-001 read    invoice:INV-1042       rule=allow-own-tenant-read              policy=2026.10.1 dry_run=false
[require_approval] req-002 refund  invoice:INV-2091       rule=require-approval-high-value-refund policy=2026.10.1 dry_run=false
                   obligations=four_eyes,finance_ticket
[            deny] req-003 delete  db:prod-orders         rule=deny-prod-destructive              policy=2026.10.1 dry_run=false
[require_approval] req-004 export  table:customers        rule=require-approval-sensitive-export  policy=2026.10.1 dry_run=true
                   obligations=data_owner_approval,watermark
log=/tmp/ch12-decisions.jsonl events=3
chain=chain OK events=3

四行分别演示:同租户只读被允许;大额退款需要审批;生产删除被拒绝;敏感导出在 dry-run 下只计算、不写日志。最后一行 events=3 是 dry-run 的直接证据——四条决策只落了三条。

python3 experiments/ch12/approval_flow.py demo --log /tmp/ch12-approvals.jsonl
[1] read     invoice:INV-1042         risk=low      status=approved mode=auto        approvers=[] wait=0s
[2] update   config:checkout          risk=medium   status=approved mode=human       approvers=['sre-lead'] wait=90s
[3] refund   invoice:INV-3099         risk=high     status=approved mode=human       approvers=['finance-2', 'finance-4'] wait=150s
[4] export   table:customers          risk=critical status=denied   mode=human       approvers=[] wait=300s
    degraded_to=deny_and_page_oncall reason=timeout
[5] rollback service:checkout         risk=critical status=approved mode=break_glass approvers=[] wait=0s
    post_review_due=2026-10-20T10:18:20Z incident=INC-2026-1019

rejected attempts:
  support-3    apr-003 reason=self_approval

stats:
  submitted=5 auto=1 human=2 timeout=1 break_glass=1 rejected=1
  approval_rate=0.667 mean_wait_s=120.0 anomalous_release=1
  chain=chain OK events=12 ok=true
log=/tmp/ch12-approvals.jsonl

五步覆盖五种路径:低风险自动、中风险单人、高风险四眼(含自批被拒)、超时拒绝并记录降级动作、break-glass 带事件号。实验状态机只保留 pending / approved / denied 三态;生产还要补 expired / cancelled,以及对应的通知与撤销路径。approval_rate=0.667 的分母是「人工批准 + 超时」,不含自动批准与 break-glass;anomalous_release=1 只统计 break-glass,因为它是唯一绕过常规审批的放行。

12.8.2 测试与观测

python3 -m unittest discover -s experiments/ch12 -q
----------------------------------------------------------------------
Ran 16 tests in 0.006s

OK

耗时随机器变化;测试数量与断言内容是判据。测试里最值得看的是四条负向断言:

  • test_default_deny:没有规则命中时必须是 deny;
  • test_policy_error_fails_closed:规则条件抛异常时不能跳过,必须 deny;
  • test_timeout_fails_closed:到达 deadline 仍未凑够人数 → denied,且审计带 wait_s=600;
  • test_break_glass_requires_incident_and_is_audited:无事件号拒绝,有事件号放行但写 post_review_within_24h。

从审计日志统计审批通过率、平均等待与异常放行,可以用实验自带命令,也可以直接用 jq:

python3 experiments/ch12/approval_flow.py stats /tmp/ch12-approvals.jsonl
submitted=5 auto=1 human=2 timeout=1 break_glass=1 rejected=1
approval_rate=0.667 mean_wait_s=120.0 anomalous_release=1
jq -s '{granted: ([.[] | select(.event_type == "approval_granted")] | length),
        timeout: ([.[] | select(.event_type == "approval_timeout")] | length),
        break_glass: ([.[] | select(.event_type == "break_glass_used")] | length),
        mean_wait_s: ([.[] | select(.event_type == "approval_granted") | .evidence.wait_s] | (add / length))}
       | . + {approval_rate: (.granted / (.granted + .timeout))}' /tmp/ch12-approvals.jsonl
{
  "granted": 2,
  "timeout": 1,
  "break_glass": 1,
  "mean_wait_s": 120,
  "approval_rate": 0.6666666666666666
}

这条查询说明什么:approval_rate 只应由「有明确结论的人工审批」计算;把自动批准塞进分母,会让通过率虚高;把 break-glass 塞进分子,会掩盖「绕过常规审批」的比例。指标口径要在代码里写死,而不是在报表里解释。

12.8.3 代码骨架:三个关键判断

策略引擎的核心只有十几行,但三个顺序不能错:

def evaluate(self, request):
    for rule in self.bundle.ordered():      # 高优先级 + 更保守的 tie-break
        try:
            hit = rule.matches(request)
        except Exception:                    # 策略写错时 fail-closed
            return Decision(..., decision="deny", reason="policy_error")
        if hit:
            return Decision(..., decision=rule.effect, matched_rule=rule.id)
    return Decision(..., decision="deny", reason="default_deny")

审批状态机的四个判断同样是顺序敏感:

if approver == request.requester:
    reject("self_approval")
elif approver in state.approvers:
    reject("duplicate_approver")
elif APPROVER_ROLE not in roles:
    reject("role_not_allowed")
elif risk.requires_security and "security" not in roles and not already_has_security:
    reject("security_approver_required")

先查身份,再查重复,再查角色,最后查风险特例。顺序反了会出现「先记了一次批准,再发现是自批」的脏状态。审计日志的哈希链则只有一条规则:hash 覆盖除自己以外的全部字段,prev_hash 指向上一条;任何字段变动都会让后续链断掉。

12.8.4 从实验到生产:还缺什么

这个实验刻意省略了生产必需的六件事,读者不应把它直接部署:

缺项实验现状生产要求
身份审批人是字符串接 IdP,验证认证强度与会话
策略语言Python 函数Rego/Cedar/DSL;策略与代码同仓同流水线,可按团队分目录
存储本地 JSONL对象存储 + 对象锁 + 外部锚定
时钟注入整数秒单调时钟 + NTP 监控 + 漂移告警
通知无审批提醒、超时升级、break-glass 告警
权限仅检测差集对接 IAM/租约库,自动回收与复核

这张表说明什么:实验证明的是语义——优先级、默认拒绝、四眼、超时、break-glass、哈希链——不是生产强度。把语义跑对,再接身份、存储与通知,比先接一堆基础设施却说不清策略语义更可靠。

母本对应

母本章节本书落点程度
NIST AI RMF 1.0(2023)Govern / Map / Measure / Manage12.6.1 四函数与工程产物背景
NIST AI 600-1(2024)Generative AI Profile12.6.1 生成式 AI 风险项背景
ISO/IEC 42001:2023AI 管理体系要求12.6.2 文件、记录与证据背景
EU AI Act(Regulation 2024/1689)分阶段适用、高风险义务12.6.3 时间表与工程映射交叉引用
OPA / Rego 文档策略求值、bundle、决策日志12.5.2 Rego 思路背景
Cedar 文档类型化授权、forbid 覆盖12.5.2 Cedar 思路背景
NIST SP 800-92日志管理与保留12.2 事件、保留、完整性背景
《Linux 系统与内核》第 2 篇2.9 进程凭证、用户、组与能力12.3 权限机制的上游交叉引用
本书第 4 章capability、seccomp、landlock12.3 工具级/环境级约束的机制层交叉引用
本书第 10 章trace、日志、回放12.2 证据引用的来源交叉引用
本书第 11 章威胁模型、注入、出口约束12.1 风险输入的来源交叉引用

读这张表要抓住两点:合规框架只提供要求的方向,不提供实现;本书对 OS 与安全章节只做交叉引用,不重复讲 capability 与 seccomp 的内核细节。表中「背景」表示正文只借用概念与术语,读者需要时再回母本。

本章陷阱

#陷阱症状正确做法去哪
1把治理写成文档有制度、有委员会,但策略引擎里没有对应规则四元组落到求值器、凭证、日志、复核12.1
2把责任推给模型复盘结论是「模型幻觉」,无人改系统按「谁能改变决定」分责任12.1.2
3审计只记最终答案不知道谁批准、按哪版策略六问 + 八元组事件12.2
4把日志当审计字段不全、无关联键、事故时不可用schema、关联 id、完整性、保留12.2
5哈希链当 WORM有链无锚定,整链可重写WORM + 外部锚定 + 条数锚定12.2.1
6审计日志存完整 PII日志成为隐私仓库,删除困难白名单、令牌化、引用原文12.2.2
7只做工具级 scope数据、时间、环境三层未收四层组合;租约参数化12.3
8临时权限无 TTL一次授权活到转岗/离职JIT 租约 + 到期自动回收12.3.4
9审批点撒得太广低风险淹没高风险,96% 点同意风险路由;低风险自动 + 抽样12.4.5
10审批卡只有结论没有差异、影响面、回滚方案九元组审批卡;冻结参数摘要12.4.2
11超时自动放行没人看等于默认同意fail-closed;升级或降级为只读12.4.4
12策略硬编码、无版本改策略靠发版,事后无法复现策略即代码 + 测试 + 决策日志12.5

这张表说明什么:十二个陷阱里,只有第 2 个是「归因错误」,其余都是可落地的工程缺口。它们不会因为换模型、换框架而消失;第 9 与第 11 条最危险,因为它们让「有审批」看起来像「有控制」。

本章出口检查

检查项达标线没达标去哪
能说清治理四对象决策权、责任、证据、时效各给一个产物12.1
能画决策链请求 → 策略 → 审批 → 执行 → 审计,标出关联 id12.1
能写审计事件六问齐全;含策略版本、参数摘要、证据引用12.2
能解释哈希链边界能证明篡改可见,也能说出截断/重写/锚定失效12.2.1
能做保留与最小化按数据类分层;原文用引用而非副本12.2.2
能设计权限租约八元组齐全;执行器 fail-closed 检查12.3.1
能设计 break-glass事件号、TTL、通知、复核、不可静默12.3.3
能落地四眼与超时自批/重复/角色/security 四道检查12.4.3、12.4.4
能写审批卡差异、影响面、回滚、证据、超时齐全12.4.2
能组织策略生命周期测试、影子、灰度、回滚、决策日志12.5
能映射合规要求每个条款有产物、证据、负责人12.6
能跑实验16 条测试全过;demo 输出 allow/approval/deny12.8
即时自测 · 12

Q1 已经启用了哈希链,为什么还需要 WORM 与外部锚定?

Q2 高风险审批超时后自动放行,会带来什么问题?如果业务无法接受拒绝,应该怎么改?

Q3 策略版本为什么要写进每一条决策日志?只写「当前策略」有什么问题?

答案

A1 哈希链只能发现「记录之间不一致」,不能阻止有写权限的人重写整条链并重算所有哈希;它也不能防止删除尾部记录后剩下的链仍然自洽。WORM 在保留期内阻止删除与覆盖,外部锚定把链头(连同条数/最后序号)放到另一个信任域,使整链重写无法同时自洽。三者防的是不同威胁:单点修改、删除、整链重写。

A2 超时自动放行等于把「没人看」变成「默认同意」,高风险动作会在注意力最稀缺的时候被放行,且审计上仍然显示「已批准」。如果业务不能接受拒绝,正确做法是缩短审批链、增加备份审批人、把动作拆成可逆的小步,或降级为只读/预览/排队;不能把超时改成同意。

A3 策略会变。只写「当前策略」,事后无法知道当时的阈值、规则与义务;同名版本被覆盖后更无法复现。每条决策日志要带 policy_version 与 bundle_hash,才能回答「当时按哪版策略、命中哪条规则、如果换成新策略会怎样」。版本是审计证据的一部分,不是运维元数据。

下一站:13 项目一:可沙箱执行的代码 agent 把本章的策略、审批与审计接进一个真实的执行器:工具注册、沙箱、限额、trace 与红队用例。