平常不動作的防護機制的部署驗證:設定被接受、元件狀態正常、執行失敗被吞掉與測試從中途注入這些提早出現的成功訊號,驗證查詢與處置指令的假陰性,以及合成觸發本身有損害時的分段驗證
論述基礎與限制
這張卡來自一次 GCP 測試專案上的實機驗證。驗證的對象是 GCP VM 的 SSH 與 RDP 管理通道 與 GCP 上由 MIG 與 GKE 管理的 VM 的管理存取 兩篇教學文章的指令,以及一個依預算通知自動停用帳單的停損開關(kill switch,做法在 GCP 上的雲端費用失控防護)。這三篇的指令後來移到三篇實測紀錄:GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄、GCP 上 MIG 與 GKE 管理存取的實測紀錄、GCP 預算警示與停損開關的實測紀錄,原本的三篇留下機制與判斷依據。單一雲端供應商、單一個工作階段、每個現象各觀察到一到數次,所以卡上記的是這幾個機制在 2026 年 10 月當時的行為,而不是 GCP 的通則;卡要歸納的是驗證方法,不是那些行為本身。
那次驗證裡有七個防護或驗證步驟,事後看都在還沒真正生效、或還沒真正查到的時候,給出了可以結案的訊號:稽核紀錄與停損開關的觸發、權限、重試這四項給的是成功的訊號,防火牆紀錄查詢與資源清單給的是零筆,loginctl terminate-user 回報的是「使用者沒有登入」。
- IAP 的稽核紀錄:照教學文章當時的寫法(之後已改成
ADMIN_READ),在專案的稽核設定為 IAP 開啟「資料讀取」(DATA_READ)類型,設定被接受,之後經 IAP 連了十次左右,紀錄零筆。開通道時檢查的權限iap.tunnelInstances.accessViaIAP歸在「管理員讀取」(ADMIN_READ)類型。 - 停損開關的觸發:Cloud Run functions 的函式部署成功、狀態
ACTIVE,而 Eventarc 每一次推送都被 403 擋下——觸發它的服務帳號沒有roles/run.invoker。在補上權限之前,函式一次都沒有執行。 - 停損開關的權限:函式的服務帳號持有
roles/billing.projectManager,這個角色只有建立與刪除帳單連結兩個權限,讀取帳單狀態(getBillingInfo)要的resourcemanager.projects.get不在裡面,函式在停用之前的檢查拋出例外。 - 停損開關的重試:那次例外之後,Cloud Run 的請求紀錄是 HTTP 200。觸發器預設的重試政策是
RETRY_POLICY_DO_NOT_RETRY,Pub/Sub 把失敗的那則超支通知標為已確認收訊,之後不再送,直到數小時後的下一則預算通知為止帳單照常計費。 - 防火牆紀錄的驗證查詢:新建的、只放行 IAP 來源的防火牆規則開啟 logging 之後的最初幾分鐘,紀錄裡的
rule_details是空的,用規則名稱過濾的查詢回傳零筆,而新規則確實已經放行了那幾次連線。 - 帳單停用後的資源清單:帳單被停損開關解除連結之後,
gcloud compute routers list回傳零筆;重新連結之後,Cloud Router 與 Cloud NAT 都還在。 - 結束工作階段:一台 VM 上
pam_systemd登記工作階段失敗之後,loginctl terminate-user回報使用者沒有登入,而那個使用者的十個行程仍在執行。
〈測試從中途注入〉與分段驗證不在這七項裡:測試從中途注入由同一次實測裡 costAmount 為 0.0、而那個預算扣掉抵免推出(之後的重跑看到同一段期間扣抵免的預算報 0.05、不扣抵免的報 0.37);分段驗證是審查時補上的做法,之後的重跑以換了名稱與主題的複本加 Policy Troubleshooter 實際做過一次。
稽核紀錄與停損開關是防護機制本身;防火牆紀錄的查詢、資源清單與 terminate-user 是用來確認防護生效或執行處置的步驟。每一項單獨看都有一個看得懂的成因,而它們共用同一個驗收習慣:拿一個中間訊號當成終點。
核心原則
平常不動作的防護機制,部署完成的完整證據只有一種:讓它要防的那件事實際發生一次,在終點看到它要產生的效果。 停損開關的終點是專案的 billingEnabled 變成 false,稽核紀錄的終點是一筆記著帳號與目標的紀錄,告警的終點是通知送到值班的人手上。部署指令的回傳值、元件的狀態欄、函式是否被呼叫,都在從觸發條件發生到效果出現的這條路徑中途,它們是綠的不代表終點會發生。
這類機制特別需要這一條,是因為它們的失效沒有訊號。一般的功能壞掉,使用者下一次使用時就會遇到錯誤;停損開關、稽核紀錄與告警平常本來就不動作,「壞掉」與「還沒輪到它動作」在任何儀表上都長得一樣,第一次露出破綻的時候,就是它該發揮作用而沒有發揮的那一次。可核對的錨要跟它該偵測的狀態連動說的是同一件事:一個錨(用來判定合格與否的可核對指標)或防護機制的價值在於該偵測的事情發生時它是否會動作,而部署驗證要做的就是讓那件事發生一次。
成功的訊號會在四個位置提早出現,每一個位置各自需要一個終點檢查,才分辨得出看似成功與真正生效:
- 設定被接受:設定指令執行成功、值寫進去了,而那個值描述的不是需要的設定(稽核紀錄開了錯的類型)。檢查設定的讀回值只證明寫入成功,證明不了寫的是對的。
- 元件顯示正常:元件部署成功、狀態欄是
ACTIVE或Ready,而通往它的路徑或它需要的權限有一段斷掉(觸發器沒有呼叫權限、角色少了一個讀取權限)。狀態欄描述的是元件自己,不描述它與其他元件之間的連線。 - 執行有發生而失敗被吞掉:元件確實執行了,失敗發生在中途,而錯誤沒有傳回呼叫的一方(例外之後回 200、訊息被確認收訊)。這一個位置最難發現,因為「有沒有執行」的證據是充分的,執行紀錄裡有它被叫起來的那一筆。
- 測試從中途注入:合成的條件從路徑中途的一個入口送進去,終點也出現了,而入口之前、把真實事件轉成那個條件的那一段沒有被行使到,真實情況下條件可能遲遲不成立(測試訊息直接發到主題;而預算的預設抵免設定會扣掉抵免,帳戶有大額抵免時,真實通知裡的花費在抵免用完之前都接近 0,門檻不會觸發)。
驗證步驟本身也會給錯的答案,方向是假陰性:該查得到的紀錄或資源查不到(紀錄的欄位還沒填、帳單停用時清單回傳空的)。假陰性的危險在於零筆會被讀成一個結論、並據此行動——「新規則沒有命中」會讓人回頭去改一條本來就正確的規則,「資源都沒了」會讓人跳過清理。執行處置的步驟有同一種形態:loginctl terminate-user 回報「使用者沒有登入」而那個使用者的行程仍在執行,處置指令的回報不等於處置的結果,完成要以行程清單(pgrep -u <user>)為準。
反模式
用部署指令的結果當驗收。 部署的人把「gcloud functions deploy 成功、狀態 ACTIVE」寫進驗收紀錄,接著去做下一件事。這一句描述的是部署管線,不是停損開關。那次驗證裡這句話成立的時候,函式一次都還沒有執行過。
只驗證不動作的那一條分支。 停損開關收到一則低於預算的通知、記錄 action=none、帳單維持啟用——這證明訊息送得到、判斷邏輯會跑,而「切斷」那一條分支要用到的權限(讀取帳單狀態、解除帳單連結)一次都沒有被行使。權限缺一個,只有在實際要切斷的那一刻才會現形。
把零筆當成「沒有發生」。 驗證查詢回傳零筆,結論寫成「新規則沒有被命中」或「資源已經被刪除」。零筆同時是「沒有發生」與「查詢看不到」的輸出,沒有對照組就分不開兩者。
修好一個失效點就宣告完成。 補上 run.invoker 之後,低於預算的通知能讓函式執行並記錄 none,看起來整條鏈通了;而權限不足與預設不重試這兩個失效點,都要等到超支的訊息進來才會出現。失效點是串聯的,每修好一個,下一個才有機會露出來,所以修完之後要從頭再觸發一次終點,而不是只確認剛修的那一段。
修法
驗證一個平常不動作的防護機制,做法是合成觸發(synthetic trigger,與 synthetic monitoring 同一類做法):在不造成實際損害的範圍內,造出它要偵測的那個條件,看終點有沒有發生。
- 先寫下終點:部署之前就寫出「生效時看得到什麼」,而且寫成這個機制要產生的那個狀態——帳單已停用、紀錄裡有這個帳號、值班手機收到通知。寫不出終點的時候,代表還不知道這個機制要做什麼。
- 造出條件,並寫下測試送進路徑的位置(注入點):停損開關用一則花費超過預算的測試訊息、稽核紀錄用一次真實的連線、告警用一次故障注入。測試訊息從觸發器的入口送進去(發到真實的 topic,而不是直接呼叫函式),入口之後的每一段連線與權限都會被行使到;入口之前的那一段(停損開關的預算範圍、抵免設定、發布到 topic 的權限)不會,要另外用一筆真實的輸入確認它產得出這個條件——停損開關讀第一則真實的預算通知,對照帳單報表上的金額。
- 兩條分支都要走:會動作的那一條與不會動作的那一條各觸發一次。停損開關的低於預算訊息確認不會誤觸,超出預算訊息確認會切斷;兩者用到的權限不同。
- 修完之後從頭再觸發一次:任何一段修好之後,回到「造出條件」那一步重新觸發,直到終點出現為止。
- 驗證查詢配一個正對照:查詢回傳零筆時,先用一條不依賴同一個欄位的查詢,查一件確定存在的事(例如改用來源 IP 段查同一批連線、重新連結帳單之後再列一次資源)。正對照也查不到,問題在查詢,不在被查的對象。
- 失敗要能被看見:觸發失敗時重試(Cloud Run functions 部署時加
--retry)、讓錯誤回到呼叫的一方,並在終點以外再放一個心跳監看(dead man’s switch):監看它多久沒有收到觸發來源的訊息,讓靜默本身也成為訊號。合成觸發驗的是這一次會動作,不保證之後的每一次——平台改版、權限被別人調整都會讓它再次失效——心跳監看補的正是這一段。觸發來源不會定期送訊息時(WAF 規則、告警規則只在事件發生時才有輸入),改成排程送一筆合成的輸入,監看它的結果有沒有在期限內出現在終點。心跳監看本身也是平常不動作的機制,照本節驗收一次。
合成觸發排在部署當下與每次修改之後做。停損開關的合成觸發會實際停用帳單,可能連資料一起刪除,所以那一次要在測試專案、或在專案還沒有正式服務與要保留的資料時做;這個成本正是它不會被順手做掉的原因,所以要寫進部署程序,而不是留給記憶。
合成觸發本身會造成不可回復的損害時(處置會刪除資料,或正式環境隔離不出一個可以停的副本),改成分段驗證:在結構相同、可以拋棄的環境完整觸發一次,驗程式、路徑與重試;在正式環境只驗不經觸發也查得到的那幾段——權限用權限檢查工具(GCP 的 Policy Troubleshooter)查,路徑用不會動作的那一條分支走。這樣做缺的是正式環境上那一次真正的處置,驗收紀錄要寫明缺了這一段,心跳監看也照常放。
判讀徵兆
- 驗收紀錄裡最後一個證據是部署指令的輸出、元件的狀態欄或「設定已套用」,沒有任何一筆來自這個機制要產生的效果。
- 一個停損、隔離或告警機制要防的那件事(超支、資料遺失、入侵)從部署到現在沒有發生過,也沒有人模擬過、在終點看到結果;機制本身每天都在跑的(例如排程備份),終點是還原成功,不是作業成功。
- 合成觸發從路徑中途注入,驗收紀錄沒有寫注入點,入口之前的那一段也沒有用真實輸入確認過。
- 測試只送過「不該動作」的那一種輸入。
- 一條驗證查詢回傳零筆,就直接寫成結論,旁邊沒有對照的查詢。
- 執行紀錄裡有例外或錯誤訊息,而呼叫端記錄的是成功(HTTP 200、訊息已確認、exit 0)。
跟其他抽象層原則的關係
- #266 可核對的錨要跟它該偵測的狀態連動:母原則。#266 的情境是寫作判定的覆蓋率指標;本卡把同一個要求套到已部署的防護機制上,補上它沒有處理的三個位置——元件狀態欄、被吞掉的失敗與從中途注入的測試——以及驗證查詢與處置指令的假陰性。
- #371 觀察者與限制的獨立性:停損開關切斷帳單之後,它自己也跑在那個專案上,此後的通知它都處理不了;被保護的專案裡被入侵的身分若有編輯權限,也能直接刪掉它。合成觸發會讓前一種當場浮現,後一種要另外問;兩者是否可接受,都要用 #371 的失效域問一次。
- #253 寫註解的動機是怕被改壞時,要處理的是那個約束:#253 說防護要交給會發聲的機制、而判定靠當場破壞;本卡說交出去之後,那個機制自己是否會發聲也要當場破壞一次來確認。
- #291 掃描指令壞掉時不會報錯,它報一個數字:驗證查詢回傳零筆是同一個現象在雲端紀錄上的形態,修法也相同——先用一個已知會命中的輸入確認查詢本身。
- #384 只能在建立時決定的設定:本卡的「元件顯示正常」處理部署之後怎麼驗;#384 把同一個訊號放到建立之前:元件狀態正常而組合不在支援範圍時,要在建立之前從支援清單與 Known Issues 查出來,建好之後才發現,補救的代價是整個資源。
- #227 可重現性只有乾淨機器重跑才驗得出:同樣是「讀設定驗不出來、要實際跑一次」的一類;#227 驗的是能否重建,本卡驗的是建好之後是否會動作。