部署腳本的最後一行檢查, 抓到我們自己的服務帳戶是 Owner
做完 Owner 金鑰案的復盤後,我們把「專案 IAM 必須為空」寫進了 proxy 的部署腳本。今天重新部署,它印出 roles/owner。這篇講它為什麼是紅燈、為什麼 proxy 根本不需要專案角色、拿掉要先確認什麼。
為什麼現在很紅:這不是外部新聞,是我們自己今天踩到的:一支只該讀寫試算表的服務帳戶,在專案層是 Owner。距離上一次因為同樣形狀燒掉 US$146、被停權 3 天,才過了 5 天。
- sheets-proxy 以一個 GCP:Service Account 的身分在 Cloud Run 上跑,靠自我模擬換出只有 Drive/Sheets 範圍的短期 token;它需要的權限全部來自「檔案有沒有分享給它」,不需要任何專案層角色。
- 部署腳本第 6 步會列出這個帳戶在專案層的角色,要求必須為空。今天印出 roles/owner:帳戶能開 VM、改防火牆、叫 AI、建金鑰。同一個破口形狀,5 天前才付過帳單。
- 拿掉是一行指令,proxy 不受影響。拿掉前要做的只有一件事:查稽核記錄,確認過去 30 天沒有別的程式用這個帳戶呼叫 Drive/Sheets 以外的 API。
本篇目錄
發生了什麼
把 GCP 專案想成一棟辦公大樓。檔案分享是「哪幾間會議室的門卡給了你」;專案層 IAM 角色是「你在整棟大樓的身分」。roles/owner 是大樓管理員:每一扇門都開得了,還能發門卡給別人、裝新的門。
我們的 sheets-proxy 是一支跑在 Cloud Run 上的小程式,替各個應用讀寫 Google 試算表與 Drive,讓應用端不必持有任何 Google 私鑰。它用一個 GCP:Service Account 的身分執行,需要的只有幾間會議室的門卡:白名單裡的試算表和資料夾分享給它就夠了。它不需要在大樓裡有任何身分。
做完 Owner 金鑰案的復盤後,我們把這件事寫進了部署腳本:第 6 步查這個帳戶在專案層有什麼角色,必須為空。今天為了把新的共用雲端硬碟加進白名單而重新部署,第 6 步印出:
==> 6. 專案 IAM 是否仍乾淨(必須為空) roles/owner
再用 gcloud 直接查一次,確認不是腳本誤判:這個帳戶在專案層確實綁著 roles/owner。
- 2026-09-12Owner 金鑰案:一支只寫試算表的 Bot 帳戶是 Owner,金鑰外洩 15 天,US$146.10、停權 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 金鑰案(INC-2026-0827) | 今天 | |---|---|---| | 帳戶的工作 | LINE Bot 讀寫試算表 | proxy 讀寫試算表與 Drive | | 實際權限 | roles/owner | roles/owner | | 憑證形式 | Google Service KEY JSON,6 年沒換,散在 repo 與部署平台 | 沒有金鑰檔,只有短期 token | | 被冒用的途徑 | 金鑰外洩 | 要攻破 proxy 或 Cloud Run | | 結果 | 15 天、US$146.10、停權 3 天 | 還沒發生 |
同樣的破口形狀,只差一個條件。Owner 金鑰案教的是「不發金鑰」,今天教的是四道防線的第二道:外洩了也沒權限。第一道做好了不代表第二道可以省。
攻擊者 vs 防守者
- 找到一個能讓 proxy 替他發請求的洞(SSRF、token 外洩、Cloud Run 環境變數被讀):拿到的是 Owner 級的短期 token。
- 一小時的 token 夠他做完 Owner 金鑰案的前四步:接受模型條款、開防火牆、開 VM、掛開機腳本。
- 然後用 Owner 替自己建一個新的服務帳戶金鑰,把一小時變成永久。
這篇提到的項目要花多少錢
| 項目 | 用量 | 單價 | 期間 | 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 | ||||
重點學習
- 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)'看到什麼代表什麼:找到那一天,就知道是「先讓它能跑」還是別的原因。 - 拿掉(確認前兩步都沒有意外之後)
gcloud projects remove-iam-policy-binding PROJECT_ID \ --member='serviceAccount:SA_EMAIL' --role='roles/owner'看到什麼代表什麼:這一步會改設定,不是唯讀;跑完立刻重跑你的部署檢查(或第 1 步),應該變成空的。 - 確認 proxy 還活著
curl -sS https://PROXY_URL/v1/whoami -H 'Authorization: Bearer TOKEN'看到什麼代表什麼:還能回 allowed_folders,代表它本來就不需要那個角色。
來源
- Google Cloud:Basic roles(Owner/Editor/Viewer 的範圍)
- Google Cloud:Service account impersonation(自我模擬取得短期 token)
- 本站防暴筆記:一把 Owner 金鑰,被用了 15 天
本文為公開報導與官方公告的整理與拆解,非第一手鑑識;證據等級依來源可信度標註。
M2 身分與金鑰 · M3 雲端層偵測 · 想在自己的環境做一次同樣的盤點,從 自我盤點 開始。
每篇新文章一封:三點摘要、攻擊者/防守者視角、今天能做的一件事。或先 加入會員 記錄你完成的任務。