本层判断标准

在临时分支提交一个假密钥,提交前与合并前这两处都会在几秒内变红吗?

只装了本地钩子、门禁里没有这一项,等于没有兜底:钩子可以被一次绕过,助手也不经过它。两处都要有,缺一处就有一个窗口。

最容易漏的出口

扫描扫的是「像密钥的字符串」,所以先要知道自己这边的密钥长什么样、从哪些位置出去。下面后三处是扫描的盲区——它们的共同点是密钥在运行时才成形。

出口扫得到吗靠什么兜
代码里的常量扫得到,模式明确移进环境配置,按环境配置的两份文件写法
误提交的环境文件扫得到真实那份进忽略名单,示例那份只放占位符
提交信息里的密钥要扫提交历史才看得到本地钩子默认只看暂存区,这一项只能靠门禁的全量扫描兜
拼接出来的密钥(两段字符串相加、从环境读出后写入文件)扫不到靠评审:差异里出现按片段拼接的长字符串,要求说明用途
日志里打印的凭据头扫不到,运行时才出现日志脱敏,见日志
持续集成里回显敏感变量扫不到工作流里出现回显敏感变量一律退回

这一节的边界:扫描只看得见静态文本。所以「前端不得持有服务端密钥」「日志不得打印凭据」这两条是约定,不是工具能查的事。

长什么样子

一条密钥告警是「位置 + 规则号 + 命中形态」,且不回显原文——终端与持续集成日志都会留档。

一条密钥告警

[FAIL] backend/app/conf/ds.py:12
  规则 generic-api-key
  命中 AKIA****************    (不回显原文)
  范围 全量工作树 + 提交历史

要认的是这个形状:命中靠模式,所以误报天然偏高。处理只有「按路径或指纹放行」一种;把规则本身放宽,会让后来真的密钥一起漏过。

助手交出来的样子

[allowlist]
patterns = ["AKIA.*"]        能匹配真密钥,等于整条规则放行
或  DSN = os.getenv("DSN")   # 原值 sk-live-xxxx   写进注释

认它的办法:看放行模式有多宽。能匹配真密钥的放行等于把这条规则关掉;把原值留在注释或文档里,扫描不报但人看得见——而日志与历史都会留档。允许清单只按假密钥的固定前缀放行,不放宽规则本身。

典型应用示例:临时分支上的一个假密钥

场景:为了验证门禁是不是真的在拦,在临时分支提交一个假密钥,开一次合并请求。

有它 提交那一刻本地钩子就拦下,绕过之后合并请求里门禁再拦一次并阻断,失败信息只列出命中项、不打印密钥本身。两处都验过,才知道这条链路是通的——配好不等于生效,最常见的失效方式是配置对、但事件没触发。
没有它 只有一句「不要把密钥写进代码」。没人违反过,也没人验证过:等真密钥被提交上去,发现的时间取决于有没有人去翻提交历史,而那时候已经过去几周。

基本注意点

本地钩子管「别提交」,门禁管「别合并」,两者缺一段就有一个窗口。顺序按「快 → 全」:钩子几秒,门禁几十秒。

拦截点覆盖范围漏掉什么
提交前钩子暂存区,几秒返回被绕过;提交信息;已有历史
门禁全量工作树与提交历史,几十秒运行时才成形的密钥
合并请求模板的勾选项人看过一次靠人记得勾

误报率天然偏高,因为「像密钥」靠模式匹配,测试夹具和示例文档里全是长得像的东西。处理只有两种:按路径或指纹放行,或者放宽规则本身——只能选前一种。放宽规则会让后来真的密钥一起漏过,而且没人记得关过。允许清单里的匹配要写得窄,只匹配假密钥的固定前缀,不放宽规则本身。

本地钩子的完整配置在钩子那一页;扫描命令挂进门禁那一条入口命令,全仓库只有那一处定义。提交前只扫暂存区、几秒返回,跑不完的检查放进门禁;合并前做全量扫描,输出不打印密钥本身——终端与持续集成日志都会留档。允许清单按路径或指纹放行,不改规则本身;复制一份默认规则集再改,工具升级后新增的规则就不会生效。

泄漏之后先做什么

顺序不能换,而且第一步不是清理,是轮换。清历史不是安全措施——任何已经拉过仓库的人手里还有那份副本,密钥在他那里照样能用。

按这个顺序 先轮换:把泄漏的那一个作废、签发新的,旧密钥立刻失效,这才是止血;再确认旧密钥真的失效,用旧密钥调一次接口,期望拿到 401;然后才清提交历史;之后查影响面,按密钥类型查调用记录,确认泄漏期间有没有异常调用;最后回去把入口补上,并加一条能复现的检查。
做反的代价 先清历史再轮换,窗口期就变成「从清完历史到所有人重新克隆完成」——这段时间泄漏的密钥仍然有效。轮换在前,窗口以秒计;清历史在前,窗口以小时计。

每一步都要有可确认的判据:签发完成、旧密钥调用返回 401、全量扫描在该规则上不再命中。第 5 步不补,下次同一条路还会进来。

密钥从哪里来

随机数是这一层最靠前的一环:生成方式错了,后面怎么管都补不回来。math/rand 这类随机数用来生成测试数据和洗牌,不用来生成密钥。用当前时间做种子时熵不够,生成的会话密钥可以被人猜到。

可以这么做 在 shell 里跑 openssl rand -base64 32 生成;在语言里用 os.urandom() 或 secrets.token_urlsafe();生成后写进 .env,并当场确认它在 .gitignore 里。
不要这么做 用 math/rand 或 random 生成会话密钥;拿时间戳、进程号或用户 id 当种子;把生成出来的密钥贴进代码或提交信息——贴进提交信息后,它在仓库历史里一直可查,删掉文件也没用。

这一项和「不把密钥写进仓库」是两件事:生成方式错了,即使只把它放在 .env 里,也不算通过——攻击者猜得到种子,就能算出会话密钥。可以自己复现:拿两个用时间做种子的随机源,在同一秒各生成一串密钥,比对相同即可预测。生成之后怎么放见环境配置,怎么在提交前拦住它见基本注意点。