每個開發者的噩夢:你提交程式碼,推送到儲存庫,突然意識到你的 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 的憑證視為完全洩露,無論儲存庫可見性如何。立即輪換,徹底清理歷史,並實施全面的預防機制以避免未來的事件。考慮遷移到自動過期的短期憑證——它們透過限制暴露視窗大幅減少了未來洩露的影響。正確的密鑰管理的營運負擔遠小於憑證洩露的成本和事後回應的複雜性。