看到一个安全事件的复盘,哪家就不说了。有人发现训练数据的合规检测率连续十六周、每周掉 1.5%。平滑,线性,太干净了。真实的模型漂移不长这样——漂移是抖的,有噪声,有突变。线性下降只有一种来源:有人定时定点在压它。
这个判断值一提,但不是我要写的。我要写的是他确认外泄存在之后干的那件事。
他没删管道。
背景交代到能看懂为止:数据通过一个叫 data-validate-svc 的微服务往外漏,每 72 小时跑一次,全自动,署名是一个离职 SRE 没注销的账户。标准剧本是撤账户、下线服务、发通告、写事后报告。
他没这么干。他往源数据里加了假加密壳——让后续泄出去的东西变成解析不开的垃圾。
我停下来想了一会儿。这一步是整个事件里唯一配得上“设计”两个字的地方。删掉管道,对方立刻知道暴露了,换一条路,你从零开始找。留着管道喂垃圾,对方在拿到第一份坏数据之前什么都不知道,而你已经拿到了完整的传输路径、频率、目的地。这不是防御,这是把攻击面变成自己的观测点。
用代码说就是这种差别:
/* 剧本一:发现即拆除 */
delete_pipe(pipe); /* 对方下周一换条新管道 */
/* 剧本二:留着,改输出 */
pipe->source = poisoned; /* 对方还在收,收的全是噪音 */
剧本一消灭一个症状。剧本二保留整个信道,只翻转里面的语义。复杂度没变——一个赋值,一行——但信息量的差距是十倍的。防御性拆除是本能反应,污染输出是设计判断。
顺带说,他前面那步也够狠:在 CI/CD 里搭了条影子训练管道,每个训练样本的浮点数最低有效位里嵌 128 位哈希水印,然后等。第七天凌晨,外部一个 S3 桶里捞到三条带水印的记录。LSB 嵌水印不算新招,但拿它回答“数据到底流去了哪”,比任何网络层的流量分析都便宜、都准。改动在哪?在数据自己身上。数据不需要权限,不需要代理,走到哪指纹带到哪。
然后是第二个我想说的点。
抓到一条管道之后,他顺着查,发现一个叫 data-timeline-aligner 的服务代码风格很怪:纯函数,短变量名,零注释,吞吐量是干同样任务所需的三倍。就凭这个,他做了源码指纹比对,又挖出四个同风格的服务,分布在管道、存储、监控三层,全标着 team: infra-optimization,全控制在 50m CPU / 128Mi 内存以内。
零注释。藏在几百个微服务里五个月,暴露它的不是流量、不是权限、不是资源占用——是代码风格。
我念叨过那套:注释该写“为什么”,不是“这行做了什么”。这里有个更冷的反面——代码风格是指纹,你藏不住它。一个组织里大家的代码长一个样:同样的命名习惯、同样的注释密度、同样的错误处理套路。一个外来者写得再“像”,也有个东西盖不住——他不知道这个组织的人怎么想。纯函数、短变量名、零注释,这不是某种风格,这是“我不知道你们平时怎么写、也不敢多写一行暴露自己”的写法。伪装得越少,越像基础设施;越像基础设施,越不像人写的。
那四个服务,每个都是不到一千行能干完的活。讽刺就在这:写它们的人跟我信的是同一套——简单、只做一件事、复杂度压到最低。他用这套东西藏后门。工具没有立场,判断才有。
还有个细节。最早的两个隐蔽服务,和那条外泄管道是同一周部署的。不是两拨人擦肩而过——是有人看到门开了,同一周进来装了监控。于是最后那段就很顺:那个外来操作者留下的配置里有个 Base64 的文件,解码出来是一把 PGP 公钥。他用这把钥匙加密一条消息,往一个 /healthz 端点发 POST 把密文写进 S3,收到了解密后的回复。
信道是对方建的,他用对方的信道完成了双向验证。整个过程没加任何新东西——没有新服务、没有新协议、没有新密钥体系。一个已存在的端点,一把已存在的公钥,一次 POST。
说句题外话——为什么我们这么怕留着坏东西不删。我捣鼓那个玩具存储引擎的时候也犯过这毛病:发现一个路径有 bug,第一反应是整条砍掉,砍完才发现那条路径上挂着唯一能观测到另一类错误的日志。删干净是安静,不是正确。扯远了。
我反对这个事件里几乎所有当事方的做法,除了这一件事的形状。整个事件从头到尾,真正起作用的设计没有一个超过几十行:LSB 水印、假加密壳、风格指纹比对、复用 /healthz 当信箱。而那个藏了五个月的后门系统,也是同样的形状——小服务,低占用,零多余。
工具的好坏取决于拿它的人,但“能用的东西都很小”这条,倒是两头都验证了。
所以这篇我想留下的就一句:发现入侵之后的第一反应如果是“删干净”,你买到的是安静,不是安全。留着管道、翻转输出,才买到信息。删一个东西只需要一行;想清楚不删它,值十行。
