如何从 Git 历史中删除敏感数据(这次真的)
Source: Dev.to
不行。那个 API 密钥、那个 .env 文件、那个包含数据库凭证的内部配置——它们仍然在你的 git 历史中安然无恙,等着任何使用 git log 并稍加好奇的用户去发现。
我大约四年前才吃了这口苦——当时同事提醒我,我们的预发布数据库密码在公共仓库里可见。我已经在三个月前把文件删掉了。结果不管用。Git 记得一切。
为什么删除文件并没有真正删除它
Git 是一个内容可寻址的文件系统。每一次提交都是当时整个项目的快照。当你 git rm secrets.env 并提交时,你创建了一个 新的 快照,其中没有该文件——但所有之前的快照仍然包含它。
任何人都可以看到它:
# 查找所有涉及特定文件的提交,包括已删除的文件
git log --all --full-history -- path/to/secrets.env
# 在特定提交中显示该文件的内容
git show a1b2c3d:path/to/secrets.env这本就是设计如此。Git 的全部目的就是永不丢失数据。这对源代码来说很好,但对机密信息来说却很糟糕。
错误的修复:git revert
我经常看到有人这样做。他们运行 git revert 以为可以撤销破坏,实际上并不能。revert 会创建一个 新 的提交来逆转更改——包含你机密信息的原始提交仍然在历史记录中。
对已经推送的提交使用 git commit --amend 也是同样的情况。旧的提交对象仍然存在于 reflog 中,甚至可能已经在远程仓库里。
正确的解决方案:git filter-repo
旧的建议是使用 git filter-branch,但它非常慢且容易出错。Git 项目现在推荐 git-filter-repo。
从整个历史中清除文件
# 安装 git-filter-repo(需要 Python 3.5+)
pip install git-filter-repo
# 克隆一个全新的副本 —— filter-repo 需要全新克隆
git clone --mirror https://github.com/you/your-repo.git
cd your-repo.git
# 从所有历史记录中删除指定文件
git filter-repo --path secrets.env --invert-paths
# 从所有历史记录中删除目录
git filter-repo --path config/internal/ --invert-paths--invert-paths 标志的含义是“保留 除该路径之外的所有内容”。如果不加此标志,Git 只会保留指定的路径并删除其它所有内容。问问我怎么知道的吧。
清除特定字符串(例如硬编码的 API 密钥)
# 在所有历史记录中替换特定字符串
git filter-repo --replace-text REDACTED推送已改写的历史
git push origin --force --all
git push origin --force --tags重要:告知你的团队
强制推送会重写历史。每位协作者都需要重新克隆或仔细地变基他们的本地分支。如果他们推送了旧的本地副本,所有被清除的数据会立刻恢复。请在强制推送前发送消息并协调时间。
关于 BFG Repo Cleaner?
BFG Repo Cleaner 是另一个可靠的选项,尤其是当你更喜欢基于 Java 的工具时。它比 filter-branch 更快,并且为常见操作提供了更简洁的界面。
# Remove files by name from all history
java -jar bfg.jar --delete-files secrets.env your-repo.git
# Replace specific text patterns
java -jar bfg.jar --replace-text passwords.txt your-repo.git
# Then clean up and push
cd your-repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push origin --force --allBFG 有意不修改你的最新提交,只影响历史记录。这是一项安全特性——它假设你当前的 HEAD 已经是干净的。
从根本上防止此类问题
重写历史非常痛苦。下面介绍如何避免必须这样做。
1. 使用真正有效的 .gitignore
# Environment and secrets
.env
.env.*
*.pem
*.key
*.p12
# Cloud provider configs
.aws/credentials
.gcp-credentials.json
# IDE and OS junk that sometimes contains paths/tokens
.idea/
.vscode/settings.json
.DS_Store2. 设置 Pre‑commit Hook
pre‑commit 与检测机密的插件可以在提交前捕获大多数意外提交:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']运行 pre-commit install 一次,它会在每次提交时扫描可能是机密的内容——高熵字符串、已知的 API‑key 模式、私钥等。
3. 使用环境变量或密钥管理器
这听起来很显而易见,但我仍然会看到有人在 PR 中硬编码连接字符串。开发时使用环境变量,生产环境使用正式的密钥管理器(Vault、云提供商的原生方案,或个人项目的 pass 等)。
经验法则: 如果某个值在落入他人之手后会造成损害,它 绝不能 放入版本控制。永远如此。
清理后:全部轮换
这是人们常常跳过的步骤,但可以说是最重要的一步。重写 Git 历史会把秘密从 your repository 中移除。但它 不会 从以下位置移除:
- GitHub 的缓存副本以及任何 fork
- CI/CD 日志或制品存储
- 已经获取了秘密的本地克隆
- 可能已缓存数据的任何第三方服务
因此,在你清理完仓库后:
- 轮换 所有受影响的凭证(API 密钥、密码、证书等)。
- 作废 任何已泄露的令牌。
- 审计 日志,查找可能使用泄露秘密的可疑活动。
- 通知 任何可能受到影响的利益相关者或客户。
TL;DR
- 在 Git 中删除文件并不会从历史记录中抹除它。
git revert和git commit --amend并不能解决此问题。- 使用
git filter-repo(或 BFG)来 真正 清除机密。 - 强制推送重写后的历史 并且 与团队协作同步。
- 通过完善的
.gitignore、提交前的机密扫描以及正确的机密管理来防止未来泄漏。 - 清理完毕后,务必轮换所有已泄露的凭证。
# Secrets in Git – What Happens When You Accidentally Push a Credential?密钥可能泄露的地方
- 你的仓库的 Fork
- 任意人的本地克隆
- 搜索引擎缓存
- 类似 Wayback Machine 的服务
如果密钥曾经被推送到公共仓库,即使时间很短,也要假设它已经泄露。
立即轮换凭证。重新生成 API 密钥。更改密码。更新证书。
GitHub 的文档对此直言不讳:他们明确指出,强制推送并不会从缓存视图或克隆副本中删除数据。如果你把令牌推送到了公共仓库,请视为已失效。