如何防止Git提交中泄露API密钥等敏感信息?

文章导读
很多项目在配置 API 密钥时,第一步通常是把密钥写进本地配置文件,比如 .env 或 application.yml,然后顺手提交到 Git 仓库。等发现时,第一反应往往是删除文件并提交一个新版本,但这个动作通常解决不了问题。
📋 目录
  1. A 先确认泄露已经发生到什么程度
  2. B 先别急着改配置,确认 .gitignore 的边界
  3. C 本地钩子:能拦截大部分手滑
  4. D 服务端扫描:把检查放到推送之后
  5. E 重写历史:从仓库里彻底移除
  6. F 密钥轮换和源头管理
  7. G 改完后看这几个信号
A A

很多项目在配置 API 密钥时,第一步通常是把密钥写进本地配置文件,比如 .env 或 application.yml,然后顺手提交到 Git 仓库。等发现时,第一反应往往是删除文件并提交一个新版本,但这个动作通常解决不了问题。

先确认泄露已经发生到什么程度

很多泄露不是发生在初次提交,而是发生在后续修改中。开发者习惯把 API 密钥写进配置文件,比如 .env 或 application.yml,然后为了调试临时把文件加入版本控制。即使之后删除文件并提交新版本,密钥依然留在 Git 历史里——只要提交过,就会永久存在,除非重写历史。判断一个文件是否已被跟踪,不要只看当前目录,要运行 git ls-files 检查。如果文件在列表中,即使当前内容被 .gitignore 忽略,下一次提交也不会排除它。常见坑是以为删除文件等于删除泄露,实际上任何克隆过仓库的人都能从历史中恢复那个文件。

所以第一步是判断严重程度:如果密钥只存在于本地未推送的提交,处理成本很低;如果已经推送到远程仓库,就要走完整的历史重写流程。无论哪种情况,都先把该密钥视为已泄露,到服务商后台吊销或轮换,再继续处理代码仓库。

先别急着改配置,确认 .gitignore 的边界

.gitignore 只能拦截未被跟踪的文件,无法对已跟踪文件生效。所以添加规则前,先执行 git rm --cached 将文件从索引中移除,注意这个命令会保留本地文件,但下一次提交后远程仓库中的相应路径会被删除。执行完之后立即提交一次,再添加 .gitignore 规则。另一个容易忽略的点是 .gitignore 中的目录匹配规则可能会被同名文件覆盖,比如 logs/ 不会忽略 logs 文件。建议用 git check-ignore -v 验证规则是否生效,这个命令会输出匹配到哪一条规则,如果没输出说明未被忽略,需要调整。

这里要特别提醒:git rm --cached 的作用是从版本控制中移除文件,不等于删除文件,也不等于抹掉历史记录。做完这一步,远程仓库当前分支里确实看不到这个文件了,但旧提交里仍然有。如果密钥已经推送过,必须继续往下走历史清理。

本地钩子:能拦截大部分手滑

在提交前拦截是最经济的做法。可以安装 pre-commit 框架,在配置文件中启用现成的密钥检测钩子,也可以自己写一个简短的脚本:在 pre-commit 钩子里执行 git diff --cached 扫描隐藏文件或极高熵值字符串。注意钩子只在本地运行,任何人可以用 git commit --no-verify 跳过,所以它只能作为第一道防线。扫描时不要只搜索明文密钥,还要覆盖 base64 编码后的值,因为常见的密钥格式一望便知,编码后却容易混过正则。另外,钩子失败时要输出具体文件和行号,否则开发者不知道该改哪里。

自己写钩子时,建议先用 gitleaks 这类现成工具,而不是从零写正则。工具规则更新通常比自定义脚本及时,而且支持多种密钥格式。钩子脚本要放在 .git/hooks/pre-commit,如果是团队协作,可以用 pre-commit 框架把配置文件提交到仓库,让每个成员安装同一套钩子。不过要记得,钩子只是行为约束,不是权限控制。

服务端扫描:把检查放到推送之后

