清理 Git 仓库里的泄露密钥
密钥一旦进了 Git 历史,就不是删掉当前文件一行那么简单了。我们从吊销密钥开始,用镜像仓库和 git-filter-repo 重写完整历史,再验证、清理本地对象并强制推回远端,顺手把原脚本里的几个坑也填上。
密钥被人提交进 Git 仓库,哪怕只是停留了几分钟,也应该按已经泄露来处理。
删掉当前分支里的那一行,心理上会舒服一点,安全上却远远不够,大写滑稽,小写尴尬。
因为 Git 不是一张会自动覆盖的白纸,旧提交、标签、分支和本地对象都可能还握着那份内容。
先把密钥作废
改历史不是止血
我们先不急着执行清理命令。
第一件事是去密钥所属的平台吊销、禁用或轮换它。
比如云服务访问密钥、数据库密码、部署令牌、Webhook 密钥,都应该先换成新的值。
已经进入 Git 的密钥,就不能再当作秘密保存。
即使我们马上把提交重写了,也无法证明没人提前 clone、下载、截图或从缓存里拿走它。
历史清理解决的是“仓库里还留着什么”,轮换解决的是“旧凭证还能不能继续开门”。
这两个动作不能混成一个。
还要记录影响范围:密钥属于哪个系统、权限有多大、从哪个提交和文件泄露、哪些 CI 或服务器使用过它。
Git 历史清理不会自动删除 Docker 镜像、构建日志、制品和部署平台里的环境变量,这些地方要分别处理。
准备完整镜像仓库
清理历史要在隔离副本里做,别直接拿平时开发目录开刀。
我们先拿到远端的完整裸仓库:
# 镜像克隆会带上分支、标签等远端引用,不是只拿当前分支
git clone --mirror https://git.mebugs.com/mebugs.git
# 裸仓库目录通常以 .git 结尾,里面没有普通工作区
cd mebugs.git
# 先确认远端和引用都在,避免对错仓库动手
git remote -v
git show-ref
上面的 mebugs.git 没有普通工作区文件,这是正常现象。
如果仓库很大,目录要放在磁盘空间充足的位置,并提前确认没有其他人继续向远端推送。
git-filter-repo 通常会为了避免误操作移除 origin,所以真正推送前要重新检查远端。
如果远端已经存在,不要重复执行 git remote add origin,否则会看到:
error: remote origin already exists.
应该根据实际情况使用:
# 已经存在 origin 就修改地址
git remote set-url origin https://git.mebugs.com/mebugs.git
# origin 确实不存在时才新增
git remote add origin https://git.mebugs.com/mebugs.git
这里建议先确认 URL,再开始改历史。
把一份干净仓库推到错误项目上,属于另一种很有创造力的事故。
用过滤器重写历史
准备替换规则
我们创建一个 expressions.txt 文件,只放待匹配的敏感内容和替换结果。
真实密钥不要写进文章、聊天记录、提交信息或脚本示例里,下面只用占位符表示思路:
# 按字面量匹配一段已确认泄露的占位示例
leaked-token-placeholder==>***REMOVED***
# 用正则匹配一类配置值,等号右边是统一替换文本
regex:API_KEY=[A-Za-z0-9_\-]+==>API_KEY=***REMOVED***
# 按字面量替换一个配置文件中的敏感行片段
password-placeholder==>***REMOVED***
规则文件通常是一行一条规则,左边是匹配表达式,右边是替换文本,中间用 ==> 分隔。按字面量匹配时直接写内容,需要匹配一类格式时使用 regex: 前缀。
规则不要一上来写得过于宽泛,先用测试副本验证,再逐步补齐,比一条正则把半个配置文件吞掉稳妥得多。
执行历史替换:
# 用规则文件重写所有相关历史,不只处理当前分支
git filter-repo --replace-text ../expressions.txt
如果规则文件不在上一级目录,就换成实际路径。过滤器会生成新的提交对象,哪怕文件内容看起来没变,提交哈希也可能因为父提交变化而全部变化。
整体关系可以按下面这条线理解:
泄露密钥
│
├─→ 吊销旧密钥并轮换
├─→ clone --mirror 获取完整引用
├─→ expressions.txt 定义匹配和替换
├─→ git-filter-repo 重写历史
├─→ 多维度搜索残留
├─→ 清理 reflog 和本地对象
└─→ force --mirror 推回远端
这几个节点不能只做最后一步。
直接强推没有清理历史,等于把旧问题原封不动地重新发布一遍。
验证每个藏身处
清理完不能靠“命令没报错”判断成功。Git 仓库里可能藏着多个分支、标签、文件重命名前的旧路径,以及提交信息里的敏感字符串。
我们至少从关键字、路径、分支和标签四个角度查一遍。
# -S 查找指定字符串数量发生变化的提交
git log --all --full-history -S 'leaked-token-placeholder' --oneline
# 检查替换后的占位符是否出现
git log --all --full-history -S '***REMOVED***' -p
# 检查泄露文件的历史路径
git log --all --full-history -- 'config/mebugs.yaml'
# 列出所有本地分支和标签
git for-each-ref --format='%(refname)' refs/heads refs/tags
git log -S 很适合查某个字符串什么时候进入或离开文件,但不是万能扫描器。
复杂格式还要结合路径、提交内容和人工抽查。
镜像仓库里可能有不在日常开发界面展示的引用。
不要只打开默认分支网页看一眼,那只能说明默认分支当前长什么样。
清理本地残留对象
过滤器改的是引用和提交历史,本地仓库还可能暂时保留旧提交对象。
我们把 reflog 和不可达对象也处理掉:
# 让 reflog 不再保留旧引用的时间线
git reflog expire --expire=now --all
# 清理不可达对象,让旧提交不再留在这个本地镜像里
git gc --prune=now --aggressive
这两条命令主要处理当前本地副本,不等于远端服务器、其他电脑、fork 或平台缓存也被清掉了。
所以“处理孤儿分支”不能简单理解成只跑 reflog 和 gc。
真正让敏感提交从仓库历史里消失,靠的是过滤器重写并把新历史推到远端;孤儿或残留引用是否存在,还要靠引用枚举和关键字验证确认。
如果只是普通仓库维护,我不建议随手使用 --aggressive。
这是敏感数据清理场景,耗时更长,但我们需要尽量处理当前本地镜像的残留。
强推并通知所有人
确认验证结果后,再把重写后的完整镜像推回远端:
# 确认 origin 指向正确项目后,整体覆盖远端引用
git push --force --mirror origin
这里的 origin 是远程名,--mirror 会同步所有引用,包含删除远端已经不存在于本地镜像的引用。
远端平台可能启用了分支保护,强推会被拒绝。
需要管理员临时放开相关限制,推送完成后立刻恢复。
影响范围
强推完成后,提交哈希会变化,受影响的不只是当前电脑:
- 其他人的旧克隆仍会保留旧提交。
- fork 可能继续引用旧历史。
- Pull Request 页面可能存在旧 diff、缓存快照或讨论内容。
- Git LFS 对象可能需要按平台能力单独清理。
- CI 缓存、构建日志、发布制品和备份也要分别检查。
平台上的敏感数据清理通常还需要联系托管方支持团队,不要假设 force push 会瞬间抹掉所有副本。
协作者重新同步
不要让同事直接在旧克隆目录里执行普通 git pull,那可能把旧历史再次合并回来。
更稳妥的方式是删除旧目录后重新 clone;如果本地改动很多,先备份未提交改动,再按团队方案重新变基到新历史。
远端完成历史重写
│
├─→ 暂停旧目录继续推送
├─→ 备份未提交改动和必要分支
├─→ 删除或隔离旧克隆副本
├─→ 重新 clone 新历史
└─→ 将业务改动重新变基或手工迁移
重新 clone 是最省脑子的做法。
看到“远端已经干净”后顺手一把 git pull,事故可能又从旧历史里复活。
收尾
一次完整处理可以压缩成几条检查线:
- 先吊销并轮换泄露密钥,再处理 Git 历史。
- 用
git clone --mirror保存完整远端引用,并留好原始备份。 - 用
git-filter-repo --replace-text按规则重写所有历史。 - 按关键字、文件路径、分支和标签检查残留。
- 执行 reflog 过期和垃圾回收,清掉当前本地镜像的旧引用与悬空对象。
- 确认远端名和分支保护后,用
git push --force --mirror origin发布新历史。 - 处理 fork、PR、缓存、LFS、制品和协作者旧克隆。
最容易犯的错,是把“文件现在看不到了”当成“历史已经干净了”。
Git 的提交像一摞复印件,撕掉最上面那张,不代表下面没有同样内容。
我们既要让旧密钥失效,也要让仓库历史不再继续传播它。
记录安全事件时只写类型、位置和处理结果,不要把真实密钥再抄一遍。
我个人会把规则文件、验证结果和远端推送时间保存到受控的内部记录里,然后把临时镜像和原始副本按安全团队要求销毁或隔离。
这样做起来麻烦一点,却比下个月又从某台旧电脑里翻出一把“已经删掉”的钥匙靠谱。
参考链接:
