留痕与证据链
导出与模型调用的留痕写不进就不交付,链防业务层误操作,不防运维方
SZV 把留痕当证据,不当日志:导出和模型调用两处,留痕与动作同事务,写不进留痕就不交付。留痕里带着投影版本,事后能回答当时给了模型什么。这条链只防业务账号与误操作造成的局部改删;要防握有库管理权限的人,须把链头存到库外。
日志与证据的差别
日志是事后补记:动作做完,再往一个文件里写一行,写失败了动作照样成功。证据的设计要求反过来:动作与它的记录同生共死。
| 日志 | 证据的设计要求 | |
|---|---|---|
| 写入时点 | 动作之后,通常异步 | 与动作同一次提交 |
| 写失败时 | 动作照常完成,缺一行没人知道 | 动作不交付 |
| 事后能说明什么 | 大概发生过什么 | 谁、何时、对什么、做了什么,且中间没有被悄悄改动 |
SZV 在两处做到了这一条。导出文件交出前,必须先在同一事务里写下导出留痕,写不进就不交文件,见查询与导出操作记录。模型调用的留痕写不进,对调用方是失败,页面回到规则生成的文本,宁可降级,不无痕调用,见模型能碰什么 不能碰什么。同事务这一条,只就导出和模型调用这两处而言。
被拒绝的尝试同样留痕。业务拒绝会让整个请求事务回滚,拒绝痕若写在同一个事务里,会随回滚一起消失,所以它在请求事务之外的独立连接里写,只记命令、对象和拒绝码,不记载荷原文。
审计时问「这次导出是谁做的」,答案在导出那一刻就已写好,不必事后追查。
一条链三类痕迹
留痕不是一张表,而是三类痕迹各守各的保证:
| 类 | 存什么 | 保证 |
|---|---|---|
| 统一审计链 | 事件类型、来源命令、对象、载荷指纹、结果分类、时刻、前一条指纹、本条指纹 | 每个租户的每个工作区一条链,序号连续,写入时串行 |
| 领域专表 | 权限变更的前后值与原因,规则表自身的历史,审批单的签核记录,模型调用,导出,日终修订 | 各表自己拒绝改删,业务角色无改删权限 |
| 业务事实表 | 如资金流水 | 只追加,冲回是新增一行,不改旧行 |
统一审计链里不放原始个人信息:写入前扫描黑名单字段名,命中即拒。载荷只存指纹,不存原文。指纹挡住了个人信息进入审计,代价是链本身读不出内容,要看当时的前后值,得回到对应的领域专表。
依据的规则版本也是分散记录的:审批单记签法版本与命中的金额档快照,模型调用记投影版本,规则表有自己的历史。
核对留痕时,先问某件事落在哪一类,再去对应的表核对,不必把所有痕迹当成一张大表。
链能防什么 不能防什么
每条记录带前一条的指纹,指纹由内容算出,内容变一点,指纹就变。校验函数从头逐条重算,能找出这些问题:序号缺口、顺序倒置、前一条指纹不符、本条指纹不符。
触发器拒绝对链表的 UPDATE、DELETE 和清空,业务角色另被收回改删权限。
| 谁、做了什么 | 能否发现或挡住 | 说明 |
|---|---|---|
| 业务账号想改或删一条 | 挡住 | 触发器拒绝,权限也已收回 |
| 绕开应用,改动或删掉链中间的一条 | 能发现 | 序号缺口,或后一条记的指纹对不上 |
| 删掉链尾最新的几条 | 链内发现不了 | 没有后继记录来与它对不上 |
| 库管理员重算并改写整条链 | 链内发现不了 | 全部指纹重新算一遍,链自洽 |
| 表属主停用触发器后改写 | 触发器挡不住 | PostgreSQL 文档允许属主停用触发器,或改变它的触发条件 |
后三行说明了一个事实:同库里有写权限的人,对库内的哈希链没有不可逾越的障碍。这条链防的是业务层面的误操作与局部改删,不防运维方。
要防运维方,须把链头存到库外
透明日志是这一问题的成熟答案。RFC 9162(证书透明 2.0)用 Merkle 树组织记录,日志方对树头签名,外部监视者保存各个时刻的树头,并用一致性证明核对「新树头确实是旧树头的延伸」。开源实现如 Tessera 把这一套做成按瓦片存储的日志。它们让改写历史变得会被第三方发现,依靠的不是库自己的权限。
图里蓝色的一格是库外锚点:周期性把链上最新的序号与指纹写到库外,由企业保管,或写进只能追加的对象存储。核对时,用锚点上的序号与指纹去比链:链尾被截断,或整条被重写,都会与锚点对不上。
统一审计事件的字段里没有签名列,也没有树头,也没有外部监视者。库外锚点是对照透明日志得出的做法,不属于现有能力。有这项要求的企业,可在签约前把它列为交付条件:由谁保管锚点,多久写一次,核对由谁做。
限制与不适用
- 排队:每个租户的每个工作区一条链,追加用事务级咨询锁,锁到事务提交才放。所有要写这条链的事务在它上面排队,单个工作区的写入并发是一个上限。
- 内容不可直读:链只存载荷指纹。要看前后值,到领域专表查。
- 校验靠调用:校验函数被调用时,才从头逐条重算。
- 范围边界:留痕只记系统内发生的写入动作。线下沟通不在其中,导出文件离开系统之后的使用也不在其中。
- 要求防运维方,或要求第三方能独立验证时,库内的这条链不够,要加外部锚点与签名。
与审计标准对照
| 要求 | 来源 | 这条链的覆盖 |
|---|---|---|
| 保护审计信息不被未授权访问与篡改 | NIST SP 800-53 AU-9 | 防业务账号与局部改删;防运维方须另设库外存放 |
| 不可否认 | 同上 AU-10 | 不覆盖:字段中没有签名,不能向第三方证明「某人确实做了」 |
| 审计记录写到独立的系统或组件 | 同上 AU-9 的增强项 | 未覆盖:记录与业务数据同库 |
| 把语句级审计写到库外 | pgAudit 的做法,经数据库日志再由运维外送 | 不同层次:它记数据库收到的语句,SZV 记业务动作及其对象 |
同库存放与库外存放有张力:同库,导出与模型调用的留痕才能与动作共用一次提交,写不进就不交付;库外才能防运维方。两者同时要,办法是同库写、再把链头外放,即上一节的锚点。
其他做法
| 做法 | 它解决什么 | 为什么不用 |
|---|---|---|
| 载荷明文写进审计链 | 审计直接读到全部内容 | 个人信息进入审计,访问范围随之扩大;SZV 禁止原始个人信息字段进审计,载荷只存指纹,内容放在领域专表 |
| 拒绝痕写在请求事务里 | 少开一个连接 | 业务拒绝让整个请求事务回滚,拒绝痕随之消失;SZV 在请求事务之外的独立连接里写 |
常见的另一种做法是动作完成后异步写日志,部署简单,动作不被拖慢;代价是写失败时动作已经成功,缺口没人知道。SZV 的导出与模型调用没有走这条路。
验收时可以问的问题
- 请演示:导出一份文件,导出留痕在文件交出之前已经写入。
- 请演示:对一个工作区运行校验函数,并说明它能找出的问题。
- 请现场查一条被拒绝的尝试,确认它只记了命令、对象和拒绝码。
- 需要库外锚点的,请在合同中约定由谁保管、多久写一次、核对由谁做。
相关页
- 谁能做什么:权限变更与被拒的判定怎样留下记录。
- 修改规则参数的唯一入口:规则与业务的写入怎样先过检查,再追加,再留痕。
- 查询与导出操作记录:怎样按对象查一件事的经过,导出怎样先留痕再交付。
- 安全边界:防谁、不防谁的整体边界。