每个开发者的噩梦:你提交代码,推送到仓库,突然意识到你的 AWS 访问密钥、数据库密码或 API 令牌现在已经在 Git 历史中了。恐慌随之而来。你快速删除凭证并再次提交,但损害已经造成——这些秘密仍然保留在 Git 历史中,任何克隆仓库的人都可以访问。
这种情况每天在各个组织中上演数千次。响应通常涉及疯狂地搜索 Google、尝试重写 Git 历史,并希望凭证尚未被泄露。然而,正确的凭证泄露响应不仅仅是删除文件——它需要理解 Git 的内部机制、立即轮换凭证以及实施预防机制。
Git 历史问题
Git 的设计使凭证泄露特别危险。理解原因需要检查 Git 如何存储数据。
为什么删除还不够
当你提交包含凭证的文件并稍后删除它时,凭证仍然保留在 Git 历史中:
🚫 删除的假象
开发者通常做什么
- 意外提交包含凭证的代码
- 意识到错误
- 删除文件或移除凭证
- 提交删除操作
- 认为问题已解决
为什么这会失败
- Git 保留所有提交的完整历史
- 之前的提交仍然包含凭证
- 任何人都可以检出旧提交
git log -p会显示凭证- 克隆仓库包含完整历史
现实情况
- 凭证无限期地保持可访问
- 所有仓库克隆都包含泄露
- 分支和镜像保留凭证
- 搜索引擎可能已索引泄露
- 自动扫描器持续监控公共仓库
简单地在新提交中删除凭证就像在网上发布钥匙后锁门——损害已经造成。
Git 的不可变历史
Git 的内容可寻址存储使历史修改变得复杂:
🔗 Git 存储模型
Git 如何存储数据
- 每个提交都有唯一的 SHA-1 哈希
- 哈希从提交内容、父哈希和元数据计算
- 更改任何提交都会更改其哈希
- 所有后代提交也会更改哈希
- 创建历史的加密链
对凭证删除的影响
- 删除凭证需要重写历史
- 泄露后的所有提交都必须重新创建
- 新哈希会破坏现有引用
- 协作者必须丢弃他们的本地副本
- 分支和镜像不受影响
协调挑战
- 每个开发者必须获取重写的历史
- 旧提交必须被垃圾回收
- 远程仓库必须强制推送新历史
- CI/CD 系统必须更新引用
- 备份系统可能保留旧历史
这种架构意味着凭证泄露无法真正"撤销"——只能通过轮换和历史重写来缓解。
立即响应:先轮换,后清理
当凭证泄露到 Git 时,优先级顺序至关重要。
步骤 1:立即轮换凭证
在尝试任何 Git 历史操作之前,轮换被泄露的凭证:
⚠️ 清理前先轮换
为什么轮换优先
- Git 历史重写需要时间
- 凭证可能已经被泄露
- 自动扫描器在几分钟内检测到泄露
- 清理 Git 不会撤销访问权限
- 攻击者可能已经克隆了仓库
需要轮换什么
- API 密钥和令牌
- 数据库密码
- SSH 私钥
- OAuth 客户端密钥
- 加密密钥
- 服务账户凭证
轮换清单
- 立即生成新凭证
- 更新所有使用凭证的服务
- 完全撤销旧凭证
- 监控未经授权的访问尝试
- 记录事件以供安全审查
- 考虑未来使用短期凭证
立即轮换凭证限制了漏洞窗口。清理 Git 历史很重要但次要。
⏱️ 短期凭证建议
为什么短期凭证很重要
- 在定义的期限后自动过期
- 如果泄露,减少暴露窗口
- 无需手动轮换
- 限制泄露的影响范围
实施选项
- AWS STS 临时凭证(15 分钟 - 12 小时)
- Vault 动态密钥(几分钟到几小时)
- 具有短过期时间的 OAuth 令牌
- 具有 TTL 的服务账户令牌
事后行动
- 尽可能迁移到短期凭证
- 减少未来泄露的影响
- 查看预防策略了解实施细节
步骤 2:评估暴露范围
确定凭证传播的范围:
🔍 暴露评估
需要回答的问题
- 仓库是公共的还是私有的?
- 凭证暴露了多长时间?
- 有多少人有仓库访问权限?
- 是否有分支或镜像?
- CI/CD 系统是否访问了凭证?
- 提交是否推送到多个远程?
公共仓库暴露
- 假设凭证已完全泄露
- GitHub、GitLab、Bitbucket 通知安全研究人员
- 自动扫描器在几分钟内检测到密钥
- 搜索引擎可能已索引内容
- 凭证轮换是强制性的,而非可选
私有仓库暴露
- 评估谁有仓库访问权限
- 检查访问日志是否有异常活动
- 审查凭证使用的审计日志
- 考虑内部威胁场景
- 作为预防措施轮换凭证
暴露评估决定了响应行动的紧迫性和范围。
从 Git 历史中删除凭证
轮换凭证后,清理 Git 历史以防止未来暴露。
使用 git filter-branch(传统方法)
传统方法使用 git filter-branch 重写历史:
# 从所有提交中删除特定文件
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch path/to/credentials.txt" \
--prune-empty --tag-name-filter cat -- --all
# 强制垃圾回收
git reflog expire --expire=now --all
git gc --prune=now --aggressive
⚠️ filter-branch 的局限性
为什么 filter-branch 有问题
- 在大型仓库上极其缓慢
- 复杂的语法容易出错
- 不处理所有边缘情况
- 如果中断可能损坏仓库
- Git 文档推荐替代方案
何时仍可能使用
- 没有 BFG 或 filter-repo 的传统系统
- 简单的单文件删除场景
- 历史有限的小型仓库
- 当其他工具不可用时
现代工具为大多数场景提供了更好的替代方案。
使用 BFG Repo-Cleaner(推荐)
BFG Repo-Cleaner 提供更快、更简单的方法:
# 下载 BFG(需要 Java)
# https://rtyley.github.io/bfg-repo-cleaner/
# 删除特定文件
java -jar bfg.jar --delete-files credentials.txt repo.git
# 删除匹配模式的文件
java -jar bfg.jar --delete-files "*.key" repo.git
# 替换所有文件中的凭证
echo "password123" > passwords.txt
java -jar bfg.jar --replace-text passwords.txt repo.git
# 清理
cd repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
✅ BFG 的优势
为什么 BFG 更好
- 比 filter-branch 快 10-720 倍
- 简单、直观的语法
- 默认保护 HEAD 提交
- 高效处理大型仓库
- 经过充分测试和广泛使用
BFG 功能
- 按名称或模式删除文件
- 跨所有文件替换文本
- 删除大文件
- 按大小剥离 blob
- 保留最近的提交
BFG 是大多数凭证删除场景的推荐工具。
使用 git filter-repo(现代替代方案)
git filter-repo 提供最强大和灵活的方法:
# 安装 filter-repo
pip install git-filter-repo
# 删除特定文件
git filter-repo --path path/to/credentials.txt --invert-paths
# 删除匹配模式的文件
git filter-repo --path-glob '*.key' --invert-paths
# 使用回调替换凭证
git filter-repo --replace-text <(echo "password123==>REDACTED")
# 从特定目录删除凭证
git filter-repo --path secrets/ --invert-paths
🔧 filter-repo 功能
高级功能
- 在大型仓库上性能快
- 强大的过滤选项
- 基于 Python 的可扩展性
- 全面的安全检查
- 详细的操作日志
复杂场景
- 条件文件删除
- 内容转换
- 路径重写
- 提交消息修改
- 作者信息更新
对于复杂场景或大型仓库,git filter-repo 提供了最佳的功能和安全性平衡。
强制推送和协调
重写历史后,将更改传播到所有仓库副本。
强制推送到远程
重写的历史需要强制推送:
# 强制推送到 origin
git push origin --force --all
# 强制推送标签
git push origin --force --tags
# 替代方案:force-with-lease(更安全)
git push origin --force-with-lease --all
⚠️ 强制推送风险
强制推送的危险
- 覆盖远程历史
- 破坏其他开发者的本地副本
- 如果不协调可能丢失提交
- 可能被仓库设置阻止
- 需要特殊权限
使用 --force-with-lease
- 比普通 --force 更安全
- 检查远程是否有意外更改
- 防止意外覆盖
- 仍需要协调
- 推荐使用而非 --force
强制推送应与所有仓库用户协调。
与团队成员协调
所有协作者必须更新他们的本地副本:
👥 团队协调步骤
需要的沟通
- 在重写前通知所有团队成员
- 解释为什么要重写历史
- 提供清晰的更新说明
- 设定本地更新的截止日期
- 验证每个人都已成功更新
团队成员的说明
# 保存任何本地工作
git stash
# 获取重写的历史
git fetch origin
# 重置以匹配远程
git reset --hard origin/main
# 清理旧引用
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# 恢复本地工作
git stash pop
**处理冲突**
- 本地提交必须变基到新历史
- 可能需要手动解决冲突
- 考虑在更新前创建补丁
- 更新后彻底测试
协调不当可能导致旧历史被重新引入。
处理分支和镜像
仓库分支和镜像保留原始历史。
分支问题
分支是保留泄露凭证的独立副本:
🚫 分支保留泄露
为什么分支有问题
- 分支是独立的仓库
- 重写父历史不影响分支
- 分支所有者可能不知道泄露
- 公共分支是可发现的
- 没有机制强制分支更新
缓解策略
- 直接联系分支所有者
- 请求他们删除或更新分支
- 为公共分支提交 DMCA 删除请求(极端情况)
- 监控分支的凭证使用
- 接受某些分支可能保留
对于公共仓库,假设分支凭证已永久暴露。
处理镜像和备份
镜像和备份系统可能保留旧历史:
⚠️ 镜像注意事项
常见镜像位置
- CI/CD 系统缓存
- 备份系统
- 代码审查工具
- IDE 仓库缓存
- 容器镜像层
- 部署工件
清理操作
- 清除 CI/CD 缓存
- 更新备份系统
- 重建容器镜像
- 重新部署应用程序
- 清除 IDE 缓存
- 更新文档仓库
全面清理需要识别所有仓库副本。
预防:更好的方法
虽然本文重点关注恢复,但预防比修复更有效。全面的预防策略涉及多个层次:
🛡️ 预防层次
配置
- 凭证文件的
.gitignore模式 - 个人模式的全局 gitignore
- 指导的模板文件(
.env.example)
代码实践
- 环境变量而非硬编码凭证
- 启动时的配置验证
- 凭证检测的代码审查
自动扫描
- 预提交钩子(git-secrets、detect-secrets)
- CI/CD 管道扫描
- 平台提供的扫描(GitHub、GitLab)
密钥管理
- AWS Secrets Manager、HashiCorp Vault
- 运行时密钥注入
- 自动凭证轮换
有关防止凭证泄露的全面指南,包括详细的代码示例和实施策略,请参阅在 Git 中防止凭证:分层防御策略。
在预防上的投资消除了复杂恢复程序、Git 历史重写和紧急凭证轮换的需要。在构建预防系统上花费的每一小时都能节省数天的事件响应时间。
真实世界事件:AWS 密钥泄露
一个常见场景说明了完整的响应过程:
⚠️ 事件时间线
第 1 天:泄露
- 开发者在配置文件中提交 AWS 访问密钥
- 推送到公共 GitHub 仓库
- GitHub 密钥扫描在 5 分钟内检测到密钥
- AWS 收到通知并警告账户所有者
- 自动扫描器开始尝试使用密钥
第 1 小时:发现和响应
- 安全团队收到 GitHub 警报
- 立即轮换 AWS 访问密钥
- 审查 CloudTrail 日志是否有未经授权的访问
- 发现启动了加密货币挖矿实例
- 终止未经授权的资源
第 2-4 小时:清理
- 使用 BFG 从 Git 历史中删除密钥
- 强制推送清理后的历史
- 联系分支所有者更新
- 更新 CI/CD 系统
- 清除备份系统
第 2-7 天:事后
- 实施预提交钩子
- 将 AWS 模式添加到 git-secrets
- 进行安全培训
- 审查其他仓库是否有泄露
- 记录事件和响应
这个事件表明为什么立即轮换至关重要——攻击者在几分钟内利用泄露的凭证。
结论
管理提交到 Git 的凭证需要理解仅删除是不够的。Git 的不可变历史无限期地保留泄露的凭证,任何有仓库访问权限的人都可以访问。正确的响应优先考虑立即凭证轮换而非 Git 历史清理——攻击者在几分钟内利用泄露的凭证,而历史重写需要数小时或数天。
从 Git 历史中删除凭证涉及使用 BFG Repo-Cleaner 或 git filter-repo 等工具重写提交,然后强制推送并与所有协作者协调。然而,分支、镜像和备份系统可能保留原始历史,使公共仓库的完全删除变得不可能。这一现实强化了凭证轮换是强制性的,而非可选的。
关键见解:将任何提交到 Git 的凭证视为完全泄露,无论仓库可见性如何。立即轮换,彻底清理历史,并实施全面的预防机制以避免未来的事件。考虑迁移到自动过期的短期凭证——它们通过限制暴露窗口大大减少了未来泄露的影响。正确的密钥管理的运营负担远小于凭证泄露的成本和事后响应的复杂性。