我們寫了一道關卡防自己,
第二次部署它就亮了紅燈
為什麼現在很紅:這不是外部新聞,是我們自己今天踩到的:一支只該讀寫試算表的服務帳戶,在專案層是 Owner。距離上一次因為同樣形狀燒掉 US$146、被停權 3 天,才過了 5 天。
- 一句話結論
- sheets-proxy 以一個 GCP:Service Account 的身分在 Cloud Run 上跑,靠自我模擬換出只有 Drive/Sheets 範圍的短期 token;它需要的權限全部來自「檔案有沒有分享給它」,不需要任何專案層角色。
- 他要的是什麼
- 他不需要你的 proxy 有漏洞。只要這個帳戶的任何一份憑證出現在任何地方,他拿到的就是整個專案,不是幾張試算表。
- 你今天做一件事
- 今天就列出所有掛 Owner 或 Editor 的服務帳戶。每一個都問同一句:它的工作需要專案角色嗎?答案幾乎永遠是不需要。 直接去做 →
- 哪些能信
- 3 可證明1 推論1 缺口 每個主張都標了等級,缺口是誠實寫出來的。
- 破口速查
- 受害端一律去識別化;寫「未公開」代表公開資料查不到。
- 伺服器服務商
- GCP Cloud Run(sheets-proxy 以服務帳戶身分執行,沒有金鑰檔,只有記憶體裡的短期 token)
- 網路服務商
- Google Cloud(Cloud Run 提供的 HTTPS 端點,呼叫要帶 Bearer token)
- 漏洞出現的地方
- 身分層(尚未出事):只需要檔案分享權限的 proxy 服務帳戶,在專案層被綁了 roles/owner
- 馬上自我檢測
gcloud projects get-iam-policy PROJECT_ID --flatten='bindings[].members' --filter='bindings.members:serviceAccount AND (bindings.role:roles/owner OR bindings.role:roles/editor)' --format='table(bindings.role,bindings.members)'- 漏洞修補方式
- 先查 30 天稽核記錄確認沒有別的程式用這個帳戶呼叫 Drive/Sheets 以外的 API,再移除它的專案層角色;權限只留檔案分享;把「專案 IAM 必須為空」寫進部署腳本當關卡
有 15 分鐘再看:其餘 2 點
- 部署腳本第 6 步會列出這個帳戶在專案層的角色,要求必須為空。今天印出 roles/owner:帳戶能開 VM、改防火牆、叫 AI、建金鑰。同一個破口形狀,5 天前才付過帳單。
- 拿掉是一行指令,proxy 不受影響。拿掉前要做的只有一件事:查稽核記錄,確認過去 30 天沒有別的程式用這個帳戶呼叫 Drive/Sheets 以外的 API。
沒有攻擊、沒有外洩、沒有帳單,這是一份設定稽核紀錄:一支只該讀寫試算表的 GCP:Service Account,在專案層是 Owner。拿掉只要一行,拿掉前要先查什麼?
本篇目錄
一支只該讀寫試算表的帳戶,在專案層是 Owner
把 GCP 專案想成一棟辦公大樓。檔案分享是「哪幾間會議室的門卡給了你」;專案層 IAM 角色則是「你在整棟大樓的身分」。roles/owner 是大樓管理員:每一扇門都開得了,還能發門卡給別人、裝新的門。
我們的 sheets-proxy 是一支跑在 Cloud Run 上的小程式,替各個應用讀寫 Google 試算表與 Drive,讓應用端不必持有任何 Google 私鑰。它用一個 GCP:Service Account 的身分執行,需要的只有幾間會議室的門卡——白名單裡的試算表和資料夾分享給它就夠了。它不需要在大樓裡有任何身分。
昨天晚上重新部署時,它被自己的部署腳本抓到:這個帳戶在專案層綁著 roles/owner。先講清楚這篇的性質,沒有攻擊、沒有外洩、沒有帳單,這是一份寫在處置完成之前的設定稽核紀錄。距離上一次因為同樣形狀真的付過錢,才過了五天。
- 2026-09-12Owner 金鑰案:一支只寫試算表的 Bot 帳戶是 Owner,金鑰外洩 15 天,付了帳單也被停權 3 天本站防暴筆記 INC-2026-0827。
- 2026-09-13 → 14建 sheets-proxy,應用端不再持有 Google 私鑰;部署腳本加入「專案 IAM 必須為空」檢查SOP repo 的 deploy.sh 與部署提示。
- 2026-09-16這支 proxy 首次部署,供活動網站上傳圖片變更紀錄。
- 2026-09-17 21:4x為了新的共用雲端硬碟重新部署,第 6 步印出 roles/owner;gcloud 確認部署輸出與 gcloud projects get-iam-policy。
- 不明這個 Owner 角色是什麼時候、為了什麼被綁上去的尚未查稽核記錄。
這次沒有東西外洩,但把這個帳戶手上的憑證攤開來,能看出一件反直覺的事。
| 服務 | 憑證類型 | 用途 | 存在形式 | 放在哪 | 權限範圍 | 狀態 |
|---|---|---|---|---|---|---|
| GCP IAM 服務帳戶(sheets-proxy 的執行身分) | 沒有金鑰檔;只有 Cloud Run 附加身分換出的短期 access token | 以自我模擬取得 Drive/Sheets 範圍 token,代替各應用讀寫白名單內的試算表與資料夾 | 短期 OAuth access token(記憶體內,約 1 小時,不落地);本案沒有 JSON 金鑰 | 只存在 Cloud Run 執行個體記憶體 | 應該只有檔案層分享;實際上還有專案層 roles/owner | 未外洩。Owner 角色待移除 |
注意「存在形式」那一欄:這個帳戶本來就沒有金鑰檔,攻擊者撿不到 JSON。這是好消息,卻也正是它更不該有 Owner 的理由——唯一能冒用它的方式是攻破 proxy 本身或 Cloud Run,而一旦攻破,Owner 讓損害從「幾張試算表」變成「整個專案」。
抓到它的不是人,是部署腳本寫死的一行檢查
做完 Owner 金鑰案的復盤之後,我們把一件事寫進了部署腳本:第 6 步查這個帳戶在專案層有什麼角色,必須為空。昨晚為了把新的共用雲端硬碟加進白名單而重新部署,第 6 步印出了兩個字。
==> 6. 專案 IAM 是否仍乾淨(必須為空) roles/owner
腳本亮紅燈的第一反應不該是相信它,而是再查一次。用 gcloud projects get-iam-policy 直接確認,兩個來源一致。接下來的判斷都建立在四條線索上:三條能證明,一條只能推論,還有一條是動手前必須先補的缺口。
三條能證明的說的是同一件事:這個角色存在,而且沒有存在的理由。推論那條解釋它為什麼會在;缺口那條決定我們能多快把它拿掉。
這次沒有攻擊者,但路線圖五天前就跑過一次
下面這條鏈,本案一個階段都沒有發生,所以每一格都標問號。它不是憑空推測的:除了第一步的入口不同,其餘每一步 INC-2026-0827 都實際跑過一遍。
把兩件事並排,差別只有一個條件。
| Owner 金鑰案(INC-2026-0827) | 今天 | |
|---|---|---|
| 帳戶的工作 | LINE Bot 讀寫試算表 | proxy 讀寫試算表與 Drive |
| 實際權限 | roles/owner | roles/owner |
| 憑證形式 | Google Service KEY JSON,6 年沒換,散在 repo 與部署平台 | 沒有金鑰檔,只有短期 token |
| 被冒用的途徑 | 金鑰外洩 | 要攻破 proxy 或 Cloud Run |
| 結果 | 15 天,付了帳單也被停權 | 還沒發生 |
Owner 金鑰案教的是第一道防線「不發金鑰」;今天這件事教的是第二道,外洩了也要沒權限。第一道做好了,不代表第二道可以省。
我們怎麼處理:先查稽核記錄,再拿掉那一行綁定
偵測在昨晚 21 點多就結束了:腳本第 6 步印出 roles/owner,gcloud projects get-iam-policy 複查一致,確認不是腳本誤判。
接著是隔離,而這一步意外地輕鬆——因為根本沒有東西需要隔離。帳戶沒有金鑰檔可以撤銷,憑證只有 Cloud Run 執行個體記憶體裡約一小時的短期 token,也沒有任何外洩跡象。這份輕鬆不是運氣,是第一道防線當初做對了的紅利。
真正的清除動作還沒做,因為它有一個前提:先查 30 天稽核記錄,確認沒有別的程式用這個帳戶呼叫 Drive/Sheets 以外的 API。不知道誰在用就直接拔,換來的會是另一場事故。確認之後才移除專案層的 roles/owner 綁定。
最後的驗收條件也已經寫好:重跑部署腳本第 6 步,輸出必須回到空的;再打一次 proxy 的 whoami,確認它本來就不需要那個角色。這兩件事沒做完,這條待辦不算結案。
這次做對了三件,漏了四件
做對的第一件,是把復盤結論寫進部署腳本而不是待辦清單——待辦清單會過期,腳本不會。第二件是腳本亮紅燈後沒有直接相信它,而是用 gcloud 再查一次。第三件從更早就開始了:proxy 從設計就不發金鑰檔,靠自我模擬換短期 token,應用端不持有任何 Google 私鑰。還有一個比較不像「做對」的做對:發現之後沒有為了乾淨直接拔掉角色,而是先問「有沒有別人在用」。
漏掉的四件比較刺眼。9/16 第一次部署時,第 6 步的輸出印在畫面上,卻沒有人看——一個只會提醒、不會中止的檢查,跟沒有檢查的差別只在心理安慰。這個角色是誰、什麼時候、為什麼綁上去的,到現在還沒查 SetIamPolicy 稽核記錄。沒有對 SetIamPolicy 設記錄型警示,所以下次有人順手給 Owner,一樣要等下一次部署才會知道。而同樣的檢查目前只存在這一支部署腳本裡,其他服務都沒有。
四件漏掉的事裡有三件是同一個病根:我們把「檢查」做成了輸出,沒做成關卡。
如何避免
當天就能做完的有三件。先列出專案裡所有掛 roles/owner 或 roles/editor 的服務帳戶,每一個都問同一句:它的工作需要專案角色嗎。再查 30 天稽核記錄,確認這個帳戶沒有呼叫過 Drive/Sheets 以外的 API。確認之後才移除綁定,然後重跑部署腳本第 6 步,輸出必須是空的。
這幾週要補的是把檢查變成關卡:把「專案 IAM 必須為空」放進每一支部署腳本,而且不為空就中止部署,不是印出來就算;對 SetIamPolicy 設記錄型警示(我們把它取名 alert-setiampolicy-owner-s2,之後翻警示清單一眼就對得上這篇),讓下次有人順手給 Owner 當天就知道;順便查出這個 Owner 是哪一天、被誰綁上去的,把原因記進變更紀錄。
這一季之後的方向是讓預設值站在自己這邊:新建服務帳戶一律不給專案層角色,需要什麼再逐項加;把四道防線的第二道寫進 SOP,也就是假設憑證會外洩、外洩了也要沒權限;並定期盤點所有專案的 IAM 綁定,讓「誰在專案層有角色」變成一份可追蹤的清單,而不是每次重新部署才發現一次。
這篇提到的項目要花多少錢
這次沒有損失,所以這張表真正在比的是兩個數字:拿掉一個角色要花多少,以及沒拿掉的那一次花了多少。
| 項目 | 用量 | 單價 | 期間 | US$ | NT$ | 舉證 |
|---|---|---|---|---|---|---|
| 移除 roles/owner 綁定 | 1 條 gcloud 指令 | 免費 | 一次 | 0.00 | 0 | IAM 變更不計費 |
| sheets-proxy Cloud Run 估;目前流量落在免費額度附近 | 1 支,1Gi、最多 5 個執行個體 | 依請求與 CPU 秒計費 | 每月 | 5.00 | 160 | Cloud Run 牌價;帳單未單獨拆出 |
| 同一形狀上次的直接損失 | 3 台 VM、AI 呼叫 | — | 15 天 | 146.10 | 4,675 | 本站防暴筆記 INC-2026-0827 帳單報表 |
| 同一形狀若在這個專案發生 | 不明:這個專案還掛著其他服務 | — | — | — | — | 無法在事前量化 |
| 合計(僅可證明列) | 146.10 | 4,675 | ||||
第一列免費、第三列有收據、第四列答不出來——這三列放在一起,就是「今天做」與「事後做」的價差。
你可以學到什麼
- 01讀寫試算表的帳戶不需要任何專案角色。Drive 與 Sheets 的授權來自檔案分享,不查專案 IAM。
- 02沒有金鑰檔不等於安全。Owner 讓一小時的短期 token 足夠開完 VM、改防火牆,再幫自己建一把永久金鑰。
- 03檢查要能中止流程才算檢查。印在畫面上沒有人看,等於沒有檢查。
- 04同一個破口形狀,5 天前才付過 US$146.10 與停權 3 天。形狀比事件本身更值得記。
- 05拿掉權限之前先查稽核記錄。不知道誰在用就拔,換來的是另一場事故。
- 06第一道防線(不發金鑰)做好了,不代表第二道(外洩了也沒權限)可以省。
- 01roles/owner基本角色三個之最,什麼都能做。服務帳戶掛著它,帳戶被冒用等於專案被拿走。
- 02檔案分享 vs 專案 IAMDrive/Sheets 的授權來自檔案分享,不查專案角色。讀寫試算表的帳戶不需要任何專案角色。
- 03自我模擬(impersonation)proxy 靠它換出只有 drive/spreadsheets 範圍的短期 token,沒有金鑰檔可外洩。
- 04短期 token 的存在形式只在記憶體、約一小時;比 KEY JSON 安全,但 Owner 讓一小時足夠開完 VM。
- 05deploy.sh 第 6 步把「必須為空」寫進腳本,才會在重部署時被自己抓到。人不會每次記得看主控台。
- 06gcloud projects get-iam-policy一行就列出誰在專案層有什麼角色;用 --filter 只看 serviceAccount。
- 07remove-iam-policy-binding拿掉角色的那一行指令;先確認再跑。
- 08SetIamPolicy 稽核記錄誰、何時、為什麼給了 Owner,只有這裡查得到;也是該設警示的方法名。
- 09四道防線的第二道外洩了也沒權限。第一道(不發金鑰)做好了,不代表第二道可以省。
- 10先查再拿拿掉角色前查 30 天稽核記錄,確認沒有別的程式靠這個帳戶做 Drive 以外的事。
這篇的結論其實只有一句:你不必等到出事,才知道自己有沒有同一個形狀。下面四步全部唯讀,十五分鐘就能問完自己。
全部唯讀,不改任何設定;做完你會知道自己有沒有同樣的破口。
- 列出專案裡掛 Owner 或 Editor 的服務帳戶
gcloud projects get-iam-policy PROJECT_ID --flatten='bindings[].members' \ --filter='bindings.members:serviceAccount AND (bindings.role:roles/owner OR bindings.role:roles/editor)' \ --format='table(bindings.role,bindings.members)'看到什麼代表什麼:每一筆都問「它的工作需要專案角色嗎」。讀寫試算表、上傳 Drive、寄信、跑排程,答案都是不需要。 - 看這個帳戶 30 天內用了哪些 API(Drive/Sheets 不會出現在這裡,因為它們不寫專案稽核記錄)
gcloud logging read 'protoPayload.authenticationInfo.principalEmail="SA_EMAIL"' \ --project=PROJECT_ID --freshness=30d --format='value(protoPayload.serviceName,protoPayload.methodName)' | sort | uniq -c看到什麼代表什麼:空的代表它只做 Drive/Sheets;有 compute、iam、serviceusage 之類的,就要先弄清楚是誰在用再拿角色。 - 查 Owner 是誰、何時給的
gcloud logging read 'protoPayload.methodName="SetIamPolicy"' --project=PROJECT_ID --freshness=400d \ --format='table(timestamp,protoPayload.authenticationInfo.principalEmail)'看到什麼代表什麼:找到那一天,就知道是「先讓它能跑」還是別的原因。 - 若負責人已經移除綁定,用這個指令確認 proxy 還活著(移除本身是前一節的當天工作,不在這裡做)
curl -sS https://PROXY_URL/v1/whoami -H 'Authorization: Bearer TOKEN'看到什麼代表什麼:還能回 allowed_folders,代表它本來就不需要那個角色;再跑一次第 1 步,那個帳戶應該從清單裡消失。
環境背景、時間線、攻擊鏈對照 ATT&CK、關鍵證據、根因、處置與檢討都在完整版,約 6 分鐘。
來源
- Google Cloud:Basic roles(Owner/Editor/Viewer 的範圍)
- Google Cloud:Service account impersonation(自我模擬取得短期 token)
- 本站防暴筆記:排程回報「找不到帳戶」,被停權的是整個專案
本文為公開報導與官方公告的整理與拆解,非第一手鑑識;證據等級依來源可信度標註。
本文由 AI 團隊協助研究與撰寫,經隱私與事實檢查後發布;證據等級依來源可信度標註。
複製下面這段,貼給你在用的 AI 工具(Claude Code、Cursor、Copilot、ChatGPT 都可以)。它會用本篇的破口位置、自我檢測與學習重點,對你的專案或環境做一次資安掃描,列出潛在問題與建議的調整或加強方式。
預設是唯讀:只發現問題、提出建議,不會改你任何檔案或設定。
M2 身分與金鑰 · M3 雲端層偵測 · 想在自己的環境做一次同樣的盤點,從 自我盤點 開始。
4 題單選,答對每題 +10 分、全對再 +10 分。加入 LINE 官方帳號、在 LINE 裡作答,成績與積分直接存入;在網頁作答會拿到一組序號,傳到 LINE 就能累積。
1 分鐘重點回饋 →每篇新文章一封:三點摘要、關鍵字定義、今天能做的一件事。或先 加入會員 記錄你完成的任務。