gitleaks 生产落地案例
生产落地案例
案例一:密钥误提交进公开仓库后的应急止血
背景。一个团队的前端仓库是公开的。某天有人在排查线上问题时发现,一个云存储的访问密钥曾经被提交进这个仓库——提交者本地调试时把它写进了配置文件,过了大约二十分钟才用一次新的提交删掉那一行。问题在于:仓库是公开的,任何人在那二十分钟内都可能已经克隆过;而且删除那一行的提交本身,并不会把密钥从 git 历史里抹掉。
怎么做。顺序非常重要,第一动作不是改历史,而是止血。他们的处理链路是:
- 立即吊销并轮换密钥。先去云厂商控制台把这对密钥禁用、删除,再生成新的凭据并按最小权限重新授权。这一步是唯一真正能让泄露失效的动作,其他所有步骤都只是补救。
- 评估影响面。拉该密钥近期的访问日志与操作记录,确认有没有异常来源 IP、异常时段的调用;同时确认这对密钥的权限范围,判断最坏情况下的损失边界。
- 清理历史。确认轮换完成之后,再改写 git 历史,把密钥从所有提交里彻底移除。工具上用
git filter-repo(或 BFG Repo-Cleaner),而不是简单 revert 或新提交覆盖:
# 用 git filter-repo 替换历史中出现的密钥字符串
# 强烈建议先在仓库的镜像副本上演练,确认无误再对主仓库执行
git clone --mirror git@example.com:group/repo.git repo-mirror.git
cd repo-mirror.git
cat > ../replacements.txt <<'EOF'
literal:AKIAIOSFODNN7EXAMPLE==>REMOVED
EOF
git filter-repo --replace-text ../replacements.txt
# 推回前务必与所有协作者约定时间,改写历史会影响所有人的本地副本
git push --force --all
git push --force --tags
- 通知与复盘。通知所有 fork 过仓库的人与 CI 缓存的持有者;复盘为什么 pre-commit 没有拦住、为什么二十分钟内没人发现。
关键配置。事后他们补上了两处防线。pre-commit hook 拦住本地提交,CI 里以 --log-opts="--all" 全历史扫描作为兜底,且全仓库开启 fetch-depth: 0。同时在平台上打开了推送保护类的功能(如果托管平台提供),让平台侧再拦一道。
效果。由于密钥在第一时间就被吊销,实际风险被限制在很小的窗口内,日志核查没有发现异常调用。后续的 pre-commit 与 CI 扫描把同类问题挡在了提交阶段,没有再出现第二次。
坑与解决。第一个坑,也是最常见的错误认知:只 revert 或新提交删掉那行代码,密钥仍然完整地留在 git 历史里,任何人 clone 之后 git log -p 就能看到。删除提交不等于安全,这是必须反复强调的一点。第二个坑是改完历史就以为万事大吉:如果仓库被人 fork 过、或者 CI 系统缓存过旧的克隆、或者有镜像推到了别的地方,那些副本里的历史依然是旧的。解决方式是先把密钥吊销(让旧副本失去价值),再逐一排查这些副本。第三个坑是 git push --force 直接把所有人的本地分支冲掉,导致协作者大面积冲突——正确做法是提前约定时间窗,让所有人先提交、再统一重新克隆。
案例二:把 baseline 从「永久豁免」改成「有主的清单」
背景。另一个团队在一个已经开发了数年的单体仓库上接入 gitleaks。全历史扫描一跑,报出上百条命中,绝大多数是早期提交里的测试假数据、示例配置、以及早就失效的旧 token。如果直接卡门禁,流水线会红到没人愿意看,于是他们生成了基线文件让流水线先跑起来。
怎么做。问题随之而来:半年后他们回头看,基线文件从一百多条变成了两百多条——因为每次遇到误报,处理方式都是「丢进基线」,没有人清理,也没有人知道哪条是谁加的。他们做了一次专项治理:把基线从「一个大文件」改成「有所有者、有到期时间的清单」。具体做法是维护一份配套的记录,每条命中标注三件事:是真实凭据还是误报、如果是真实凭据则记录是否已轮换、以及谁负责跟进。真实凭据类的命中统一走吊销与轮换,误报类的命中则改成在 .gitleaks.toml 里用 allowlist 精确排除,而不是塞进基线。
关键配置。误报走规则白名单,真实的历史存量才留在基线里:
# .gitleaks.toml 中精确排除误报,而不是丢进 baseline
[[rules]]
id = "generic-api-key"
[rules.allowlist]
description = "已确认的测试假数据与示例占位"
paths = [
'''(^|/)tests?/fixtures/''',
'''\.example$''',
]
regexes = [
'''api_key\s*=\s*["']test[_-]?key["']''',
'''(^|/)docs?/.*\.md$''',
]
效果。基线不再增长,且每一条都有人认领。新增的泄露因为规则收紧而更精准,告警的处置率明显提升。治理之后的基线规模远小于治理前,而且这个数字是可解释的——每一类剩余项都能说清楚为什么还在。
坑与解决。第一个坑是前面提到的熵阈值:为了让更多东西被扫出来而调低阈值,结果是误报淹没有效告警,团队会很快学会「无视这个红叉」,这比不装工具更糟。第二个坑是基线当成垃圾桶。解决思路是把「误报」和「存量」分开处理:误报应该在规则层用 allowlist 消灭(因为它不是真的问题),真实存量才进基线(因为它需要有人跟进)。第三个坑是 CI 里的浅克隆:早期他们的流水线只拉了最近一次提交,扫描永远是绿的,看起来「很干净」,实际上历史从未被扫过。修正为 fetch-depth: 0 之后,第一次全历史扫描就发现了若干历史遗留项。这也说明一件事:一个永远通过的检查,往往比一个偶尔失败的检查更值得怀疑。

