如何从 Git 历史中删除敏感数据(这次真的)

发布: (2026年4月5日 GMT+8 10:01)
9 分钟阅读
原文: Dev.to

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 --all

BFG 有意不修改你的最新提交,只影响历史记录。这是一项安全特性——它假设你当前的 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_Store

2. 设置 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 日志或制品存储
  • 已经获取了秘密的本地克隆
  • 可能已缓存数据的任何第三方服务

因此,在你清理完仓库后:

  1. 轮换 所有受影响的凭证(API 密钥、密码、证书等)。
  2. 作废 任何已泄露的令牌。
  3. 审计 日志,查找可能使用泄露秘密的可疑活动。
  4. 通知 任何可能受到影响的利益相关者或客户。

TL;DR

  • 在 Git 中删除文件并不会从历史记录中抹除它。
  • git revertgit commit --amend 并不能解决此问题。
  • 使用 git filter-repo(或 BFG)来 真正 清除机密。
  • 强制推送重写后的历史 并且 与团队协作同步。
  • 通过完善的 .gitignore、提交前的机密扫描以及正确的机密管理来防止未来泄漏。
  • 清理完毕后,务必轮换所有已泄露的凭证。
# Secrets in Git – What Happens When You Accidentally Push a Credential?

密钥可能泄露的地方

  • 你的仓库的 Fork
  • 任意人的本地克隆
  • 搜索引擎缓存
  • 类似 Wayback Machine 的服务

如果密钥曾经被推送到公共仓库,即使时间很短,也要假设它已经泄露。
立即轮换凭证。重新生成 API 密钥。更改密码。更新证书。

GitHub 的文档对此直言不讳:他们明确指出,强制推送并不会从缓存视图或克隆副本中删除数据。如果你把令牌推送到了公共仓库,请视为已失效。

0 浏览
Back to Blog

相关文章

阅读更多 »

逐步 Git 命令指南

初始设置 bash git config --global user.name 'Your Name' git config --global user.email 'your@email.com' 初始化一个新仓库 git init 添加远程…

Cx 开发日志 — 2026-04-05

概述:合并分支通常不是项目中最激动人心的部分,但它对于保持整体视图的同步至关重要。今天我专注于分支...

设置 config.py

每个项目的开始都是一样的……你硬编码一些值,随意插入一些 os.getenv 调用,然后对自己说“我以后再把它们清理干净”。以后从未到来。