清單全部打勾的第 13 天,整個專案被停權了
問題不在做得快不快,在於輪替的單位是「位置」還是「身分」。同一個身分散在六處平台變數、五個 repo 與本機;這篇給憑證清冊、24 小時流程與無金鑰代理。
- 平台匯出的環境變數清單只能說明那個平台現在存了什麼。它不包含過去放進 Git 的檔案、其他排程使用的私鑰,也不涵蓋開發者本機。
- 同一個服務帳戶的金鑰散在 6 個平台變數、5 個 repo 的 key 檔與本機。輪替以「身分」為單位,才不會漏掉副本。
- 根本解法是讓應用程式不持有私鑰:一個無金鑰的 Sheets/Drive 代理,只發可撤銷的呼叫 token。
從一個打勾開始
8 月 30 日下午,輪替清單上的最後一項打了勾。AWS 金鑰換完、資料庫密碼換完,兩天之內全部做完,速度不算慢。當下的判斷很單純:處理完了,可以回去做正事。
十三天後,9 月 12 日晚上八點多,所有跟試算表有關的排程同時停擺,每一支都吐同一句錯誤:invalid_grant: account not found。Google 停權了整個專案。順著這句話往回翻,才看見那張清單上從頭到尾就沒有這把 GCP 服務帳戶金鑰——它從外洩通知那天起,又活了 17 天。
這篇不打算再講攻擊者拿那把金鑰做了什麼,那些在〈排程回報「找不到帳戶」,被停權的是整個專案〉裡寫過了。這篇只講一件事:**當你說「金鑰已經換了」,你換的到底是什麼的單位。**如果 8 月 30 日那個打勾之後,有人多花十秒鐘拿舊憑證去撞一次 API,這篇文章就不存在。
一個身分,十二個住所
要輪替的不是一台主機,是一個「身分」。而這個身分在事件當下同時住在很多地方。
身分本體是一個 GCP IAM 服務帳戶,權限是 roles/owner,實際工作只有讀寫試算表和上傳檔案到 Drive。它的憑證是一把 2020-09-10 建立、沒有到期日的 Google Service KEY JSON。應用程式與排程跑在部署平台上,私鑰以整段 JSON 或 base64 字串塞在環境變數裡;同一份檔案也被 commit 進幾個 repo 的設定目錄,開發者本機還留著一份。
當時唯一存在的流程只有一條:收到外洩通知,就照平台匯出的環境變數清單換。那份清單很好用,但它是位置清單,不是身分清單——它只知道那個平台此刻存了什麼,不知道過去有誰把同一份檔案放進 Git,更看不到任何人的筆電。沒有憑證清冊、沒有到期機制、沒有「撤銷後要驗證」的驗收條件,也沒有任何 repo 憑證掃描。
事後把這個身分攤開來盤點,下表前三列是這次真的外洩的;後面幾列沒有證據顯示外洩,但它們是同一份清冊本來就該涵蓋的類別。清冊不完整的那一天,你不會知道自己漏的是哪一類。
| 服務 | 憑證類型 | 用途 | 存在形式 | 放在哪 | 權限範圍 | 狀態 |
|---|---|---|---|---|---|---|
| GCP:Service Account(IAM 服務帳戶) | Google Service KEY JSON | 訊息機器人讀寫試算表、上傳照片到 Drive | JSON 檔,內含 private_key 的 PEM 區塊;也可能整段 base64 塞進環境變數 | 6 個部署平台環境變數 + 5 個 repo 內的 key 檔 + 開發者本機 | roles/owner(實際只需要把檔案分享給它) | 9/13 刪除;這就是漏掉的那一把 |
| AWS:IAM 使用者 | access key ID + secret access key | 應用程式存取後端雲端服務 | 明文環境變數 | 部署平台環境變數(在平台清單上) | 未逐一比對 | 8/29~8/30 已輪替 |
| 資料庫 | 連線帳號密碼 | 應用程式與排程連資料庫 | 明文環境變數或整串連線字串 | 部署平台環境變數(在平台清單上) | 該資料庫讀寫 | 8/29~8/30 已輪替 |
| Google:OAuth 2.0 用戶端 | client ID + client secret,以及換來的 refresh token | 代表使用者授權存取 Google 服務的網頁登入流程 | JSON 設定檔或明文環境變數;refresh token 常另存在資料庫或本機快取檔 | 應用程式設定、token 快取檔 | 使用者同意的 scope;refresh token 不會自己過期 | 本案未證實外洩,但清冊必須列它:它是最容易被遺忘的長期憑證 |
| GitHub | 個人存取權杖(PAT) | CI 拉私有 repo、自動化推 commit | 明文字串 | CI secret、本機 .netrc 或 git 認證快取 | 依 token scope | 本案未證實外洩;清冊必須列 |
| Google Cloud:API 金鑰 | API key(AIza 開頭) | 呼叫地圖、AI 等計費 API | 明文字串,可能被打包進前端 | 前端程式碼、環境變數 | 依金鑰限制設定 | 本案未證實外洩;清冊必須列,因為它會直接產生帳單 |
| 訊息平台與郵件服務 | channel access token、SMTP 帳密 | 推播訊息、寄信 | 明文環境變數 | 部署平台環境變數、應用程式設定 | 代表你的品牌對外發訊息 | 本案未證實外洩;清冊必須列,外洩後傷的是信譽不是帳單 |
| 平台事件中其他被取走的環境變數 | 不明 | 不明 | 推測為明文環境變數 | 部署平台 | — | 平台未提供事件細節;這正是「以身分為單位輪替」比「照清單輪替」重要的原因 |
數完之後才知道,那張清單涵蓋的只有其中六個。剩下的那一半沒有人負責,也沒有人知道它們存在。
十九天的時間線
把日期排開來看,值得注意的不是攻擊者哪天做了什麼,而是我們哪一天「以為已經處理完了」。
- 8/27部署平台通知環境變數外洩,我方依平台清單開始輪替平台通知信與輪替紀錄。
- 8/28 23:00那把沒被列進清單的 GCP 金鑰,開始被陌生 IP 使用Cloud Audit Logs:callerIp 非我方、principal 是 Bot 帳戶。
- 8/29 → 8/30AWS 金鑰與資料庫密碼輪替完成,清單上的項目全部打勾輪替紀錄。
- 9/12Google 停權整個專案,我方才發現還有一把沒換到服務帳戶回 invalid_grant: account not found。
- 9/13刪除專案內全部 7 把使用者金鑰包含 5 個服務帳戶的既有金鑰;立即讓所有副本失效。
- 9/13拿掉 5 個服務帳戶的 Owner/EditorSheets Bot 不需要任何專案角色,檔案分享給帳戶就夠。
- 9/13 → 9/14建立無金鑰的 Sheets/Drive 代理應用程式改走代理,只拿可撤銷的呼叫 token;代理本身用 Workload Identity。
- 9/14 → 9/15盤點所有 repo 內的 key 檔並移除commit 只是把最新版本刪掉;外部已取得的副本不會因此失效,所以前一步的撤銷才是關鍵。
- 待辦另一個仍是 Owner 且金鑰仍有效的服務帳戶比照處理同一個坑不要跳兩次。
最貴的一段是中間那條空白:8/30 到 9/12,整整十三天,我們的狀態是「已結案」,而對方的狀態是「還在用」。
攻擊鏈上,這篇只負責第五格
七個階段裡,這一篇只有一格是主角。其餘六格的完整版在〈排程回報「找不到帳戶」,被停權的是整個專案〉與〈Crusader 殭屍網路解剖〉,這裡只留骨架,讓第五格有上下文。
其他六格至少都留下了痕跡:稽核記錄、systemd 單元、出口流量、帳單曲線。只有第五格什麼都沒有。私鑰檔躺在 repo 或筆電上不會產生任何日誌,它唯一的防線是流程。
證據不在日誌裡,在盤點結果裡
也因為這樣,這一篇最有力的證據不是雲端日誌,是 9 月 13 日那天數出來的東西。
三條可以證明的、一條只能推論的、一條註定是缺口的,湊起來剛好說明一件事:這種事件的真相,多半不在攻擊者留下的痕跡裡,而在你自己有沒有本事把家底數清楚。
往下追四層,越追越不像技術問題
表面上的原因一句話就講完了:8 月底那次輪替漏掉一把 GCP 服務帳戶金鑰,它一直到 9/13 才失效,中間有 15 天被攻擊者實際使用。這是稽核記錄可以證明的。
再往下一層是技術。長期私鑰是一種可以無限複製、而且複製不留痕跡的東西:它會被 commit、被貼進環境變數、被下載到筆電。更麻煩的是它沒有到期日,使用者管理的服務帳戶金鑰 validBeforeTime 寫的是 9999 年,所以一把 2020 年建立的金鑰,2026 年照樣能簽出有效的 token。
往上追到管理,問題就變成:輪替的工作單位被定義成「平台清單上的環境變數」。這句話決定了誰有責任、範圍到哪裡,而 repo 與開發者本機的副本,剛好落在所有人的責任之外。同一層還有另一個缺漏:流程裡沒有「撤銷後驗證」這個驗收條件,所以一次沒做完的輪替,和一次做完的輪替,在紀錄上長得一模一樣。
最後一層是制度,這一層我只能寫成推測,因為它本來就無從舉證:沒有以身分為單位的憑證清冊與定期複核,也沒有限制長期私鑰存在的組織政策,於是每次收到外洩通知,我們只能照別人給的清單做——範圍由別人定義。第三層是這一次的直接原因,第四層是下一次的原因。
訊號都在,只是沒有人被指派去看
回頭看,可以攔下這件事的機會至少有六次,而且沒有一次需要買新工具。
金鑰不在平台匯出的清單上,這件事本身就是訊號,但我們把那份清單當成了完整清單。金鑰的建立日期停在六年前、而且永不過期,這也是訊號,只是沒有任何系統會在第兩千多天跳出來提醒你。8/30 宣告輪替完成的那一刻,只要有人拿舊憑證撞一次 API,十秒鐘就會看到它還活著,十七天的破口會當場縮成三天——但流程裡沒有這一步,所以沒有人做。
另外三個訊號在系統裡,只是沒人在看:兩個 repo 的 git 歷史裡有 key 檔,而 CI 沒有跑 trufflehog 或 gitleaks;沒有人知道這個身分被幾個服務在用,那個數字是事後才數出來的;最有辨識力的是 8/28 起這把金鑰的 API 用量突然出現 aiplatform 與 compute——一個只寫試算表的身分開始呼叫運算 API,這比任何 IOC 都明顯,但我們沒有依 credential_id 分組的用量監控,沒有人看得到它。
六條裡有四條,補起來需要的是一份表格和一個驗收動作,不是一套系統。
那一夜之後的三天:我們實際做了什麼
9 月 12 日晚上八點多,排程全停,我們才真正知道自己漏了一把。所謂「偵測」,是業務停擺替我們做的——這件事的起點從頭到尾都是別人的通知和別人的停權,範圍也是別人定義的。
隔天的第一個動作,事後看是這次唯一正確的直覺:不是「刪掉我知道的那一把」,而是刪光這個專案內全部 7 把使用者建立的金鑰,包含 5 個服務帳戶的既有金鑰。因為已經不知道副本散在哪裡,只有把身分底下的金鑰全部清掉,才能讓所有不知道在哪裡的副本一起失效。同一天順手把那 5 個服務帳戶的 Owner/Editor 拿掉——一個 Sheets Bot 不需要任何專案層角色,把檔案分享給它就夠了。但我們沒有刪除服務帳戶本身:它擁有大量 Drive 檔案,刪帳戶會連檔案一起消失,這個陷阱剛好閃過去。
服務停著不能過夜,所以 9/13 到 9/14 接著做的是復原,順便把架構一起換掉:建一個無金鑰的 Sheets/Drive 代理,應用程式只拿一個可撤銷的呼叫 token,代理本身用 Workload Identity 取得憑證,白名單限定哪些 token 能碰哪些檔案,逐一驗證各服務健康之後完成切換。切換完成的那一刻,應用程式端已經沒有私鑰檔可以再外洩。
最後兩天(9/14 到 9/15)才是清場:把所有 repo 裡的 key 檔盤出來移除,同時記下本機副本的位置。順序不能顛倒——commit 只是把最新版本刪掉,外部已經取得的副本不會因為你刪檔而失效,所以前面那一步的撤銷才是真正止血的動作。受害主機重建、原始碼移除秘密、憑證撤銷,是三件不同的工作,做完其中一件不等於做完另外兩件。
這一連串動作後來被我們寫成一條固定順序:列出這個身分的所有金鑰 ID → 建新金鑰 → 找出所有使用位置逐一切換 → 驗證健康 → 刪除所有舊金鑰 ID → 用舊憑證實際呼叫一次,看到 invalid_grant 才算結束。做完最後一步,才叫輪替完成。
這次的速度沒問題,範圍有問題
先講讓人意外的部分:這次的反應速度其實不差,清單上的每一項都在兩天之內換完,以小團隊來說沒有落後。真正錯的是範圍——我們換的是一份位置清單,而外洩的是身分。
9/13 那天的判斷也大多是對的:撤銷選擇「刪光全部」而不是「刪掉已知的那一把」,順手縮小了五個服務帳戶的權限,也避開了刪除帳戶會連帶弄丟 Drive 檔案的陷阱;盤點還留下一張「部署服務 → 授權 → 試算表 → 權限」的對照表,後續的批量移除與代理白名單都靠它。問題是,這張表是事後才有的。事前沒有清冊,切換的時候只能邊做邊猜,猜錯就是一次停機。
該改的其實也就三件:repo 與開發者本機從來不在任何盤點範圍內,也沒有人負責;流程裡沒有人親眼確認過舊憑證真的死了,就宣告完成;還有一個仍是 Owner、金鑰仍然有效的服務帳戶,到復盤當下都還沒處理。把這些事排在時間軸上看,結論很清楚:做得好的那幾件事都發生在 9/13 一天之內,做不好的那幾件,每一件都發生在 8/27 到 8/30 之間。當你拿到的是一份不完整的清單,執行得再快也只是更快地做完一半。
之後我們改了什麼
第一件要做的事不是建清冊,是撤銷;清冊是為了下一次。
當天就能做完的有四件:對每一個高權限身分跑一次 keys list --managed-by=user,不認得的金鑰全部刪掉;撤銷之後用舊憑證實際呼叫一次,看到 invalid_grant 才把這一項打勾,並把這一步寫進紀錄;把那個還是 Owner、金鑰仍有效的服務帳戶比照處理;然後用 trufflehog 與 gitleaks 掃一遍 repo 歷史與部署目錄,看同一個身分還有沒有別的副本躺著。
接下來一個月做的是把臨場反應變成流程:建立以身分為單位的憑證清冊(我們那份就叫「身分清冊 C-82」;敏感值放 Secret Manager,清冊只記識別資訊與管理方式),把「平台通知外洩 → 24 小時內以身分為單位輪替全部憑證」寫成 SOP 並明確把 repo 與本機寫進範圍,讓所有應用程式改走無金鑰代理,並對高權限身分排定期複核。
這一季以後的目標則是讓這個坑不可能再挖出來:用組織政策 iam.disableServiceAccountKeyCreation 把「建立服務帳戶金鑰」這件事直接關掉,例外要審核;能不發私鑰就不發,用 Workload Identity 與短期可撤銷 token 取代長期金鑰;每次復盤把新發現的盲點回寫進清冊與 SOP;連「不要刪除擁有大量 Drive 檔案的服務帳戶本身」這種陷阱也寫進處置手冊。
清冊本身不難,難的是它要回答得出問題。我們要求它至少回答這五題:
- 這個身分負責什麼業務,由哪個角色維護?
- 它有哪些有效憑證,建立時間與預計到期時間是什麼時候?
- 哪些服務在用它,分別部署在什麼環境(平台變數、repo、排程、本機)?
- 它可以操作哪些資源,有沒有超出業務需求?
- 撤銷之後要怎麼驗證,服務要怎麼切換?
不過清冊做得再好,只要私鑰檔還存在,它就一定會被複製、被 commit、被貼進環境變數。所以真正的終點不是更勤勞的輪替,而是讓私鑰只存在一個地方,而且那個地方不是任何一台應用程式主機:
應用程式(Bot、排程、後台)
│ 帶一個可撤銷的呼叫 token
▼
sheets-proxy(Cloud Run,Workload Identity,無金鑰檔)
│ 以服務帳戶身分呼叫
▼
Google Sheets / Drive API
應用程式只拿到一個對代理的 token,可以隨時個別撤銷,權限僅限於「透過代理讀寫白名單上的試算表」;代理自己用 Workload Identity 取得 Google 憑證,沒有 JSON 檔可以外洩。就算那個 token 流出去,攻擊者也開不了 VM、叫不了 AI。
這套改法要花多少錢?下面這張表把外洩的帳單和補救的人力放在一起算,順便把攻擊者那邊的成本也列進來。
| 項目 | 用量 | 單價 | 期間 | US$ | NT$ | 舉證 |
|---|---|---|---|---|---|---|
| 漏掉這把金鑰的直接帳單 帳單實付。詳細 SKU 拆解見〈排程回報「找不到帳戶」,被停權的是整個專案〉 | 15 天被盜用:3 台 VM、Gemini、流量 | 帳單實付 | 8/28 → 9/12 | 146.10 | 4,675 | GCP 帳單報表(依服務/依 SKU 分組)截圖 2026-09-15 |
| 盤點:找出同一個身分的所有副本 估:副本數量可證,工時與日費為估算 | 6 個平台環境變數、5 個 repo 內 key 檔、開發者本機,約 4 小時 | 顧問日費 NT$27,500(估)/8 小時 | 一次性 | 429.69 | 13,750 | 盤點結果(6+5+本機)可證;工時為估算 |
| 撤銷與切換 估:動作與數量有稽核記錄,工時為估算 | 刪 7 把使用者金鑰、拿掉 5 個服務帳戶的 Owner/Editor、逐一驗證健康,約 6 小時 | 同上 | 9/13 | 644.53 | 20,625 | 9/13 稽核記錄可證動作與數量;工時為估算 |
| 改建無金鑰代理 估:這是唯一一次性、不會重複付的支出 | 設計、實作、白名單、切換,約 2 人日 | 同上 | 9/13 → 9/14 | 1718.75 | 55,000 | 代理上線時間可證;工時為估算 |
| 代理的執行成本(Cloud Run) 估:完全落在免費額度內(每月 200 萬次請求、180,000 vCPU·秒、360,000 GiB·秒) | 估每月 30 萬次請求 × 200 ms × 512 MiB | US$0.40/百萬請求;US$0.000024/vCPU·秒 | 每月 | 0.00 | 0 | Cloud Run 官方牌價與免費額度 2026-09;用量為估計值 |
| 秘密保管(Secret Manager) 估:扣掉免費的 6 個版本與 1 萬次存取後的金額。把秘密管好,一個月不到 12 元台幣 | 估 10 個 active secret version、每月 5 萬次存取 | US$0.06/版本·月;US$0.03/萬次存取 | 每月 | 0.36 | 12 | Secret Manager 官方牌價 2026-09;用量為估計值 |
| 攻擊者驗證這批憑證的成本 估:他不需要贏全部,只需要你漏一把 | 一次撈到上百組,自動化全部試一遍 | 一支腳本,邊際成本趨近 0 | 數小時 | 0.00 | 0 | 8/28 首次使用距平台通知僅 36 小時,符合批次自動驗證的節奏;他的實際支出無從得知 |
| 攻擊者拿走的是哪一份副本 時間相關性指向平台事件,但 repo 裡的檔案理論上也可能早被撈走 | 平台環境變數?repo 裡的 key 檔?本機? | — | — | — | — | 平台未提供事件細節;repo 與本機都沒有讀取記錄,無從判定 |
| 合計(僅可證明列) | 146.10 | 4,675 | ||||
這張表只有第一列有收據,其餘幾列是估算,但比例已經夠清楚:外洩本身的帳單不到補救人力的二十分之一。清冊之所以值得做,不是因為它是個好文件,是因為它便宜。
帶得走的五件事
從攻擊者的角度看,這整件事其實很划算:平台事件一次撈到上百組憑證,他不會逐一研究,寫一支腳本全部試一遍就好,哪一組還活著就用哪一組。我們輪替 AWS 金鑰的那兩天,他正拿 GCP 金鑰測試 AI 模型。他賭的不是你不換金鑰,是你的清冊不完整——而且長期私鑰沒有到期日這件事,讓他連時間都站在同一邊。
- 01平台給你的是位置清單,外洩的卻是身分。只換位置就一定會漏;這一次,同一個身分住在十二個地方。
- 02輪替是一個有驗收條件的變更,驗收條件是「舊憑證回 invalid_grant」。沒有親眼看到那句話,就還沒做完。
- 03commit 刪掉不等於失效。把金鑰從最新版本刪掉,外部已取得的副本不會因此消失,只有撤銷會。
- 04清冊不是文件工作,是省錢工作。帳單 US$146.10,補救人力 US$2,793,差了將近二十倍。
- 05最根本的輪替,是讓應用程式根本沒有私鑰可以輪。代理、Workload Identity、短期 token 都比更勤勞的輪替有效。
如果只想記十個詞,就記下面這十個;順序是照「讀完最該帶走」排的,不是照它們在文章裡出現的先後。
- 01以「身分」為單位輪替整篇的核心。平台給你的是位置清單,外洩的卻是身分;只換位置,就一定會漏。
- 02invalid_grant: account not found輪替的驗收條件。沒有親眼看到舊憑證回這句話,那次輪替就還沒做完。
- 03keys list --managed-by=user同一個服務帳戶可以有很多把金鑰。只刪你知道的那一把,等於沒刪。
- 04Git 歷史裡的秘密commit 刪掉只是刪掉最新版本。外部已取得的副本不會因為你刪檔而失效,撤銷才會。
- 05Workload Identity讓應用程式根本沒有私鑰檔可以被 commit、被複製、被放進環境變數。比任何輪替流程都根本。
- 06短期可撤銷 token把外洩的爆炸半徑從「整個專案」縮成「白名單裡那幾張試算表」。
- 07iam.disableServiceAccountKeyCreation組織政策層級把「發長期私鑰」這件事關掉,例外要走審核。這是唯一擋得住人性的做法。
- 08trufflehog / gitleaks盤點 repo 與部署目錄的標準動作。攻擊者用它撿,你用它先撿一次自己的。
- 09秘密管理系統(Secret Manager)清冊只記識別資訊與管理方式,敏感值放這裡。一個月不到 12 元台幣,比人力便宜太多。
- 10憑證清冊的五個問題誰用、有哪些憑證、部署在哪、能開什麼、怎麼撤銷。出事時能不能在 24 小時內回答,就看這五題。
如果那封信今天再寄一次
假設 8 月 27 日那封平台通知信今天重新寄來一次,我會這樣用掉第一個小時。
前十五分鐘不會去讀平台的清單。先列出「這個環境裡有哪些身分」,再把平台清單降級成其中一個位置來源;對每一個身分跑一次金鑰列表,把金鑰 ID 與建立日期抄下來;用 grep 數出這個身分被幾個服務在用,那個數字就是等一下必須同步切換的數量。
第二個十五分鐘從高權限身分開始,而且先拿掉 Owner/Editor 再談換金鑰——權限縮小之後,就算漏換一把,代價也跟著縮小。接著刪掉該身分的全部舊金鑰 ID,不是只刪清單上那一個位置,然後用舊憑證實際呼叫一次,看到錯誤才把這一項打勾。
剩下的半小時用來收拾現場:逐一驗證每個使用位置的健康狀態,壞掉的服務優先切到不需要私鑰的路徑;在所有 repo 的 git 歷史裡搜私鑰檔,命中就當作已外洩,記錄下來但別以為刪檔就處理完了;本機與部署目錄也找一遍散落的金鑰檔,把位置寫進清冊。一小時之後才輪到那些慢工:補完清冊的五個問題、把應用程式換成不持有私鑰的架構、把 SOP 寫下來並在下一次演練它。
回到 8 月 30 日下午那個打勾的動作。那天我們確實照清單做完了每一項,真正的問題是清單本身從來沒有被檢查過。所以下一次收到外洩通知時,第一個該問的不是「清單上有幾項」,而是「這份清單是誰整理的,它看得到我的 repo 嗎」。
這個問題今天就可以問自己一次,下面五步全部唯讀,十五分鐘內做得完。
全部唯讀,不改任何設定;做完你會知道自己有沒有同樣的破口。
- 挑一個你認為「已經處理好」的服務帳戶,列出它所有使用者建立的金鑰。
gcloud iam service-accounts keys list \ --iam-account=SA_EMAIL --managed-by=user \ --format='table(name.basename(), validAfterTime)'看到什麼代表什麼:只要回傳超過一把,或有一把是你不認得的,就代表你的清冊記的是位置、不是身分。 - 把你紀錄中「已撤銷」的金鑰 ID,回到上一步的輸出裡對一次。看到什麼代表什麼:還找得到,代表那次輪替只換了環境變數、沒有真的撤銷。這是本案最貴的一課。
- 在所有 repo 裡找 JSON 私鑰檔,連歷史一起找。
grep -rIl 'private_key_id' --include='*.json' . git log --all --name-only -S 'BEGIN.*PRIVATE KEY' --pretty=format:'%h %ad' | head -40看到什麼代表什麼:歷史裡命中就是已外洩。把它 commit 刪掉不算處理,撤銷該身分的所有金鑰才算。 - 在本機與部署目錄找散落的金鑰檔。
find ~ /srv /opt /var/www -maxdepth 4 -type f \( -name '*service*account*.json' -o -name '*credential*.json' -o -name '.env' \) -ls 2>/dev/null看到什麼代表什麼:同一把金鑰出現在三個以上的位置,就是本案的形狀:你不會記得全部,所以必須用身分去反查。 - 數一數同一個身分被幾個服務在用。
grep -rl 'GOOGLE_APPLICATION_CREDENTIALS\|GOOGLE_SA_JSON' /path/to/projects 2>/dev/null | wc -l看到什麼代表什麼:這個數字就是你輪替時必須同步切換的服務數量。事前不知道它,事中就會停機。
每篇新文章一封:三點摘要、關鍵字定義、今天能做的一件事。