你执行 git push 的一分钟后,收到了 GitHub 的安全告警邮件:「Secret scanning detected a GitHub Personal Access Token in commit abc1234」。或者同事发现 .env 文件里的 AWS Access Key 出现在了公开仓库的某个 commit 里。不管是几分钟前还是几天前推送的,第一反应不应该是删文件再 commit——那样做历史里还有原始 commit,任何人都能通过 git show <sha> 找到明文密钥。正确的处理顺序是:先吊销密钥,再清除历史,最后修复根因。
常见原因
1. .env 文件没有加入 .gitignore
最高命中率。项目创建时忘记把 .env 加入 .gitignore,或者 .env 在 .gitignore 添加之前就被 git add 追踪了,后来加了 .gitignore 也不会自动取消追踪已有文件。
怎么判断:git ls-files .env 若有输出,说明该文件被 Git 追踪;cat .gitignore | grep .env 检查是否有忽略规则。
2. 硬编码在源代码里的凭证
开发者为了「临时测试」在代码里直接写了 api_key = "sk-abc123",本打算之后替换成环境变量,但忘记了,一起提交推送。
怎么判断:git log -p | grep -E '(api_key|password|secret|token|credential)' | head -20 扫描历史里的敏感关键词。
3. AI 编码工具在生成代码时插入了示例凭证
AI 工具(如 Claude Code、Copilot)有时会在生成的代码中包含看起来像真实 API key 的字符串(实际上是示例值但格式完全一样),若不加识别就直接提交,可能触发 secret scanning 告警,或者在某些情况下真的是从上下文里提取了真实凭证。
怎么判断:git diff HEAD~1 HEAD | grep -E '[A-Za-z0-9]{32,}' 查看最新 commit 里的长字符串,手动确认是否是真实凭证。
4. GitHub Actions workflow 文件里打印了 secret
workflow 里有 echo ${{ secrets.API_KEY }} 或者某个工具在 debug 模式下把所有环境变量打印到日志,虽然 GitHub 会自动 mask,但如果 secret 以其他形式(如 base64 编码后)出现,可能绕过 mask。
怎么判断:检查 Actions 运行日志,搜索已知 secret 的前几个字符(不要搜索完整 secret)。
5. git stash 里的 secret 通过 patch 导出泄露
执行 git stash show -p > patch.txt 后把 patch 文件提交,patch 里包含了之前 stash 的 .env 改动。
怎么判断:git log --all -- '*.patch' '*.diff' 查看是否有 patch 文件被提交。
最短修复路径
Step 1:立即吊销(revoke)泄露的凭证 — 这一步最优先
不管后续怎么操作,先去对应平台吊销 key:
- GitHub PAT:Settings > Developer settings > Personal access tokens > Delete
- AWS:IAM Console > Users > Security credentials > 设置 key 为 Inactive 并创建新 key
- OpenAI:Platform > API keys > Revoke
- 数据库密码:立即修改数据库密码
即使历史清除干净了,旧凭证也必须吊销,因为在你清除期间已经可能被爬取。
Step 2:把仓库设置为私有(临时措施)
在 GitHub Settings > General > Danger Zone > Change repository visibility 临时改为私有,阻止进一步暴露。
Step 3:用 git-filter-repo 从历史中彻底删除 secret
# 安装
pip install git-filter-repo
# 删除包含 secret 的文件(如 .env)
git filter-repo --path .env --invert-paths
# 或者替换文件中的 secret 字符串
git filter-repo --replace-text <(echo 'sk-realkey123==>REDACTED')
Step 4:清理本地对象并 force push
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# force push 所有分支和 tags
git push --force-with-lease --all
git push --force-with-lease --tags
Step 5:联系 GitHub 支持清除缓存
GitHub 即使删除了历史,仍可能在缓存里保留旧对象数天。提交 support ticket 请求清除缓存,说明「secret was pushed, history has been rewritten, please remove cached objects」。
Step 6:修复根因,防止复发
# 添加 .gitignore 规则
echo ".env" >> .gitignore
echo ".env.*" >> .gitignore
echo "!.env.example" >> .gitignore
# 取消追踪已追踪的 .env
git rm --cached .env
git commit -m "chore: remove .env from tracking and add to .gitignore"
预防建议
- 安装 pre-commit hook 的 secret scanning 插件:
pip install detect-secrets,配合 pre-commit framework 在提交前自动扫描。 - 使用
.env.example存放变量名模板(不含真实值),所有人都提交这个模板,真实.env由各自本地创建。 - GitHub 开启「Secret scanning」和「Push protection」(Settings > Security > Code security and analysis),在 push 时自动阻断包含已知格式 secret 的提交。
- 使用 HashiCorp Vault、AWS Secrets Manager 或 Doppler 等专用 secret 管理工具,代码里只引用 secret 的路径/名称,不存储真实值。
- 在团队 onboarding 文档里明确:永远不在代码里硬编码凭证,即使是「临时测试用」的也不行。
- 定期运行
git log -p | grep -E '(api.key|password|secret|token|private.key)' | head -50审查历史里的敏感关键词。 - 配置 GitHub App 或 GitGuardian 做实时 secret 检测,在 push 后几秒内发送告警。
常见问答 (FAQ)
Q: 我已经删除了包含 secret 的文件并重新提交,是不是安全了?
A: 不安全。删除文件只是新增了一个「删除文件」的 commit,原来那个包含 secret 的 commit 依然在历史里,git show <old-sha> 可以完整看到。必须用 filter-repo 重写历史才能彻底清除。
Q: force push 之后别人 fetch 还能看到旧 commit 吗? A: 如果别人在 force push 之前 clone 了仓库,他们本地有旧的对象,能看到旧 commit。这就是为什么必须同时吊销凭证——历史删除是尽力而为,凭证吊销是最终防线。
Q: GitHub secret scanning 扫描到的告警如何处理? A: 在 GitHub Security 标签页找到告警,点击「Close as revoked」并确认已经在凭证提供方吊销了该 key。若没有吊销就关闭告警,GitHub 会持续提醒。
Q: 仓库转为私有后,之前 fork 的公开仓库还有旧 commit 吗? A: 有。fork 是独立的仓库副本,仓库转私有不影响已有的 fork。需要请求 GitHub 删除相关 fork(通过 support ticket),或者联系 fork 拥有者删除。若凭证已吊销,这个影响可以接受。
相关阅读
- Claude Code commit 了 secret
- 历史里的大文件让 push 失败
- Clone 之后 git hooks 不执行
- 凭证 helper 锁住,pull / push 全失败
- Force push 覆盖了队友的 commit
- 找回 Git 历史里的旧版本文件
标签: #git #version-control #排查