DISPATCH-2026-0917Bmedium自家 sheets-proxy 服務帳戶掛著專案 Owner(未出事)

部署腳本的最後一行檢查, 抓到我們自己的服務帳戶是 Owner

做完 Owner 金鑰案的復盤後,我們把「專案 IAM 必須為空」寫進了 proxy 的部署腳本。今天重新部署,它印出 roles/owner。這篇講它為什麼是紅燈、為什麼 proxy 根本不需要專案角色、拿掉要先確認什麼。

為什麼現在很紅:這不是外部新聞,是我們自己今天踩到的:一支只該讀寫試算表的服務帳戶,在專案層是 Owner。距離上一次因為同樣形狀燒掉 US$146、被停權 3 天,才過了 5 天。

事件期間 2026-09-16 部署 → 2026-09-17 重部署時發現·整理於 2026-09-17·6 分鐘·3 可證明1 推論1 缺口
GCPIAM服務帳戶最小權限sheets-proxy
tl;dr
  1. sheets-proxy 以一個 GCP:Service Account 的身分在 Cloud Run 上跑,靠自我模擬換出只有 Drive/Sheets 範圍的短期 token;它需要的權限全部來自「檔案有沒有分享給它」,不需要任何專案層角色。
  2. 部署腳本第 6 步會列出這個帳戶在專案層的角色,要求必須為空。今天印出 roles/owner:帳戶能開 VM、改防火牆、叫 AI、建金鑰。同一個破口形狀,5 天前才付過帳單。
  3. 拿掉是一行指令,proxy 不受影響。拿掉前要做的只有一件事:查稽核記錄,確認過去 30 天沒有別的程式用這個帳戶呼叫 Drive/Sheets 以外的 API。
◉ 攻擊者視角他不需要你的 proxy 有漏洞。只要這個帳戶的任何一份憑證出現在任何地方,他拿到的就是整個專案,不是幾張試算表。
◎ 防守者視角今天就列出所有掛 Owner 或 Editor 的服務帳戶。每一個都問同一句:它的工作需要專案角色嗎?答案幾乎永遠是不需要。
本篇目錄

發生了什麼

把 GCP 專案想成一棟辦公大樓。檔案分享是「哪幾間會議室的門卡給了你」;專案層 IAM 角色是「你在整棟大樓的身分」。roles/owner 是大樓管理員:每一扇門都開得了,還能發門卡給別人、裝新的門。

我們的 sheets-proxy 是一支跑在 Cloud Run 上的小程式,替各個應用讀寫 Google 試算表與 Drive,讓應用端不必持有任何 Google 私鑰。它用一個 GCP:Service Account 的身分執行,需要的只有幾間會議室的門卡:白名單裡的試算表和資料夾分享給它就夠了。它不需要在大樓裡有任何身分。

做完 Owner 金鑰案的復盤後,我們把這件事寫進了部署腳本:第 6 步查這個帳戶在專案層有什麼角色,必須為空。今天為了把新的共用雲端硬碟加進白名單而重新部署,第 6 步印出:

deploy.sh 第 6 步(節錄)READ-ONLY
==> 6. 專案 IAM 是否仍乾淨(必須為空)
roles/owner

再用 gcloud 直接查一次,確認不是腳本誤判:這個帳戶在專案層確實綁著 roles/owner

  1. 2026-09-12
    Owner 金鑰案:一支只寫試算表的 Bot 帳戶是 Owner,金鑰外洩 15 天,US$146.10、停權 3 天
    本站防暴筆記 INC-2026-0827。
  2. 2026-09-13 → 14
    建 sheets-proxy,應用端不再持有 Google 私鑰;部署腳本加入「專案 IAM 必須為空」檢查
    SOP repo 的 deploy.sh 與部署提示。
  3. 2026-09-16
    這支 proxy 首次部署,供活動網站上傳圖片
    變更紀錄。
  4. 2026-09-17 21:4x
    為了新的共用雲端硬碟重新部署,第 6 步印出 roles/owner;gcloud 確認
    部署輸出與 gcloud projects get-iam-policy。
  5. 不明
    這個 Owner 角色是什麼時候、為了什麼被綁上去的
    尚未查稽核記錄。

這次外洩了什麼

