這次事件發生在一個使用外部 CSV 的 side project。我整理資料時,誤把其中一份含有機密內容的 CSV 加進 Git。發現後,即使再提交一次刪除檔案的變更,也只會讓它從最新版本消失;舊 Git object 仍可能在本機或遠端被取回。
需要處理的是兩件事:已暴露的資料,以及仍可存取的 Git 歷史。若資料是 token、密碼或憑證,要先撤銷或輪替;若是無法撤銷的機密內容,則要限制存取、檢查稽核紀錄與副本,並依事件需求處理通知。刪除歷史無法收回別人已取得的資料。
以下以 data/external/private-source.csv 示範。操作前須確認 repository 的 refs、branch protection 與協作方式。
刪除檔案會留下什麼
git rm data/external/private-source.csv
git commit -m "remove committed confidential CSV"
目前 branch 不再包含檔案,但早期 commit 仍有原始內容。把值換成 REDACTED 也一樣,Git 會保存前一個 blob。
加入 .gitignore、關閉 merge request、把 repository 改為 private,或只 force-push 目前 branch,都不能證明舊 object 已清除,也不能讓憑證失效。
先控制外洩範圍
- 撤銷或輪替已暴露的憑證;無法撤銷的機密資料則先限制存取,評估可能取得資料的人與系統。
- 檢查供應商的 access 與 audit logs。
- 若外洩仍在持續,暫時限制 repository 存取。
- 請協作者暫停 push,直到歷史改寫完成。
- 找出所有曾保存該資料的路徑與檔名。
同一個檔案若有多份憑證,要全部處理;同一份憑證若用在其他系統,也要納入範圍。CI logs、artifacts、package caches、forks、備份、對話與其他人的 clone 都可能留有副本。
找出受影響的 commits 與 objects
先查改動過該路徑的 commits,再列出目前 refs 能到達的 objects:
git log --all --name-status -- data/external/private-source.csv
git rev-list --objects --all | rg ' data/external/private-source\.csv$'
若檔案改過名稱或移過位置,所有舊路徑都要查。也要檢查哪些 branches、tags 與其他 refs 包含受影響的 commit:
git branch --all --contains <commit>
git tag --contains <commit>
git for-each-ref --contains <commit>
如果 secret 是混在正常檔案裡的一段字串,只查路徑不夠。可以用安全且足以辨認的片段,例如 key identifier,搜尋歷史;不要把完整 secret 輸入指令:
git log -S'<safe-identifier>' --all -p
將受影響路徑、最早的 commits 與已知 blob IDs 記在私有事件筆記中,作為改寫後的查驗目標。
在全新 clone 改寫歷史
使用全新 clone,避免把無關的工作、stash 或 reflog 混入清理。Git 官方建議使用 git-filter-repo 取代舊的 git filter-branch。整份檔案都應移除時:
git filter-repo \
--sensitive-data-removal \
--invert-paths \
--path data/external/private-source.csv
有舊檔名時,將每條路徑加入同一次操作:
git filter-repo \
--sensitive-data-removal \
--invert-paths \
--path data/external/private-source.csv \
--path imports/private-source.csv
只需替換正常檔案中的特定值時,可用 --replace-text。在受保護的本機環境準備替換檔,避免原值進入 shell history、終端錄影或 CI logs。
不要習慣性加上 --force;git-filter-repo 的 fresh-clone check 是為了避免改寫錯誤的工作目錄。舊環境中的 git filter-branch 可以作為受限情況下的替代,但還需處理 backup refs 與 reflogs。
驗證本機歷史
下面兩項查詢應找不到該路徑:
git log --all -- data/external/private-source.csv
git rev-list --objects --all | rg ' data/external/private-source\.csv$'
若已記錄暴露的 blob ID,再確認它是否仍存在於本機 object database:
git cat-file -e <exposed-blob-id>
移除成功時,git cat-file -e 應失敗。若成功,表示仍有 ref 或本機 object database 留著它,需要繼續查明。
也要執行專案原有的測試與建置,確認程式、部署或 CI 不再依賴被刪除的檔案。需要的 runtime 設定應改由環境變數、secret manager、SOPS、Vault 或部署平台提供。
協調後再更新遠端
改寫會產生新的 commit IDs。推送前確認協作者已暫停 push、未合併工作已保存、branch protection 允許預定更新,並了解供應商對隱藏 refs 與快取的清理程序。
Mirror force push 可更新一般 refs:
git push --force --mirror origin
這會覆寫 branches 與 tags,也可能丟掉清理開始後別人推送的工作。只能從已檢查的改寫 clone,在協調好的時段執行。
Pull/merge request refs、cached diffs、forks、LFS objects 與 server backups 可能不受一般 push 控制,需要供應商的清理流程。GitLab 的一般檔案 purge 也不同於 sensitive-data removal;密碼或金鑰應使用 Repository maintenance 的 Remove blobs 或 Redact text,並完成後續 housekeeping 與 unreachable-object pruning。GitLab 文件說明了這項差別。
另建 mirror clone 稽核遠端
推送完成後,從 server 重新取得一份 mirror clone:
git clone --mirror <repository-url> remote-audit.git
cd remote-audit.git
git log --all -- data/external/private-source.csv
git rev-list --objects --all | rg ' data/external/private-source\.csv$'
git cat-file -e <exposed-blob-id>
分開記錄每項結果能證明的範圍:
- 本機:refs 已無法到達受影響路徑,已知 blob 也不再存在。
- 遠端:全新 mirror clone 無法從 server 公開的 refs 找回該路徑或 blob。
- 資料外洩:可撤銷憑證已失效,其他機密內容已依事件程序處理。
- 外部副本:forks、快取、artifacts 與其他 clones 已處理,或已記錄仍存在的副本。
Mirror clone 只能驗證 server 提供給它的內容,不能證明供應商隱藏的資料或外部副本全部消失。
更換或清理舊 clone
舊 clone 可能在下次 push 時把污染的歷史帶回來。通常可先保存未推送的 patches,停止使用舊 clone,重新 clone 清理後的遠端,再把 patches 套到新歷史。
必須保留舊 clone 時,要移除舊 refs、處理 reflogs 與 unreachable objects,並確認已知的問題 IDs 不再存在。git reflog expire --expire=now --all 和 git gc --prune=now 會永久移除恢復資料;執行前要確認目標 clone,並保存仍需要的工作。
防止再次提交 secret
用 .gitignore 排除本機 secret 檔:
.env
.env.*
!.env.example
*.p8
*.p12
*.pem
*.key
*.mobileprovision
這只能避免未追蹤檔案被意外加入,不能移除已追蹤檔案,也無法偵測貼進程式碼的 secret。
CI 可以先檢查敏感檔名:
git ls-files | rg '(^|/)(\.env($|\.)|.*\.(p8|p12|pem|key|mobileprovision)$)'
實際檢查需為 .env.example 與安全的 test fixtures 設定有限的 allowlist,找到禁止追蹤的路徑時讓 CI 失敗。
另外在 pre-commit/pre-push 與 CI 掃描內容,檢查 token 格式、private-key headers 與供應商憑證。Local hooks 可以被略過,因此 CI 仍需執行。誤判要逐項核對,避免停用整個 repository 的規則。
Runtime 值可由 Vault 與 External Secrets提供;若需要把加密值版本控管,則使用 SOPS並另外管理解密金鑰。把加密檔和私鑰一起提交,仍會讓讀取 repository 的人取得原值。
結案前的檢查清單
- 可撤銷的憑證已撤銷或輪替。
- 不可撤銷的機密資料已依事件與通知程序處理。
- 已檢查供應商 audit logs。
- 所有歷史路徑、branches、tags 與其他 refs 都已納入。
- 本機歷史不再包含該路徑,已知問題 IDs 無法解析。
- 刪除檔案後,應用程式測試與部署檢查仍通過。
- 已在協調好的時段更新遠端 refs。
- 全新 mirror clone 通過獨立查驗。
- 供應商快取、隱藏 refs、forks 與 LFS objects 已處理。
- 舊 clone 已更換或清理。
- 已加入忽略規則、檔名檢查與內容掃描。
- 已記錄替代的 secret delivery 方式。