刪掉 secret 只是開始。Git 歷史要能驗收
Simon Willison 在 2026-09-24 發了 commit-rewriter 0.2,工具可以協助偵測和改寫 Git commit history 裡的敏感內容。看起來很實用,但我看到這類工具時,第一個問題通常不是它能不能刪乾淨,而是刪完之後我們能不能證明 repository 還是同一個東西,CI、remote 和其他副本也沒有留下一個誰都沒想到的洞。
說穿了就是,secret removal 只是 rewrite 的一個動作,驗收才是完整工作。
先把目標定義清楚
上個月處理一個測試 API key 被 commit 的案子,AI 很快就把幾個 blob 重寫掉,搜尋新 branch 看不到 key,大家差點直接 force push。後來我把 rewrite 前後的 tree 做比對,才發現某個 generated config 在重寫過程被一起改了。內容只差一行,卻讓 staging 的 deployment manifest 不再能套用。
所以 rewrite 前先建立基準,不要只記得「哪個字串要消失」。至少留這些資料:
git rev-parse --show-toplevel
git log --all --format='%H %T %P %s' > /tmp/before-log.txt
git ls-tree -r --full-tree HEAD > /tmp/before-tree.txt
git count-objects -vH > /tmp/before-objects.txt
如果是要保留可追溯性,還要列出受影響 commit、tag、branch,以及預期不變的檔案 hash。rewrite 後重新輸出,先比 tree,不要先比 commit hash。commit hash 變掉是預期結果,tree hash 若在不該變的地方變掉,才是事故訊號。
git ls-tree -r --full-tree HEAD > /tmp/after-tree.txt
diff -u /tmp/before-tree.txt /tmp/after-tree.txt
git diff --stat OLD_TIP NEW_TIP
git diff --exit-code OLD_TIP NEW_TIP -- ':!path/to/expected-secret-file'
上面最後一行不是萬靈丹,因為 rewrite 可能改的是歷史中的檔案版本,不是目前 HEAD。實務上我會對每一個受影響 commit 的 tree 做 mapping,再對照測試分支跑 build、unit test 和 deployment dry run。至少要驗證三件事,檔案路徑沒有莫名消失,非敏感內容沒有變,重新 checkout 後能產出同樣的 artifact。
搜尋不只做一次
第二層是 secret scan。不要只對目前 checkout 跑 grep,因為敏感資料可能躲在舊 commit、annotated tag、packed object 或 binary 裡。rewrite 後我會分三個範圍掃:
git grep -n -I -E 'AKIA[0-9A-Z]{16}|BEGIN (RSA|OPENSSH|EC) PRIVATE KEY' $(git rev-list --all)
git fsck --full --no-reflogs --unreachable
git fsck --full --no-reflogs --lost-found
再用公司固定的 secret scanner 掃整個 bare mirror,而不是只掃工作目錄。工具名稱可以是 gitleaks、trufflehog 或內部 scanner,重點是版本和規則要記錄。曾經遇過 scanner A 回報 0 筆,scanner B 卻從一個舊 tag 抓出 token。兩者不是誰一定錯,規則集不同而已。驗收紀錄要寫清楚用哪個版本、哪份 allowlist、掃了哪個 ref。
git fsck 也不是「沒有輸出就安全」的證明。你如果先刪掉 reflog,會讓檢查範圍改變。比較穩的做法是 rewrite 前先把所有 ref、reflog、tag、remote tracking branch 匯出保存,rewrite 後掃一次,再針對即將刪除的舊 refs 做第二次確認。
force push 不是按鈕,是變更審批
我不會讓產生 rewrite 的人自己批准 force push。最少需要 repository
owner 或 security owner 加一位 service owner,確認三個問題:
- 新 tip 的 tree 和測試結果符合預期。
- 所有 active clone、mirror、remote、CI runner cache 都有處理方式。
- secret 已經 rotate。
GitHub、GitLab 上的 branch protection、webhook payload、PR patch、CI log、artifact registry 都可能留著舊內容。尤其 CI artifact 常被當成「只是測試輸出」,實際上 retention 可能是 30、90 天,甚至永久保留。你把 main rewrite 得很漂亮,但下一個人從舊 build artifact 下載設定檔,事情還是回到原點。
什麼時候根本不該 rewrite
如果 secret 曾經進過 shared remote、PR、CI log,或你不知道它被誰拉過,第一個動作永遠是 revoke 或 rotate。rewrite 只是在降低未來誤觸的機率,不會讓已經看過 secret 的人失憶。
我通常這樣判斷:
secret 可用,或權限範圍不明 先 revoke / rotate
只在未推送的 local commit 直接 reset 或 amend,仍要 scan
已推送但無法確認誰讀過 rotate,再評估 rewrite
公開 repository 的高風險憑證 rotate、封鎖舊憑證、rewrite、全面通知
低風險測試值且已明確失效 可 rewrite,但保留驗證紀錄
```AI 很適合幫忙找候選 secret、產生 rewrite plan、列出受影響 commit,甚至自動跑一輪檢查。但批准的人要看的是語意和可驗證性。它刪掉的內容是不是該刪的,沒被刪的內容是否真的安全,tree 和 artifact 是否仍然可用,所有殘留面是否有人負責。
最後驗收我會留下四份東西,before 和 after 的 ref 清單,tree 與測試比對結果,secret scan 和 fsck 輸出,還有 rotate、remote、CI artifact 的清理紀錄。這些東西比一句「AI 幫我清好了」有用很多。因為下次出事時,真正要回答的不是誰按了 rewrite,而是我們怎麼知道 rewrite 沒有順手製造另一個問題。
作者:CtrlC