INC-2026-0827highGCP 服務帳戶金鑰外洩

一把 Owner 金鑰, 被用了 15 天

一支 LINE Bot 用來讀寫試算表的服務帳戶金鑰,放了六年沒換,而且帳戶本身是專案 Owner。金鑰外洩後,攻擊者先盜用 AI 模型、再開 VM,直到 Google 停權整個專案我們才發現。

事件期間 2026-08-28 → 09-12·整理於 2026-09-17·12 分鐘·8 可證明2 推論4 缺口
GCPIAM服務帳戶帳單異常MITRE ATT&CK
tl;dr
  1. 金鑰 2020 年建立、以 JSON 檔 commit 進至少兩個 repo,也放進部署平台的環境變數。平台事件後 36 小時內開始被盜用。
  2. 攻擊者第一個動作不是開 VM,是測試能用哪些 AI 模型;接著兩分鐘內開防火牆、同一秒在 4 個區域開 5 台 VM。
  3. 損失 US$146.10,全部可歸因;金額不大是運氣好,不是防禦好。同樣權限換 GPU 機型,15 天能燒掉幾萬美元。
◉ 攻擊者視角他根本不在乎你是誰。一把有 Owner 權限的金鑰,等於把整張變現菜單免費送給他:AI token 轉租、VM 出租、寄信信譽。
◎ 防守者視角讀寫試算表其實不需要任何專案角色,檔案有分享給帳戶就夠了。這把金鑰不該有 Owner,甚至不該存在。
本篇目錄
VMAISheetsIAMroles/owner · 只需要寫試算表

數字先講

15 天
從第一次被盜用(8/28)到 Google 停權(9/12)
roles/owner
服務帳戶的權限;它的工作只需要試算表
US$146.10
盜用造成的費用;此專案平常是 $0
6 年
金鑰從建立到外洩都沒輪替(2020-09 建立)

這次外洩了什麼

主角只有一把:一個 GCP 服務帳戶的 Google Service KEY JSON。它的工作是讓一支訊息機器人讀寫試算表、把照片丟進 Drive,但它掛著 roles/owner。同一次平台事件裡,另外兩類憑證有被列在平台給的清單上,8/29~8/30 就換掉了;沒被列上的,就是這一把。

leaked credentials這次外洩了什麼:服務、用途、存在形式
服務憑證類型用途存在形式放在哪權限範圍狀態
GCP:Service Account(IAM 服務帳戶)Google Service KEY JSON(長期私鑰,無到期日)訊息機器人讀寫試算表、上傳照片到 DriveJSON 檔,內含 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 權限,所以一把原本只用來寫試算表的金鑰,能打開整個專案的所有服務。

發生了什麼 · 推定
金鑰以 JSON 檔形式被 commit 進至少兩個 repo,也可能被放進部署平台的環境變數。8/27 平台發生事件,攻擊者取走了符合憑證格式的環境變數。第一次盜用發生在隔天晚上。
當時留下的訊號
沒有。金鑰在 repo 裡放了好幾年都沒被濫用,卻在平台出事後 36 小時內開始被使用。
能擋下它的控制
不發長期金鑰:改用 Workload Identity 或不需私鑰的代理服務。非發不可就設組織政策讓金鑰自動過期。

通知後金鑰仍有效:17 天

8/27 收到平台通知後,我們依環境變數清單逐一輪替,8/29~8/30 處理了 AWS 金鑰與資料庫密碼。以 JSON 檔形式散落在 repo 裡的 GCP 服務帳戶金鑰,一直到 9/13 才被處理。 從通知到刪除,這把金鑰多活了 17 天,其中 15 天在被使用。

  1. 8/27
    部署平台通知外洩
    依平台列出的環境變數清單開始輪替。
  2. 8/28 23:00
    攻擊者首次使用金鑰(Vertex AI)
  3. 8/29 → 8/30
    我方輪替 AWS 金鑰、DB 密碼
    GCP JSON 金鑰不在清單上,未處理。
  4. 9/3
    防火牆 + 3 台 VM
  5. 9/5 → 9/11
    VM 被大量使用;9/7 起 Gemini 盜用
  6. 9/12
    Google 停權專案
  7. 9/13
    刪除專案內 7 把使用者金鑰、拿掉 5 個服務帳戶的 Owner/Editor
  8. 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 攤開來的每一列。我們刻意不用「大約」:每一筆都對得上帳單頁面的用量與金額。

