小Cの已经记不起来的博客

一次线上 数据脱敏 故障的复盘记录

事情过去几天了,想想还是记一下。不是那种“高度重视、举一反三”的复盘,就是给自己和后来人提个醒:脱敏这种看起来不起眼的逻辑,真出事的时候一点都不小。

事情是怎么发现的

下午两点多,客服在群里问了一句:“刚导出的用户列表,手机号怎么是完整的?”我当时还没太当回事,以为是谁把测试环境的文件发过去了。结果点开她发的截图一看,还真是明文手机号,中间四位没打星号。再一问,运营那边也导了两份,同样的问题。

我头皮一麻,赶紧去看脱敏服务的日志。服务本身没挂,健康检查全是绿的,错误日志也少得可怜,只有几条 mask rule miss 的 warning,混在一堆访问日志里,平时根本不会有人注意。说白了,它没报错,它只是把不该放过的字段放过去了。

先止损,再说别的

第一反应是回滚。前几天刚上了一个脱敏规则重构版本,把原来写死在代码里的正则和字段名匹配改成了配置化。当时觉得挺优雅,现在看就是给自己挖坑。

回滚命令倒不复杂:

kubectl -n data-mask rollout undo deploy/data-mask-api
kubectl -n data-mask rollout status deploy/data-mask-api

回滚完之后,我先让客服和运营把已经导出的文件全部删掉,能撤回的撤回,撤回不了的就登记一下。另一边让负责同步任务的同事把下游测试库先停掉,别继续往里灌了。后来统计了一下,大概两个小时里导出了 17 份文件,涉及 3200 多条手机号,测试库那边也有几张表进了明文数据。量不算特别大,但足够让人难受。

根因其实不复杂

问题出在新规则只做了精确字段名匹配。旧代码里有一层兜底逻辑,字段名只要包含 phone、mobile、tel 这类关键词,就会走手机号脱敏。重构的时候我觉得“精确匹配更可控”,就把兜底去掉了,规则大概长这样:

rules:
  - field: phone
    type: mobile
    keep_prefix: 3
    keep_suffix: 4
  - field: id_card
    type: idcard
    keep_prefix: 6
    keep_suffix: 4

看着挺清楚,对吧?问题是上游字段名根本不统一。有的叫 phone,有的叫 mobile,有的叫 contact_phone,还有一个嵌套在 user.contact.mobile 里。新规则只认 phone 和 id_card,其他一律原样返回。更坑的是,脱敏服务对嵌套 JSON 只处理第一层,user.contact.mobile 这种压根没进规则匹配。

排查的时候我用日志里的字段名做了个统计:

grep -i "mask rule miss" mask.log \
  | awk '{print $6}' \
  | sort | uniq -c \
  | sort -nr

排前面的就是 mobile、contact_phone、user.contact.mobile。看到这个结果,基本就不用再往下猜了。

几个让我后怕的细节

第一,测试数据没覆盖到这些字段名。我们造测试数据的时候习惯用 phone,因为看着顺眼,结果线上真实字段名比测试数据野多了。

第二,上线前没有做新旧版本双跑对比。其实只要把同一批样本分别丢给旧版和新版, diff 一下输出,很容易就能发现新版少脱了哪些字段。当时赶时间,觉得“规则都配了,能有多大事”,就跳过了。

第三,监控只监控了服务存活和接口耗时,没有监控脱敏命中率。如果加一个 mask_hit_rate 指标,或者对“未匹配任何规则的敏感字段”打点,这事可能十分钟内就能发现,而不是等客服来问。

后来补了哪些东西

紧急处理完之后,我们补了几件事,不复杂,但都是能落地的:

  1. 把字段名枚举扫了一遍,所有包含 phone、mobile、tel、idcard、id_card、email 的字段全部加进规则,并且恢复关键词兜底匹配。精确规则优先,兜底规则保底。
  2. 对嵌套 JSON 改成递归处理,不再只扫第一层。
  3. 加了一个默认拒绝策略:导出任务里如果出现未匹配任何脱敏规则的字段,并且字段名命中敏感词库,直接拒绝导出并告警。宁可错杀,不能放过。
  4. 上线前强制双跑对比,脚本大概这样:
python compare_mask.py \
  --old old_mask_result.json \
  --new new_mask_result.json \
  --fail-on-diff
  1. 监控里加了脱敏命中率和未匹配敏感字段数,阈值先设得保守一点,后面再慢慢调。

最后

这事说到底不是什么高深的技术问题,就是重构的时候把一条不起眼的兜底逻辑弄丢了,测试没覆盖到,监控也没兜住。脱敏服务平时不出声,一出事就是直接往外吐明文。以后再做这种“看起来只是配置化”的改动,我大概会先问自己一句:旧代码里那些丑但有用的兜底,是不是被我顺手删了?

就这些,记录一下,免得下次又踩。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放