iT邦幫忙

2026 iThome 鐵人賽

DAY 3
1
AI Security

你該防的不是駭客,是你自己的 AI:在本機驗證你的防線系列 第 3

Day 3|第一步是讓舊鑰失效,但送進 AI 的那些收不回來

  • 分享至 

  • xImage
  •  

大家好,我是中義。

昨天那行指令跑完,我朋友的歷史裡真的有一份 .env,半年前那個 commit。我叫他先去供應商後台把金鑰撤掉,歷史先不要動。今天講為什麼是這個順序。順序走完之後還有一份,那份你收不回來。

那三十秒

他的第一個訊息是:「我把那個 commit 砍掉就好了吧?我看網路上都這樣寫。」

第二個訊息是三十秒後:「還是我直接把 repo 刪掉重開?反正也沒幾個 star。」

我完全理解這個反應。收到 GitHub 寄來那封「我們在你的公開 repo 裡偵測到憑證」的信,或是自己跑指令查出來的那一刻,腦子裡只有一個念頭:把它弄不見。

而且刪 repo 是真的做得到。三分鐘,Settings 拉到最底,打一次專案名字確認,整個東西就不見了。GitHub 上搜不到,clone 不下來,那封信講的東西從網路上消失。

看起來很乾淨。

你刪掉的東西,供應商不知道

但你有沒有想過,那把金鑰是誰發的?

不是 GitHub。是模型供應商。GitHub 上那串字只是一份拷貝,真正決定它能不能用的,是供應商後台那筆紀錄。你把 repo 刪光、把 commit 砍掉、force push 蓋掉整段歷史,供應商那邊什麼都沒發生。它還是那把可以用的鑰匙。

而在你刪掉之前,那串字在公開網路上躺了半年。

這件事有數字。GitGuardian 追蹤 2022 年在公開 repo 上確認有效的憑證,四年後還有 64% 是有效的,而且還躺在公開 repo 上。三分之二沒被撤銷。

GitGuardian 2026 報告的洩漏憑證有效比例統計圖,標題是「The majority of leaked secrets remain exploitable for years」。四根柱子按偵測年份分:2022 年那批 64% 仍有效、36% 已撤銷,2023 年 66%、2024 年 70%、2025 年 77% 仍有效。左側註明「We tracked valid secrets detected in 2022. Four years later, 64% are still active and exploitable, sitting in public repositories.」,下方寫「The problem isn't detection. It's remediation.」

圖裡那四根柱子是按偵測年份分的,越舊的那批撤銷比例越高,因為它們有比較長的時間被處理。這張圖不能拿來判斷整體在變好或變壞,能看的是另一件事:2022 那批放了四年,撤掉的也才三成六。

公開 repo 會被自動掃,這件事不需要有人盯著。所以推上去的那一刻就要當成它已經在別人手上,不能拿「我很快就刪掉了」當作沒事的理由。刪掉的是你這份,別人抓走的那份不會跟著消失。

所以第一個要達成的只有一件事:讓舊鑰失效。

外洩憑證處置流程圖。發現外洩後有三條路:刪掉那個 commit、整個 repo 刪掉重開,這兩條都通往「金鑰還活著,你只是把它藏起來」。第三條是到供應商後台換掉那把鑰匙(有正式流量就先發新鑰再撤舊),接著拿舊鑰打一次 API 要看到憑證無效的錯誤、查用量與請求紀錄且區間拉到最早外洩那天、裝 pre-commit hook 今天一定做完,最後清 git 歷史排進待辦不必今天。查用量旁邊另有一條虛線分支:「這把金鑰進過 AI 對話嗎,刪不刪得掉你驗不了」

這張流程圖右邊那條是今天要走的路:換鑰、驗證、查用量、裝 hook,四步今天做完,接在後面的清歷史排進待辦就好。左邊兩條不是「比較慢」,它們處理的是散布,不是憑證的效力。

「這把金鑰進過 AI 對話嗎」那一格先記著。它用虛線掛在查用量旁邊,因為它屬於盤點但不在主流程上,也因為它沒有「做完」這個狀態。最後一節會回來講。

