我們寫了一道關卡防自己,
第二次部署它就亮了紅燈

速讀:5 分鐘掌握重點

為什麼現在很紅:這不是外部新聞,是我們自己今天踩到的:一支只該讀寫試算表的服務帳戶,在專案層是 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 點
  1. 部署腳本第 6 步會列出這個帳戶在專案層的角色,要求必須為空。今天印出 roles/owner:帳戶能開 VM、改防火牆、叫 AI、建金鑰。同一個破口形狀,5 天前才付過帳單。
  2. 拿掉是一行指令,proxy 不受影響。拿掉前要做的只有一件事:查稽核記錄,確認過去 30 天沒有別的程式用這個帳戶呼叫 Drive/Sheets 以外的 API。

沒有攻擊、沒有外洩、沒有帳單,這是一份設定稽核紀錄:一支只該讀寫試算表的 GCP:Service Account,在專案層是 Owner。拿掉只要一行,拿掉前要先查什麼?

自家 sheets-proxy 服務帳戶掛著專案 Owner(未出事)·2026-09-16 部署 → 2026-09-17 重部署時發現·全文 6 分鐘
本篇目錄

一支只該讀寫試算表的帳戶,在專案層是 Owner

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

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

昨天晚上重新部署時,它被自己的部署腳本抓到:這個帳戶在專案層綁著 roles/owner。先講清楚這篇的性質,沒有攻擊、沒有外洩、沒有帳單,這是一份寫在處置完成之前的設定稽核紀錄。距離上一次因為同樣形狀真的付過錢,才過了五天。

  1. 2026-09-12
    Owner 金鑰案:一支只寫試算表的 Bot 帳戶是 Owner,金鑰外洩 15 天,付了帳單也被停權 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 金鑰案的復盤之後,我們把一件事寫進了部署腳本:第 6 步查這個帳戶在專案層有什麼角色,必須為空。昨晚為了把新的共用雲端硬碟加進白名單而重新部署,第 6 步印出了兩個字。

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

腳本亮紅燈的第一反應不該是相信它,而是再查一次。用 gcloud projects get-iam-policy 直接確認,兩個來源一致。接下來的判斷都建立在四條線索上:三條能證明,一條只能推論,還有一條是動手前必須先補的缺口。

三條能證明的說的是同一件事:這個角色存在,而且沒有存在的理由。推論那條解釋它為什麼會在;缺口那條決定我們能多快把它拿掉。

這次沒有攻擊者,但路線圖五天前就跑過一次

下面這條鏈,本案一個階段都沒有發生,所以每一格都標問號。它不是憑空推測的:除了第一步的入口不同,其餘每一步 INC-2026-0827 都實際跑過一遍。

發生了什麼 · MITRE ATT&CK T1190
若 proxy 或 Cloud Run 上出現一個能讓它替別人發請求的洞(SSRF、環境變數被讀出來),攻擊者就有了一條進來的路。本案沒有發生,也沒有任何跡象。
當時留下的訊號
Cloud Run 請求日誌出現不屬於白名單用途的路徑或參數。
能擋下它的控制
端點一律要 Bearer token;使用者輸入不得決定 proxy 要連哪個位址。

把兩件事並排,差別只有一個條件。

Owner 金鑰案(INC-2026-0827)今天
帳戶的工作LINE Bot 讀寫試算表proxy 讀寫試算表與 Drive
實際權限roles/ownerroles/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 綁定,讓「誰在專案層有角色」變成一份可追蹤的清單,而不是每次重新部署才發現一次。

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

這次沒有損失,所以這張表真正在比的是兩個數字:拿掉一個角色要花多少,以及沒拿掉的那一次花了多少。

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 帳單。「舉證」欄標明每筆損失的證據來源與等級( 可證明 推論 缺口)。

第一列免費、第三列有收據、第四列答不出來——這三列放在一起,就是「今天做」與「事後做」的價差。

你可以學到什麼

lessons本案的 6 個教訓
  1. 01
    讀寫試算表的帳戶不需要任何專案角色。Drive 與 Sheets 的授權來自檔案分享,不查專案 IAM。
  2. 02
    沒有金鑰檔不等於安全。Owner 讓一小時的短期 token 足夠開完 VM、改防火牆,再幫自己建一把永久金鑰。
  3. 03
    檢查要能中止流程才算檢查。印在畫面上沒有人看,等於沒有檢查。
  4. 04
    同一個破口形狀,5 天前才付過 US$146.10 與停權 3 天。形狀比事件本身更值得記。
  5. 05
    拿掉權限之前先查稽核記錄。不知道誰在用就拔,換來的是另一場事故。
  6. 06
    第一道防線(不發金鑰)做好了,不代表第二道(外洩了也沒權限)可以省。
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. 若負責人已經移除綁定,用這個指令確認 proxy 還活著(移除本身是前一節的當天工作,不在這裡做)
    curl -sS https://PROXY_URL/v1/whoami -H 'Authorization: Bearer TOKEN'
    看到什麼代表什麼:還能回 allowed_folders,代表它本來就不需要那個角色;再跑一次第 1 步,那個帳戶應該從清單裡消失。
想看全部細節?

環境背景、時間線、攻擊鏈對照 ATT&CK、關鍵證據、根因、處置與檢討都在完整版,約 6 分鐘。

來源

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

本文由 AI 團隊協助研究與撰寫,經隱私與事實檢查後發布;證據等級依來源可信度標註。

看完的心情
ai prompt把這篇的切入點帶回你的環境

複製下面這段,貼給你在用的 AI 工具(Claude Code、Cursor、Copilot、ChatGPT 都可以)。它會用本篇的破口位置、自我檢測與學習重點,對你的專案或環境做一次資安掃描,列出潛在問題與建議的調整或加強方式。

預設是唯讀:只發現問題、提出建議,不會改你任何檔案或設定。

§
對應課程模組

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

quiz · 資安意識積分
讀完了?1 分鐘重點回饋,累積資安意識積分

4 題單選,答對每題 +10 分、全對再 +10 分。加入 LINE 官方帳號、在 LINE 裡作答,成績與積分直接存入;在網頁作答會拿到一組序號,傳到 LINE 就能累積。

1 分鐘重點回饋 →
newsletter
下一篇拆解直接寄到信箱

每篇新文章一封:三點摘要、關鍵字定義、今天能做的一件事。或先 加入會員 記錄你完成的任務。