cost損失舉證:每一列都對得上帳單 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/1239.051,250帳單依 SKU 分組頁面(222 個 SKU 中的第 1 名)
E2 Instance Core(us-east4 區)
帳單實付。同一批機器跨兩區開,單價也不同
895.10 vCPU·小時約 US$0.0246/vCPU·小時9/3 → 9/1221.99704帳單依 SKU 分組頁面
E2 Instance Ram(兩區合計)
帳單實付:Americas 20.93 + Virginia 11.79。e2 機型的 vCPU 與記憶體是分開計價的
10,741.17 GiB·小時隨機型綁定計價9/3 → 9/1232.721,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.89636帳單依 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/1224.80794帳單依 SKU 分組頁面(四列相加)
其餘 212 個 Compute Engine SKU
帳單實付:Compute Engine 小計 143.32 減去上面六列
零碎項目計費期間內4.87156帳單依服務分組(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.839/6 → 9/122.7989帳單依 SKU 分組頁面;Cloud Monitoring request_count 依 credential_id 分組(GenerateContent 200 共 208+3 次)
Networking:負載平衡與位址
帳單實付
原價 2.02優惠全額折抵計費期間內0.000帳單依服務分組頁面
還在跑的磁碟帳單
估:GCP 對 9/15 的預估費用已升到 192.84,多出來的就是這個。鑑識做完前不能刪,所以這是證據保存的價格
3 顆 200 GB pd-balanced,VM 已 TERMINATED 但磁碟未刪約 US$0.10/GB·月 → 600 GB 約 US$60/月刪除前持續60.001,920帳單預估值 192.84(含 9/13~15 預估)+ pd-balanced 公開牌價;實際金額要等帳單結算
事故處理人力
估:取中間值 NT$27,500。帳單 146 元,處理它的人力貴 6 倍
約 1 人日:磁碟鑑識、金鑰輪替、申訴顧問日費 NT$20,000~35,000(估)9/13 → 9/15859.3827,500處置時間軸(9/13 撤金鑰、9/14 申訴、9/15 解封);日費為假設值
攻擊者這一趟的投入
估:邊際成本趨近 0,所以他不需要挑目標
一把撿來的金鑰、一支腳本、幾次 SSH他付 US$0,帳單記在我們頭上15 天0.000稽核記錄顯示全程以外洩金鑰直打 REST API,未動用他自己的運算資源;他的實際支出無從得知
試算表與 Drive 有沒有被讀
只能假設有
服務帳戶是 Owner,可讀所有分享給它的檔案8/28 → 9/12資料存取稽核(Data Access audit logs)沒開,GCP 端無從查證
AI 被拿去生成了什麼
只知道花了多少 token,不知道內容
212 次成功呼叫,文字與圖片都送9/6 → 9/12Vertex AI 請求記錄沒開,只有 Monitoring 的次數與 token 計費量
那 230 GiB 送到哪裡去了
推論是被當成代理或跳板轉送流量,但無法證實
目的地含美洲與亞太9/5 → 9/12VPC Flow Logs 沒開;帳單只告訴我們目的地大區,不告訴我們對象
專案停權造成的業務中斷
本站不估算營收損失
排程全部停擺約 3 天9/12 → 9/15停權與解封時間可證,但中斷金額沒有計算基礎
合計(僅可證明列)146.114,676
匯率假設 1 USD ≈ NT$32。單價為公開牌價或帳單實付,來源:GCP 帳單報表(依服務/依 SKU 分組)截圖 2026-09-15 17:55(台北時間);Cloud Monitoring serviceruntime request_count 依 credential_id 分組;Cloud Audit Logs 14 筆。「舉證」欄標明每筆損失的證據來源與等級( 可證明 推論 缺口)。

八列可證明的加起來是 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.resourceLocationscompute.vmExternalIpAccessvertexai.allowedModels | | 4. 出事馬上知道 | 每個專案設預算警示;對高風險動作設記錄型警示;開資料存取稽核與 VPC Flow Logs | gcloud billing budgets create;警示條件 methodName=~"instances.insert|firewalls.insert|SetIamPolicy|CreateServiceAccountKey|AcceptPublisherModelEula" |

另外補一條流程面的:任何平台通知外洩時,24 小時內以「身分」為單位輪替全部憑證,包含散落在 repo 和本機的 JSON 金鑰檔,而不是只照平台列出的環境變數清單。

他第一個動作是找 AI 模型,不是開 VM。
  • AI API token 有現成黑市,轉租算力給第三方是即時變現。Claude 全部 404/429 才退而求其次用 Gemini。
  • 開 VM 刻意不掛服務帳戶:一來 VM 被查也偷不到 GCP 內部權限,二來大幅減少稽核足跡。
  • 同一秒在 4 個區域開 5 台,某區資源不足或被封也有別台能用。e2-standard-4 便宜又不會被中途回收。
  • 9/8 起每小時列一次 VM 清單,他在確認機器還活著。這是自動化的巡檢,不是人。

附錄:鑑識怎麼查(全部唯讀)

gcloud logging readREAD-ONLY
# 誰開了 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_idresource.label.method 分組。主控台的 API 用量頁看不到依憑證的分組。VM 流量曲線用 compute.googleapis.com/instance/network/received_bytes_countsent_bytes_count,每 6 小時 ALIGN_SUM。

重點學習

keywords重點學習:最需要學習的 10 個關鍵字10
  1. 01
    roles/owner
    本案唯一的放大器。沒有它,一把外洩金鑰只能讀幾張試算表;有了它,攻擊者拿到的是整張變現菜單。
  2. 02
    服務帳戶金鑰(JSON 私鑰)
    它沒有到期日。2020 年發的那一把,2026 年一樣能簽出 access token,而且你不會收到任何通知。
  3. 03
    iam.disableServiceAccountKeyCreation
    唯一能讓「金鑰外洩」這個劇本從源頭不存在的組織政策;例外專案再用 iam.serviceAccountKeyExpiryHours 補。
  4. 04
    instances.insert
    全案最值得設即時告警的一個方法名。9/3 12:40 那一秒,五筆請求同時出現在四個地區。
  5. 05
    Cloud Audit Logs(Admin Activity)
    預設就開、保留 400 天、不用錢。本案能還原到「哪一把金鑰在哪一秒從哪個 IP 做了什麼」,全靠它。
  6. 06
    callerIp 與 serviceAccountKeyName
    稽核記錄裡判斷「這是不是我們」的兩個欄位。先知道它們叫什麼,出事時才查得動。
  7. 07
    credential_id(api/request_count)
    依憑證分組看 API 用量,是唯一能回答「哪一把金鑰在被誰用」的角度;主控台的 API 用量頁看不到。
  8. 08
    預算警示(budgets)
    平常 $0 的專案 9/4 那天花了 $13。這是最便宜、最不需要資安知識就能設的一道線。
  9. 09
    資料存取稽核(Data Access audit logs)
    沒開,所以「試算表有沒有被讀」這題永遠無解。它要錢,但它決定你事後能不能給客戶一個答案。
  10. 10
    VPC Flow Logs
    沒開,所以 230 GiB 送到哪裡去也無解。如果流出去的是違法內容,被追的是你的 IP。

今天就可以來試試

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

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

  1. 列出所有掛著 Owner 或 Editor 的服務帳戶。
    gcloud asset search-all-iam-policies \
      --query='policy:(roles/owner OR roles/editor) memberTypes:serviceAccount' \
      --format='table(resource, policy.bindings.role)'
    看到什麼代表什麼:只要有一列是「只做一件小事」的 Bot 帳戶,你就有跟本案一樣的形狀。零列才是正常。
  2. 挑一個服務帳戶,看它還有幾把使用者建立的金鑰、什麼時候建的。
    gcloud iam service-accounts keys list \
      --iam-account=SA_EMAIL --managed-by=user \
      --format='table(name.basename(), validAfterTime, validBeforeTime)'
    看到什麼代表什麼:validAfterTime 在一年以上、validBeforeTime 是 9999 年,等於一組永不過期又不會被通知的密碼。
  3. 看這個專案到底開了哪些 API。
    gcloud services list --enabled --project=PROJECT
    看到什麼代表什麼:業務上用不到卻開著 compute 或 aiplatform,代表金鑰一旦外洩,攻擊者有現成的變現路徑不必自己開通。
  4. 查最近 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,就不是同事在操作。
  5. 確認有沒有人會在帳單漲起來時被通知。
    gcloud billing budgets list --billing-account=BILLING_ACCOUNT_ID
    看到什麼代表什麼:空的,代表帳單從 $0 漲到 $21 一天都不會有人知道;本案就是這樣過了 9 天。
newsletter
下一篇拆解直接寄到信箱

每篇新文章一封:三點摘要、攻擊者/防守者視角、今天能做的一件事。或先 加入會員 記錄你完成的任務。