動手之前先數一下有幾把。那份 .env 不會只有一行,資料庫連線字串、雲端服務的憑證、webhook 網址、第三方 API 的 token,同一份檔案裡的東西一起進了歷史,也一起躺了半年。GitHub 的告警只會指出它認得的那幾種,剩下的沒有人會替你列。

所以打開那份 .env,逐行寫下來:這是哪個服務的、在哪裡撤、撤掉會影響什麼。今天講的處置,每一把都要各走一遍。清單先列完再動手,撤到一半才發現還有三把,那時候你已經在切服務了。

然後到供應商後台處理。個人專案或還沒有人在用的服務,直接砍掉、發新的、切過去。

有正式流量在跑的話順序要反過來:先發新鑰、切過去、確認服務正常,再回頭砍舊的。先砍會停機,而停機不是必要的代價。

兩種順序的終點是同一個:能立刻撤就立刻撤,需要切換的就切完立刻撤。 中間那段是舊鑰還活著的視窗,越短越好,它不是一個可以排到明天的東西。

在那之前,刪 commit、刪 repo 這些動作減少的是散布,沒有一個讓那把鑰匙失去效力。

金鑰是在供應商那邊失效的,不是在你的 repo 裡失效的。

怎麼確定它真的死了

按下 Revoke 之後不要相信畫面上那行綠字。用舊鑰親手打一次 API,看它回什麼。

curl -sS -w '\nHTTP %{http_code}\n' \
 https://api.provider.example/v1/models \
 -H "Authorization: Bearer $OLD_KEY"

不要只看狀態碼,要看供應商在回應裡講了什麼。我用一把無效的鑰匙打 OpenAI,拿到的是 "code": "invalid_api_key"。這種明講憑證無效的錯誤才算數。

只看數字會誤判。401 是「這次請求沒通過驗證」,403 可能是鑰匙還活著只是權限不夠,429 是被限流,000 是根本沒連上。這幾個都不是撤銷成功。

上面那段只是 Bearer token 的示意,不要只換網址。 網址、方法、驗證標頭、必要參數,全部照你那家的文件設。這不是龜毛,是我實測踩到的:拿 Bearer 去打 Anthropic,回的是 "type": "authentication_error",訊息 Invalid bearer token;而真正無效的金鑰配上正確的 x-api-key 標頭,回的 type 一模一樣,只有訊息是 invalid x-api-key。標頭寫錯,你會看到一個跟「撤銷成功」長得幾乎一樣的畫面。

記住這個形狀,今天後面還會一再遇到:看起來跟成功一模一樣的失敗。 所以連訊息那一行都要讀完,不能只看 type。

還有一點:你要打的那個端點,必須是撤銷前用同一把鑰匙打得通的。拿一個本來就沒權限的端點去測,看到的拒絕跟撤銷無關。

curl-sS 不要用 -s。差別是連不上的時候 -s 什麼都不說,-sS 會告訴你 curl: (6) Could not resolve host。你需要分得出「打不到」跟「打到了被拒絕」。

這一步聽起來多餘,但我看過兩種它不多餘的情況。一種是後台有好幾把鑰匙、名字取得很像,撤到隔壁那把去了。另一種是撤銷有快取,看起來已經 revoked,實際上還能用幾分鐘。

沒驗過的撤銷不算撤銷。 跟第一天在 DevTools 裡親眼看那個 header 一樣,是同一件事的不同場合:你要的是那台伺服器自己回的東西,不是後台畫面上的宣稱。

這一步也是整個處置流程裡最不能交出去的一步。歷史清理的指令可以叫 AI 起草,帳單怎麼看可以問 AI,但「舊鑰真的死了」必須你自己打那一發。AI 沒有你的後台,它只能根據你貼給它的敘述說「那應該已經失效了」。

被盜用不一定有人通知你

新鑰發好了,接下來要問一個沒人想問的問題:那半年裡,有沒有人用過它?