leaked credentials這次外洩了什麼:服務、用途、存在形式
服務憑證類型用途存在形式放在哪權限範圍狀態
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 做什麼,是它以誰的身分跑。
  • 找到一個能讓 proxy 替他發請求的洞(SSRF、token 外洩、Cloud Run 環境變數被讀):拿到的是 Owner 級的短期 token。
  • 一小時的 token 夠他做完 Owner 金鑰案的前四步:接受模型條款、開防火牆、開 VM、掛開機腳本。
  • 然後用 Owner 替自己建一個新的服務帳戶金鑰,把一小時變成永久。

這篇提到的項目要花多少錢

cost拿掉一個角色的成本,對照沒拿掉的代價
項目用量單價期間US$NT$舉證
移除 roles/owner 綁定1 條 gcloud 指令免費一次0.000IAM 變更不計費
sheets-proxy Cloud Run
估;目前流量落在免費額度附近
1 支,1Gi、最多 5 個執行個體依請求與 CPU 秒計費每月5.00160Cloud Run 牌價;帳單未單獨拆出
同一形狀上次的直接損失3 台 VM、AI 呼叫15 天146.104,675本站防暴筆記 INC-2026-0827 帳單報表
同一形狀若在這個專案發生不明:這個專案還掛著其他服務無法在事前量化
合計(僅可證明列)146.104,675
匯率假設 1 USD ≈ NT$32。單價為公開牌價或帳單實付,來源:GCP 牌價 2026-09;本站防暴筆記 INC-2026-0827 帳單。「舉證」欄標明每筆損失的證據來源與等級( 可證明 推論 缺口)。

重點學習

keywords重點學習:最需要學習的 10 個關鍵字10
  1. 01
    roles/owner
    基本角色三個之最,什麼都能做。服務帳戶掛著它,帳戶被冒用等於專案被拿走。
  2. 02
    檔案分享 vs 專案 IAM
    Drive/Sheets 的授權來自檔案分享,不查專案角色。讀寫試算表的帳戶不需要任何專案角色。
  3. 03
    自我模擬(impersonation)
    proxy 靠它換出只有 drive/spreadsheets 範圍的短期 token,沒有金鑰檔可外洩。
  4. 04
    短期 token 的存在形式
    只在記憶體、約一小時;比 KEY JSON 安全,但 Owner 讓一小時足夠開完 VM。
  5. 05
    deploy.sh 第 6 步
    把「必須為空」寫進腳本,才會在重部署時被自己抓到。人不會每次記得看主控台。
  6. 06
    gcloud projects get-iam-policy
    一行就列出誰在專案層有什麼角色;用 --filter 只看 serviceAccount。
  7. 07
    remove-iam-policy-binding
    拿掉角色的那一行指令;先確認再跑。
  8. 08
    SetIamPolicy 稽核記錄
    誰、何時、為什麼給了 Owner,只有這裡查得到;也是該設警示的方法名。
  9. 09
    四道防線的第二道
    外洩了也沒權限。第一道(不發金鑰)做好了,不代表第二道可以省。
  10. 10
    先查再拿
    拿掉角色前查 30 天稽核記錄,確認沒有別的程式靠這個帳戶做 Drive 以外的事。

今天就可以來試試

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

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

  1. 列出專案裡掛 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、寄信、跑排程,答案都是不需要。
  2. 看這個帳戶 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 之類的,就要先弄清楚是誰在用再拿角色。
  3. 查 Owner 是誰、何時給的
    gcloud logging read 'protoPayload.methodName="SetIamPolicy"' --project=PROJECT_ID --freshness=400d \
      --format='table(timestamp,protoPayload.authenticationInfo.principalEmail)'
    看到什麼代表什麼:找到那一天,就知道是「先讓它能跑」還是別的原因。
  4. 拿掉(確認前兩步都沒有意外之後)
    gcloud projects remove-iam-policy-binding PROJECT_ID \
      --member='serviceAccount:SA_EMAIL' --role='roles/owner'
    看到什麼代表什麼:這一步會改設定,不是唯讀;跑完立刻重跑你的部署檢查(或第 1 步),應該變成空的。
  5. 確認 proxy 還活著
    curl -sS https://PROXY_URL/v1/whoami -H 'Authorization: Bearer TOKEN'
    看到什麼代表什麼:還能回 allowed_folders,代表它本來就不需要那個角色。

來源

本文為公開報導與官方公告的整理與拆解,非第一手鑑識;證據等級依來源可信度標註。

§
對應課程模組

M2 身分與金鑰 · M3 雲端層偵測 · 想在自己的環境做一次同樣的盤點,從 自我盤點 開始。

newsletter
下一篇拆解直接寄到信箱

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