排程回報「找不到帳戶」,被停權的是整個專案
一支只負責把資料寫進試算表的小程式,用的帳號卻是整個雲端專案權限最大的角色。金鑰六年沒換、外流後被用了 15 天:帳單 US$146,但「資料有沒有被看過」我們永遠答不出來。
- 一支只負責寫試算表的自動化程式,用的帳號卻擁有整個雲端專案的最高權限。它的金鑰檔 2020 年產生、六年沒換,外流之後被陌生人用了 15 天,我們一次都沒發現。
- 對方的第一個動作不是開機器,是測試我們的專案能用哪些 AI 模型;一週後兩分鐘內開了一條對全網敞開的連線規則,並在同一秒於 3 個區域送出 5 台虛擬機的建立請求。
- 帳單 US$146.10,每一塊錢都對得上。金額小是運氣:同樣的權限換成高階機型,15 天能燒掉幾萬美元。更貴的是三種沒開的紀錄,讓「資料有沒有被看過」永遠答不出來。
一行錯誤訊息,掀出十五天
2026 年 9 月 12 日晚上 8 點 27 分,一支每天固定把資料寫進 Google 試算表的排程程式回報失敗,錯誤訊息只有一句 invalid_grant: account not found,意思是「這個帳戶不存在」。第一個反應很自然:大概有人不小心把帳號刪了。
沒有人刪帳號。是 Google 把我們整個雲端專案停權了,而按下停權鍵的不是我們,是它的濫用偵測系統。