外洩可能會有人通知你,開頭那封 GitHub 的信就是。被盜用就不一定了。有些供應商有異常偵測會寄信,但你不能指望它,因為盜用的長相就是一個正常的 API 請求,帶著一把有效的憑證。

用量是最容易查的那條線索,但它不是唯一一條。供應商如果有請求紀錄、稽核紀錄、預算警示,一起看;程式碼平台的 secret scanning 有時候比你先發現。

反過來說,用量正常不能證明沒被用過,只能證明沒被大量用過。

到後台把用量拉成按日或按小時的圖,看兩件事。第一是有沒有你根本沒在跑的時段冒出流量,半夜三點、你出國那一週。第二是有沒有你沒用過的模型或端點被打過。盜用的人不會照著你的使用習慣走。

要查的區間是從最早可能外洩的那天到你撤銷完成為止。以故事裡那個例子就是整整半年,不是最近七天。多數後台預設只給你看最近幾天,要自己把範圍拉開。

看完就記下來,一行字:查了哪段區間、什麼粒度、有沒有異常、幾點看的。粒度要記是因為按日看跟按小時看能發現的東西不一樣。這行的用處在後面,如果之後帳單真的爆掉,你知道自己當初查到哪裡為止。

沒發現異常也要記。「在查得到的期間與粒度裡沒看到異常」本身就是一個結論,只是要照這樣寫,不要寫成「沒被用過」。

盤點還有一條,多數人不會想到:這把金鑰有沒有進過你的 AI 對話? 貼設定檔問「這樣寫對嗎」、把錯誤訊息連同環境變數一起丟進去,都算。這條跟前面幾條不一樣,一樣記一行:想得起來的有哪幾次,還是完全沒有。

先擋住最常見的下一次

到這裡火滅了。接下來這一步才是今天真正留在專案裡的東西:裝一個在 commit 之前就攔下來的檢查。

要用到 gitleaksbrew install gitleaks 或到 releases 頁抓一個 binary 丟進 PATH 都行,它是單一執行檔沒有相依。

因為說實話,這次是你自己查出來的,下次不一定。人不會因為痛過一次就記得住,趕工的時候尤其不會。

git config core.hooksPath .githooks
mkdir -p .githooks

.git/hooks/ 不進版控,換一台機器就沒了,協作的人也不會有。改用 core.hooksPath 指到一個進版控的目錄,hook 檔案就跟著專案走。

跟著走的只有檔案。core.hooksPath 這行設定在每個人自己的 git config 裡,clone 不會帶過來,等一下會講我怎麼被這件事咬過。這行也會蓋掉你原本設的路徑,先 git config --get core.hooksPath 看一眼。

cat > .githooks/pre-commit <<'EOF'
#!/usr/bin/env bash
# 擋的是還沒進歷史的那一刻。已經在歷史裡的,這個 hook 看不到也管不著
command -v gitleaks >/dev/null || {
 echo "這個 hook 需要 gitleaks,但系統上找不到它"
 echo "  macOS: brew install gitleaks / 其他:github.com/gitleaks/gitleaks/releases"
 exit 1
}
gitleaks git --staged --redact -v --exit-code 2
case $? in
 0) ;;
 2) echo "偵測到疑似祕密,commit 中止"; exit 1 ;;
 *) echo "gitleaks 沒跑完,這次等於沒掃,commit 中止"; exit 1 ;;
esac
EOF
chmod +x .githooks/pre-commit

第一段那個 command -v 不是防禦性寫法,是我踩過。沒有它的話,你機器上沒裝 gitleaks 的時候,command not found 讓那行回非零,你看到的是「偵測到疑似祕密,commit 中止」。跟真的攔截到一模一樣。commit 被擋了,你以為 hook 在保護你,其實它一次都沒掃過。

後面那個 --exit-code 2 是同一個坑的第二層。gitleaks 預設用離開碼 1 表示「找到祕密」,但它自己跑不起來的時候回的也是 1。我拿一個不存在的設定檔試,離開碼跟真的找到祕密一模一樣。

