无法外发的日志:由客户亲自运行的支持包
一句话总结
对于不能把日志送到外面的客户,需要的不是“请把日志发给我们”,而是一个客户可以在自己的服务器上运行、亲自检查之后再批准的采集器。按白名单收集、写明脱敏了多少处、并且让相同输入打出相同的字节,安全负责人才能批准。
为什么需要它
故障第三天,客户运维团队说:“按公司规定,日志不能出公司。请把你们需要的整理出来,我们来导出。”FDE 列了几个文件名发过去,寄回的压缩包里有配置文件的原件。数据库密码和支付 API 密钥原封不动。客户安全团队得知这个文件已经出去之后,表示从下一个包起全部要事先审查,审查开始每次要花两天。
问题不在人,而在流程。每次都由人来挑选放进什么,就会每次挑得不同,而如果没有记录遮掉了什么,审查人员就只能把文件从头读到尾。审查人员要想快速批准,必须看到三样东西:放进了什么(白名单)、遮掉了什么以及多少处(manifest)、现在收到的文件是不是审查过的那个文件(哈希)。由脚本每次以完全相同的方式替人做出这三样,就是支持包采集器。
工作原理
1. 按白名单收集。“除了有机密的文件,其余全部”是黑名单,而黑名单会放过不认识的文件。版本(VERSION)、配置(etc 下)、当前日志(logs 正下方的 *.log)、服务的环境——要按名称规定只放入哪些。已轮转的 app.log.1、客户数据目录和符号链接不在规则里,所以自然被排除。跟随链接的话,服务器之外的文件(例如主机的认证文件)可能被带进来。
2. 环境读取的是服务的,而不是 shell 的。运行采集器的 shell 的环境变量与故障无关。在 Linux 上,proc_pid_environ(5) 所说明的 /proc/<pid>/environ 中,以 NUL 字节分隔,保存着进程启动时的环境。正如同一份文档所指出的,进程启动之后自行改变的值不会反映出来。顺序可能每次运行都不同,所以要转成行之后按名称排序。
3. 脱敏要窄而准。规则太宽的话,连 password_min_length: 12 或 token_ttl_sec: 3600 这样的配置也会消失,包就没用了;太窄的话,机密就会泄漏。所以要按形态来规定,比如“键名以 password、secret、token、api_key 结尾的值”“Bearer 之后的值”“带域名的邮箱”“从 BEGIN 到 END 的私钥块”。顺序也很重要。私钥是多行的,所以要先于逐行规则,整块一起替换。被遮掉的位置要留下种类,如 [REDACTED:token]。审查人员可以数标记,与 manifest 里的数字核对,FDE 光凭“这里曾有一个令牌”这一事实,也能缩小原因的范围。
4. 脱敏之后再截断。日志很大,所以每个文件设置上限,只保留最近的行。如果先截断再脱敏,横跨截断边界的私钥块会因为丢失 BEGIN 行而不命中规则,只剩下正文。不在行中间截断也是同样的原因。
5. manifest 里只放确定性的值。每个文件写下路径、大小、sha256、脱敏次数、是否被截断。虽然会想放进生成时间,但一放进去,用相同输入制作的包就每次都不同了。
6. 相同输入得到相同字节。reproducible-builds.org 的归档文档总结说,tar 会记录修改时间、文件顺序、所有者名称和编号、权限,使结果不稳定,并建议对 GNU tar 使用 --sort=name(自 1.28 起)、--mtime、--owner=0 --group=0 --numeric-owner。如果用 Python 打包,可以在 tarfile 文档中的 TarInfo 里直接设定 mtime、uid、gid、uname、gname、mode,而且还多一层。gzip 文档写道,如果不给 GzipFile 指定 mtime,头部会写入当前时间,需要不依赖时间的结果时,要指定 mtime=0。
下面是在本实验镜像里,等待 1 秒并只改变原件的 mtime 后重新打包,再比较 sha256 的结果(实测:GNU tar 1.35、gzip 1.12、Python 3.12.3)。
| 打包方法 | 两次打包的 sha256 |
|---|---|
不加选项的 tar -czf |
不同 |
tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner -czf |
相同 |
用 Python tarfile.open(..., "w:gz") 对目录 add |
不同 |
固定了 TarInfo 的时间,但没有给 GzipFile 指定 mtime |
不同 |
固定 TarInfo 的时间、所有者、权限 + GzipFile(mtime=0) |
相同 |
GNU tar 的 -z 在实测中,gzip 头部的时间被写成了 0。相反,Python 的 w:gz 会在头部写入当前时间,即使文件内容相同,哈希也不同。
support-bundle/
VERSION
env.txt ← run/environ 을 줄로 바꿔 정렬
etc/... ← 가린 설정
logs/app.log ← 가린 뒤 최근 줄만
manifest.json ← path·size·sha256·redactions·truncated
在现场相遇的样子
与客户安全负责人的对话变了。以前对“怎么保证这个文件里没有敏感信息?”无话可答。有了采集器,就能说:“白名单是这八行,脱敏规则是这四种,这次的包里遮掉了 33 处令牌和 45 处邮箱。收到的文件的 sha256 是这个值。”审查人员数一数标记的数量,比较哈希,然后批准。
每个客户还会有需要额外脱敏的东西,比如工号、内部主机名、内部工单号。如果把这些写死在采集器代码里,就得为每个客户维护不同的版本,客户也无法亲自修改规则。通过规则文件接收并应用,并把这些种类也合并进 manifest 的次数展示出来,才能长久。
实际工作中真正重要的事
- 用白名单,而不是黑名单。不认识的文件不放进去。
- 过度脱敏也是失败。要与脱敏次数一起确认,不是机密的配置是否还在。
- 脱敏之后再截断,并在行边界截断。
- 包必须是确定性的,“审查过的文件与收到的文件相同”这句话才能成立。时间写在包之外(交付记录)。
- 客户的规则通过文件接收,而不是写进代码。
下一项实验要做什么
在作为材料提供的客户服务器副本上确定收集范围,依次扩展出脱敏过滤器和采集器。逐步加入 manifest、日志大小上限、确定性的 tar.gz 和客户规则文件,最后用材料制作交付包以及哈希和脱敏报告。评分器每次都会用重新埋入机密的服务器树,亲自运行你的采集器。