個案

清單全部打勾的第 13 天,整個專案被停權了

問題不在做得快不快,在於輪替的單位是「位置」還是「身分」。同一個身分散在六處平台變數、五個 repo 與本機;這篇給憑證清冊、24 小時流程與無金鑰代理。

30 秒看懂
  • 平台匯出的環境變數清單只能說明那個平台現在存了什麼。它不包含過去放進 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 憑證掃描。

事後把這個身分攤開來盤點,下表前三列是這次真的外洩的;後面幾列沒有證據顯示外洩,但它們是同一份清冊本來就該涵蓋的類別。清冊不完整的那一天,你不會知道自己漏的是哪一類。

leaked credentials外洩的三類,以及清冊必須涵蓋的其他類別
服務憑證類型用途存在形式放在哪權限範圍狀態
GCP:Service Account(IAM 服務帳戶)Google Service KEY JSON訊息機器人讀寫試算表、上傳照片到 DriveJSON 檔,內含 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 帳密推播訊息、寄信明文環境變數部署平台環境變數、應用程式設定代表你的品牌對外發訊息本案未證實外洩;清冊必須列,外洩後傷的是信譽不是帳單
平台事件中其他被取走的環境變數不明不明推測為明文環境變數部署平台—平台未提供事件細節;這正是「以身分為單位輪替」比「照清單輪替」重要的原因
憑證本身已遮蔽或失效;這裡只說明它是哪個服務、拿來做什麼、以什麼形式存放,讓你能對照自己的環境。

數完之後才知道,那張清單涵蓋的只有其中六個。剩下的那一半沒有人負責,也沒有人知道它們存在。

十九天的時間線

把日期排開來看,值得注意的不是攻擊者哪天做了什麼,而是我們哪一天「以為已經處理完了」。

  1. 8/27
    部署平台通知環境變數外洩,我方依平台清單開始輪替
    平台通知信與輪替紀錄。
  2. 8/28 23:00
    那把沒被列進清單的 GCP 金鑰,開始被陌生 IP 使用
    Cloud Audit Logs:callerIp 非我方、principal 是 Bot 帳戶。
  3. 8/29 → 8/30
    AWS 金鑰與資料庫密碼輪替完成,清單上的項目全部打勾
    輪替紀錄。
  4. 9/12
    Google 停權整個專案,我方才發現還有一把沒換到
    服務帳戶回 invalid_grant: account not found。
  5. 9/13
    刪除專案內全部 7 把使用者金鑰
    包含 5 個服務帳戶的既有金鑰;立即讓所有副本失效。
  6. 9/13
    拿掉 5 個服務帳戶的 Owner/Editor
    Sheets Bot 不需要任何專案角色,檔案分享給帳戶就夠。
  7. 9/13 → 9/14
    建立無金鑰的 Sheets/Drive 代理
    應用程式改走代理,只拿可撤銷的呼叫 token;代理本身用 Workload Identity。
  8. 9/14 → 9/15
    盤點所有 repo 內的 key 檔並移除
    commit 只是把最新版本刪掉;外部已取得的副本不會因此失效,所以前一步的撤銷才是關鍵。
  9. 待辦
    另一個仍是 Owner 且金鑰仍有效的服務帳戶比照處理
    同一個坑不要跳兩次。

最貴的一段是中間那條空白:8/30 到 9/12,整整十三天,我們的狀態是「已結案」,而對方的狀態是「還在用」。

攻擊鏈上,這篇只負責第五格

七個階段裡,這一篇只有一格是主角。其餘六格的完整版在〈排程回報「找不到帳戶」,被停權的是整個專案〉與〈Crusader 殭屍網路解剖〉,這裡只留骨架,讓第五格有上下文。