換成 --exit-code 2 才分得開:0 是掃過而且乾淨,2 是真的找到,其他都是工具出事。所以 case 最後一支寫的是「這次等於沒掃」,不是「有祕密」。

gitleaks git --staged 是現在的寫法。舊版那個 protect 還能跑,但已經從 --help 的清單裡拿掉了,哪天消失不會有人通知你。)

裝完不要就這樣算了。裝了沒測,跟沒裝的差別只有你心裡比較安穩,而那份安穩正是最貴的東西。

餵一個一定會被抓的字串給它:

openssl genrsa -out leak-test.pem 2048
git add leak-test.pem
git commit -m "should be blocked"

commit 要被擋下來。擋下來之後把它清掉,兩步都要,reset 只退索引不刪檔:

git reset leak-test.pem && rm leak-test.pem

為什麼用 openssl 現生一把,而不是隨手打一串假金鑰?因為越假的字串越測不出東西

我本來寫的是 AKIAIOSFODNN7EXAMPLE,AWS 官方文件的範例值。實測(gitleaks 8.30.1)它一個都沒被抓到,換成真實格式的 AWS key 也一樣;同一個 ghp_ 開頭的 token,全填 0 的抓不到,隨機字元的抓得到。掃描規則有熵的門檻,因為它要壓誤報。

所以你拿一眼就假的字串去測,測到的是「這個字串太假」,不是「hook 沒作用」。openssl genrsa 生的是真私鑰,只是它從沒離開過你的機器,而 private key 的規則不看熵。

跟昨天那個誘餌是同一句話:驗證用的假資料要設計,隨手編一個會讓你驗到假的結果。

我自己第一次裝的時候,測試順利被擋,我很滿意。過兩天發現同事那邊完全沒作用,因為他 clone 下來之後沒跑那行 git config。hook 目錄進版控了,設定沒有。所以這行指令要寫進 README 的環境設定那一段,或是包進安裝腳本裡,不要只留在你的機器上。

還有一件事講在前面,免得你之後誤會它壞了:hook 擋的是未來,跟昨天的 ignore 一樣。 它不會回頭掃歷史,也不該期待它掃。昨天那句換皮又出現了一次,這已經是第二次,後面還會有。

也要知道它擋不掉什麼。git commit --no-verify 一個參數就繞過去。所以它是你這台機器上的安全網,不是一道關卡;要當關卡得靠 CI 掃描或平台端的 push protection。今天先把自己這台補起來,它擋的是趕工時最容易犯的那一次。

歷史清理排到後面

最後才輪到清歷史。

git filter-repo 這類工具參數很細,很適合叫 AI 起草指令,但砍掉哪些路徑、哪些 commit 一定要你自己看過再按 Enter。這是今天唯一一個「AI 起草、人核對」比「人自己寫」快的地方。

在本機砍錯還有救,這份 clone 丟掉重抓就好。回不去的是 force push 之後,遠端跟每個協作者的歷史一起被改寫,那時候要收拾的就不只是你自己了。

然後你會發現一個很掃興的事實:清完歷史,別人 clone 過的那份還在,fork 出去的還在,CI 的快取可能還在,GitHub 的 pull request 頁面上那段 diff 常常也還在。

所以歷史清理的意義是什麼?是不要繼續散布,是讓下一個接手的人不會又撿到一份。它不是止血,止血在第一步就做完了。

這就是為什麼它排最後,而且今天做不完不算失敗。filter-repo 裝不起來、參數看不懂、大 repo 跑很久,不要熬夜硬幹。金鑰已經撤銷了,歷史裡那把是死的,你正在對著一串失效的字元做外科手術。

但要記得那不是免死金牌。前面撤銷的是 API 金鑰,它撤得掉。同一份 .env 如果還躺著簽章私鑰、個資、或是那種發出去就收不回的東西,歷史裡那份就不是死的,那要另外排時間,不能跟金鑰一起輕輕放下。

排序是這樣的:撤銷跟驗證今天一定要做完,接著查一眼用量,hook 今天裝好。歷史清理排進待辦,這個系列後面不會再回頭催你,所以現在就給它一個日期。