既然提交钩子能被绕过,中心化的检查就必须放在服务端。可以在 CI 流水线中加入一步,对每次推送的分支运行 gitleaks 或 trufflehog,它们会扫描新增提交的全量 diff,失败则让流水线中止,这样即使有人在本地跳过钩子,密钥也进不了默认分支。配置时注意不要只扫描最新 commit,因为历史 commit 可能已经藏有密钥,建议对完整分支历史运行一次基线扫描。常见的坑是 CI 中使用的扫描 token 本身是明文写在配置里的,需要把扫描工具的鉴权信息放到 secrets 中,否则等于又泄露一次。

CI 扫描的定位是“最后一层闸门”,它不能替代历史重写。如果扫描发现历史提交里有密钥,流水线通常会阻止后续合并,但已经推上去的分支还是需要人工处理。此时不要试图在 CI 里做自动清理,清理动作要由仓库管理员单独执行。

如何防止Git提交中泄露API密钥等敏感信息?

重写历史:从仓库里彻底移除

如果密钥已经推进了远程仓库,光靠新提交修复是没用的,必须重写历史。优先使用 git filter-repo,而不是 git filter-branch,前者更快且提供了误操作保护。最直接的操作是 git filter-repo --invert-paths --path secret.env 删除指定文件,或者用 --replace-text 把密钥替换成占位符。重写历史会改变所有 commit hash,任何协作者的本地仓库都会与远程冲突,需要他们重新 clone。推送时如果远程仓库禁止强制推送,需要先调整分支保护规则。清理完成后还要把被泄露的密钥视为已公开,立即在服务商后台轮换,因为历史克隆无法撤销。

执行 git filter-repo 之前,先备份整个仓库。建议在本地克隆一份镜像仓库,在镜像上操作,确认过滤结果正确后再推送到远程。重写历史后,旧 commit 仍然可能残留在其他协作者的本地、CI 缓存或 Fork 仓库里,所以不能只清理主仓库。如果项目在 GitHub 或 GitLab 上,需要检查是否有关联的 Fork,Fork 中的历史不会自动跟随主仓库重写,需要逐一处理或联系平台支持。

密钥轮换和源头管理

从源头上减少密钥进代码,比事后扫描可靠得多。建议把 API 密钥放进环境变量或专门的密钥管理服务,代码里只读取环境变量,本地开发时用 .env 文件保存,但该文件必须列入 .gitignore。检查项目里是否还有硬编码密钥,可以运行 git grep -n -E 'sk-[A-Za-z0-9]{20,}' 之类的命令,同时要留意配置模板里是否留了占位符或示例值。风险边界在于:环境变量在构建日志、错误堆栈里可能被打印出来,所以打印配置时要过滤敏感字段,日志收集系统也要加脱敏规则。另外,密钥应定期轮换,一旦怀疑泄露立即吊销,而不是等扫描工具发现。

处理完仓库后,建议顺手做两件事:一是把被泄露的密钥加入服务商的黑名单或撤销列表,二是检查该密钥是否出现在其他项目的历史里。很多团队在清理完主仓库后,发现同一个密钥还被复制在文档、测试配置或 Dockerfile 中。可以用 grep 扫描整个代码目录,包括非 Git 目录。

改完后看这几个信号

验证历史清理是否成功,不要只看当前文件,要直接搜索所有历史提交。可以先用 git log --all --oneline 确认提交数,再用 git rev-list --objects --all 检查敏感文件是否还存在。更直接的办法是在一个干净的克隆仓库里执行搜索命令,比如 git grep 指定密钥字符串,如果没有任何输出,说明该字符串已经从当前所有分支的历史中移除。但要注意,如果密钥是动态生成或分段的,正则可能漏掉,建议同时用 gitleaks 对完整历史跑一次全量扫描。

如果清理后团队成员反馈仓库状态异常,很可能是本地旧历史没有被重置。这时不要要求他们手动拉取,最省事的方式是让他们重新 clone。对于无法重新 clone 的自动构建任务,需要清理 CI 缓存并重新触发一次。

最后需要明确:任何历史重写操作都会影响所有协作者,所以重写之前先通知团队,约定一个时间窗口,避免有人在清理途中推送新提交。如果仓库很大或历史很深,重写过程可能很慢,建议在稳定的网络环境下操作,先处理默认分支,再处理其他长期分支。