發生了什麼 · 8/28 23:00· MITRE ATT&CK T1078.004
外洩的 Google Service KEY JSON 簽 JWT 換 access token,以服務帳戶身分呼叫 Google API。輪替完成日是 8/30,首次盜用是 8/28,兩者差了兩天。
當時留下的訊號
Cloud Audit Logs:陌生 callerIp、principal 是 Bot 帳戶。
能擋下它的控制
外洩通知一到就以身分為單位撤銷,不要等盤點完再動手。

其他六格至少都留下了痕跡:稽核記錄、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 檔案的服務帳戶本身」這種陷阱也寫進處置手冊。

清冊本身不難,難的是它要回答得出問題。我們要求它至少回答這五題:

  1. 這個身分負責什麼業務,由哪個角色維護?
  2. 它有哪些有效憑證,建立時間與預計到期時間是什麼時候?
  3. 哪些服務在用它,分別部署在什麼環境(平台變數、repo、排程、本機)?
  4. 它可以操作哪些資源,有沒有超出業務需求?
  5. 撤銷之後要怎麼驗證,服務要怎麼切換?

不過清冊做得再好,只要私鑰檔還存在,它就一定會被複製、被 commit、被貼進環境變數。所以真正的終點不是更勤勞的輪替,而是讓私鑰只存在一個地方,而且那個地方不是任何一台應用程式主機:

  應用程式(Bot、排程、後台)
        │  帶一個可撤銷的呼叫 token
        ▼
  sheets-proxy(Cloud Run,Workload Identity,無金鑰檔)
        │  以服務帳戶身分呼叫
        ▼
  Google Sheets / Drive API

應用程式只拿到一個對代理的 token,可以隨時個別撤銷,權限僅限於「透過代理讀寫白名單上的試算表」;代理自己用 Workload Identity 取得 Google 憑證,沒有 JSON 檔可以外洩。就算那個 token 流出去,攻擊者也開不了 VM、叫不了 AI。

這套改法要花多少錢?下面這張表把外洩的帳單和補救的人力放在一起算,順便把攻擊者那邊的成本也列進來。

cost這次補救的帳:帳單一列,人力五列
項目用量單價期間US$NT$舉證
漏掉這把金鑰的直接帳單
帳單實付。詳細 SKU 拆解見〈排程回報「找不到帳戶」,被停權的是整個專案〉
15 天被盜用:3 台 VM、Gemini、流量帳單實付8/28 → 9/12146.104,675GCP 帳單報表(依服務/依 SKU 分組)截圖 2026-09-15
盤點:找出同一個身分的所有副本
估:副本數量可證,工時與日費為估算
6 個平台環境變數、5 個 repo 內 key 檔、開發者本機,約 4 小時顧問日費 NT$27,500(估)/8 小時一次性429.6913,750盤點結果(6+5+本機)可證;工時為估算
撤銷與切換
估:動作與數量有稽核記錄,工時為估算
刪 7 把使用者金鑰、拿掉 5 個服務帳戶的 Owner/Editor、逐一驗證健康,約 6 小時同上9/13644.5320,6259/13 稽核記錄可證動作與數量;工時為估算
改建無金鑰代理
估:這是唯一一次性、不會重複付的支出
設計、實作、白名單、切換,約 2 人日同上9/13 → 9/141718.7555,000代理上線時間可證;工時為估算
代理的執行成本(Cloud Run)
估:完全落在免費額度內(每月 200 萬次請求、180,000 vCPU·秒、360,000 GiB·秒)
估每月 30 萬次請求 × 200 ms × 512 MiBUS$0.40/百萬請求;US$0.000024/vCPU·秒每月0.000Cloud Run 官方牌價與免費額度 2026-09;用量為估計值
秘密保管(Secret Manager)
估:扣掉免費的 6 個版本與 1 萬次存取後的金額。把秘密管好,一個月不到 12 元台幣
估 10 個 active secret version、每月 5 萬次存取US$0.06/版本·月;US$0.03/萬次存取每月0.3612Secret Manager 官方牌價 2026-09;用量為估計值
攻擊者驗證這批憑證的成本
估:他不需要贏全部,只需要你漏一把
一次撈到上百組,自動化全部試一遍一支腳本,邊際成本趨近 0數小時0.0008/28 首次使用距平台通知僅 36 小時,符合批次自動驗證的節奏;他的實際支出無從得知
攻擊者拿走的是哪一份副本
時間相關性指向平台事件,但 repo 裡的檔案理論上也可能早被撈走
平台環境變數?repo 裡的 key 檔?本機?————平台未提供事件細節;repo 與本機都沒有讀取記錄,無從判定
合計(僅可證明列)146.104,675
匯率假設 1 USD ≈ NT$32。單價為公開牌價或帳單實付,來源:GCP 帳單報表截圖 2026-09-15(外洩費用);Cloud Run 與 Secret Manager 官方牌價,查價 2026-09;工時為本案處置紀錄,日費為假設值。「舉證」欄標明每筆損失的證據來源與等級( 可證明 推論 缺口)。

