example-petstore.com

示例域名 · 并非真实服务 · 浏览器访问显示此页面 · API 请求返回 410 Gone

指南 · 安全

从 Git 历史记录中删除机密信息

密钥、令牌或密码被提交到了某个 commit 中。在新的 commit 中删除该文件并不能解决问题:旧的 commit 仍然包含它,存在于每个克隆和每个 fork 中。本指南介绍正确的顺序:先撤销,再重写历史记录,然后清理托管平台,并确保不再发生。

第一步:撤销机密信息

即使是在私有仓库中,也应将已提交的机密信息视为已泄露:克隆、fork、CI 日志和备份中可能已经存有副本,而自动扫描工具会在几分钟内发现公开仓库中的新 commit。

  1. 在签发该密钥、令牌或密码的服务中将其撤销或轮换。
  2. 将新的凭据保存在配置或密钥管理工具中,而不是代码中。
  3. 检查该服务的日志,查找您无法识别的使用记录。

在常见服务提供商处撤销密钥的位置,请参阅凭据泄露:现在该怎么办。重写历史记录是在此之后进行的步骤,绝不能代替撤销。

是否重写历史记录

机密信息一旦撤销就不再具有访问权限,GitHub 也指出这可能已经足够。但在以下情况下,仍然值得重写:

  • 机密信息无法撤销,或者文件中还包含其他敏感数据,例如个人数据或客户记录;
  • 仓库是公开的,或者将要公开;
  • 您希望扫描工具不再报告旧的机密信息。

重写会改变之后每个 commit 的 ID。协作者需要对自己的工作执行 rebase,未关闭的 pull request 可能会丢失评审意见,commit 签名也会被移除。请与所有相关人员约定时间,并先合并或关闭所有打开的 pull request。

找出所有出现位置

# 哪些 commit 添加或删除了该字符串?
git log --all --oneline -S 'the-secret-value'

# 它出现在哪些文件中?
git grep 'the-secret-value' $(git rev-list --all)

# 扫描整个历史记录中的已知机密信息格式
gitleaks git -v

记下机密信息出现过的每个文件路径,如果文件曾被移动或重命名,也要包括以前的名称。

使用 git-filter-repo 重写

git-filter-repo 是 GitHub 推荐的工具。请使用 2.47 或更高版本,该版本提供 --sensitive-data-removal 选项,并在全新的克隆中操作。

# 安装(或使用您的包管理器)
brew install git-filter-repo        # macOS
pip install git-filter-repo         # 任何装有 Python 的环境

git clone https://github.com/YOUR-ORG/YOUR-REPO
cd YOUR-REPO

# 方法 1:从全部历史记录中删除整个文件
git-filter-repo --sensitive-data-removal --invert-paths --path config/secrets.yml

# 方法 2:在所有出现的位置替换机密信息
git-filter-repo --sensitive-data-removal --replace-text ../replacements.txt

替换文件每行列出一个值。默认情况下,每个匹配项都会变成 ***REMOVED***;使用 ==> 可以自行指定替换内容,使用 regex: 则按模式匹配:

# ../replacements.txt(放在仓库之外)
sk_live_51Hx0000000000000000000000
AKIA0000000000000000==>AWS_ACCESS_KEY_ID_REMOVED
regex:password\s*=\s*"[^"]+"==>password = "REMOVED"

使用 git log --all -S 'the-secret-value' 检查结果:应当没有任何输出。然后覆盖远程仓库:

git push --force --mirror origin

如果分支保护会阻止强制推送,需要在此期间暂时关闭。推送之后,重写将无法撤消。

清理 GitHub

强制推送之后,旧的 commit 仍可通过 pull request、缓存视图和 fork 访问。

  • Pull request 和缓存视图:联系 GitHub 支持,并提供受影响的 pull request 数量(grep -c '^refs/pull/.*/head$' .git/filter-repo/changed-refs)以及 git-filter-repo 输出的“First Changed Commit(s)”。只有在轮换凭据无法消除风险时,GitHub 才会提供帮助。
  • Fork:fork 中的 commit 会保留在那里。请联系所有者删除或清理该 fork;GitHub 不会提供他们的联系方式。
  • 同事的克隆:每个人都必须将自己的分支 rebase 到新的历史记录上,而不是 merge。只要一次 merge,就会把旧的 commit 带回来。

在 GitLab、Bitbucket 或自托管服务器上,步骤大致相同;请查阅相应平台的文档,了解它如何删除旧对象和缓存视图。

替代方案:BFG

BFG Repo-Cleaner 是一款较早的、基于 Java 的工具,目前仍被广泛使用。它在镜像克隆上运行,并且默认不改动最新的 commit,因此请先通过一次普通 commit 从当前版本中删除机密信息。

git clone --mirror https://github.com/YOUR-ORG/YOUR-REPO.git
java -jar bfg.jar --replace-text replacements.txt YOUR-REPO.git
cd YOUR-REPO.git
git reflog expire --expire=now --all && git gc --prune=now --aggressive
git push

防止下一次泄露

  • 推送保护:在 GitHub 上,启用推送保护的机密扫描会在推送到达仓库之前,阻止包含已知机密信息格式的推送。
  • 提交前检查:将 gitleaks 或 git-secrets 作为 pre-commit 钩子运行,并在 CI 中再运行一次,以覆盖跳过钩子的人。
  • 用配置,而不是代码:从环境变量或密钥管理工具中读取机密信息,并在第一次 commit 之前将 .env 等文件添加到 .gitignore。
  • 先看再提交:逐个暂存文件并检查 git diff --cached,而不是使用 git add . 或 git commit -a。

这个密钥是否也发送到了 api.example-petstore.com 这样的占位地址?那么它也已到达一台不受您控制的服务器:配置 API 客户端和 SDK。

来源