<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>監控 on Tarragon</title><link>https://tarrragon.github.io/blog/tags/%E7%9B%A3%E6%8E%A7/</link><description>Recent content in 監控 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 24 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/%E7%9B%A3%E6%8E%A7/index.xml" rel="self" type="application/rss+xml"/><item><title>觀察者與限制的獨立性：被保護的一方能不能讓它失效，從失效域、寫入路徑與輸出管道判斷</title><link>https://tarrragon.github.io/blog/report/independence-is-what-the-guarded-side-cannot-disable/</link><pubDate>Wed, 14 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/report/independence-is-what-the-guarded-side-cannot-disable/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>這張卡來自一組文章的撰寫與五輪審查：單台主機的監控盲區、第三方心跳監看、兩到三台主機的配置，以及兩個網路之間的連線安全。同一種缺陷在四篇的初稿裡反覆出現，每一次都被當成已經解決，而每一次被指出時的形狀都不同：&lt;/p>
&lt;ul>
&lt;li>心跳監看放在另一個業者上，被判定為獨立於監控主機；而心跳只證明它自己那一條路通，通知管道的網址寫錯時心跳照常送達，人收不到任何告警。&lt;/li>
&lt;li>同一個心跳監看，在監控主機被入侵時看不出異狀：攻擊者手上有監控主機存著的心跳網址，停掉告警鏈之後自己送心跳。&lt;/li>
&lt;li>外部探測被判定為不經過監控主機的觀察者，而它的告警寄到監控主機也存著密碼的信箱，攻擊者登入信箱設一條過濾規則就刪得掉。&lt;/li>
&lt;li>防火牆限制寫在「另一端」，被判定為被入侵的雲端主機改不到；而雲端主機上存著能改 ACL 的 API 金鑰、能改業者防火牆的 token、能推送部署設定的金鑰。&lt;/li>
&lt;li>備份放在另一個業者，被判定為與服務主機分開；而寫入備份的存取金鑰就在服務主機上，上傳權限本身包含覆寫。&lt;/li>
&lt;li>評估時「兩端各假設一次被入侵」，漏掉管理者的筆電：它存著兩台主機的 SSH 金鑰，被入侵時兩端的每一條限制都失效。&lt;/li>
&lt;/ul>
&lt;p>證據的限制：六個實例出自同一個作者、同一組文章、同一段時間，審查者與作者共用同一個模型；跨主機的限制效果沒有實機驗證。卡上的原則是從這組實例歸納的，換到別的領域（例如稽核紀錄的獨立性、CI 的簽章驗證）是否同樣成立，尚未對照。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>一個觀察者或一條限制獨立於被保護的對象，條件是&lt;strong>被保護的一方沒有任何方式讓它失效&lt;/strong>。這要同時檢查三件事：&lt;/p>
&lt;p>&lt;strong>共用的失效域&lt;/strong>：它與被保護的對象有沒有共用會一起停掉的依賴（同一台主機、同一個業者帳號、同一條網路、同一個 DNS）。共用時，被保護的對象停掉，它跟著停。&lt;/p>
&lt;p>&lt;strong>寫入路徑&lt;/strong>：被保護的一方（或被假設入侵的那一台）能不能改寫它，方式包括存在那台機器上或它取得得到的存取憑證（API 金鑰、業者 token、雲端實例角色）、已登入的工作階段（管理介面、信箱、業者主控台）、它能推送的設定來源、它在控制平面上的角色（簽署節點、自架的協調伺服器）。有任何一條，存放在別處的限制仍然改得到。&lt;/p>
&lt;p>&lt;strong>輸出管道&lt;/strong>：觀察者的結論要送到人手上，送的那一段（通知管道、收件信箱、推播頻道）也要通過前兩項的檢查。觀察者本身獨立而通知經過被保護的一方，結果與沒有觀察者相同。&lt;/p>
&lt;p>三件事都通過才算獨立。評估的單位是&lt;strong>每一台會被假設入侵的機器&lt;/strong>，包括由人操作的裝置；管理者的筆電通常持有最寬的寫入路徑，因為它存著登入每一台主機的憑證。&lt;/p>
&lt;h2 id="反模式以存放位置判斷獨立">反模式：以存放位置判斷獨立&lt;/h2>
&lt;p>最常見的判斷是「它放在另一台／另一個業者，所以獨立」。存放位置是三件事的第一步，而它回答的只有三件事裡的失效域：被保護的一方手上有沒有改寫它的憑證、它的輸出經過哪裡，它都沒有回答。&lt;/p>
&lt;p>這個判斷在故障情境下多半成立，在入侵情境下多半不成立。故障時被保護的對象是停掉了，停掉的機器不會去改寫別台；入侵時被保護的對象還在運作，而且由一個會去找寫入路徑的人操作。同一個觀察者，故障演練全數通過，入侵情境下一步就被繞過。所以一份只做過故障演練的設計，對入侵的獨立性沒有任何證據。&lt;/p>
&lt;p>另一個形態是&lt;strong>固定角色&lt;/strong>：先決定「雲端是會被入侵的那一端、地端是可信的那一端」，之後所有的判斷都以這個指派為前提。它讓地端被入侵、管理者的裝置被入侵這兩個方向完全不被檢查，而兩者在一般的配置裡都不罕見（地端主機兼作日常電腦、管理者的筆電用來收信）。&lt;/p>
&lt;h2 id="為什麼前幾輪審查抓不到">為什麼前幾輪審查抓不到&lt;/h2>
&lt;p>這個缺陷在每一輪都換了形狀，而每一輪的修法都只補了被指出的那一個實例：心跳的通知管道被補上了獨立的檢查項目，限制的「另一端」被補上了「前提是沒有寫入路徑」，備份被補上了物件鎖定。每一次修法都讓那一處正確，同時讓整份設計看起來更完整，而缺的那一個維度（被保護的一方能不能讓它失效）沒有被寫成判斷標準本身，所以下一個新加入的成員（管理者的筆電、簽署裝置、清除舊備份用的機器、對外的儀表板主機）又照著存放位置被判定為獨立。&lt;/p>
&lt;p>偵測它的是個案實跑：拿四個極端個案（個人裝置混在同一個 VPN、兩台都對外、推送方向與 NAT、地端被入侵）照判斷標準走一次，個案停住的地方並排之後才看出缺的是同一個維度。逐段閱讀的審查對它的偵測率很低，因為每一段單獨讀都說得通。&lt;/p>
&lt;h2 id="修法">修法&lt;/h2>
&lt;p>逐台列出會被假設入侵的機器，含由人操作的裝置。每一台、對每一個要依賴的觀察者與限制，問三題並寫下答案：&lt;/p>
&lt;ol>
&lt;li>它與這一台共用哪些會一起停掉的依賴。&lt;/li>
&lt;li>這一台有沒有任何改寫它的方式：存著或取得得到的憑證、已登入的工作階段、能推送的設定來源、控制平面上的角色。逐項列，不憑印象。&lt;/li>
&lt;li>它的輸出（告警、通知、紀錄）送到人手上的那一段，是否也通過前兩題。&lt;/li>
&lt;/ol>
&lt;p>任何一題的答案是「有」，那個觀察者或那條限制對這一台不獨立：把它搬到這一台改不到的地方、拿掉這一台的那條寫入路徑，或在設計文件裡寫明這一台被入侵時它不成立。&lt;/p>
&lt;p>由人操作的裝置要單獨處理：它的寫入路徑通常無法在網路層收窄，因為它走的是本來就允許的管理連線。能切斷的做法是讓管理只從一台不日常上網的專用裝置進行；硬體金鑰、再驗證這類做法只縮小範圍。&lt;/p>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;ul>
&lt;li>設計文件寫「放在另一台」「放在另一個業者」，而沒有列出被保護的那一台上有哪些憑證。&lt;/li>
&lt;li>只做過故障演練（停掉主機、拔網路），沒有做過「假設這一台被拿下」的推演。&lt;/li>
&lt;li>修法一再補上「前提是那一台沒有能改寫它的憑證」這類句子，而判斷標準的定義本身仍然只看位置。&lt;/li>
&lt;li>個案推演得出「這一端的限制都改不到，所以安全」，而攻擊者走的是清單上本來就允許的連線。&lt;/li>
&lt;li>觀察者的告警寄到的信箱或推播頻道，其帳號存在被觀察的機器上，或存在管理者每天用的那台電腦上。&lt;/li>
&lt;li>備份寫入憑證放在服務主機上，而設計文件說的是「備份在另一個業者，所以安全」。&lt;/li>
&lt;/ul>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/axis-named-by-proxy-not-mechanism/" data-link-title="軸名取了次好的代理變數，而正確的機制寫在括號裡" data-link-desc="一組判斷標準讀起來都對、套到個案上卻分不開時，判斷問題是不是出在軸的名字——真正在做功的機制常常已經寫在括號或下一句的理由裡">#257 軸名取了次好的代理變數&lt;/a>：「存放位置」是「寫入路徑」的代理變數，正確的機制最初只出現在修法的括號裡（「前提是沒有能改寫它的憑證」），這張卡把它提升為判斷標準本身。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/criteria-fold-away-the-condition-that-produces-the-symptom/" data-link-title="判斷標準只收表徵，把產生表徵的那個條件從輸入端折疊掉" data-link-desc="判斷標準寫完、各條件的後果也齊全，而讀者帶著具體個案走一遍卻得不出判定時，用來檢查輸入端少收了哪個維度">#297 判斷標準只收表徵&lt;/a>：固定角色是同一種折疊，「哪一台被假設入侵」這個輸入在指派之後就從判斷標準裡消失，個案實跑時才浮現。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/replacement-exemption-comes-from-its-fix-list/" data-link-title="替補結構的豁免來自它修掉的清單，重問殺死前任的問題驗不到它" data-link-desc="採納一個為了修掉既有問題而生的替代結構時，用來判斷該怎麼驗它——重問上一輪的問題不夠">#283 替補結構的豁免來自它修掉的清單&lt;/a>：每一輪補上的修法（獨立的通知檢查、「每端各假設一次」）都修掉了被指出的實例，而它們引進的新成員沒有被同一個問題檢驗；這張卡的修法要求對新加入的成員重跑三題。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/review-scope-never-includes-the-instrument/" data-link-title="審查的射程只涵蓋被審查的對象，工具與共用產物在射程外" data-link-desc="一輪多 frame 的審查全部通過之後，用來找出從頭到尾沒有任何一個 frame 會碰到的那幾類對象">#337 審查的射程只涵蓋被審查的對象&lt;/a>：同構的另一面。審查者不會審查自己用的工具，觀察者的獨立性也常常沒有把「觀察者自己依賴什麼」算進去。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/review-scale-does-not-buy-independent-origin/" data-link-title="同一個 session 派出的 reviewer 共用它的盲區，規模買不到異源視角" data-link-desc="跑完多輪審查、字句層掃描回報乾淨，而外部一眼就看到違規時，用來判斷該加輪次還是該換來源">#296 規模買不到異源視角&lt;/a>：在審查流程上的同一個原則。同一個 session 派出的審查者與作者共用盲區，數量增加不構成獨立；判斷的是兩者共用了什麼，不是有幾個。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>這張卡來自一組文章的撰寫與五輪審查：單台主機的監控盲區、第三方心跳監看、兩到三台主機的配置，以及兩個網路之間的連線安全。同一種缺陷在四篇的初稿裡反覆出現，每一次都被當成已經解決，而每一次被指出時的形狀都不同：</p>
<ul>
<li>心跳監看放在另一個業者上，被判定為獨立於監控主機；而心跳只證明它自己那一條路通，通知管道的網址寫錯時心跳照常送達，人收不到任何告警。</li>
<li>同一個心跳監看，在監控主機被入侵時看不出異狀：攻擊者手上有監控主機存著的心跳網址，停掉告警鏈之後自己送心跳。</li>
<li>外部探測被判定為不經過監控主機的觀察者，而它的告警寄到監控主機也存著密碼的信箱，攻擊者登入信箱設一條過濾規則就刪得掉。</li>
<li>防火牆限制寫在「另一端」，被判定為被入侵的雲端主機改不到；而雲端主機上存著能改 ACL 的 API 金鑰、能改業者防火牆的 token、能推送部署設定的金鑰。</li>
<li>備份放在另一個業者，被判定為與服務主機分開；而寫入備份的存取金鑰就在服務主機上，上傳權限本身包含覆寫。</li>
<li>評估時「兩端各假設一次被入侵」，漏掉管理者的筆電：它存著兩台主機的 SSH 金鑰，被入侵時兩端的每一條限制都失效。</li>
</ul>
<p>證據的限制：六個實例出自同一個作者、同一組文章、同一段時間，審查者與作者共用同一個模型；跨主機的限制效果沒有實機驗證。卡上的原則是從這組實例歸納的，換到別的領域（例如稽核紀錄的獨立性、CI 的簽章驗證）是否同樣成立，尚未對照。</p>
<h2 id="核心原則">核心原則</h2>
<p>一個觀察者或一條限制獨立於被保護的對象，條件是<strong>被保護的一方沒有任何方式讓它失效</strong>。這要同時檢查三件事：</p>
<p><strong>共用的失效域</strong>：它與被保護的對象有沒有共用會一起停掉的依賴（同一台主機、同一個業者帳號、同一條網路、同一個 DNS）。共用時，被保護的對象停掉，它跟著停。</p>
<p><strong>寫入路徑</strong>：被保護的一方（或被假設入侵的那一台）能不能改寫它，方式包括存在那台機器上或它取得得到的存取憑證（API 金鑰、業者 token、雲端實例角色）、已登入的工作階段（管理介面、信箱、業者主控台）、它能推送的設定來源、它在控制平面上的角色（簽署節點、自架的協調伺服器）。有任何一條，存放在別處的限制仍然改得到。</p>
<p><strong>輸出管道</strong>：觀察者的結論要送到人手上，送的那一段（通知管道、收件信箱、推播頻道）也要通過前兩項的檢查。觀察者本身獨立而通知經過被保護的一方，結果與沒有觀察者相同。</p>
<p>三件事都通過才算獨立。評估的單位是<strong>每一台會被假設入侵的機器</strong>，包括由人操作的裝置；管理者的筆電通常持有最寬的寫入路徑，因為它存著登入每一台主機的憑證。</p>
<h2 id="反模式以存放位置判斷獨立">反模式：以存放位置判斷獨立</h2>
<p>最常見的判斷是「它放在另一台／另一個業者，所以獨立」。存放位置是三件事的第一步，而它回答的只有三件事裡的失效域：被保護的一方手上有沒有改寫它的憑證、它的輸出經過哪裡，它都沒有回答。</p>
<p>這個判斷在故障情境下多半成立，在入侵情境下多半不成立。故障時被保護的對象是停掉了，停掉的機器不會去改寫別台；入侵時被保護的對象還在運作，而且由一個會去找寫入路徑的人操作。同一個觀察者，故障演練全數通過，入侵情境下一步就被繞過。所以一份只做過故障演練的設計，對入侵的獨立性沒有任何證據。</p>
<p>另一個形態是<strong>固定角色</strong>：先決定「雲端是會被入侵的那一端、地端是可信的那一端」，之後所有的判斷都以這個指派為前提。它讓地端被入侵、管理者的裝置被入侵這兩個方向完全不被檢查，而兩者在一般的配置裡都不罕見（地端主機兼作日常電腦、管理者的筆電用來收信）。</p>
<h2 id="為什麼前幾輪審查抓不到">為什麼前幾輪審查抓不到</h2>
<p>這個缺陷在每一輪都換了形狀，而每一輪的修法都只補了被指出的那一個實例：心跳的通知管道被補上了獨立的檢查項目，限制的「另一端」被補上了「前提是沒有寫入路徑」，備份被補上了物件鎖定。每一次修法都讓那一處正確，同時讓整份設計看起來更完整，而缺的那一個維度（被保護的一方能不能讓它失效）沒有被寫成判斷標準本身，所以下一個新加入的成員（管理者的筆電、簽署裝置、清除舊備份用的機器、對外的儀表板主機）又照著存放位置被判定為獨立。</p>
<p>偵測它的是個案實跑：拿四個極端個案（個人裝置混在同一個 VPN、兩台都對外、推送方向與 NAT、地端被入侵）照判斷標準走一次，個案停住的地方並排之後才看出缺的是同一個維度。逐段閱讀的審查對它的偵測率很低，因為每一段單獨讀都說得通。</p>
<h2 id="修法">修法</h2>
<p>逐台列出會被假設入侵的機器，含由人操作的裝置。每一台、對每一個要依賴的觀察者與限制，問三題並寫下答案：</p>
<ol>
<li>它與這一台共用哪些會一起停掉的依賴。</li>
<li>這一台有沒有任何改寫它的方式：存著或取得得到的憑證、已登入的工作階段、能推送的設定來源、控制平面上的角色。逐項列，不憑印象。</li>
<li>它的輸出（告警、通知、紀錄）送到人手上的那一段，是否也通過前兩題。</li>
</ol>
<p>任何一題的答案是「有」，那個觀察者或那條限制對這一台不獨立：把它搬到這一台改不到的地方、拿掉這一台的那條寫入路徑，或在設計文件裡寫明這一台被入侵時它不成立。</p>
<p>由人操作的裝置要單獨處理：它的寫入路徑通常無法在網路層收窄，因為它走的是本來就允許的管理連線。能切斷的做法是讓管理只從一台不日常上網的專用裝置進行；硬體金鑰、再驗證這類做法只縮小範圍。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>設計文件寫「放在另一台」「放在另一個業者」，而沒有列出被保護的那一台上有哪些憑證。</li>
<li>只做過故障演練（停掉主機、拔網路），沒有做過「假設這一台被拿下」的推演。</li>
<li>修法一再補上「前提是那一台沒有能改寫它的憑證」這類句子，而判斷標準的定義本身仍然只看位置。</li>
<li>個案推演得出「這一端的限制都改不到，所以安全」，而攻擊者走的是清單上本來就允許的連線。</li>
<li>觀察者的告警寄到的信箱或推播頻道，其帳號存在被觀察的機器上，或存在管理者每天用的那台電腦上。</li>
<li>備份寫入憑證放在服務主機上，而設計文件說的是「備份在另一個業者，所以安全」。</li>
</ul>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><a href="/blog/report/axis-named-by-proxy-not-mechanism/" data-link-title="軸名取了次好的代理變數，而正確的機制寫在括號裡" data-link-desc="一組判斷標準讀起來都對、套到個案上卻分不開時，判斷問題是不是出在軸的名字——真正在做功的機制常常已經寫在括號或下一句的理由裡">#257 軸名取了次好的代理變數</a>：「存放位置」是「寫入路徑」的代理變數，正確的機制最初只出現在修法的括號裡（「前提是沒有能改寫它的憑證」），這張卡把它提升為判斷標準本身。</li>
<li><a href="/blog/report/criteria-fold-away-the-condition-that-produces-the-symptom/" data-link-title="判斷標準只收表徵，把產生表徵的那個條件從輸入端折疊掉" data-link-desc="判斷標準寫完、各條件的後果也齊全，而讀者帶著具體個案走一遍卻得不出判定時，用來檢查輸入端少收了哪個維度">#297 判斷標準只收表徵</a>：固定角色是同一種折疊，「哪一台被假設入侵」這個輸入在指派之後就從判斷標準裡消失，個案實跑時才浮現。</li>
<li><a href="/blog/report/replacement-exemption-comes-from-its-fix-list/" data-link-title="替補結構的豁免來自它修掉的清單，重問殺死前任的問題驗不到它" data-link-desc="採納一個為了修掉既有問題而生的替代結構時，用來判斷該怎麼驗它——重問上一輪的問題不夠">#283 替補結構的豁免來自它修掉的清單</a>：每一輪補上的修法（獨立的通知檢查、「每端各假設一次」）都修掉了被指出的實例，而它們引進的新成員沒有被同一個問題檢驗；這張卡的修法要求對新加入的成員重跑三題。</li>
<li><a href="/blog/report/review-scope-never-includes-the-instrument/" data-link-title="審查的射程只涵蓋被審查的對象，工具與共用產物在射程外" data-link-desc="一輪多 frame 的審查全部通過之後，用來找出從頭到尾沒有任何一個 frame 會碰到的那幾類對象">#337 審查的射程只涵蓋被審查的對象</a>：同構的另一面。審查者不會審查自己用的工具，觀察者的獨立性也常常沒有把「觀察者自己依賴什麼」算進去。</li>
<li><a href="/blog/report/review-scale-does-not-buy-independent-origin/" data-link-title="同一個 session 派出的 reviewer 共用它的盲區，規模買不到異源視角" data-link-desc="跑完多輪審查、字句層掃描回報乾淨，而外部一眼就看到違規時，用來判斷該加輪次還是該換來源">#296 規模買不到異源視角</a>：在審查流程上的同一個原則。同一個 session 派出的審查者與作者共用盲區，數量增加不構成獨立；判斷的是兩者共用了什麼，不是有幾個。</li>
</ul>
]]></content:encoded></item><item><title>平常不動作的防護機制的部署驗證：設定被接受、元件狀態正常、執行失敗被吞掉與測試從中途注入這些提早出現的成功訊號，驗證查詢與處置指令的假陰性，以及合成觸發本身有損害時的分段驗證</title><link>https://tarrragon.github.io/blog/report/protection-proves-itself-by-firing/</link><pubDate>Sat, 24 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/report/protection-proves-itself-by-firing/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>這張卡來自一次 GCP 測試專案上的實機驗證。驗證的對象是 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/gcp-iap-ssh-access/" data-link-title="GCP VM 的 SSH 與 RDP 管理通道：從預設的全開規則改走 IAP TCP forwarding" data-link-desc="GCP 預設網路自帶的防火牆規則與它們留下的暴露面、IAP TCP forwarding 的連線路徑與日常連線指令、切換到 IAP 的順序為什麼這樣排、IAP 通道連不上時的依層判讀，以及 IAM 授權範圍、OS Login 的啟用條件與工作階段的結束、拿掉外部 IP 之後的出站路徑、Organization Policy、階層式防火牆、稽核紀錄與 Security Command Center 的 OPEN_SSH_PORT finding 在 GCP 上的處理">GCP VM 的 SSH 與 RDP 管理通道&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/gke-mig-management-access/" data-link-title="GCP 上由 MIG 與 GKE 管理的 VM 的管理存取：instance template、node pool、kubectl debug 與控制平面存取" data-link-desc="VM 由 managed instance group 或 GKE node pool 管理時，管理通道的設定為什麼要寫在 instance template 與 node pool 而不是逐台修改、MIG 救援用重建、退回 template 與 Serial Console 各自適用的情形、GKE 的控制平面與節點兩個存取面、kubectl debug 取代節點 SSH 的權限邊界（RBAC 與 Pod Security Admission），以及 GKE 控制平面三種端點（authorized networks、DNS-based endpoint 與 private endpoint）的差別">GCP 上由 MIG 與 GKE 管理的 VM 的管理存取&lt;/a> 兩篇教學文章的指令，以及一個依預算通知自動停用帳單的停損開關（kill switch，做法在 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/" data-link-title="GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷" data-link-desc="GCP 上為費用失控設防的機制：預算警示寄出通知的時機、範圍、抵免與通知對象、強制執行支出上限涵蓋的服務與暫停期間，以預算通知觸發函式停止用量的兩種處置（解除帳單連結、只停止花錢的資源）該選哪一種，帳單資料延遲對這些機制反應時間的影響，停損開關放在另一個專案的理由，以及驗收為什麼分成真實通知與測試訊息兩段、停用後的恢復為什麼要先調預算">GCP 上的雲端費用失控防護&lt;/a>）。這三篇的指令後來移到三篇實測紀錄：&lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/iap-ssh-migration-walkthrough/" data-link-title="GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄：盤點、救援入口、IAP 規則與 IAM、停用舊規則驗證、OS Login 啟用與移除外部 IP" data-link-desc="要把 GCP 專案裡對 0.0.0.0/0 開放 22 與 3389 port 的預設規則換成只放行 IAP，想照一次實跑過的順序做、看每一步沒做或換一種做法的結果時讀；也用在停用舊規則之後連不上、OS Login 啟用後有人登不進去，或拿掉外部 IP 之後要確認出站走了 Cloud NAT 的時候">GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/mig-gke-access-walkthrough/" data-link-title="GCP 上 MIG 與 GKE 管理存取的實測紀錄：instance template 與 canary 滾動更新、重建與主機金鑰、kubectl debug 的 namespace、node pool 設定與控制平面端點的切換" data-link-desc="要把 IAP 的 tag、OS Login 與移除外部 IP 套到 MIG 或 GKE 上，想照一次實跑過的順序做、看每一步換一種做法的結果時讀；也用在 MIG 重建之後 SSH 出現 REMOTE HOST IDENTIFICATION HAS CHANGED、kubectl debug 停在等待，或關掉 GKE 控制平面的 IP 端點之前要確認每個人都改連的時候">GCP 上 MIG 與 GKE 管理存取的實測紀錄&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄&lt;/a>，原本的三篇留下機制與判斷依據。單一雲端供應商、單一個工作階段、每個現象各觀察到一到數次，所以卡上記的是這幾個機制在 2026 年 10 月當時的行為，而不是 GCP 的通則；卡要歸納的是驗證方法，不是那些行為本身。&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>這張卡來自一次 GCP 測試專案上的實機驗證。驗證的對象是 <a href="/blog/devops/infra/cloud-providers/gcp/gcp-iap-ssh-access/" data-link-title="GCP VM 的 SSH 與 RDP 管理通道：從預設的全開規則改走 IAP TCP forwarding" data-link-desc="GCP 預設網路自帶的防火牆規則與它們留下的暴露面、IAP TCP forwarding 的連線路徑與日常連線指令、切換到 IAP 的順序為什麼這樣排、IAP 通道連不上時的依層判讀，以及 IAM 授權範圍、OS Login 的啟用條件與工作階段的結束、拿掉外部 IP 之後的出站路徑、Organization Policy、階層式防火牆、稽核紀錄與 Security Command Center 的 OPEN_SSH_PORT finding 在 GCP 上的處理">GCP VM 的 SSH 與 RDP 管理通道</a> 與 <a href="/blog/devops/infra/cloud-providers/gcp/gke-mig-management-access/" data-link-title="GCP 上由 MIG 與 GKE 管理的 VM 的管理存取：instance template、node pool、kubectl debug 與控制平面存取" data-link-desc="VM 由 managed instance group 或 GKE node pool 管理時，管理通道的設定為什麼要寫在 instance template 與 node pool 而不是逐台修改、MIG 救援用重建、退回 template 與 Serial Console 各自適用的情形、GKE 的控制平面與節點兩個存取面、kubectl debug 取代節點 SSH 的權限邊界（RBAC 與 Pod Security Admission），以及 GKE 控制平面三種端點（authorized networks、DNS-based endpoint 與 private endpoint）的差別">GCP 上由 MIG 與 GKE 管理的 VM 的管理存取</a> 兩篇教學文章的指令，以及一個依預算通知自動停用帳單的停損開關（kill switch，做法在 <a href="/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/" data-link-title="GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷" data-link-desc="GCP 上為費用失控設防的機制：預算警示寄出通知的時機、範圍、抵免與通知對象、強制執行支出上限涵蓋的服務與暫停期間，以預算通知觸發函式停止用量的兩種處置（解除帳單連結、只停止花錢的資源）該選哪一種，帳單資料延遲對這些機制反應時間的影響，停損開關放在另一個專案的理由，以及驗收為什麼分成真實通知與測試訊息兩段、停用後的恢復為什麼要先調預算">GCP 上的雲端費用失控防護</a>）。這三篇的指令後來移到三篇實測紀錄：<a href="/blog/devops/infra/cloud-providers/gcp/hands-on/iap-ssh-migration-walkthrough/" data-link-title="GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄：盤點、救援入口、IAP 規則與 IAM、停用舊規則驗證、OS Login 啟用與移除外部 IP" data-link-desc="要把 GCP 專案裡對 0.0.0.0/0 開放 22 與 3389 port 的預設規則換成只放行 IAP，想照一次實跑過的順序做、看每一步沒做或換一種做法的結果時讀；也用在停用舊規則之後連不上、OS Login 啟用後有人登不進去，或拿掉外部 IP 之後要確認出站走了 Cloud NAT 的時候">GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄</a>、<a href="/blog/devops/infra/cloud-providers/gcp/hands-on/mig-gke-access-walkthrough/" data-link-title="GCP 上 MIG 與 GKE 管理存取的實測紀錄：instance template 與 canary 滾動更新、重建與主機金鑰、kubectl debug 的 namespace、node pool 設定與控制平面端點的切換" data-link-desc="要把 IAP 的 tag、OS Login 與移除外部 IP 套到 MIG 或 GKE 上，想照一次實跑過的順序做、看每一步換一種做法的結果時讀；也用在 MIG 重建之後 SSH 出現 REMOTE HOST IDENTIFICATION HAS CHANGED、kubectl debug 停在等待，或關掉 GKE 控制平面的 IP 端點之前要確認每個人都改連的時候">GCP 上 MIG 與 GKE 管理存取的實測紀錄</a>、<a href="/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄</a>，原本的三篇留下機制與判斷依據。單一雲端供應商、單一個工作階段、每個現象各觀察到一到數次，所以卡上記的是這幾個機制在 2026 年 10 月當時的行為，而不是 GCP 的通則；卡要歸納的是驗證方法，不是那些行為本身。</p>
<p>那次驗證裡有七個防護或驗證步驟，事後看都在還沒真正生效、或還沒真正查到的時候，給出了可以結案的訊號：稽核紀錄與停損開關的觸發、權限、重試這四項給的是成功的訊號，防火牆紀錄查詢與資源清單給的是零筆，<code>loginctl terminate-user</code> 回報的是「使用者沒有登入」。</p>
<ul>
<li><strong>IAP 的稽核紀錄</strong>：照教學文章當時的寫法（之後已改成 <code>ADMIN_READ</code>），在專案的稽核設定為 IAP 開啟「資料讀取」（<code>DATA_READ</code>）類型，設定被接受，之後經 IAP 連了十次左右，紀錄零筆。開通道時檢查的權限 <code>iap.tunnelInstances.accessViaIAP</code> 歸在「管理員讀取」（<code>ADMIN_READ</code>）類型。</li>
<li><strong>停損開關的觸發</strong>：Cloud Run functions 的函式部署成功、狀態 <code>ACTIVE</code>，而 Eventarc 每一次推送都被 403 擋下——觸發它的服務帳號沒有 <code>roles/run.invoker</code>。在補上權限之前，函式一次都沒有執行。</li>
<li><strong>停損開關的權限</strong>：函式的服務帳號持有 <code>roles/billing.projectManager</code>，這個角色只有建立與刪除帳單連結兩個權限，讀取帳單狀態（<code>getBillingInfo</code>）要的 <code>resourcemanager.projects.get</code> 不在裡面，函式在停用之前的檢查拋出例外。</li>
<li><strong>停損開關的重試</strong>：那次例外之後，Cloud Run 的請求紀錄是 HTTP 200。觸發器預設的重試政策是 <code>RETRY_POLICY_DO_NOT_RETRY</code>，Pub/Sub 把失敗的那則超支通知標為已確認收訊，之後不再送，直到數小時後的下一則預算通知為止帳單照常計費。</li>
<li><strong>防火牆紀錄的驗證查詢</strong>：新建的、只放行 IAP 來源的防火牆規則開啟 logging 之後的最初幾分鐘，紀錄裡的 <code>rule_details</code> 是空的，用規則名稱過濾的查詢回傳零筆，而新規則確實已經放行了那幾次連線。</li>
<li><strong>帳單停用後的資源清單</strong>：帳單被停損開關解除連結之後，<code>gcloud compute routers list</code> 回傳零筆；重新連結之後，Cloud Router 與 Cloud NAT 都還在。</li>
<li><strong>結束工作階段</strong>：一台 VM 上 <code>pam_systemd</code> 登記工作階段失敗之後，<code>loginctl terminate-user</code> 回報使用者沒有登入，而那個使用者的十個行程仍在執行。</li>
</ul>
<p>〈測試從中途注入〉與分段驗證不在這七項裡：測試從中途注入由同一次實測裡 <code>costAmount</code> 為 0.0、而那個預算扣掉抵免推出（之後的重跑看到同一段期間扣抵免的預算報 0.05、不扣抵免的報 0.37）；分段驗證是審查時補上的做法，之後的重跑以換了名稱與主題的複本加 Policy Troubleshooter 實際做過一次。</p>
<p>稽核紀錄與停損開關是防護機制本身；防火牆紀錄的查詢、資源清單與 <code>terminate-user</code> 是用來確認防護生效或執行處置的步驟。每一項單獨看都有一個看得懂的成因，而它們共用同一個驗收習慣：拿一個中間訊號當成終點。</p>
<hr>
<h2 id="核心原則">核心原則</h2>
<p><strong>平常不動作的防護機制，部署完成的完整證據只有一種：讓它要防的那件事實際發生一次，在終點看到它要產生的效果。</strong> 停損開關的終點是專案的 <code>billingEnabled</code> 變成 <code>false</code>，稽核紀錄的終點是一筆記著帳號與目標的紀錄，告警的終點是通知送到值班的人手上。部署指令的回傳值、元件的狀態欄、函式是否被呼叫，都在從觸發條件發生到效果出現的這條路徑中途，它們是綠的不代表終點會發生。</p>
<p>這類機制特別需要這一條，是因為它們的失效沒有訊號。一般的功能壞掉，使用者下一次使用時就會遇到錯誤；停損開關、稽核紀錄與告警平常本來就不動作，「壞掉」與「還沒輪到它動作」在任何儀表上都長得一樣，第一次露出破綻的時候，就是它該發揮作用而沒有發揮的那一次。<a href="../checkable-anchor-must-be-able-to-fall/">可核對的錨要跟它該偵測的狀態連動</a>說的是同一件事：一個錨（用來判定合格與否的可核對指標）或防護機制的價值在於該偵測的事情發生時它是否會動作，而部署驗證要做的就是讓那件事發生一次。</p>
<p>成功的訊號會在四個位置提早出現，每一個位置各自需要一個終點檢查，才分辨得出看似成功與真正生效：</p>
<ul>
<li><strong>設定被接受</strong>：設定指令執行成功、值寫進去了，而那個值描述的不是需要的設定（稽核紀錄開了錯的類型）。檢查設定的讀回值只證明寫入成功，證明不了寫的是對的。</li>
<li><strong>元件顯示正常</strong>：元件部署成功、狀態欄是 <code>ACTIVE</code> 或 <code>Ready</code>，而通往它的路徑或它需要的權限有一段斷掉（觸發器沒有呼叫權限、角色少了一個讀取權限）。狀態欄描述的是元件自己，不描述它與其他元件之間的連線。</li>
<li><strong>執行有發生而失敗被吞掉</strong>：元件確實執行了，失敗發生在中途，而錯誤沒有傳回呼叫的一方（例外之後回 200、訊息被確認收訊）。這一個位置最難發現，因為「有沒有執行」的證據是充分的，執行紀錄裡有它被叫起來的那一筆。</li>
<li><strong>測試從中途注入</strong>：合成的條件從路徑中途的一個入口送進去，終點也出現了，而入口之前、把真實事件轉成那個條件的那一段沒有被行使到，真實情況下條件可能遲遲不成立（測試訊息直接發到主題；而預算的預設抵免設定會扣掉抵免，帳戶有大額抵免時，真實通知裡的花費在抵免用完之前都接近 0，門檻不會觸發）。</li>
</ul>
<p>驗證步驟本身也會給錯的答案，方向是假陰性：該查得到的紀錄或資源查不到（紀錄的欄位還沒填、帳單停用時清單回傳空的）。假陰性的危險在於零筆會被讀成一個結論、並據此行動——「新規則沒有命中」會讓人回頭去改一條本來就正確的規則，「資源都沒了」會讓人跳過清理。執行處置的步驟有同一種形態：<code>loginctl terminate-user</code> 回報「使用者沒有登入」而那個使用者的行程仍在執行，處置指令的回報不等於處置的結果，完成要以行程清單（<code>pgrep -u &lt;user&gt;</code>）為準。</p>
<hr>
<h2 id="反模式">反模式</h2>
<p><strong>用部署指令的結果當驗收。</strong> 部署的人把「<code>gcloud functions deploy</code> 成功、狀態 ACTIVE」寫進驗收紀錄，接著去做下一件事。這一句描述的是部署管線，不是停損開關。那次驗證裡這句話成立的時候，函式一次都還沒有執行過。</p>
<p><strong>只驗證不動作的那一條分支。</strong> 停損開關收到一則低於預算的通知、記錄 <code>action=none</code>、帳單維持啟用——這證明訊息送得到、判斷邏輯會跑，而「切斷」那一條分支要用到的權限（讀取帳單狀態、解除帳單連結）一次都沒有被行使。權限缺一個，只有在實際要切斷的那一刻才會現形。</p>
<p><strong>把零筆當成「沒有發生」。</strong> 驗證查詢回傳零筆，結論寫成「新規則沒有被命中」或「資源已經被刪除」。零筆同時是「沒有發生」與「查詢看不到」的輸出，沒有對照組就分不開兩者。</p>
<p><strong>修好一個失效點就宣告完成。</strong> 補上 <code>run.invoker</code> 之後，低於預算的通知能讓函式執行並記錄 <code>none</code>，看起來整條鏈通了；而權限不足與預設不重試這兩個失效點，都要等到超支的訊息進來才會出現。失效點是串聯的，每修好一個，下一個才有機會露出來，所以修完之後要從頭再觸發一次終點，而不是只確認剛修的那一段。</p>
<hr>
<h2 id="修法">修法</h2>
<p>驗證一個平常不動作的防護機制，做法是<strong>合成觸發</strong>（synthetic trigger，與 synthetic monitoring 同一類做法）：在不造成實際損害的範圍內，造出它要偵測的那個條件，看終點有沒有發生。</p>
<ul>
<li><strong>先寫下終點</strong>：部署之前就寫出「生效時看得到什麼」，而且寫成這個機制要產生的那個狀態——帳單已停用、紀錄裡有這個帳號、值班手機收到通知。寫不出終點的時候，代表還不知道這個機制要做什麼。</li>
<li><strong>造出條件，並寫下測試送進路徑的位置（注入點）</strong>：停損開關用一則花費超過預算的測試訊息、稽核紀錄用一次真實的連線、告警用一次故障注入。測試訊息從觸發器的入口送進去（發到真實的 topic，而不是直接呼叫函式），入口之後的每一段連線與權限都會被行使到；入口之前的那一段（停損開關的預算範圍、抵免設定、發布到 topic 的權限）不會，要另外用一筆真實的輸入確認它產得出這個條件——停損開關讀第一則真實的預算通知，對照帳單報表上的金額。</li>
<li><strong>兩條分支都要走</strong>：會動作的那一條與不會動作的那一條各觸發一次。停損開關的低於預算訊息確認不會誤觸，超出預算訊息確認會切斷；兩者用到的權限不同。</li>
<li><strong>修完之後從頭再觸發一次</strong>：任何一段修好之後，回到「造出條件」那一步重新觸發，直到終點出現為止。</li>
<li><strong>驗證查詢配一個正對照</strong>：查詢回傳零筆時，先用一條不依賴同一個欄位的查詢，查一件確定存在的事（例如改用來源 IP 段查同一批連線、重新連結帳單之後再列一次資源）。正對照也查不到，問題在查詢，不在被查的對象。</li>
<li><strong>失敗要能被看見</strong>：觸發失敗時重試（Cloud Run functions 部署時加 <code>--retry</code>）、讓錯誤回到呼叫的一方，並在終點以外再放一個<a href="/blog/backend/knowledge-cards/dead-mans-switch/" data-link-title="Dead Man&#39;s Switch（心跳監看）" data-link-desc="需要知道監控或排程工作自己停掉了、而它停掉時發不出任何告警時，以「時限內沒收到訊號就通知」反過來判斷">心跳監看（dead man&rsquo;s switch）</a>：監看它多久沒有收到觸發來源的訊息，讓靜默本身也成為訊號。合成觸發驗的是這一次會動作，不保證之後的每一次——平台改版、權限被別人調整都會讓它再次失效——心跳監看補的正是這一段。觸發來源不會定期送訊息時（WAF 規則、告警規則只在事件發生時才有輸入），改成排程送一筆合成的輸入，監看它的結果有沒有在期限內出現在終點。心跳監看本身也是平常不動作的機制，照本節驗收一次。</li>
</ul>
<p>合成觸發排在部署當下與每次修改之後做。停損開關的合成觸發會實際停用帳單，可能連資料一起刪除，所以那一次要在測試專案、或在專案還沒有正式服務與要保留的資料時做；這個成本正是它不會被順手做掉的原因，所以要寫進部署程序，而不是留給記憶。</p>
<p>合成觸發本身會造成不可回復的損害時（處置會刪除資料，或正式環境隔離不出一個可以停的副本），改成分段驗證：在結構相同、可以拋棄的環境完整觸發一次，驗程式、路徑與重試；在正式環境只驗不經觸發也查得到的那幾段——權限用權限檢查工具（GCP 的 Policy Troubleshooter）查，路徑用不會動作的那一條分支走。這樣做缺的是正式環境上那一次真正的處置，驗收紀錄要寫明缺了這一段，心跳監看也照常放。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>驗收紀錄裡最後一個證據是部署指令的輸出、元件的狀態欄或「設定已套用」，沒有任何一筆來自這個機制要產生的效果。</li>
<li>一個停損、隔離或告警機制要防的那件事（超支、資料遺失、入侵）從部署到現在沒有發生過，也沒有人模擬過、在終點看到結果；機制本身每天都在跑的（例如排程備份），終點是還原成功，不是作業成功。</li>
<li>合成觸發從路徑中途注入，驗收紀錄沒有寫注入點，入口之前的那一段也沒有用真實輸入確認過。</li>
<li>測試只送過「不該動作」的那一種輸入。</li>
<li>一條驗證查詢回傳零筆，就直接寫成結論，旁邊沒有對照的查詢。</li>
<li>執行紀錄裡有例外或錯誤訊息，而呼叫端記錄的是成功（HTTP 200、訊息已確認、exit 0）。</li>
</ul>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../checkable-anchor-must-be-able-to-fall/">#266 可核對的錨要跟它該偵測的狀態連動</a></strong>：母原則。#266 的情境是寫作判定的覆蓋率指標；本卡把同一個要求套到已部署的防護機制上，補上它沒有處理的三個位置——元件狀態欄、被吞掉的失敗與從中途注入的測試——以及驗證查詢與處置指令的假陰性。</li>
<li><strong><a href="../independence-is-what-the-guarded-side-cannot-disable/">#371 觀察者與限制的獨立性</a></strong>：停損開關切斷帳單之後，它自己也跑在那個專案上，此後的通知它都處理不了；被保護的專案裡被入侵的身分若有編輯權限，也能直接刪掉它。合成觸發會讓前一種當場浮現，後一種要另外問；兩者是否可接受，都要用 #371 的失效域問一次。</li>
<li><strong><a href="../protective-comment-signals-missing-enforcement/">#253 寫註解的動機是怕被改壞時，要處理的是那個約束</a></strong>：#253 說防護要交給會發聲的機制、而判定靠當場破壞；本卡說交出去之後，那個機制自己是否會發聲也要當場破壞一次來確認。</li>
<li><strong><a href="../broken-scan-reports-a-plausible-number/">#291 掃描指令壞掉時不會報錯，它報一個數字</a></strong>：驗證查詢回傳零筆是同一個現象在雲端紀錄上的形態，修法也相同——先用一個已知會命中的輸入確認查詢本身。</li>
<li><strong><a href="../launch-time-settings-need-support-check-and-second-path/">#384 只能在建立時決定的設定</a></strong>：本卡的「元件顯示正常」處理部署之後怎麼驗；#384 把同一個訊號放到建立之前：元件狀態正常而組合不在支援範圍時，要在建立之前從支援清單與 Known Issues 查出來，建好之後才發現，補救的代價是整個資源。</li>
<li><strong><a href="../clean-room-reproduce-reveals-non-repo-state/">#227 可重現性只有乾淨機器重跑才驗得出</a></strong>：同樣是「讀設定驗不出來、要實際跑一次」的一類；#227 驗的是能否重建，本卡驗的是建好之後是否會動作。</li>
</ul>
]]></content:encoded></item></channel></rss>