invalid_grant 的另一面:Google 的停權通知把原因寫得很直白,服務帳戶憑證在公開來源外洩、被第三方拿去啟動資源。專案名稱已去識別化。停權通知把原因寫得很白:有一把憑證流到公開的地方,被第三方拿去啟動資源。這裡說的憑證,是一個檔案——程式拿著它就能代表某個帳號去操作雲端,等於一把不會過期、可以無限複製的鑰匙。
順著那句錯誤往回翻紀錄,對方的第一個動作停在 8 月 28 日晚上 11 點,比停權早了 15 天。
那把鑰匙的來歷一句話講得完:2020 年產生的一個檔案,用途是讓一支訊息機器人讀寫試算表、把照片丟進 Google Drive。問題不在它做的事,在它的身分——這個帳戶被設成專案的 roles/owner,也就是這個雲端專案裡權限最大的角色,能開機器、能改權限、能動用任何服務。於是那把鑰匙打開的不是幾張試算表,是整個專案。接下來的兩週,對方測了 AI 模型、開了一條對全世界開放的連線規則、在美國三個地區開起虛擬機,我們一次都沒發現。
這篇要講的只有一件事:權限決定損害的上限,金鑰的壽命決定機會窗口,而這兩件事都跟你做什麼生意無關。
也先說清楚答不出來的部分:那幾台機器裡面跑了什麼、試算表有沒有被讀過、流量最後送去哪裡,到今天仍然無解,原因寫在後面那一節。時間一律為台灣時間(UTC+8),整理於 2026-09-15,受害端資訊去識別化後發布。
接下來的順序是這樣:先把事件說完(背景與時間線),再進技術鑑識(攻擊怎麼一步一步發生、證據在哪),然後是管理與制度(為什麼會發生、我們改了什麼),最後留一件帶得走的事。每一段交界都會告訴你可以跳過什麼。
事件前,這是一個沒有人打開過的專案
先說「專案」是什麼。在 Google Cloud 裡,專案是資源與帳單的容器,一個專案就像一間獨立的小倉庫,裡面的機器、金鑰、帳單都算在一起。
這間倉庫安靜到不像有人在用:每天費用 US$0,主控台沒有人打開,只有排程在跑。架構也單純,一支訊息機器人加幾支排程跑在部署平台上,資料落在 Google 試算表與 Drive,沒有資料庫叢集,沒有任何對外的虛擬機,對外服務只有機器人接收訊息的那個網址。所有 Google 這一端的動作,都由同一個「服務帳戶」完成——服務帳戶就是給程式用的帳號,沒有人會登入它,它靠一把金鑰檔證明自己是誰。
放大器藏在權限裡。這個只需要讀寫試算表的服務帳戶掛著 roles/owner,也就是專案層全權限;而且它不是特例,同一個專案另外還有 4 個服務帳戶掛著 Owner 或 Editor,全專案共 7 把由人建立的金鑰。
還有兩件事後來變得很關鍵:這個專案開著兩個業務上其實用不到的服務——可以開虛擬機的 Compute Engine,以及可以呼叫 AI 模型的 Vertex AI。前者是我方 2026-03 為了一次測試自己開的,之後沒有關。攻擊者後來走的每一條管道,都是我們自己先鋪好的。
偵測這一側,原本只有 Google 預設給的那一層:Cloud Audit Logs 的 Admin Activity,也就是「誰在什麼時候對這個專案做了什麼」的流水帳,預設開啟、保留 400 天、不用錢。除此之外,沒有預算警示,沒有網路連線紀錄,沒有檔案存取紀錄,也沒有任何一條會主動通知人的告警。
一句話:能被攻擊者利用的東西我們都先備好了,能讓我們早點發現的東西一樣都沒開。細目如下。
深入閱讀事件當時的權限、API 與紀錄設定全貌一張表,給要照著盤點自家專案的人
| 項目 | 事件當時的狀態 |
|---|---|
| 主角服務帳戶 | 實際工作只有讀寫試算表與上傳 Drive;權限 roles/owner,專案層全權限 |
| 其他高權限身分 | 另有 4 個服務帳戶掛 Owner 或 Editor |
| 使用者建立的金鑰 | 全專案 7 把;主角那把 2020-09 建立、無到期日 |
| 已啟用的 API | Compute Engine(我方 2026-03 為一次測試開啟,未關)、Vertex AI(更早就開著) |
| 有開的紀錄 | Cloud Audit Logs/Admin Activity:預設開、保留 400 天、免費,含完整請求參數 |
| 沒開的紀錄 | 資料存取稽核(誰讀了哪個檔案)、VPC Flow Logs(網路連線紀錄)、Vertex AI 請求記錄(送進 AI 的內容) |
| 告警與通知 | 無預算警示、無以方法名為條件的記錄型警示、無人訂閱 Security Command Center 的高風險發現 |
同一次平台事件外洩的憑證有三類,差別只在有沒有被列進平台給的那張清單:列上的兩類 8/29~8/30 就換掉了,沒列上的那一把是這篇的主角。
深入閱讀三類外洩憑證的格式、存放位置與處置狀態逐項列出:是什麼憑證、放在哪裡、能做什麼、什麼時候換掉
| 服務 | 憑證類型 | 用途 | 存在形式 | 放在哪 | 權限範圍 | 狀態 |
|---|---|---|---|---|---|---|
| GCP:Service Account(IAM 服務帳戶) | Google Service KEY JSON(長期私鑰,無到期日) | 訊息機器人讀寫試算表、上傳照片到 Drive | JSON 檔,內含 private_key 的 PEM 區塊;在部署平台則是整段 JSON 或 base64 字串塞進環境變數 | 兩個 repo 的 git 歷史(application 目錄下的 key 檔)+ 部署平台環境變數 + 開發者本機 | roles/owner,專案層級全權限 | 2020-09 建立、2026-09-13 連同專案內 7 把使用者金鑰一併刪除 |
| AWS:IAM 使用者 | access key ID + secret access key | 應用程式存取物件儲存與寄信等後端服務 | 明文環境變數(AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY) | 部署平台環境變數(有列在平台提供的清單上) | 未逐一比對,至少涵蓋該應用程式用到的服務 | 2026-08-29~08-30 已輪替 |
| 資料庫(關聯式資料庫多組執行個體) | 連線帳號密碼 | 應用程式與排程連資料庫 | 明文環境變數,或整串連線字串(DATABASE_URL 形式) | 部署平台環境變數 | 該資料庫的讀寫 | 2026-08-29~08-30 已輪替 |
| 其他符合憑證格式的環境變數 | 不明 | 不明 | 推測同樣是明文環境變數 | 部署平台 | — | 平台未提供事件細節,無法確認攻擊者實際取走哪些 |
沒列上的那一把,其實同時放在三個地方:程式碼庫的歷史紀錄、部署平台的環境變數、開發者的電腦。同一個身分,三個位置,只有一個位置有人管。這句話就是後面那 17 天的全部原因。
把日期排成一條線
外洩通知其實 8 月 27 日就到了。我們照著部署平台列出的環境變數清單逐一輪替,兩天之內換完 AWS 金鑰與資料庫密碼,動作不算慢。問題是那把 GCP 金鑰是以 JSON 檔的形式散在 repo 裡,不在那張清單上——於是它繼續活著。
- 8/27部署平台通知外洩依平台列出的環境變數清單開始輪替。
- 8/28 23:00攻擊者首次使用金鑰(Vertex AI)稽核記錄:來源 IP 不是我們的,操作身分卻是機器人帳戶。
- 8/29 → 8/30我方輪替 AWS 金鑰、DB 密碼GCP JSON 金鑰不在清單上,未處理。
- 9/3一條防火牆規則+3 台虛擬機稽核記錄:一筆建防火牆(firewalls.insert)、五筆開機器(instances.insert),成功三台。
- 9/5 → 9/11虛擬機被大量使用;9/7 起 Gemini 被盜用監控數據:機器的流量曲線,以及依金鑰分組的 API 呼叫次數。
- 9/12Google 停權專案排程回報 invalid_grant;三台機器的狀態變成已終止。
- 9/13刪除專案內 7 把使用者金鑰、拿掉 5 個服務帳戶的 Owner/Editor稽核記錄:刪金鑰(DeleteServiceAccountKey)、改權限(SetIamPolicy)。
- 9/14 → 9/15申訴 → 解封申訴單與解封通知。
從收到通知到真正刪除,這把金鑰多活了 17 天,其中 15 天在被使用。逐分鐘的紀錄與每一步的稽核記錄節錄,在教學版:四道防線只要做到一道,這 15 天就不會發生。
到這裡,你其實已經掌握這起事件最重要的部分:一把不該有最高權限的金鑰流到外面,被用了 15 天,專案被 Google 停權三天,帳單是三位數美金,而「資料有沒有被看過」至今無解。
如果你關心的是為什麼會發生、以及公司該改什麼,可以直接跳到〈真正的原因往下有四層〉繼續讀,中間兩節不看也不影響理解。如果你想知道我們是怎麼從稽核記錄一步一步還原這 15 天,下面開始進入技術鑑識。
攻擊者的十五天,一步一步拆開
先用白話走一次,七個步驟。
**第一步,他不需要破解任何東西。**拿著那個 JSON 檔,就能向 Google 換到一張短期通行證,之後所有動作都是「合法」的 API 呼叫。沒有登入畫面、沒有密碼、沒有二階段驗證可以攔,因為這條路本來就是設計給程式走的。
**第二步,他先去找錢在哪裡。**8 月 28 日晚上 11 點的第一個動作不是開機器,是連續呼叫 Vertex AI(Google 的 AI 模型服務)27 次,全部被擋下來(回 404 或 429,模型不存在或被限流)。隔天凌晨 3 點 21 分,他替我們的專案「接受了模型使用條款」,那是開通模型的必要步驟。真正動手是 9 月 3 日中午:12 點 38 分列出可用地區與網路,兩分鐘後建立一條名為 crusader-allow-ssh 的防火牆規則,對整個網際網路開放遠端登入與一大段連接埠;12 點 40 分,同一秒鐘在三個地區、五個機房送出五台虛擬機的建立請求,成功三台。動作之間只隔幾秒,是腳本,不是人在操作。
**第三步,讓機器自己活下去。**建立請求裡就附好開機腳本,機器一開起來就自動設定密碼、打開遠端登入、回報自己的 IP。他在 9/3 到 9/5 間登入幾次把工具裝上、設成開機自動執行,之後再也沒有登入過。他還刻意不讓這些機器綁任何服務帳戶——機器就算被查,也偷不到我們雲端內部的權限,同時大幅減少留下的足跡。
**第四步是空的。**攻擊鏈裡有一格叫「提權」,指攻擊者想辦法把自己的權限變大。400 天的紀錄裡完全沒有他修改權限或新增金鑰的動作。原因不是他做不到,是他不需要:這把金鑰一開始就是最大權限。
**第五步,金鑰是怎麼流出去的。**這是全篇唯一一條只能推論的事:8 月 27 日部署平台發生事件,攻擊者取走了符合憑證格式的環境變數。這把金鑰在 repo 裡放了好幾年都沒被濫用,卻在平台出事後 36 小時內開始被使用,時間相關性很強;但平台未提供事件細節,沒有直接證據。
**第六步,機器主動連回去領工作。**那三台機器不開任何對外的服務埠等人來連,而是自己用 HTTP 連回攻擊者的派工主機,領工單、回傳結果。這種形狀在雲端的控制紀錄裡完全看不到,只有把磁碟拆下來看才查得到。
**第七步是變現。**前兩天半機器幾乎閒置,CPU 只有 2 到 3%。9 月 5 日晚上 8 點開始流量暴增,三台合計收進 3,186 GiB、送出 284 GiB;同一週我們專案的 Gemini 被成功呼叫 212 次。算力、頻寬、AI 額度,三種都能轉手賣掉。9 月 8 日起他每小時列一次機器清單確認還活著,那是自動化巡檢,不是人。
接下來這張表會稍微技術一些。你不需要先懂 MITRE ATT&CK(一套國際通用、把攻擊手法編號化的公開分類法),它的用處是讓你跟團隊、稽核或委外廠商指著同一個編號講同一件事;表裡每一格都附了當時該出現的訊號,以及對應的防堵設定。
深入閱讀七個階段對照 MITRE ATT&CK:時間、證據、訊號、對應設定第 04 格是空的,那格的空白本身就是結論
**攻擊者沒有提權,因為我們已經先把權限給滿了。**他連開通 API 的力氣都省下來:第二步用到的運算與 AI 兩個服務,本來就開著。
他在雲端做的事全都有紀錄,他在機器裡做的事一件都沒有
雲端可以分成兩層來看。一層是「你對雲端下的指令」——開機器、改權限、建規則,這一層 Google 預設就替你記得清清楚楚,連請求參數都留著,保留 400 天,不用加錢,業界叫它控制平面。另一層是「機器裡面發生的事」——誰登入、跑了什麼程式、資料往哪裡送,這一層雲端不會自動替你記,要自己打開對應的紀錄。
我們的情況是:第一層完整,第二層只剩下磁碟,網路與內容那一層什麼都沒有。所以事後最想問的問題,有些答得出來,有些永遠答不出來。
| 要回答的問題 | 還原程度 | 資料來源 | 保留期限 |
|---|---|---|---|
| 建了什麼、改了什麼權限 | 完整:時間、IP、金鑰、完整參數 | Admin Activity 稽核記錄 | 400 天 |
| 呼叫了哪些 API、AI 模型 | 只有次數、方法、地區,沒有內容 | Monitoring api/request_count | 6 週 |
| VM 用了多少資源 | 每台 CPU、收送流量曲線 | Monitoring VM 指標 | 6 週 |
| 誰 SSH 進來、執行了什麼 | 磁碟鑑識可查到 | 3 顆 200GB 磁碟 | 刪除即消失 |
| 流量從哪裡來、傳到哪裡 | 無 | VPC Flow Logs 沒開 | — |
| AI 提示詞和回應 | 無 | Vertex AI 請求記錄沒開 | — |
| 試算表、Drive 有沒有被讀 | GCP 端無 | 資料存取稽核沒開 | — |
磁碟那一列後來被打開了:3 顆磁碟的離線解剖結果,見 Crusader 殭屍網路解剖。
把這一節壓成三句可證明的話:控制平面的重建不需要額外採購任何工具,預設就開的那份紀錄已經夠用,真正缺的是有人去看它;攻擊者用的每一條管道都是我們自己先打開的,他連 API 都不必自己開通;帳單上的每一塊錢都對得上 SKU,不需要跟正常用量切割。另外兩句比較難聽:金鑰從哪一份副本流出去只能推論,而資料有沒有被讀,永遠答不出來。下面是這五條的原文與出處。
深入閱讀五條證據的原文:三條有收據、一條只能推論、一條永遠空白含 14 筆稽核記錄的內容、保留期限與無法證實的部分
技術鑑識到這裡結束。接下來幾節不需要任何雲端知識,講的是管理與制度:這件事為什麼會發生、為什麼沒有人發現、當天我們怎麼處置、哪些做對了,以及最後改了什麼。如果你是要向公司說明這件事的人,從下面開始才是重點。
真正的原因往下有四層
表面上的原因一句話講完:一把 2020 年建立的金鑰檔被外部拿去換通行證,開了 3 台虛擬機、呼叫了 212 次 Gemini。這一層有 14 筆稽核記錄與一份帳單可以證明。
再下一層是技術。一個只需要讀寫試算表的服務帳戶掛著專案最高權限,而它的憑證是一把沒有到期日、可以無限複製的長期私鑰檔;專案同時開著業務用不到的運算與 AI 服務。權限給了損害上限,金鑰給了時間,開著的服務給了現成管道,三樣湊齊才會有後面那張帳單。
再往上是管理,這一層同樣有紀錄可證:8/27 收到通知後,輪替範圍照的是平台列出的環境變數清單,散在 repo 與開發者本機的 JSON 金鑰不在任何人的責任裡;而這個專案平常沒有費用,也就沒有人固定看帳單。
最後一層只能寫成推測,因為它本來就無從舉證:沒有以身分為單位的憑證清冊與到期機制,沒有禁止建立服務帳戶金鑰的組織政策,高權限服務帳戶也沒有定期複核。六年前為了方便做的一個決定,就這樣一路帶到今天。
四層裡,前兩層是技術問題,補起來花不了什麼錢;後兩層是管理與制度問題,而會重複發生的正是後兩層。前三層修完,下一個專案照樣會長出一把 Owner 金鑰。
訊號有六條,沒有一條會自己來找人
8/28 晚上 11 點,一個陌生 IP 以機器人帳戶的身分呼叫 AI 服務。這筆紀錄從頭到尾都在,但沒有人在看來源 IP,也沒有任何一條以「是誰做的」為條件的告警——日誌有寫,不等於有人讀。隔天凌晨 3 點 21 分,有人替我們的專案接受了 AI 模型使用條款;AcceptPublisherModelEula 這個動作,一個專案一輩子大概只會出現一次,這種名字本來就該被列進告警清單,而我們沒有那份清單。
9/3 12:40 那一秒,五筆開機器的請求同時落在 3 個區域、5 個可用區,外加一條對全網開放的防火牆規則。沒有即時通知,Security Command Center(Google Cloud 內建的資安告警中心)的高風險發現也沒有人訂閱。隔天帳單就動了:平常 US$0 的專案花掉 US$13,但帳單帳戶沒有設任何預算警示,而這個專案平常不需要有人打開報表。
剩下兩條更明顯,也更沒用。9/5 20:00 起,三台機器每 6 小時流入的資料量從 3 GiB 跳到 100 GiB 以上,但監控只監控你知道存在的東西,這三台不在任何儀表板上,也沒有基準線可以比。9/7 起,機器人帳戶的 API 用量裡開始出現運算與 AI——一個只寫試算表的身分開始呼叫運算 API,這比任何 IOC 都有辨識力,可惜這個角度要用特定的查法才看得到,主控台的 API 用量頁沒有這個維度(查法在下一節)。
六條訊號,最早一條在第一天,我們一條都沒有接住。
9/13 那一天:撤銷、快照、申訴
真正的偵測不是我們做的。9/12 20:27 排程回報 invalid_grant,我們先以為帳戶被誤刪,追下去才看到整個專案被停權、三台不認識的虛擬機狀態是 TERMINATED。偵測來源是業務中斷,不是任何一條告警。
隔天的第一個動作是斷手:刪除專案內全部 7 把使用者建立的金鑰,而不是只刪已知的那一把,再拿掉 5 個服務帳戶的 Owner/Editor,讓所有不知道放在哪裡的副本一次失效。有一件事是刻意不做的——沒有刪除服務帳戶本身,它擁有大量 Drive 檔案,刪帳戶會連檔案一起消失。
接著才是清場。攻擊者留下的 3 顆 200 GB 磁碟先建快照保存證據,再做離線解剖;同時確認專案裡沒有會自動補機器的受管執行個體群組與執行個體範本,這一步決定了「刪掉 VM 就會永久停止」這句話敢不敢說。鑑識完成前磁碟不能刪,所以帳單還在跑,那是證據保存的價格。9/14 送出申訴,9/15 解封,受影響的排程改走新建的無金鑰代理,不再持有私鑰;切換的細節見〈清單全部打勾的第 13 天,整個專案被停權了〉。
上面每一個查證動作,用的都是唯讀指令——只看,不改。把 PROJECT 換成你的專案就能跑。
深入閱讀當天用到的唯讀查證指令,以及「哪一把金鑰在被誰用」的查法gcloud logging read 與 Cloud Monitoring 指標名稱
# 誰開了 VM、用哪把金鑰、從哪個 IP gcloud logging read 'protoPayload.methodName="v1.compute.instances.insert"' \ --project=PROJECT --freshness=45d \ --format="table(timestamp,protoPayload.authenticationInfo.serviceAccountKeyName,protoPayload.requestMetadata.callerIp)" # 攻擊者 IP 的所有動作(Admin Activity 保留 400 天) gcloud logging read 'protoPayload.requestMetadata.callerIp="93.123.109.230"' --project=PROJECT --freshness=400d # 攻擊者有沒有自己開 API gcloud logging read 'protoPayload.methodName=~"EnableService"' --project=PROJECT --freshness=400d # 權限被怎麼改過 gcloud logging read 'protoPayload.methodName="SetIamPolicy"' --project=PROJECT --freshness=10d --format=json
API 呼叫次數要依憑證分組才看得出「哪一把金鑰在被誰用」,用 Cloud Monitoring 的 serviceruntime.googleapis.com/api/request_count,以 resource.label.credential_id 與 resource.label.method 分組;主控台的 API 用量頁看不到這個維度。VM 流量曲線則用 compute.googleapis.com/instance/network/received_bytes_count 與 sent_bytes_count,每 6 小時 ALIGN_SUM。
做對的四件事免費,做錯的四件事也免費
這次有幾個動作是對的,而且不是靠運氣。稽核記錄預設就開、保留 400 天,所以控制平面的每個動作都還原得到,連請求參數都在,這是零成本的正確決定;停權當天沒有急著刪 VM,先對磁碟建快照,才有後來的離線解剖與工具能力清單;撤銷選的是「刪光這個專案所有使用者金鑰」而不是「刪那一把」,一次切掉所有不知道在哪的副本;最後是忍住沒有刪除服務帳戶本身。
該改的四件,成本也都接近零。8/27 收到通知時,輪替範圍照的是位置清單不是身分清單,這把金鑰因此多活了那 17 天;檔案存取紀錄、網路連線紀錄、AI 請求記錄三種日誌都沒開,最想回答的三個問題全部無解;沒有預算警示,帳單從 US$0 漲到一天 US$21,連續 9 天沒有人知道;Compute Engine API 為了一次測試開啟後沒有關掉,等於替攻擊者把變現管道先鋪好。
兩邊的成本都接近零,差別只在有沒有人想到。做得好的那四件事都發生在 9/13 一天之內;做不好的那四件,每一件在事發之前就已經決定了。
我們真正改了什麼
第一件事是撤銷,不是調查。
當天就做完的有四件:撤銷該身分的所有使用者金鑰、拿掉 Owner/Editor,並用舊憑證呼叫一次確認它真的回 invalid_grant;鑑識完成後刪掉攻擊者的 3 台 VM、3 顆磁碟與 crusader-allow-ssh 防火牆規則,停止磁碟繼續計費;停用這個專案用不到的 Compute Engine 與 Vertex AI API;替每個專案設一條預算警示,門檻取平常費用的 2 倍。
一個月內補的是偵測與架構:把 instances.insert、firewalls.insert、SetIamPolicy、CreateServiceAccountKey、AcceptPublisherModelEula 五個方法名設成記錄型警示,也就是「只要紀錄裡出現這幾個動作就發通知」,這條警示我們取名 alert-hot-methods-a9r,之後翻通知一眼就對得上這篇;打開這次害我們答不出問題的那三種日誌(至少 Sheets、Drive、Storage 的資料存取稽核,加上 VPC Flow Logs);應用程式改走無金鑰代理,只拿可撤銷的呼叫 token;另一個仍是 Owner、金鑰仍有效的服務帳戶比照處理。
這一季要做的是讓這個坑不可能再挖出來:用組織政策(整個公司的雲端帳號一體適用的強制規則)把「發長期服務帳戶私鑰」這件事直接關掉,例外專案加上金鑰到期時數;限制可用地區、禁止外部 IP、限制可用 AI 模型,不用 VM 的專案把 CPU 配額設 0;憑證清冊改以身分為單位;每月複核一次高權限服務帳戶。
這些措施其實只是四道防線的分期付款。這次四道都沒有設,所以一把金鑰一路通到底;只要其中任何一道有做到,這 15 天就不會發生。最後一欄是每一道對應的具體設定,給要落地的人。
| 防線 | 做法 | 設定 |
|---|---|---|
| 1. 金鑰根本不存在 | 不發服務帳戶私鑰。應用程式改走代理服務,只拿可撤銷的呼叫 token;repo 掃描並清除歷史裡的金鑰檔 | 組織政策 iam.disableServiceAccountKeyCreation;例外專案加 iam.serviceAccountKeyExpiryHours |
| 2. 外洩了也沒權限 | 服務帳戶一律不給 Owner/Editor。只讀寫試算表的 Bot 不需要任何專案角色 | gcloud asset search-all-iam-policies --query="policy:(roles/owner OR roles/editor) memberTypes:serviceAccount" 每月跑一次 |
| 3. 有權限也做不了大事 | 關掉用不到的 API;限制可用地區和 AI 模型;不用 VM 的專案把 CPU 配額設 0 | gcloud services disable compute.googleapis.com;組織政策 gcp.resourceLocations、compute.vmExternalIpAccess、vertexai.allowedModels |
| 4. 出事馬上知道 | 每個專案設預算警示;對高風險動作設記錄型警示;開資料存取稽核與 VPC Flow Logs | gcloud billing budgets create;警示條件 `methodName=~"instances.insert|firewalls.insert|SetIamPolicy |
至於錢。帳單金額是 US$146.10,計費期間 2026-08-01 至 09-14,而這個專案 8/2 以前每天都是 US$0,所以每一列都全數可歸因,不必跟正常用量切割。裡面最大一筆是虛擬機的 CPU 與記憶體,其次是流量與磁碟;被盜用的 AI 只花了不到 3 美元。真正貴的其實不是帳單:處理這件事的人力約 1 人日,換算成顧問日費是帳單的六倍,而攻擊者這一趟的支出是 0。
下面把每一列攤到帳單上的 SKU(雲端帳單裡的最小計費項目),刻意不用「大約」;後面四列是花了錢也買不到答案的部分。
深入閱讀帳單攤到每一個 SKU:八列可證明,四列永遠空白每列附用量、單價、期間、證據來源與等級
| 項目 | 用量 | 單價 | 期間 | US$ | NT$ | 舉證 |
|---|---|---|---|---|---|---|
| E2 Instance Core(us-central1 區) 帳單實付。us-central1 一區就佔 Compute Engine 的 92.22 | 1,790.19 vCPU·小時 | 約 US$0.0218/vCPU·小時 | 9/3 → 9/12 | 39.05 | 1,250 | 帳單依 SKU 分組頁面(222 個 SKU 中的第 1 名) |
| E2 Instance Core(us-east4 區) 帳單實付。同一批機器跨兩區開,單價也不同 | 895.10 vCPU·小時 | 約 US$0.0246/vCPU·小時 | 9/3 → 9/12 | 21.99 | 704 | 帳單依 SKU 分組頁面 |
| E2 Instance Ram(兩區合計) 帳單實付:Americas 20.93 + Virginia 11.79。e2 機型的 vCPU 與記憶體是分開計價的 | 10,741.17 GiB·小時 | 隨機型綁定計價 | 9/3 → 9/12 | 32.72 | 1,047 | 帳單依 SKU 分組頁面(兩列相加) |
| Balanced PD Capacity(兩區合計) 帳單實付:Americas 12.82 + Virginia 7.07。VM 關機後磁碟照樣計費 | 186.21 GiB·月(3 顆 200 GB pd-balanced) | 約 US$0.10/GB·月 | 9/3 → 帳單截止 | 19.89 | 636 | 帳單依 SKU 分組頁面(兩列相加) |
| 對外流量(前 4 大流量 SKU) 帳單實付:美洲→美洲 63.33 GiB 7.48、Virginia→美洲 57.24 GiB 6.87、美洲→亞太 45.53 GiB 5.34、Carrier Peering 63.82 GiB 5.11 | 約 230 GiB 送出;流入 3,186 GiB 不計費 | US$0.08~0.12/GiB,依目的地 | 9/5 → 9/12 | 24.80 | 794 | 帳單依 SKU 分組頁面(四列相加) |
| 其餘 212 個 Compute Engine SKU 帳單實付:Compute Engine 小計 143.32 減去上面六列 | 零碎項目 | — | 計費期間內 | 4.87 | 156 | 帳單依服務分組(143.32)與依 SKU 分組前 10 名的差額 |
| Vertex AI:Gemini 帳單實付。地區以 us-west4 為主,另有 us-central1、asia-southeast1 | 27 個 SKU;成功呼叫 212 次,輸出 token 以 2.5 Pro Thinking 67,623 為最大宗 | 原價 3.62,優惠折抵 0.83 | 9/6 → 9/12 | 2.79 | 89 | 帳單依 SKU 分組頁面;Cloud Monitoring request_count 依 credential_id 分組(GenerateContent 200 共 208+3 次) |
| Networking:負載平衡與位址 帳單實付 | 原價 2.02 | 優惠全額折抵 | 計費期間內 | 0.00 | 0 | 帳單依服務分組頁面 |
| 還在跑的磁碟帳單 估:GCP 對 9/15 的預估費用已升到 192.84,多出來的就是這個。鑑識做完前不能刪,所以這是證據保存的價格 | 3 顆 200 GB pd-balanced,VM 已 TERMINATED 但磁碟未刪 | 約 US$0.10/GB·月 → 600 GB 約 US$60/月 | 刪除前持續 | 60.00 | 1,920 | 帳單預估值 192.84(含 9/13~15 預估)+ pd-balanced 公開牌價;實際金額要等帳單結算 |
| 事故處理人力 估:取中間值 NT$27,500。帳單 146 元,處理它的人力貴 6 倍 | 約 1 人日:磁碟鑑識、金鑰輪替、申訴 | 顧問日費 NT$20,000~35,000(估) | 9/13 → 9/15 | 859.38 | 27,500 | 處置時間軸(9/13 撤金鑰、9/14 申訴、9/15 解封);日費為假設值 |
| 攻擊者這一趟的投入 估:邊際成本趨近 0,所以他不需要挑目標 | 一把撿來的金鑰、一支腳本、幾次 SSH | 他付 US$0,帳單記在我們頭上 | 15 天 | 0.00 | 0 | 稽核記錄顯示全程以外洩金鑰直打 REST API,未動用他自己的運算資源;他的實際支出無從得知 |
| 試算表與 Drive 有沒有被讀 只能假設有 | 服務帳戶是 Owner,可讀所有分享給它的檔案 | — | 8/28 → 9/12 | — | — | 資料存取稽核(Data Access audit logs)沒開,GCP 端無從查證 |
| AI 被拿去生成了什麼 只知道花了多少 token,不知道內容 | 212 次成功呼叫,文字與圖片都送 | — | 9/6 → 9/12 | — | — | Vertex AI 請求記錄沒開,只有 Monitoring 的次數與 token 計費量 |
| 那 230 GiB 送到哪裡去了 推論是被當成代理或跳板轉送流量,但無法證實 | 目的地含美洲與亞太 | — | 9/5 → 9/12 | — | — | VPC Flow Logs 沒開;帳單只告訴我們目的地大區,不告訴我們對象 |
| 專案停權造成的業務中斷 本站不估算營收損失 | 排程全部停擺約 3 天 | — | 9/12 → 9/15 | — | — | 停權與解封時間可證,但中斷金額沒有計算基礎 |
| 合計(僅可證明列) | 146.11 | 4,676 | ||||
八列可證明的加起來是 146.11,GCP 頁面顯示 US$146.10,差在四捨五入。真正值得看的不是這個數字,是它下面那四列缺口:**我們付得出帳單,卻付不出答案。**而金額之所以只有三位數,是因為攻擊者只開了 3 台中型 VM;同樣的權限換成 GPU 機型或更多地區,兩週可以燒掉幾萬美元。這次是運氣好,不是防禦好。
管理與制度的部分到這裡結束。最後兩節回到所有人都用得上的東西:五件帶得走的事,和今天就能做完的 15 分鐘檢查。
帶得走的五件事
先站到他的位置看一次。他第一個動作是找 AI 模型,不是開機器——AI 的使用額度有現成的黑市,把算力轉租給第三方也能立刻換成錢(這一段動機是推測,稽核記錄只證明他做了什麼);他先試的 Claude 模型全部被擋下來(回 404 或 429),才退而求其次用 Gemini。開機器時他刻意不讓機器綁任何服務帳戶,一來機器被查也偷不到我們雲端內部的權限,二來留下的足跡少很多;同一秒在 3 個地區、5 個機房送出 5 台,是因為某一區沒有空機或被封鎖時還有別台能用,而他選的中型機種(e2-standard-4)便宜、又不會被中途回收。9/8 起他每小時列一次機器清單確認它們還活著,那是自動化巡檢,不是人。從頭到尾,他沒有花一秒鐘研究我們是誰。
- 01權限決定損害上限。一把只寫試算表的金鑰,因為帳戶是 Owner,打開的是整張變現菜單;攻擊鏈裡「提權」那一格是空的,因為不需要。
- 02服務帳戶金鑰沒有到期日。2020 年發的那一把,2026 年一樣簽得出 token,而且沒有任何人會收到通知。
- 03輪替的單位是身分,不是位置。平台給你的是位置清單,外洩的卻是身分,只換位置就一定會漏。
- 04帳單是最便宜的一道偵測線。平常 $0 的專案一天多出 $13,不需要任何資安知識就看得懂,而我們花了 9 天沒看。
- 05付得出帳單,不代表付得出答案。三種沒開的日誌,讓「資料有沒有被讀」這種客戶一定會問的問題,永遠只能回答「不知道」。
如果只想記十個詞,就記下面這十個;順序是照「讀完最該帶走」排的,不是照它們在文章裡出現的先後。看不懂的詞不必勉強,把對應的那一句話記住就夠。
- 01roles/owner本案唯一的放大器。沒有它,一把外洩金鑰只能讀幾張試算表;有了它,攻擊者拿到的是整張變現菜單。
- 02服務帳戶金鑰(JSON 私鑰)它沒有到期日。2020 年發的那一把,2026 年一樣能簽出 access token,而且你不會收到任何通知。
- 03iam.disableServiceAccountKeyCreation唯一能讓「金鑰外洩」這個劇本從源頭不存在的組織政策;例外專案再用 iam.serviceAccountKeyExpiryHours 補。
- 04instances.insert全案最值得設即時告警的一個方法名。9/3 12:40 那一秒,五筆請求同時出現在 3 個區域、5 個可用區。
- 05Cloud Audit Logs(Admin Activity)預設就開、保留 400 天、不用錢。本案能還原到「哪一把金鑰在哪一秒從哪個 IP 做了什麼」,全靠它。
- 06callerIp 與 serviceAccountKeyName稽核記錄裡判斷「這是不是我們」的兩個欄位。先知道它們叫什麼,出事時才查得動。
- 07credential_id(api/request_count)依憑證分組看 API 用量,是唯一能回答「哪一把金鑰在被誰用」的角度;主控台的 API 用量頁看不到。
- 08預算警示(budgets)平常 $0 的專案 9/4 那天花了 $13。這是最便宜、最不需要資安知識就能設的一道線。
- 09資料存取稽核(Data Access audit logs)沒開,所以「試算表有沒有被讀」這題永遠無解。它要錢,但它決定你事後能不能給客戶一個答案。
- 10VPC Flow Logs沒開,所以 230 GiB 送到哪裡去也無解。如果流出去的是違法內容,被追的是你的 IP。
如果那通「排程掛了」今天再來一次
把 9/12 20:27 當成第 0 分鐘,前 60 分鐘我會這樣用。
前十五分鐘先保存會變的東西:匯出或截圖帳單「依服務/依 SKU 分組」的報表,金額每天都在動;用 gcloud logging read 以陌生 callerIp 與那把金鑰的 ID 拉出 Admin Activity,把不是自己人的動作先列出來;再列出專案裡所有 VM 與防火牆規則,確認哪幾台不是我們開的。
第二個十五分鐘止血:刪掉這個身分的所有使用者金鑰,不是只刪你知道的那一把;拿掉 Owner/Editor,但不要刪除帳戶本身,它擁有 Drive 檔案;然後用舊金鑰實際呼叫一次,看到 invalid_grant 才算撤銷完成。
剩下的半小時留給證據:每一顆磁碟先建快照再談刪除,快照是唯一能回答「VM 裡發生什麼」的來源;確認受管執行個體群組與執行個體範本都是空的,才敢說刪掉 VM 就結束;順手刪掉對 0.0.0.0/0 開放的防火牆規則,並清點專案裡其他相同形狀的規則。一小時之後才輪到慢工:開資料存取稽核與 VPC Flow Logs、每個專案一條預算警示、把應用程式換成不持有私鑰的架構。
回到 9 月 12 日晚上那一行 invalid_grant。它是整段期間裡唯一一次「系統主動告訴我們出事了」,而發出它的不是我們的監控,是 Google 的濫用偵測。下一次同樣的事發生在別人的專案裡,那一行字會不會一樣準時,沒有人知道。
在它響之前,下面五步全部唯讀,十五分鐘內做得完,做完就知道你手上有沒有同一個形狀。指令看不懂沒關係,交給工程師跑,你只要看「代表什麼」那一欄。
全部唯讀,不改任何設定;做完你會知道自己有沒有同樣的破口。
- 列出所有掛著 Owner 或 Editor 的服務帳戶。
gcloud asset search-all-iam-policies \ --query='policy:(roles/owner OR roles/editor) memberTypes:serviceAccount' \ --format='table(resource, policy.bindings.role)'看到什麼代表什麼:只要有一列是「只做一件小事」的 Bot 帳戶,你就有跟本案一樣的形狀。零列才是正常。 - 挑一個服務帳戶,看它還有幾把使用者建立的金鑰、什麼時候建的。
gcloud iam service-accounts keys list \ --iam-account=SA_EMAIL --managed-by=user \ --format='table(name.basename(), validAfterTime, validBeforeTime)'看到什麼代表什麼:validAfterTime 在一年以上、validBeforeTime 是 9999 年,等於一組永不過期又不會被通知的密碼。 - 看這個專案到底開了哪些 API。
gcloud services list --enabled --project=PROJECT看到什麼代表什麼:業務上用不到卻開著 compute 或 aiplatform,代表金鑰一旦外洩,攻擊者有現成的變現路徑不必自己開通。 - 查最近 45 天有沒有人開過 VM、改過防火牆。
gcloud logging read 'protoPayload.methodName=("v1.compute.instances.insert" OR "v1.compute.firewalls.insert")' \ --project=PROJECT --freshness=45d \ --format='table(timestamp, protoPayload.requestMetadata.callerIp, protoPayload.methodName)'看到什麼代表什麼:出現不是公司出口的 callerIp,或一秒內好幾筆跨地區的 insert,就不是同事在操作。 - 確認有沒有人會在帳單漲起來時被通知。
gcloud billing budgets list --billing-account=BILLING_ACCOUNT_ID看到什麼代表什麼:空的,代表帳單從 $0 漲到 $21 一天都不會有人知道;本案就是這樣過了 9 天。
每篇新文章一封:三點摘要、關鍵字定義、今天能做的一件事。
延伸閱讀
列為「風險最高」的那把金鑰,我們決定晚點再撤
上一次鑑識就點名它,但它擁有一萬多個檔案、七個以上的線上服務靠它活著,所以排在「先搬消費者再停用」。一個月後,它在五個小時內替別人跑了 6,350 萬 input token,整個專案被 Google 停權。
教學版:四道防線只要做到一道,這 15 天就不會發生
同一起事件的教學版:15 天逐分鐘攤開,每一步標出為什麼會成功、哪一道控制擋得下來。不發金鑰、不給 Owner、關掉沒用的 API、預算警示,四道都沒有。
我們寫了一道關卡防自己,第二次部署它就亮了紅燈
沒有攻擊、沒有外洩、沒有帳單,這是一份設定稽核紀錄:一支只該讀寫試算表的 GCP:Service Account,在專案層是 Owner。拿掉只要一行,拿掉前要先查什麼?