一把 Owner 金鑰, 被用了 15 天
一支 LINE Bot 用來讀寫試算表的服務帳戶金鑰,放了六年沒換,而且帳戶本身是專案 Owner。金鑰外洩後,攻擊者先盜用 AI 模型、再開 VM,直到 Google 停權整個專案我們才發現。
- 金鑰 2020 年建立、以 JSON 檔 commit 進至少兩個 repo,也放進部署平台的環境變數。平台事件後 36 小時內開始被盜用。
- 攻擊者第一個動作不是開 VM,是測試能用哪些 AI 模型;接著兩分鐘內開防火牆、同一秒在 4 個區域開 5 台 VM。
- 損失 US$146.10,全部可歸因;金額不大是運氣好,不是防禦好。同樣權限換 GPU 機型,15 天能燒掉幾萬美元。
本篇目錄
數字先講
這次外洩了什麼
主角只有一把:一個 GCP 服務帳戶的 Google Service KEY JSON。它的工作是讓一支訊息機器人讀寫試算表、把照片丟進 Drive,但它掛著 roles/owner。同一次平台事件裡,另外兩類憑證有被列在平台給的清單上,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 已輪替 |
| 其他符合憑證格式的環境變數 | 不明 | 不明 | 推測同樣是明文環境變數 | 部署平台 | — | 平台未提供事件細節,無法確認攻擊者實際取走哪些 |
攻擊路徑
一切的關鍵在中間那一格:服務帳戶有 Owner 權限,所以一把原本只用來寫試算表的金鑰,能打開整個專案的所有服務。
通知後金鑰仍有效:17 天
8/27 收到平台通知後,我們依環境變數清單逐一輪替,8/29~8/30 處理了 AWS 金鑰與資料庫密碼。以 JSON 檔形式散落在 repo 裡的 GCP 服務帳戶金鑰,一直到 9/13 才被處理。 從通知到刪除,這把金鑰多活了 17 天,其中 15 天在被使用。
- 8/27部署平台通知外洩依平台列出的環境變數清單開始輪替。
- 8/28 23:00攻擊者首次使用金鑰(Vertex AI)
- 8/29 → 8/30我方輪替 AWS 金鑰、DB 密碼GCP JSON 金鑰不在清單上,未處理。
- 9/3防火牆 + 3 台 VM
- 9/5 → 9/11VM 被大量使用;9/7 起 Gemini 盜用
- 9/12Google 停權專案
- 9/13刪除專案內 7 把使用者金鑰、拿掉 5 個服務帳戶的 Owner/Editor
- 9/14 → 9/15申訴 → 解封
我們能還原到哪裡
攻擊者在 GCP 上的每一個「動作」都查得到;在 VM 裡面做了什麼、下載了什麼,當時查不到。
| 要回答的問題 | 還原程度 | 資料來源 | 保留期限 | |---|---|---|---| | 建了什麼、改了什麼權限 | 完整:時間、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 端無 | 資料存取稽核沒開 | — |
損失
帳單:US$146.10(計費期間 2026-08-01 ~ 09-14)。這個專案 8/2 以前每天都是 $0,所以這筆錢全數可歸因於這次盜用,不需要跟正常用量切割。依服務分組:Compute Engine 使用費 143.32(無折抵)、Vertex AI 3.62 折抵 0.83 後 2.79、Networking 2.02 全額折抵後 0.00。
下表是把帳單報表依 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 頁面顯示 146.10,差在四捨五入。真正值得看的不是這個數字,是它下面那四列 gap:我們付得出帳單,卻付不出答案。
帳單以外,算不出金額的:
- 資料:Owner 可讀取所有分享給該帳戶的試算表與 Drive 檔案;資料存取稽核沒開,無法確認有沒有被讀,只能假設有。
- 營運:專案停權約 3 天,排程全部停擺,緊急輪替金鑰;另花一個人日做鑑識與申訴。
金額不大,是因為攻擊者只開了 3 台中型 VM。同樣的權限換成 GPU 機型或更多地區,15 天可以燒掉幾萬美元。這次是運氣好,不是防禦好。
其實有訊號,只是沒人看
| 最早可發現 | 訊號 | 在哪裡看 | |---|---|---| | 8/28 23:00 | 陌生 IP 以 Bot 帳戶呼叫 Vertex AI | Cloud Audit Logs:callerIp | | 8/29 03:21 | 有人替專案接受 AI 模型使用條款 | Cloud Audit Logs:AcceptPublisherModelEula | | 9/3 12:40 | 新防火牆規則對全網開放、同時開 5 台 VM | Cloud Audit Logs;Security Command Center | | 9/4 | 平常 $0 的專案一天花 $13 | 帳單報表、預算警示 | | 9/5 20:00 | 三台 VM 收進流量從每 6 小時 3 GiB 暴增到 100 GiB 以上 | Monitoring:VM 網路流量 | | 9/7 | Bot 帳戶的 API 用量出現 aiplatform、compute | Monitoring:api/request_count 依 credential_id |
四道防線
這次每一道都沒有設,所以一把金鑰就一路通到底。任何一道有做,損害都會小很多。
| 防線 | 做法 | 設定 |
|---|---|---|
| 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|CreateServiceAccountKey|AcceptPublisherModelEula" |
另外補一條流程面的:任何平台通知外洩時,24 小時內以「身分」為單位輪替全部憑證,包含散落在 repo 和本機的 JSON 金鑰檔,而不是只照平台列出的環境變數清單。
- AI API token 有現成黑市,轉租算力給第三方是即時變現。Claude 全部 404/429 才退而求其次用 Gemini。
- 開 VM 刻意不掛服務帳戶:一來 VM 被查也偷不到 GCP 內部權限,二來大幅減少稽核足跡。
- 同一秒在 4 個區域開 5 台,某區資源不足或被封也有別台能用。e2-standard-4 便宜又不會被中途回收。
- 9/8 起每小時列一次 VM 清單,他在確認機器還活著。這是自動化的巡檢,不是人。
附錄:鑑識怎麼查(全部唯讀)
# 誰開了 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。
重點學習
- 01roles/owner本案唯一的放大器。沒有它,一把外洩金鑰只能讀幾張試算表;有了它,攻擊者拿到的是整張變現菜單。
- 02服務帳戶金鑰(JSON 私鑰)它沒有到期日。2020 年發的那一把,2026 年一樣能簽出 access token,而且你不會收到任何通知。
- 03iam.disableServiceAccountKeyCreation唯一能讓「金鑰外洩」這個劇本從源頭不存在的組織政策;例外專案再用 iam.serviceAccountKeyExpiryHours 補。
- 04instances.insert全案最值得設即時告警的一個方法名。9/3 12:40 那一秒,五筆請求同時出現在四個地區。
- 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。
今天就可以來試試
全部唯讀,不改任何設定;做完你會知道自己有沒有同樣的破口。
- 列出所有掛著 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 天。
每篇新文章一封:三點摘要、攻擊者/防守者視角、今天能做的一件事。或先 加入會員 記錄你完成的任務。