這張表只有第一列有收據,其餘幾列是估算,但比例已經夠清楚:外洩本身的帳單不到補救人力的二十分之一。清冊之所以值得做,不是因為它是個好文件,是因為它便宜。

帶得走的五件事

從攻擊者的角度看,這整件事其實很划算:平台事件一次撈到上百組憑證,他不會逐一研究,寫一支腳本全部試一遍就好,哪一組還活著就用哪一組。我們輪替 AWS 金鑰的那兩天,他正拿 GCP 金鑰測試 AI 模型。他賭的不是你不換金鑰,是你的清冊不完整——而且長期私鑰沒有到期日這件事,讓他連時間都站在同一邊。

lessons本案的 5 個教訓
  1. 01
    平台給你的是位置清單,外洩的卻是身分。只換位置就一定會漏;這一次,同一個身分住在十二個地方。
  2. 02
    輪替是一個有驗收條件的變更,驗收條件是「舊憑證回 invalid_grant」。沒有親眼看到那句話,就還沒做完。
  3. 03
    commit 刪掉不等於失效。把金鑰從最新版本刪掉,外部已取得的副本不會因此消失,只有撤銷會。
  4. 04
    清冊不是文件工作,是省錢工作。帳單 US$146.10,補救人力 US$2,793,差了將近二十倍。
  5. 05
    最根本的輪替,是讓應用程式根本沒有私鑰可以輪。代理、Workload Identity、短期 token 都比更勤勞的輪替有效。

如果只想記十個詞,就記下面這十個;順序是照「讀完最該帶走」排的,不是照它們在文章裡出現的先後。

keywords重點學習:最需要學習的 10 個關鍵字10 個
  1. 01
    以「身分」為單位輪替
    整篇的核心。平台給你的是位置清單,外洩的卻是身分;只換位置,就一定會漏。
  2. 02
    invalid_grant: account not found
    輪替的驗收條件。沒有親眼看到舊憑證回這句話,那次輪替就還沒做完。
  3. 03
    keys list --managed-by=user
    同一個服務帳戶可以有很多把金鑰。只刪你知道的那一把,等於沒刪。
  4. 04
    Git 歷史裡的秘密
    commit 刪掉只是刪掉最新版本。外部已取得的副本不會因為你刪檔而失效,撤銷才會。
  5. 05
    Workload Identity
    讓應用程式根本沒有私鑰檔可以被 commit、被複製、被放進環境變數。比任何輪替流程都根本。
  6. 06
    短期可撤銷 token
    把外洩的爆炸半徑從「整個專案」縮成「白名單裡那幾張試算表」。
  7. 07
    iam.disableServiceAccountKeyCreation
    組織政策層級把「發長期私鑰」這件事關掉,例外要走審核。這是唯一擋得住人性的做法。
  8. 08
    trufflehog / gitleaks
    盤點 repo 與部署目錄的標準動作。攻擊者用它撿,你用它先撿一次自己的。
  9. 09
    秘密管理系統(Secret Manager)
    清冊只記識別資訊與管理方式,敏感值放這裡。一個月不到 12 元台幣,比人力便宜太多。
  10. 10
    憑證清冊的五個問題
    誰用、有哪些憑證、部署在哪、能開什麼、怎麼撤銷。出事時能不能在 24 小時內回答,就看這五題。

