交易快要谈成了。SSO 已经打通,SCIM 已经配好,问卷一路绿灯。然后客户的安全团队发来最后一封邮件:“能给我们一份你们审计日志导出的样例吗?” 不是设置页面的截图——是导出文件,是他们的 SOC 真正要摄入的那个工件。就在这一刻,一家 SaaS 会发现自己的审计日志究竟是一个功能,还是一个有野心的调试控制台。
第 1 篇界定了谁的数据归谁;第 2 篇界定了谁的身份归谁。本篇讲的是证据:谁、对哪些数据、在什么时间、做了什么——并且记录方式要经得起攻击者和审计的双重考验。第 2 篇的问卷攻略以一张清单收尾;审计日志正是把“我们很重视安全”和“证明给我看”分开的那一项。他们的 SOC 团队会在律师开口之前先要这份导出。
审计日志不是日志
你的应用已经在产生日志——请求 trace、报错、debug 行。这些没有一个是审计日志。这三套系统经常被混淆,但它们服务的是不同的读者:
- 应用日志给你的工程师看,用来调试你的系统。啰嗦、技术化、生命周期短,可以放心包含内部细节。
- 分析事件给你的产品团队看,用来度量用户行为。采样、聚合、乐观——丢 2% 的事件不会改变任何结论。
- 审计事件给客户的安全团队看,用来证明发生在他们数据上的操作。完整、防篡改、按租户划分、保留多年。丢一条事件就是一个需要报告的缺口。
这个区分之所以重要,是因为它决定了下游的一切:审计事件不能采样、不能在负载下丢弃、不能 30 天后过期,也不能和你的 console.log 输出放在同一个桶里。如果你的“审计日志”和 debug 行发往同一个地方,那你有的只是一份 debug 日志。
一个事件的解剖结构
每个审计事件永远以同样的形状回答同样的问题。一致性本身就是功能——一个 SIEM 在解析你的一万条事件时,不应该遇到任何意外:
干活最多的字段是 tenant_id——每条事件都必须带它,没有例外,正如第 1 篇对每一张表的要求。审计日志本身就是租户数据:当 Acme 的 SOC 问“周二谁导出了我们的报表”,答案必须只来自 Acme 的事件,用和其他一切相同的已验证声明(claim)来过滤。一个跨租户的审计查看器,就是一个多绕了几步的跨租户泄露。
记什么——以及绝不记什么
经验法则:记录每一个改变访问权限、数据或配置的操作。具体来说,买家最先检查的事件是:
- 身份事件——登录成功与失败、MFA 变更、SSO 配置变更、SCIM 的开通与回收(第 2 篇的入职、转岗、离职,以证据的形式呈现)。
- 授权事件——角色的授予与撤销、权限变更、API key 的创建、邀请的发出与接受。
- 数据事件——导出、批量读取、删除、共享权限变更。导出是任何事故复盘中最受审视的事件;如果你只能记录一种数据操作,就记它。
- 管理员事件——你自己的支持人员在客户租户内做的一切。没有审计记录的支持代操作(impersonation),会让“我们绝不碰你的数据”变成一句无法证伪的话。
负空间同样重要。绝不记录密码、会话 token、API key 的值、完整请求体,或你并不需要的原始 PII。一份装满机密的审计日志就是一个名字更好听的凭证数据库——它把攻击者最想要的东西,浓缩在保留策略留存最久的地方。记录密码变更过,绝不记它的值;记录 key 的名称和前缀,绝不记 key 本身。脱敏不是以后再加的清理步骤,它是事件 schema 的一部分。
写入路径:不阻塞,不丢弃
审计事件诞生在请求路径上,但不能住在那里。同步写入意味着日志基础设施的坏日子会变成你产品的宕机日——或者更糟,工程师“临时”给这个调用包上一层吞掉异常的 try/catch,于是事件开始无声消失。
可行的形状是:把事件发到一个持久化队列,立即返回用户,再由一个独立的写入者把它追加进存储。队列吸收突发和故障;写入者负责排序和重试。“完整”是硬要求——事故窗口内的 SIEM 缺口读起来像掩盖,不像小故障。而且因为事件是租户数据,管道要把第 1 篇的纪律一路贯彻到底:tenant_id 在事件发出时就从已验证的声明里取值,绝不从请求里拷贝。
防篡改:日志必须比说谎者活得久
设计必须扛住这个让人不舒服的场景:调查泄露的人怀疑有内鬼——可能还是有数据库权限的那种。对普通表执行一次 UPDATE 或 DELETE,历史就被静默改写了。对策是两层:
- 哈希链——每条事件存储上一条事件的哈希,于是日志成链:修改或删除任意一行,之后所有的哈希都会断裂。校验时重新走一遍链,会在缺口的确切位置失败。这不能阻止篡改;它让篡改可被证明,而后者才是真正的需求。
- 存储层的不可变性——底层存储启用 object-lock / WORM 保留策略,写入和删除权限握在一个你日常数据库凭证不具备的角色手里。哈希链防的是改数字的人;权限分离防的是宁愿删掉整份日志再谎称宕机的人。
这两层都不稀奇。两者加在一起,就是“我们的日志没显示可疑内容”和“我们的日志能显示没有可疑内容,并且能证明没被编辑过”之间的差别。审计师把这个区别称为完整性证据(integrity evidence),它是一个写着你名字的勾选项。
导出才是产品
上面的一切都是为了服务一个时刻:客户的安全团队把他们的事件拉进他们的工具。这意味着读取侧本身就是一个有自己路线图的功能:
- 按租户自助导出——一个 API 加一个 CSV 下载,范围限定在租户自己的事件,不需要提工单。你手动跑的每一次导出,都是一笔支持成本,也是某人事故复盘里的一段延误。
- SIEM 转发——把事件流推到 Splunk、Datadog、Panther 或客户的 Chronicle 实例。这是企业版的真正内容:SSO 把他们的人放进来(第 2 篇);SIEM 数据流让他们的 SOC 一直盯着。
- 按套餐分级的保留期——自助版 90 天,企业版一年或更长,对齐客户合规体系的要求。保留存起来便宜,补起来昂贵;在问卷替你决定之前,先自己定好套餐。
这也是为什么 WorkOS 这类厂商把审计日志和 SSO 放在一起卖:买家、问卷条目和管理后台都是同一批。如果你在第 2 篇买了身份层,先查查审计日志是不是已经随附了,再决定要不要自建。
系列的下一站
下一扇门目前的想法是:账单安全——webhook 签名校验、你拥有和不拥有的 PCI 边界,以及为什么支付流程是“以后再加校验”会变成欺诈的唯一地方。SOC 2 报告本身——问卷反复索要的那份文件——是另一个有力候选。和之前一样:顺序到了那里再定。
练习
- 为你当前的项目写下审计事件 schema:七个字段、过去时的动作名,以及一份明确的排除清单(机密、请求体、PII)。如果你觉得排除清单没必要,说明你最近没在自己的日志里 grep 过 token。
- 挑三个操作——一次导出、一次角色变更、一次支持代操作——把每一个从请求一路追到落库的事件。如果数据库在请求中途抖了一下,事件会在哪里丢?那就是队列要填的缺口。
- 模拟内鬼:用你自己的数据库凭证,尝试
UPDATE昨天的一条审计记录。如果你能成功,你的防篡改就只是一篇博客文章而不是控制措施——加上哈希链,把删除权限移到一个你不持有的角色上。