寫進待辦之前,這件事不算排好。 意思是版控裡還留著一串已經失效的字元,這是你知情之下暫時接受的狀態,不是處理完了。

還有一份你驗不了

前面這些做完,鑰匙撤了、驗過了,hook 也裝好了。版控那份排進待辦了。

還有一個地方,它連排進待辦都排不了。

這半年你跟 AI 講過幾次話?有沒有哪一次是把整段設定檔貼過去問「這樣寫對嗎」,或者把一串錯誤訊息連同環境變數一起丟進對話框?

那些請求送到供應商的伺服器了。前端的對話紀錄你刪得掉,那一份不歸你管。

我去翻了兩家的政策(2026-08 查證,你自己動手時要重看一次,這種東西每季在變)。

OpenAI 寫的是所有 API 使用都會產生濫用監控紀錄,預設最多留三十天,法律要求或保護服務時可以更久。

Anthropic 寫的是 API 的輸入與輸出預設三十天內從後端刪除。但被自動系統標記為違反使用政策的,輸入輸出最長留兩年,而那個判定分數本身留七年。

兩家都有 Zero Data Retention。OpenAI 那邊要事前核准並接受額外條件,官方叫你聯繫業務問資格,沒有自助申請的入口。

要注意這兩份講的都是 API。你把設定檔貼進去的那個對話框可能是聊天產品、可能是代理工具,各自的刪除與保存規則不一樣。要查就查你實際在用的那個產品跟方案,不要拿 API 文件套。

這些期限不是重點。重點是你在這裡失去的不是刪除權,是驗證能力

金鑰你撤得掉,撤完還能自己打一發,看那台伺服器回一句 invalid_api_key。對話紀錄不行。你可以按刪除、可以去申請 ZDR、可以相信那個三十天,但你沒有一發可以打出來看。

回到今天第二節那句:你要的是伺服器自己回的東西,不是畫面上的宣稱。在這裡你只拿得到宣稱。

所以這一節的動作是把剛才那一行寫下來:這把金鑰有沒有進過 AI 對話、大概是什麼時候。萬一之後真的出事,你手上有一份「它從哪一天開始可能在外面」的時間點,而不是從零開始回想。

剩下的就是失效得夠快,快到那份紀錄裡的金鑰在還沒被誰用到之前就已經是死的。這是你在這條線上唯一還握得住的東西,也是為什麼它排在今天最前面。

你能讓金鑰失效,但你收不回已經送出去的字元。

這兩件事的落差就是明天要處理的東西。與其事後追著清,不如一開始就別貼出去。而「什麼可以貼、什麼不行」,多數人從來沒有寫下來過。

今天結束時你手上有什麼

那份 .env 裡每一把鑰匙的清單,一把新金鑰,一筆舊鑰打回「憑證無效」的驗證紀錄,一行用量檢查的結論,一行這把金鑰有沒有進過 AI 對話的答案,還有一個進了版控、而且測過真的會擋的 pre-commit hook。

六樣裡面有四樣是紀錄。這不是形式,是因為這種事一年後你只會記得「好像處理過」,記不得處理到哪。

前三天走完,金鑰在你這邊的路徑清楚了:不在前端、外洩了知道怎麼處置、下次多一道本機攔截。版控裡那份還在,但它是死的,而且你已經知道什麼時候要清。送出去的那些連死沒死都驗不了,那才是明天的題目。

明天換一個方向。金鑰你守得住,因為你知道它長什麼樣子。但你昨天貼給 AI 的那段錯誤訊息裡面有什麼,你說得出來嗎?

昨天那份候選清單留著,明天要用。

明天見。


上一篇:Day 2|.env 沒有魔法盾牌,你的 AI 工具也讀得到

範例專案與可以跑的驗證腳本:github.com/cyh7789/ai-security


上一篇
Day 2|.env 沒有魔法盾牌,你的 AI 工具也讀得到
下一篇
Day 4|你說不出它知道多少,因為沒人定義過什麼算敏感
系列文
你該防的不是駭客,是你自己的 AI:在本機驗證你的防線6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言