如果那封信今天再寄一次

假設 8 月 27 日那封平台通知信今天重新寄來一次,我會這樣用掉第一個小時。

前十五分鐘不會去讀平台的清單。先列出「這個環境裡有哪些身分」,再把平台清單降級成其中一個位置來源;對每一個身分跑一次金鑰列表,把金鑰 ID 與建立日期抄下來;用 grep 數出這個身分被幾個服務在用,那個數字就是等一下必須同步切換的數量。

第二個十五分鐘從高權限身分開始,而且先拿掉 Owner/Editor 再談換金鑰——權限縮小之後,就算漏換一把,代價也跟著縮小。接著刪掉該身分的全部舊金鑰 ID,不是只刪清單上那一個位置,然後用舊憑證實際呼叫一次,看到錯誤才把這一項打勾。

剩下的半小時用來收拾現場:逐一驗證每個使用位置的健康狀態,壞掉的服務優先切到不需要私鑰的路徑;在所有 repo 的 git 歷史裡搜私鑰檔,命中就當作已外洩,記錄下來但別以為刪檔就處理完了;本機與部署目錄也找一遍散落的金鑰檔,把位置寫進清冊。一小時之後才輪到那些慢工:補完清冊的五個問題、把應用程式換成不持有私鑰的架構、把 SOP 寫下來並在下一次演練它。

回到 8 月 30 日下午那個打勾的動作。那天我們確實照清單做完了每一項,真正的問題是清單本身從來沒有被檢查過。所以下一次收到外洩通知時,第一個該問的不是「清單上有幾項」,而是「這份清單是誰整理的,它看得到我的 repo 嗎」。

這個問題今天就可以問自己一次,下面五步全部唯讀,十五分鐘內做得完。

try today今天就可以來試試約 15 分鐘

全部唯讀,不改任何設定;做完你會知道自己有沒有同樣的破口。

  1. 挑一個你認為「已經處理好」的服務帳戶,列出它所有使用者建立的金鑰。
    gcloud iam service-accounts keys list \
      --iam-account=SA_EMAIL --managed-by=user \
      --format='table(name.basename(), validAfterTime)'
    看到什麼代表什麼:只要回傳超過一把,或有一把是你不認得的,就代表你的清冊記的是位置、不是身分。
  2. 把你紀錄中「已撤銷」的金鑰 ID,回到上一步的輸出裡對一次。
    看到什麼代表什麼:還找得到,代表那次輪替只換了環境變數、沒有真的撤銷。這是本案最貴的一課。
  3. 在所有 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 刪掉不算處理,撤銷該身分的所有金鑰才算。
  4. 在本機與部署目錄找散落的金鑰檔。
    find ~ /srv /opt /var/www -maxdepth 4 -type f \( -name '*service*account*.json' -o -name '*credential*.json' -o -name '.env' \) -ls 2>/dev/null
    看到什麼代表什麼:同一把金鑰出現在三個以上的位置,就是本案的形狀:你不會記得全部,所以必須用身分去反查。
  5. 數一數同一個身分被幾個服務在用。
    grep -rl 'GOOGLE_APPLICATION_CREDENTIALS\|GOOGLE_SA_JSON' /path/to/projects 2>/dev/null | wc -l
    看到什麼代表什麼:這個數字就是你輪替時必須同步切換的服務數量。事前不知道它,事中就會停機。
分享
精選拆解直送信箱

每篇新文章一封:三點摘要、關鍵字定義、今天能做的一件事。