<?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>Google Cloud（GCP） on Tarragon</title><link>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/</link><description>Recent content in Google Cloud（GCP） 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/devops/infra/cloud-providers/gcp/index.xml" rel="self" type="application/rss+xml"/><item><title>GCP VM 的 SSH 與 RDP 管理通道：從預設的全開規則改走 IAP TCP forwarding</title><link>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/gcp-iap-ssh-access/</link><pubDate>Fri, 23 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/gcp-iap-ssh-access/</guid><description>&lt;p>這篇整理 GCP Compute Engine 上管理員登入 VM 的通道（Linux 的 SSH、Windows 的 RDP），從防火牆對 &lt;code>0.0.0.0/0&lt;/code> 開放 22 與 3389 port 改成走 Identity-Aware Proxy（IAP）TCP forwarding 的機制與判斷依據：預設網路的規則與它們留下的暴露面、IAP 的連線路徑、切換的順序為什麼這樣排、連不上時的依層判讀、IAM 授權的範圍、OS Login、拿掉外部 IP 之後的出站路徑、組織層級的護欄與稽核。範圍從專案裡已經有 VM、而且用的是預設網路（&lt;code>default&lt;/code> network）開始；自建網路上手寫的全開規則，處理方式相同。從盤點、救援入口、建規則與授權、停用舊規則驗證、啟用 OS Login 到移除外部 IP 的逐步操作與實跑結果，在 &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>。IAP 這一類代理服務的共通機制見 &lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/identity-aware-proxy/" data-link-title="Identity-Aware Proxy（身分感知代理）" data-link-desc="要讓管理員或使用者不經公開入口連到內部主機與服務、評估把存取控制從來源 IP 改成帳號身分時，查身分感知代理的連線路徑、前提與代價">Identity-Aware Proxy（身分感知代理）&lt;/a>。&lt;/p>
&lt;p>管理通道為什麼要收斂、它拆成哪幾層、各層有哪些與廠商無關的做法，在&lt;a href="https://tarrragon.github.io/blog/devops/infra/03-network-foundation/management-access-channel/" data-link-title="伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口" data-link-desc="與雲端廠商無關、適用於雲端 VM、地端主機與 VPS 的管理員登入通道設計：拆成網路可達性、通道授權與主機登入驗證的模型，各層的做法（來源白名單、跳板機、VPN、身分感知代理、代理程式反向連線、SSH 憑證）、更換 port 與 fail2ban 改不了的部分，依存取者身分、連線送達方式、收回方式選擇組合的依據，以及移除公開 IP 之後的出站路徑、切換順序、救援入口、組織護欄與稽核">伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口&lt;/a>。本篇是那套模型在 GCP 上的一種組合：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>管理通道的層&lt;/th>
 &lt;th>本篇在 GCP 上的做法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>網路可達性&lt;/td>
 &lt;td>防火牆只放行 IAP 的來源位址段，VM 移除外部 IP&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>通道授權&lt;/td>
 &lt;td>Google 帳號加 IAM role &lt;code>roles/iap.tunnelResourceAccessor&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主機登入驗證&lt;/td>
 &lt;td>Linux 用 OS Login；Windows 用 VM 上的 Windows 帳號&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>救援入口用 Serial Console，它不經過上面三層的設定，有自己的 IAM 權限與開關。&lt;/p>
&lt;p>這個組合假設組織的身分提供者（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/identity-provider/" data-link-title="Identity Provider" data-link-desc="登入要交給外部系統時，用來定位對方承擔了什麼、沒承擔什麼，以及這邊還剩下哪些責任">identity provider&lt;/a>，IdP）是 Google 帳號：IAP 與 OS Login 都依 Google 的 IAM 判斷。組織用的是 Microsoft Entra ID、Okta 這類其他 IdP 時，有兩條路。一條是讓 Cloud Identity 以 SAML 單一登入接那個 IdP、並由 IdP 把帳號同步進來，使用者在原本的 IdP 登入、拿到的是 Google 身分，本篇全部適用。另一條是通道授權改用能接那個 IdP 的代理服務，網路可達性跟著那個代理走（防火牆改放行它的位址段，或改用不開入站 port 的反向連線）；這時本篇依賴 Google 帳號的幾節——〈IAP 的 IAM 授權範圍：誰拿得到通道〉、〈SSH 金鑰在 GCP 上的管理位置：metadata 金鑰與 OS Login 的啟用條件〉的 OS Login 與組織政策的 &lt;code>compute.requireOsLogin&lt;/code>、稽核紀錄裡 IAP 的 Data Access 紀錄——都不適用，Cloud NAT、移除外部 IP、Serial Console 與防火牆規則的收斂仍然適用。&lt;/p>
&lt;p>另外四種環境要先調整做法再往下讀：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>由 managed instance group（MIG）或 GKE 管理的 VM&lt;/strong>：tag、metadata、救援帳號與移除外部 IP 都要改在 instance template、node pool 設定或映像檔裡。逐台用 &lt;code>gcloud&lt;/code> 修改，VM 在自動修復、升級或縮放時重建，修改就消失，tag 消失之後 IAP 規則也套不到新的 VM。這兩種環境的設定位置、救援改用重建的做法與 GKE 控制層的存取，見&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>。&lt;/li>
&lt;li>&lt;strong>Shared VPC 的 service project&lt;/strong>：防火牆規則住在 host project，防火牆相關的指令要在 host project 執行；停用 &lt;code>default-allow-ssh&lt;/code> 這類規則會同時影響其他 service project 的 VM。&lt;/li>
&lt;li>&lt;strong>用 Terraform 這類 IaC 管理的專案&lt;/strong>：每一個 &lt;code>gcloud&lt;/code> 修改都要改寫成程式碼。只在 CLI 加的 tag、IAM 授權與 metadata，若所屬資源在程式碼裡被完整定義（例如 &lt;code>google_compute_instance&lt;/code> 寫了 &lt;code>tags&lt;/code>、用 &lt;code>google_project_iam_policy&lt;/code> 管 IAM），下一次 apply 會把它拿掉；只在 CLI 刪掉的規則與外部 IP，下一次 apply 會建回來。用 CLI 停用舊規則做驗證的期間要暫停所有 apply。兩個方向的落差與過渡期的做法見&lt;a href="https://tarrragon.github.io/blog/devops/infra/takeover/partial-iac-dual-truth-operation/" data-link-title="兩套真相並存的過渡期操作" data-link-desc="部分資源在 IaC、部分在手動時，怎麼安全操作避免比全手動更危險，以及怎麼縮短這個過渡期">兩套真相並存的過渡期操作&lt;/a>。&lt;/li>
&lt;li>&lt;strong>新專案從零開始&lt;/strong>：沒有舊規則要移走，切換前的盤點與停用驗證可以跳過，直接建 IAP 規則、OS Login 與 Cloud NAT；建 VM 時加 &lt;code>--no-address&lt;/code> 就不配外部 IP。2024 年 5 月 3 日之後建立的 Organization 會預設強制一組 Organization Policy，其中可能已經包含本篇〈組織層級的護欄：Organization Policy 與階層式防火牆〉提到的幾項；用 &lt;code>gcloud resource-manager org-policies list --organization=&amp;lt;org-id&amp;gt;&lt;/code> 查組織層級設定了哪些（&lt;code>&amp;lt;org-id&amp;gt;&lt;/code> 用 &lt;code>gcloud organizations list&lt;/code> 查）。&lt;/li>
&lt;/ul>
&lt;h2 id="預設網路自帶的防火牆規則">預設網路自帶的防火牆規則&lt;/h2>
&lt;p>專案第一次啟用 Compute Engine API 時，GCP 會自動建一個叫 &lt;code>default&lt;/code> 的 auto-mode VPC，並在上面預先建好四條入站規則。這四條規則的設計目標是讓第一次用 GCP 的人開完 VM 就連得上，所以來源範圍放得很寬。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理 GCP Compute Engine 上管理員登入 VM 的通道（Linux 的 SSH、Windows 的 RDP），從防火牆對 <code>0.0.0.0/0</code> 開放 22 與 3389 port 改成走 Identity-Aware Proxy（IAP）TCP forwarding 的機制與判斷依據：預設網路的規則與它們留下的暴露面、IAP 的連線路徑、切換的順序為什麼這樣排、連不上時的依層判讀、IAM 授權的範圍、OS Login、拿掉外部 IP 之後的出站路徑、組織層級的護欄與稽核。範圍從專案裡已經有 VM、而且用的是預設網路（<code>default</code> network）開始；自建網路上手寫的全開規則，處理方式相同。從盤點、救援入口、建規則與授權、停用舊規則驗證、啟用 OS Login 到移除外部 IP 的逐步操作與實跑結果，在 <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>。IAP 這一類代理服務的共通機制見 <a href="/blog/devops/infra/knowledge-cards/identity-aware-proxy/" data-link-title="Identity-Aware Proxy（身分感知代理）" data-link-desc="要讓管理員或使用者不經公開入口連到內部主機與服務、評估把存取控制從來源 IP 改成帳號身分時，查身分感知代理的連線路徑、前提與代價">Identity-Aware Proxy（身分感知代理）</a>。</p>
<p>管理通道為什麼要收斂、它拆成哪幾層、各層有哪些與廠商無關的做法，在<a href="/blog/devops/infra/03-network-foundation/management-access-channel/" data-link-title="伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口" data-link-desc="與雲端廠商無關、適用於雲端 VM、地端主機與 VPS 的管理員登入通道設計：拆成網路可達性、通道授權與主機登入驗證的模型，各層的做法（來源白名單、跳板機、VPN、身分感知代理、代理程式反向連線、SSH 憑證）、更換 port 與 fail2ban 改不了的部分，依存取者身分、連線送達方式、收回方式選擇組合的依據，以及移除公開 IP 之後的出站路徑、切換順序、救援入口、組織護欄與稽核">伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口</a>。本篇是那套模型在 GCP 上的一種組合：</p>
<table>
  <thead>
      <tr>
          <th>管理通道的層</th>
          <th>本篇在 GCP 上的做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>網路可達性</td>
          <td>防火牆只放行 IAP 的來源位址段，VM 移除外部 IP</td>
      </tr>
      <tr>
          <td>通道授權</td>
          <td>Google 帳號加 IAM role <code>roles/iap.tunnelResourceAccessor</code></td>
      </tr>
      <tr>
          <td>主機登入驗證</td>
          <td>Linux 用 OS Login；Windows 用 VM 上的 Windows 帳號</td>
      </tr>
  </tbody>
</table>
<p>救援入口用 Serial Console，它不經過上面三層的設定，有自己的 IAM 權限與開關。</p>
<p>這個組合假設組織的身分提供者（<a href="/blog/backend/knowledge-cards/identity-provider/" data-link-title="Identity Provider" data-link-desc="登入要交給外部系統時，用來定位對方承擔了什麼、沒承擔什麼，以及這邊還剩下哪些責任">identity provider</a>，IdP）是 Google 帳號：IAP 與 OS Login 都依 Google 的 IAM 判斷。組織用的是 Microsoft Entra ID、Okta 這類其他 IdP 時，有兩條路。一條是讓 Cloud Identity 以 SAML 單一登入接那個 IdP、並由 IdP 把帳號同步進來，使用者在原本的 IdP 登入、拿到的是 Google 身分，本篇全部適用。另一條是通道授權改用能接那個 IdP 的代理服務，網路可達性跟著那個代理走（防火牆改放行它的位址段，或改用不開入站 port 的反向連線）；這時本篇依賴 Google 帳號的幾節——〈IAP 的 IAM 授權範圍：誰拿得到通道〉、〈SSH 金鑰在 GCP 上的管理位置：metadata 金鑰與 OS Login 的啟用條件〉的 OS Login 與組織政策的 <code>compute.requireOsLogin</code>、稽核紀錄裡 IAP 的 Data Access 紀錄——都不適用，Cloud NAT、移除外部 IP、Serial Console 與防火牆規則的收斂仍然適用。</p>
<p>另外四種環境要先調整做法再往下讀：</p>
<ul>
<li><strong>由 managed instance group（MIG）或 GKE 管理的 VM</strong>：tag、metadata、救援帳號與移除外部 IP 都要改在 instance template、node pool 設定或映像檔裡。逐台用 <code>gcloud</code> 修改，VM 在自動修復、升級或縮放時重建，修改就消失，tag 消失之後 IAP 規則也套不到新的 VM。這兩種環境的設定位置、救援改用重建的做法與 GKE 控制層的存取，見<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>。</li>
<li><strong>Shared VPC 的 service project</strong>：防火牆規則住在 host project，防火牆相關的指令要在 host project 執行；停用 <code>default-allow-ssh</code> 這類規則會同時影響其他 service project 的 VM。</li>
<li><strong>用 Terraform 這類 IaC 管理的專案</strong>：每一個 <code>gcloud</code> 修改都要改寫成程式碼。只在 CLI 加的 tag、IAM 授權與 metadata，若所屬資源在程式碼裡被完整定義（例如 <code>google_compute_instance</code> 寫了 <code>tags</code>、用 <code>google_project_iam_policy</code> 管 IAM），下一次 apply 會把它拿掉；只在 CLI 刪掉的規則與外部 IP，下一次 apply 會建回來。用 CLI 停用舊規則做驗證的期間要暫停所有 apply。兩個方向的落差與過渡期的做法見<a href="/blog/devops/infra/takeover/partial-iac-dual-truth-operation/" data-link-title="兩套真相並存的過渡期操作" data-link-desc="部分資源在 IaC、部分在手動時，怎麼安全操作避免比全手動更危險，以及怎麼縮短這個過渡期">兩套真相並存的過渡期操作</a>。</li>
<li><strong>新專案從零開始</strong>：沒有舊規則要移走，切換前的盤點與停用驗證可以跳過，直接建 IAP 規則、OS Login 與 Cloud NAT；建 VM 時加 <code>--no-address</code> 就不配外部 IP。2024 年 5 月 3 日之後建立的 Organization 會預設強制一組 Organization Policy，其中可能已經包含本篇〈組織層級的護欄：Organization Policy 與階層式防火牆〉提到的幾項；用 <code>gcloud resource-manager org-policies list --organization=&lt;org-id&gt;</code> 查組織層級設定了哪些（<code>&lt;org-id&gt;</code> 用 <code>gcloud organizations list</code> 查）。</li>
</ul>
<h2 id="預設網路自帶的防火牆規則">預設網路自帶的防火牆規則</h2>
<p>專案第一次啟用 Compute Engine API 時，GCP 會自動建一個叫 <code>default</code> 的 auto-mode VPC，並在上面預先建好四條入站規則。這四條規則的設計目標是讓第一次用 GCP 的人開完 VM 就連得上，所以來源範圍放得很寬。</p>
<table>
  <thead>
      <tr>
          <th>規則名稱</th>
          <th>放行內容</th>
          <th>來源範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>default-allow-ssh</code></td>
          <td>tcp:22</td>
          <td><code>0.0.0.0/0</code></td>
      </tr>
      <tr>
          <td><code>default-allow-rdp</code></td>
          <td>tcp:3389</td>
          <td><code>0.0.0.0/0</code></td>
      </tr>
      <tr>
          <td><code>default-allow-icmp</code></td>
          <td>ICMP</td>
          <td><code>0.0.0.0/0</code></td>
      </tr>
      <tr>
          <td><code>default-allow-internal</code></td>
          <td>同網路內 TCP、UDP 所有 port 與 ICMP</td>
          <td><code>10.128.0.0/9</code></td>
      </tr>
  </tbody>
</table>
<p>GCP 的防火牆規則依 priority 數字由小到大評估。這四條的 priority 是 65534，只排在 VPC 隱含的「拒絕所有入站」規則（65535）前面，所以自己建立的規則（不指定時是 1000）都會比它們先被評估。它們也沒有指定 target tag（防火牆規則用來挑選作用對象的標記），作用對象是這個網路裡的每一台 VM。Oracle Cloud 與 AWS 預設網路的同類規則見 <a href="/blog/devops/infra/cloud-providers/vm-options/networking/" data-link-title="網路：subnet、私有 IP、公開 IP 與防火牆規則" data-link-desc="雲端主機建立頁網路區塊的選項：主機放進哪個虛擬網路與 subnet、私有 IP 與公開 IP 各自的用途與生命週期、防火牆規則掛在 subnet、網卡與作業系統哪一個位置以及各家怎麼合併，以及主控台新建的網路預設對外開放了什麼">網路：subnet、私有 IP、公開 IP 與防火牆規則</a> 的〈各家預設網路規則對 SSH 的開放〉。</p>
<p>只收掉 <code>default-allow-ssh</code> 會留下兩個暴露面。<code>default-allow-rdp</code> 讓每一台 Windows VM 的遠端桌面對整個網際網路開放，而 RDP 預設接受密碼登入。<code>default-allow-internal</code> 讓同一個網路裡的 VM 之間不受 port 限制，一台 VM 被入侵之後，攻擊者從那台機器能直接連到其他 VM 的資料庫 port 與管理 port，這種擴散方式叫<a href="/blog/backend/knowledge-cards/lateral-movement/" data-link-title="Lateral Movement（橫向移動）" data-link-desc="評估一台主機被入侵之後攻擊者還連得到哪些機器、內網或 VPN 裡的可達範圍要收到多窄時，查攻擊者以已經拿下的機器為起點往內擴散的方式">橫向移動（lateral movement）</a>。另外，這四條自動建立的規則預設沒有開啟 firewall rules logging，防火牆這一層不會留下被掃描或被連入的紀錄。管理 port 對外開放的風險與廠商無關，見<a href="/blog/devops/infra/03-network-foundation/management-access-channel/" data-link-title="伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口" data-link-desc="與雲端廠商無關、適用於雲端 VM、地端主機與 VPS 的管理員登入通道設計：拆成網路可達性、通道授權與主機登入驗證的模型，各層的做法（來源白名單、跳板機、VPN、身分感知代理、代理程式反向連線、SSH 憑證）、更換 port 與 fail2ban 改不了的部分，依存取者身分、連線送達方式、收回方式選擇組合的依據，以及移除公開 IP 之後的出站路徑、切換順序、救援入口、組織護欄與稽核">伺服器管理通道的設計</a>的〈對外開放管理 port 的實際風險〉。</p>
<p>CIS Google Cloud Platform Foundation Benchmark 的 3.1 條因此建議專案裡不保留 <code>default</code> 網路，改用自己規劃 subnet 的 custom-mode VPC。已經有服務跑在 <code>default</code> 網路上的專案，可以先收斂這四條規則，搬遷網路排進另一次變更；收斂 <code>default-allow-internal</code> 與 <code>default-allow-icmp</code> 的指令與實測結果在 <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> 的〈收斂 default-allow-internal 與 default-allow-icmp〉。</p>
<p>列出專案目前在 <code>default</code> 網路上的規則，<code>--format</code> 讓 <code>gcloud</code> 把來源範圍與 target tag 兩欄加進輸出（預設表格沒有這兩欄）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 列出 default 網路上的防火牆規則，含來源範圍、放行內容與 target tag</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute firewall-rules list --filter<span class="o">=</span><span class="s2">&#34;network:default&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --format<span class="o">=</span><span class="s2">&#34;table(name,priority,sourceRanges.list():label=SRC_RANGES,allowed[].map().firewall_rule().list():label=ALLOW,targetTags.list():label=TARGET_TAGS,disabled)&#34;</span></span></span></code></pre></div><p>輸出裡要看 <code>SRC_RANGES</code> 欄是 <code>0.0.0.0/0</code> 的列，以及 <code>TARGET_TAGS</code> 欄是空白的列——空白代表規則套用到整個網路的每一台 VM。這兩條規則要換掉、不能直接刪：刪除的當下，管理員自己的 SSH 與 RDP 連線會一起中斷，理由見〈切換到 IAP 的順序〉。從 Security Command Center 的 <code>OPEN_SSH_PORT</code> 或 <code>OPEN_RDP_PORT</code> finding 進來的，finding 裡寫著被標記的那條規則的資源名稱，以那個名稱替換指令裡的 <code>default-allow-ssh</code>；網路不是 <code>default</code> 時，把 <code>--filter</code> 裡的網路名稱一起換掉。</p>
<h2 id="iap-tcp-forwarding-的連線路徑與來源範圍">IAP TCP forwarding 的連線路徑與來源範圍</h2>
<p>IAP TCP forwarding 是 GCP 的身分感知代理：管理員的 <code>gcloud</code> 先用 HTTPS 連到 Google 的 IAP 端點，IAP 檢查這個 Google 帳號有沒有開通道的 IAM 權限，通過之後由 IAP 從 <code>35.235.240.0/20</code> 這段位址連到 VM 的內部 IP 與指定 port，再把兩端的位元組來回轉送。</p>
<p>一條連線因此由兩個元件各檢查一次，分別對應通道授權與主機登入驗證：</p>
<table>
  <thead>
      <tr>
          <th>驗證位置</th>
          <th>由誰檢查</th>
          <th>檢查什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>IAP 端點</td>
          <td>Google IAM</td>
          <td>這個 Google 帳號在這個 VM 上有沒有 <code>iap.tunnelInstances.accessViaIAP</code> 權限</td>
      </tr>
      <tr>
          <td>VM 內部</td>
          <td>Linux 上的 sshd／Windows 的遠端桌面服務</td>
          <td>SSH 金鑰在不在授權清單裡（metadata 金鑰或 OS Login）／Windows 帳號密碼</td>
      </tr>
  </tbody>
</table>
<p>IAP 只決定是否允許開通道，開了通道之後登入 VM 仍然要過 VM 自己的認證，所以兩者要分開設定、分開稽核：IAM 給得太寬，sshd 就暴露給不該碰這台 VM 的成員；VM 的金鑰管理鬆散，拿到通道的人就能用別人留下的金鑰登入。</p>
<p><code>35.235.240.0/20</code> 是 Google 文件公開的 IAP TCP forwarding 來源範圍，裡面包含 IAP 轉送時使用的全部位址。IPv6 的 VM 要另外放行 <code>2600:2d00:1:7::/64</code>；auto-mode 的 <code>default</code> 網路只有 IPv4 subnet，IPv6 的規則只在自建、開了 dual-stack subnet 的 custom-mode VPC 上用得到。這兩段位址是 Google 定義的規格值，每個專案都相同，可以直接寫進防火牆規則；Google 調整範圍時以 <a href="https://cloud.google.com/iap/docs/using-tcp-forwarding">IAP TCP forwarding 文件</a>為準。防火牆規則的關鍵是 <code>--source-ranges=35.235.240.0/20</code> 加上 <code>--target-tags</code> 限定作用的 VM、<code>--rules</code> 只開需要的 port，完整的建立指令與各引數在 <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> 的〈啟用 IAP API、開啟稽核紀錄並建立只放行 IAP 來源的防火牆規則〉。</p>
<p>設定完成之後，日常的連線指令是這幾條，<code>&lt;instance-name&gt;</code> 與 <code>&lt;zone&gt;</code> 用 <code>gcloud compute instances list</code> 查，第一欄是 VM 名稱、第二欄是 zone：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Linux：透過 IAP 通道 SSH 進 VM</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute ssh &lt;instance-name&gt; --zone<span class="o">=</span>&lt;zone&gt; --tunnel-through-iap
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># Linux：透過 IAP 通道傳檔</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">gcloud compute scp &lt;local-file&gt; &lt;instance-name&gt;:&lt;remote-path&gt; --zone<span class="o">=</span>&lt;zone&gt; --tunnel-through-iap
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># Windows：把 VM 的 3389 port 經 IAP 轉到本機的 13389，再用 RDP 用戶端連 localhost:13389</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">gcloud compute start-iap-tunnel &lt;windows-instance-name&gt; <span class="m">3389</span> --local-host-port<span class="o">=</span>localhost:13389 --zone<span class="o">=</span>&lt;zone&gt;</span></span></code></pre></div><p>主控台上 VM 旁的 SSH 按鈕（SSH-in-browser）不是這幾條指令：官方〈Connect to Linux VMs using IAP〉寫明它只在 VM 沒有外部 IP 時才走 IAP，VM 還帶外部 IP 時它連的是外部 IP，舊規則停用之後就連不上。本機的 port 用 3389 以外的號碼（這裡是 <code>13389</code>），管理員自己的 Windows 電腦開著遠端桌面時，本機的 3389 已經被占用。Ansible 這類用一般 <code>ssh</code> 連線的工具，在 ssh 設定裡用 <code>ProxyCommand</code> 讓 <code>gcloud</code> 開通道：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl"># ~/.ssh/config：&lt;vm-name&gt; 寫 VM 名稱，ssh 連線時代入 %h
</span></span><span class="line"><span class="ln">2</span><span class="cl">Host &lt;vm-name&gt;
</span></span><span class="line"><span class="ln">3</span><span class="cl">  ProxyCommand gcloud compute start-iap-tunnel %h 22 --listen-on-stdin --zone=&lt;zone&gt; --project=&lt;project-id&gt;
</span></span><span class="line"><span class="ln">4</span><span class="cl">  User &lt;os-login-username&gt;
</span></span><span class="line"><span class="ln">5</span><span class="cl">  IdentityFile &lt;private-key-file&gt;</span></span></code></pre></div><p><code>--listen-on-stdin</code> 讓 <code>gcloud</code> 把通道接到標準輸入輸出，ssh 就經由 IAP 連到 <code>%h</code>；這個旗標不出現在 <code>gcloud compute start-iap-tunnel --help</code> 的輸出裡，而功能存在。VM 啟用了 OS Login 時，一般 <code>ssh</code> 不會像 <code>gcloud compute ssh</code> 那樣自動把公鑰登錄到帳號的 OS Login profile，所以先以 Ansible 開通道所用的<a href="/blog/devops/infra/knowledge-cards/service-account/" data-link-title="Service Account（服務帳號）" data-link-desc="程式、排程或雲端服務要以自己的身分呼叫雲端 API、要決定給它哪些角色、遇到「狀態正常卻呼叫失敗」的權限問題、或看到 IAM 綁定裡的 serviceAccount: 成員時，查服務帳號是什麼、它的權限要從「它能做什麼」與「誰能以它的身分行事」兩邊設，以及它和人的帳號在管理上的差別">服務帳號</a>（<code>&lt;ci-service-account-email&gt;</code>）的身分登錄一次：<code>gcloud compute os-login ssh-keys add --key-file=&lt;public-key-file&gt; --impersonate-service-account=&lt;ci-service-account-email&gt;</code>，<code>&lt;public-key-file&gt;</code> 是 <code>&lt;private-key-file&gt;</code> 對應的公鑰檔；<code>&lt;os-login-username&gt;</code> 用同樣加上 <code>--impersonate-service-account</code> 的 <code>gcloud compute os-login describe-profile</code> 輸出裡的 <code>username</code>。</p>
<p>IAP 通道的代價在傳輸速度：所有位元組都經過 Google 的轉送，傳大檔案比直連慢，大量傳檔適合改用 Cloud Storage 當中轉。</p>
<h2 id="切換到-iap-的順序">切換到 IAP 的順序</h2>
<p>切換的順序由兩個事實決定。<code>default-allow-ssh</code> 的來源 <code>0.0.0.0/0</code> 本身就涵蓋 IAP 的位址段，所以舊規則還在的時候，經由 IAP 的連線成功，可能是舊規則放行的，證明不了新規則寫對了；驗證新規則因此要先停用舊規則。而停用舊規則的那一刻，從公開網際網路來的 SSH 與 RDP 都會被拒絕，包括管理員自己的連線與依賴這條規則的 CI、監控、廠商連線。所以：依賴舊規則的連線要先盤點出來並改走 IAP；救援入口（Serial Console 與本機救援帳號）要在停用之前準備好並實際登入過；新規則、tag 與 IAM 權限要在停用之前建好；停用之後確認新路徑可用，才刪除舊規則。與廠商無關的推導見<a href="/blog/devops/infra/03-network-foundation/management-access-channel/" data-link-title="伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口" data-link-desc="與雲端廠商無關、適用於雲端 VM、地端主機與 VPS 的管理員登入通道設計：拆成網路可達性、通道授權與主機登入驗證的模型，各層的做法（來源白名單、跳板機、VPN、身分感知代理、代理程式反向連線、SSH 憑證）、更換 port 與 fail2ban 改不了的部分，依存取者身分、連線送達方式、收回方式選擇組合的依據，以及移除公開 IP 之後的出站路徑、切換順序、救援入口、組織護欄與稽核">伺服器管理通道的設計</a>的〈切換管理通道時避免把自己鎖在外面〉。</p>
<p>這個順序裡有三件事的判斷落在 GCP 的機制上：</p>
<ul>
<li><strong>盤點靠 VM 上的登入紀錄，不只靠防火牆紀錄</strong>：全開規則的紀錄大部分是網際網路掃描程式的來源，CI 託管執行環境的 IP 每次不同，混在裡面認不出來；VM 上 sshd 紀錄的帳號與金鑰指紋才認得出是哪個人或哪個工具。</li>
<li><strong>驗證用停用而不是刪除</strong>：停用的規則用 <code>--no-disabled</code> 一行恢復原樣，刪除之後要重建、重建時可能寫錯；防火牆規則透過 GCP 的 API 修改，SSH 連不上也改得回來。</li>
<li><strong>Serial Console 的權限收不窄</strong>：連 Serial Console 需要能修改這台 VM 的權限（例如 <code>roles/compute.instanceAdmin.v1</code>），而這個角色也是一般 VM 管理會用到的權限，所以光靠 IAM 收不窄救援入口；真正把它收窄的是組織政策 <code>compute.disableSerialPortAccess</code>（見〈組織層級的護欄：Organization Policy 與階層式防火牆〉）。組織強制了這條政策時，救援名單裡要有持有組織政策權限、能對專案解除它的人。</li>
</ul>
<p>每一步的指令、實跑看到的輸出，以及每一步沒做或換一種做法時的結果，在 <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>：盤點在〈切換前的盤點：依賴舊規則的連線〉，救援入口在〈準備救援入口：Serial Console 與本機救援帳號〉，停用驗證在〈停用舊規則，用 IAP 驗證新規則〉，<code>default-allow-internal</code> 與 <code>default-allow-icmp</code> 的收斂在〈收斂 default-allow-internal 與 default-allow-icmp〉。</p>
<h2 id="iap-通道連不上時的依層判讀">IAP 通道連不上時的依層判讀</h2>
<p><code>--tunnel-through-iap</code> 的連線失敗時，錯誤訊息與它停在哪一層對得起來：</p>
<ul>
<li><strong>錯誤訊息說 API 未啟用</strong>：連線停在 IAP 端點之前，專案上還沒有啟用 <code>iap.googleapis.com</code>。</li>
<li><strong><code>4033</code>（not authorized）</strong>：停在通道授權。成因是這個帳號沒有 <code>roles/iap.tunnelResourceAccessor</code>、IAM Conditions 不允許這個 port 或 VM、條件的期限已過，或 VM 不存在、沒有在執行；帳號實際拿不拿得到權限，以 Policy Troubleshooter 的結果為準（見〈IAP 的 IAM 授權範圍：誰拿得到通道〉）。IAM 綁定送出之後約 60 到 120 秒才生效，這段時間內的 <code>4033</code> 不代表條件寫錯。</li>
<li><strong><code>4003</code>（failed to connect to backend）</strong>：IAP 已經放行，連不到 VM 的那個 port。成因在網路可達性那一層：VM 沒帶 tag、新規則的網路或 port 寫錯、組織的階層式防火牆或 network firewall policy 先擋下（這類政策優先於專案規則，專案規則寫對了也連不上），或 VM 上的作業系統防火牆（ufw、firewalld、Windows 防火牆）與 sshd 沒有在監聽。防火牆這一段由 Connectivity Tests（<code>gcloud network-management connectivity-tests create</code>，來源給 IAP 位址段裡的一個 IP、目的地給這台 VM 與 port）模擬一次 IAP 到 VM 的連線，結果指出是哪一條規則或政策擋下；完整的指令在 <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> 的〈停用舊規則，用 IAP 驗證新規則〉。VM 內部的成因要從 Serial Console 進去看。</li>
<li><strong>通道開了、登入時 <code>Permission denied (publickey)</code></strong>：停在主機登入驗證。啟用了 OS Login 時，成因是 OS Login 的登入角色或 <code>roles/iam.serviceAccountUser</code> 缺了；沒啟用時，成因是 metadata 裡沒有這把公鑰或缺修改 metadata 的權限（見〈SSH 金鑰在 GCP 上的管理位置：metadata 金鑰與 OS Login 的啟用條件〉）。</li>
</ul>
<h2 id="iap-的-iam-授權範圍誰拿得到通道">IAP 的 IAM 授權範圍：誰拿得到通道</h2>
<p>改走 IAP 之後，VM 的連線權限改由 IAM 決定，所以 IAM policy 的寬度就是 SSH 與 RDP 的暴露面。開通道的角色 <code>roles/iap.tunnelResourceAccessor</code> 可以用 IAM Conditions 限定 port（<code>destination.port == 22</code>）、單台 VM（<code>destination.ip == &quot;&lt;vm-internal-ip&gt;&quot;</code>）或期限（<code>request.time &lt; timestamp(&quot;&lt;expiry-time&gt;&quot;)</code>），各種條件的完整指令在 <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> 的〈授予開通道的 IAM 權限〉。幾個地方會讓這個範圍在不知不覺中變大或對不到人：</p>
<ul>
<li><strong>專案層級的基本角色</strong>：Owner 這一類基本角色涵蓋開通道的權限，而且不受 IAM Conditions 限制。日常登入用的帳號拿掉基本角色、改授本篇列出的具體角色，Owner 只留給緊急時才用的帳號；團隊全員都是 Owner 時，IAM Conditions 對任何人都不生效，OS Login 的單台先行驗證也找不到非 Owner 的人來做。綁定之前先用 <code>gcloud projects get-iam-policy &lt;project-id&gt;</code> 列出目前持有基本角色的成員，再用 IAM 的 Policy Troubleshooter 確認某個帳號實際拿不拿得到 <code>iap.tunnelInstances.accessViaIAP</code>；<code>get-iam-policy</code> 只列出直接綁定，群組與繼承來的權限以 Policy Troubleshooter 的結果為準。稽核要一份「所有拿得到通道的人」的清單時，用 Policy Analyzer（<code>gcloud asset analyze-iam-policy --organization=&lt;org-id&gt; --permissions=iap.tunnelInstances.accessViaIAP --expand-groups</code>），<code>--expand-groups</code> 讓結果把 Google Group 展開成成員（預設不展開，只列到群組），要逐台列出 VM 時另加 <code>--expand-resources</code>；Policy Troubleshooter 一次只回答一個帳號。</li>
<li><strong>整批授權</strong>：把 <code>roles/iap.tunnelResourceAccessor</code> 綁給 <code>domain:</code> 或大型群組，等於讓整個組織的帳號都能開通道。綁給負責維運的群組，並用 IAM Conditions 限定 port 與 VM。</li>
<li><strong>服務帳號</strong>：CI 或自動化工具用的服務帳號若持有通道權限，它的金鑰外洩就等同一把能連進所有 VM 的鑰匙。GCP 上這類帳號用 Workload Identity Federation（<a href="/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">Workload Identity</a>）取得短期存取憑證，不發長期金鑰：CI 平台簽發的 OIDC token 換成服務帳號的身分，設定方式以 GCP 的 Workload Identity Federation 文件為準，與 AWS 上用 OIDC 換取 IAM role 的對照見<a href="/blog/devops/infra/02-identity-credentials/iam-oidc-privilege-boundary/" data-link-title="身分與存取憑證地基 — IAM 模型、OIDC 短期憑證與權限邊界設計" data-link-desc="IAM 的 identity / policy / role 三元件、最小權限的持續收斂、用 OIDC 取代長期 access key，以及 SCP 與 Permissions Boundary 的環境隔離">身分與存取憑證地基 — IAM 模型、OIDC 短期憑證與權限邊界設計</a>的〈OIDC：給 CI/CD 的短期憑證〉。服務帳號要的角色與人相同：開通道的 <code>roles/iap.tunnelResourceAccessor</code>，啟用 OS Login 之後另加 OS Login 的登入角色，VM 掛著服務帳號時再加那個服務帳號的 <code>roles/iam.serviceAccountUser</code>。</li>
<li><strong>組織外的帳號</strong>：外包人員用他自己公司或個人的 Google 帳號時，OS Login 預設不讓組織外的帳號登入，要在組織層級另外授予 <code>roles/compute.osLoginExternalUser</code>（未實跑：測試帳號底下沒有 Organization）；組織若強制了 Organization Policy 的 <code>iam.allowedPolicyMemberDomains</code>（只允許組織網域的成員出現在 IAM 綁定裡），連綁定本身都會被拒絕；2024 年 5 月 3 日之後建立的 Organization 預設就強制這一條，用〈組織層級的護欄：Organization Policy 與階層式防火牆〉的 <code>org-policies list</code> 確認。這類帳號也不受組織的兩步驟驗證強制管轄，所以替外包人員在組織的 Cloud Identity 建帳號，比授權給外部帳號更容易控制，合約到期時停用帳號、或用帶 <code>request.time</code> 的條件讓綁定自動失效。</li>
</ul>
<p>通道授權改用 Google 帳號之後，帳號被釣魚，攻擊者就能從 IAP 開通道。GCP 上的對應設定是在 Google Workspace 或 Cloud Identity 對能開通道的帳號強制兩步驟驗證，管理員帳號用硬體安全金鑰或 <a href="/blog/backend/knowledge-cards/passkey/" data-link-title="Passkey" data-link-desc="要用金鑰對取代密碼、或判斷已導入的 passkey 實際擋掉了什麼時，用來定位它的保護落在哪一段、上限由哪條路徑決定">passkey</a> 這類防釣魚的方式——一次性驗證碼會在釣魚頁面上被一起騙走。<code>gcloud</code> 登入之後會在本機保存 refresh token，管理員帳號停用前這份 token 一直可用，而 token 檔可以被複製；Workspace 與 Cloud Identity 的管理控制台有 Google Cloud 工作階段控制，設定多久要重新驗證一次（1 到 24 小時）；這項設定預設關閉，啟用之後這個間隔才是本機 token 被複製之後能用多久的上限，被設成受信任而豁免的應用程式不受它限制。需要再收緊時，Context-Aware Access 可以在 IAM 綁定上加 access level 條件（Access Context Manager 定義的一組連線條件，例如只允許公司管理的裝置或特定網段）。</p>
<p>人員異動時，移出群組或移除 IAM 綁定只擋下下一次開通道；已經開著的工作階段要另外在 VM 上結束，做法見〈SSH 金鑰在 GCP 上的管理位置：metadata 金鑰與 OS Login 的啟用條件〉最後結束工作階段的那兩段。</p>
<h2 id="ssh-金鑰在-gcp-上的管理位置metadata-金鑰與-os-login-的啟用條件">SSH 金鑰在 GCP 上的管理位置：metadata 金鑰與 OS Login 的啟用條件</h2>
<p>GCP 上把 SSH 公鑰交給 Linux VM 有兩種方式，分別對應<a href="/blog/devops/infra/03-network-foundation/management-access-channel/" data-link-title="伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口" data-link-desc="與雲端廠商無關、適用於雲端 VM、地端主機與 VPS 的管理員登入通道設計：拆成網路可達性、通道授權與主機登入驗證的模型，各層的做法（來源白名單、跳板機、VPN、身分感知代理、代理程式反向連線、SSH 憑證）、更換 port 與 fail2ban 改不了的部分，依存取者身分、連線送達方式、收回方式選擇組合的依據，以及移除公開 IP 之後的出站路徑、切換順序、救援入口、組織護欄與稽核">伺服器管理通道的設計</a>〈主機登入驗證的做法：SSH 金鑰怎麼發、怎麼收回〉裡的靜態公鑰與由中央服務決定的公鑰：metadata 金鑰寫在專案或 VM 的 metadata（設在專案或 VM 上的鍵值設定）裡，由 VM 上的 guest agent（GCP 映像檔內建、把 metadata 同步進 VM 的常駐程式）寫進 <code>authorized_keys</code>；<a href="/blog/devops/infra/knowledge-cards/os-login/" data-link-title="OS Login" data-link-desc="GCP 專案要讓 SSH 登入權限跟著 Google 帳號與 IAM 走、啟用 OS Login 之後管理員或 CI 連不進 VM、或盤點專案 metadata 裡殘留的 SSH 公鑰時，查 OS Login 的運作方式、IAM role 與它和 metadata 金鑰的差別">OS Login</a> 依 Google 帳號與 IAM role 決定誰能登入、能不能用 sudo。寫在專案 metadata 的公鑰對專案內每一台沒有啟用 OS Login 的 VM 都生效，成員離職時漏刪一把，那把金鑰就持續能登入整個專案，而沒啟用 OS Login 時 <code>gcloud compute ssh</code> 預設就是把公鑰寫進專案 metadata。兩者的機制與差別見 OS Login 卡。Oracle Cloud 與 AWS 上不放金鑰時由哪些服務登入、各自包辦管理通道的哪幾層，見 <a href="/blog/devops/infra/cloud-providers/vm-options/login/" data-link-title="登入：SSH 金鑰、預設使用者、建立後加金鑰與不放金鑰時的登入服務" data-link-desc="雲端主機建立頁 SSH 金鑰欄位的幾個選項（讓業者產生、上傳、貼上、不放金鑰）各自把什麼放進主機、私鑰由誰保管，映像檔決定的預設使用者名稱，建立之後還能不能加金鑰、金鑰遺失時剩下哪些入口，以及完全不放金鑰時靠業者的哪些服務登入">登入：SSH 金鑰、預設使用者、建立後加金鑰與不放金鑰時的登入服務</a> 的〈不放金鑰時的登入服務〉。</p>
<p>啟用 OS Login（metadata 的 <code>enable-oslogin=TRUE</code>）之後，VM 不再接受 metadata 裡的金鑰，原本靠 metadata 金鑰登入的管理員與自動化工具會立刻連不進去。啟用的順序因此由三個條件決定：</p>
<ul>
<li><strong>角色先授</strong>：每一位管理員與每一個要登入 VM 的服務帳號先拿到登入角色（<code>roles/compute.osLogin</code> 一般登入，<code>roles/compute.osAdminLogin</code> 含 sudo）；VM 掛著服務帳號時（<code>gcloud compute instances describe</code> 輸出的 <code>serviceAccounts</code> 欄不是空的，用 Google Cloud 控制台以預設值建立的 VM 會掛上 Compute Engine 預設服務帳號），每一個登入者另外需要那個服務帳號的 <code>roles/iam.serviceAccountUser</code>。</li>
<li><strong>單台先行</strong>：先在單一 VM 上啟用，讓角色漏授這類錯誤只鎖住那一台；驗證要由非 Owner 的管理員做，Owner 本身持有全部權限，只由 Owner 驗證會看不出漏授。</li>
<li><strong>VM 層級蓋過專案層級</strong>：專案層級啟用之後，VM 層級設了 <code>enable-oslogin=FALSE</code> 的 VM 仍然讀 metadata 金鑰；專案 metadata 的 <code>ssh-keys</code> 留著前任與現任成員的公鑰，OS Login 一旦還原成 <code>FALSE</code> 它們就又能用，所以全部 VM 都啟用之後要清掉。</li>
</ul>
<p>啟用、還原、列出 <code>FALSE</code> 的 VM 與清除 <code>ssh-keys</code> 的指令，在 <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> 的〈啟用 OS Login：先授角色、單台先行、再做專案層級〉。OS Login 只作用在 Linux VM，Windows VM 的登入仍由 Windows 帳號決定。OS Login 也可以要求登入時的兩步驟驗證（metadata 設 <code>enable-oslogin-2fa=TRUE</code>），讓每一次 SSH 登入都多一次 Google 帳號的第二因素；沒有啟用兩步驟驗證的帳號會登不進去，所以同樣先對單一 VM 設定驗證。OS Login 文件寫明這項要求不套用在服務帳號上，CI 這類登入者不受它約束。</p>
<p>帳號停用或角色移除之後，下一次登入就會失敗；已經建立的工作階段不受影響，所以處理離職或帳號外洩時，管理員要另外登入 VM 結束那個使用者的工作階段。在 systemd 的發行版上：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 結束 systemd-logind 登記過的工作階段</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo loginctl terminate-user &lt;username&gt;
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 確認那個使用者的行程已經全部結束；有輸出就代表還有行程在跑</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">pgrep -u &lt;username&gt; -a
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 還有剩下的行程時，直接結束</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">sudo pkill -KILL -u &lt;username&gt;</span></span></code></pre></div><p><code>&lt;username&gt;</code> 是 OS Login 由 Google 帳號轉成的使用者名稱，例如 <code>alice_example_com</code>。確認的依據是 <code>pgrep</code> 的輸出，而不是 <code>terminate-user</code> 的回傳值：<code>loginctl</code> 只結束 systemd-logind 登記過的工作階段，登入時 <code>pam_systemd</code> 登記失敗的工作階段（VM 的日誌裡有 <code>pam_systemd(sshd:session): Failed to create session</code>）不在它的清單裡，這時 <code>loginctl terminate-user</code> 會回報使用者沒有登入，而行程仍在執行。Windows VM 上用 <code>query session</code> 找出那個使用者的 RDP 工作階段，再用 <code>logoff &lt;session-id&gt;</code> 結束。</p>
<h2 id="拿掉外部-ip-之後的出站路徑cloud-nat-與-private-google-access">拿掉外部 IP 之後的出站路徑：Cloud NAT 與 Private Google Access</h2>
<p>Cloud NAT 承接沒有外部 IP 的 VM 往外的連線，<code>apt update</code>、<code>docker pull</code>、呼叫第三方 API 都靠它；它和 AWS 的 NAT Gateway 承擔同一件事，機制見 <a href="/blog/devops/infra/knowledge-cards/nat/" data-link-title="NAT Gateway" data-link-desc="private subnet 或沒有外部 IP 的 VM 要主動對外連線、評估 NAT 的數量與出站成本、或在 AWS 與 GCP 之間對照出站做法時，查 NAT Gateway 的職責、部署模式與取捨">NAT</a>。Cloud NAT 建在 Cloud Router（GCP 管理 VPC 路由的元件）上，以 region 為單位生效，VM 分布在幾個 region 就要各建一組；出站 IP 可以讓 GCP 自動配發（可能變動），或改用預留的靜態 IP（外部服務要求來源 IP 白名單時）。VM 要存取 Cloud Storage、Artifact Registry 這類 Google API 時，subnet 開啟 Private Google Access，就能用內部 IP 連到 Google API，不經過 Cloud NAT，也就不產生 NAT 的資料處理費。</p>
<p>拿掉外部 IP 的判斷有兩點：</p>
<ul>
<li><strong>哪些 VM 可以拿</strong>：對外提供服務的 VM（網站、API）移除外部 IP 之前，要先把流量入口換到負載平衡器，否則移除的當下服務就中斷，入口層的設計見<a href="/blog/devops/infra/03-network-foundation/traffic-entry-layer/" data-link-title="流量入口層 — 請求怎麼從使用者到達應用" data-link-desc="自管環境要決定請求從使用者到應用經過哪幾層入口、每層承擔什麼——TLS 終結的位置、動靜分離、單機 reverse proxy 與雲端負載平衡器的選擇">流量入口層 — 請求怎麼從使用者到達應用</a>。只做管理用途的 VM 可以直接移除。</li>
<li><strong>驗證要在移除之後做</strong>：網卡還帶著外部 IP 時，Cloud NAT 不處理這張網卡的出站流量，流量走的是 VM 自己的外部 IP，所以移除之前測出站一定是通的，證明不了 Cloud NAT 有效。移除之後在 VM 上看出站的來源 IP，判斷依據是那個 IP 在不在 Cloud NAT 的 IP 清單（<code>gcloud compute routers get-nat-ip-info</code>）裡，而不是移除前後的 IP 有沒有變：自動配發的 Cloud NAT 可能正好配到這台 VM 剛釋放的那個外部 IP。</li>
</ul>
<p>建 Cloud NAT、開 Private Google Access、查 access config 名稱與移除外部 IP、驗證出站的指令與實跑結果，在 <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> 的〈建 Cloud NAT 並移除外部 IP〉。移除之後，這台 VM 只剩 IAP 一條管理路徑，連線一律加 <code>--tunnel-through-iap</code>。</p>
<h2 id="組織層級的護欄organization-policy-與階層式防火牆">組織層級的護欄：Organization Policy 與階層式防火牆</h2>
<p><code>default</code> 網路與 <code>default-allow-ssh</code>、<code>default-allow-rdp</code> 這些預設規則，每個新建的專案都會重新長出來，所以單一專案的收斂只保護那一個專案。要讓組織裡的新專案不再帶著這些規則，在 Organization Policy 設定下面幾個 constraint。這一節的指令需要專案屬於一個 Organization 資源，執行者要有組織層級的權限（<code>roles/orgpolicy.policyAdmin</code> 與 <code>roles/compute.orgFirewallPolicyAdmin</code>）；這一節的指令沒有實跑（測試帳號底下沒有 Organization）。2024 年 5 月 3 日之後建立的 Organization 會預設強制一組 constraint，先用 <code>gcloud resource-manager org-policies list --organization=&lt;org-id&gt;</code> 看哪些已經生效。</p>
<ul>
<li><code>compute.skipDefaultNetworkCreation</code>：新專案不再自動建立 <code>default</code> 網路。</li>
<li><code>compute.vmExternalIpAccess</code>：只允許列在清單上的 VM 持有外部 IP，其餘 VM 建立時帶外部 IP 會被拒絕。</li>
<li><code>compute.requireOsLogin</code>：新建的專案與 VM 啟用 OS Login，任何層級都無法用 metadata 把它關掉；既有 VM 是否已啟用要另外盤點，而既有的管理員要先拿到 OS Login 的登入角色。</li>
<li><code>compute.disableSerialPortAccess</code>：關閉 Serial Console 的存取，讓救援入口平時保持關閉；救援時由持有組織政策權限的人對那個專案解除，救援結束後恢復。</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 在組織層級強制布林型的 constraint（skipDefaultNetworkCreation、requireOsLogin、disableSerialPortAccess 同一種寫法）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud resource-manager org-policies enable-enforce compute.skipDefaultNetworkCreation <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --organization<span class="o">=</span>&lt;org-id&gt;
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 救援時，對單一專案解除 Serial Console 的限制</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">gcloud resource-manager org-policies disable-enforce compute.disableSerialPortAccess <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --project<span class="o">=</span>&lt;project-id&gt;
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 救援結束後，刪掉專案層級的例外，讓專案重新繼承組織層級的強制</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">gcloud resource-manager org-policies delete compute.disableSerialPortAccess <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --project<span class="o">=</span>&lt;project-id&gt;</span></span></code></pre></div><p><code>&lt;org-id&gt;</code> 用 <code>gcloud organizations list</code> 查。<code>compute.vmExternalIpAccess</code> 是清單型的 constraint，要用政策檔列出允許的 VM，寫法以 Organization Policy 文件為準。</p>
<p>階層式防火牆政策（hierarchical firewall policy）套用在組織或資料夾上，優先於專案的 VPC 防火牆規則評估。政策裡的規則依 priority 數字由小到大評估，命中 <code>allow</code> 或 <code>deny</code> 就停止，專案的規則不會再被看到；命中 <code>goto_next</code> 則把這條連線交給下一層（最終是專案的 VPC 規則）判斷。所以規則的相對 priority 決定結果：</p>
<ul>
<li>交給專案判斷的 IAP 規則（來源 <code>35.235.240.0/20</code>，有 IPv6 VM 時加上 <code>2600:2d00:1:7::/64</code>；tcp:22 與 tcp:3389；動作 <code>goto_next</code>）priority 要比拒絕規則小，否則 IAP 的流量先被拒絕規則擋下。它用 <code>goto_next</code> 而不用 <code>allow</code>：<code>allow</code> 會讓 IAP 的位址段直接通到組織內每一台 VM，繞過專案用 target tag 做的收斂。</li>
<li>VM 之間或從 VPN 進來的 SSH 需要保留時，它們的來源（內部網段、VPN 的位址段）同樣要有一條 priority 比拒絕規則小的 <code>goto_next</code> 規則，因為拒絕規則的 <code>0.0.0.0/0</code> 也涵蓋內部位址。</li>
<li>拒絕 <code>0.0.0.0/0</code> 連到 tcp:22 與 tcp:3389 的規則只涵蓋 IPv4，IPv6 要另寫一條拒絕 <code>::/0</code> 的規則，這兩條拒絕規則之間沒有先後依賴。</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 建立組織層級的防火牆政策，加入規則，再關聯到一個資料夾（之後再關聯到組織）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud compute firewall-policies create --short-name<span class="o">=</span>&lt;policy-name&gt; --organization<span class="o">=</span>&lt;org-id&gt;
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">gcloud compute firewall-policies rules create <span class="m">100</span> --firewall-policy<span class="o">=</span>&lt;policy-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --organization<span class="o">=</span>&lt;org-id&gt; --direction<span class="o">=</span>INGRESS --action<span class="o">=</span>goto_next <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --src-ip-ranges<span class="o">=</span>35.235.240.0/20 --layer4-configs<span class="o">=</span>tcp:22,tcp:3389
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">gcloud compute firewall-policies rules create <span class="m">1000</span> --firewall-policy<span class="o">=</span>&lt;policy-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --organization<span class="o">=</span>&lt;org-id&gt; --direction<span class="o">=</span>INGRESS --action<span class="o">=</span>deny <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --src-ip-ranges<span class="o">=</span>0.0.0.0/0 --layer4-configs<span class="o">=</span>tcp:22,tcp:3389
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">gcloud compute firewall-policies associations create --firewall-policy<span class="o">=</span>&lt;policy-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --organization<span class="o">=</span>&lt;org-id&gt; --folder<span class="o">=</span>&lt;folder-id&gt;</span></span></code></pre></div><p><code>100</code>、<code>1000</code> 是規則的 priority，只要 <code>goto_next</code> 那幾條比拒絕規則小就行，中間留空號給之後要插入的規則。<code>&lt;policy-name&gt;</code> 是自己取的政策名稱。<code>--folder=&lt;folder-id&gt;</code> 把政策關聯到一個資料夾；組織與資料夾層級的政策影響其下每一個專案，先關聯到一個測試用的資料夾，確認既有服務沒有依賴被擋掉的路徑，再關聯到組織（拿掉 <code>--folder</code>）。</p>
<h2 id="稽核與偵測firewall-logcloud-audit-logs-與-security-command-center">稽核與偵測：firewall log、Cloud Audit Logs 與 Security Command Center</h2>
<p><a href="/blog/devops/infra/03-network-foundation/management-access-channel/" data-link-title="伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口" data-link-desc="與雲端廠商無關、適用於雲端 VM、地端主機與 VPS 的管理員登入通道設計：拆成網路可達性、通道授權與主機登入驗證的模型，各層的做法（來源白名單、跳板機、VPN、身分感知代理、代理程式反向連線、SSH 憑證）、更換 port 與 fail2ban 改不了的部分，依存取者身分、連線送達方式、收回方式選擇組合的依據，以及移除公開 IP 之後的出站路徑、切換順序、救援入口、組織護欄與稽核">伺服器管理通道的設計</a>〈組織層級的護欄與稽核紀錄〉列出事後要查的三項事件，在 GCP 上分別從這幾個來源查：</p>
<ul>
<li>
<p><strong>誰在什麼時候經由哪條通道登入了哪台 VM</strong>：開通道的帳號與目標 VM 記在 Cloud Audit Logs 裡 IAP 的 Data Access 紀錄，要為 Cloud Identity-Aware Proxy 開啟「管理員讀取」（<code>ADMIN_READ</code>）類型才有：開通道時 IAP 檢查的權限 <code>iap.tunnelInstances.accessViaIAP</code> 歸在這個類型，只開「資料讀取」（<code>DATA_READ</code>）時，實測連了十次左右，開通道的紀錄一筆都沒有。開啟之後經由 IAP 連一次、查到方法名稱為 <code>AuthorizeUser</code> 的紀錄才算開對，查詢指令在 <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> 的〈啟用 IAP API、開啟稽核紀錄並建立只放行 IAP 來源的防火牆規則〉。登入 VM 的帳號記在 VM 上的 sshd 紀錄，啟用 OS Login 時是由 Google 帳號轉成的使用者名稱。啟用 OS Login 時，每一次登入另外在 Cloud Audit Logs 留下 OS Login 檢查登入權限的 Data Access 紀錄，記著 Google 帳號與目標 VM，而且存在 VM 之外，取得 VM root 的人清不掉：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 列出最近七天每一次 OS Login 的時間、Google 帳號與目標 VM；--freshness 依調查範圍調整</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud logging <span class="nb">read</span> <span class="s1">&#39;protoPayload.serviceName=&#34;oslogin.googleapis.com&#34; AND protoPayload.methodName:&#34;CheckPolicy&#34;&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --freshness<span class="o">=</span>7d --format<span class="o">=</span><span class="s2">&#34;value(timestamp,protoPayload.authenticationInfo.principalEmail,protoPayload.resourceName)&#34;</span></span></span></code></pre></div><p>實測的專案沒有為 Cloud OS Login API 另外開啟 Data Access 紀錄，<code>CheckPolicy</code> 的紀錄仍然寫進了 <code>data_access</code> 這個紀錄檔；查不到時，到「IAM 與管理 &gt; 稽核記錄」確認 Cloud OS Login API 的 Data Access 設定，並用 <code>gcloud projects get-iam-policy &lt;project-id&gt; --format=&quot;json(auditConfigs)&quot;</code> 看專案層有沒有排除它。firewall rules logging 記錄每一次放行的來源 IP、目的 VM、port 與命中的規則名稱，經由 IAP 的連線來源都是 IAP 的位址段，從這份紀錄看不出是哪個人。</p>
</li>
<li>
<p><strong>誰在什麼時候開了救援入口</strong>：開啟 Serial Console 的 metadata 修改與組織政策的解除屬於 Admin Activity 紀錄，預設就有。</p>
</li>
<li>
<p><strong>環境裡是否出現新的對外開放管理 port</strong>：Security Command Center 的 Security Health Analytics 持續掃描組織內的防火牆規則，<code>OPEN_SSH_PORT</code> 與 <code>OPEN_RDP_PORT</code> 這兩類 finding 對應的就是對外開放 22 與 3389 的規則。處理方式是照〈切換到 IAP 的順序〉把規則換掉（逐步指令在 <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>），直接刪除會中斷管理員自己的連線；規則刪除或縮小來源之後，下一次掃描會把對應的 finding 標為已解決。</p>
</li>
</ul>
<p>事件發生時才發現 IAP 的 Data Access 紀錄沒開，開啟之前的通道紀錄查不回來，剩下的來源是 Admin Activity 紀錄裡 metadata 與 IAM 的變更，以及 VM 上的 sshd 紀錄；sshd 紀錄只存在 VM 本機，取得 root 的攻擊者可以清掉，所以要保全時先把 VM 的磁碟做快照，證據的保全與分流見 <a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18 Incident Intake &amp; Evidence Triage</a>。</p>
<p>AWS 上同類的盤點流程見 <a href="/blog/devops/infra/03-network-foundation/security-group-audit-cleanup/" data-link-title="Security Group 稽核與清理" data-link-desc="盤點所有 security group 規則、找出 0.0.0.0/0 全開與未使用的 SG、依賴檢查後安全刪除、自動化治理">Security Group 稽核與清理</a>：兩邊都是先列出來源是 <code>0.0.0.0/0</code> 的規則，逐條確認有沒有服務依賴，再刪除或縮小來源。</p>
<h2 id="延伸閱讀逐步的實作與廠商無關的設計模型與-vpc-的分層規則">延伸閱讀：逐步的實作、與廠商無關的設計模型與 VPC 的分層規則</h2>
<ul>
<li>→ <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>：盤點、救援入口、IAP 規則與 IAM、停用舊規則驗證、OS Login 啟用與移除外部 IP 的逐步指令、實跑看到的輸出，以及每一步沒做或換一種做法時的結果</li>
<li>→ <a href="/blog/devops/infra/03-network-foundation/management-access-channel/" data-link-title="伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口" data-link-desc="與雲端廠商無關、適用於雲端 VM、地端主機與 VPS 的管理員登入通道設計：拆成網路可達性、通道授權與主機登入驗證的模型，各層的做法（來源白名單、跳板機、VPN、身分感知代理、代理程式反向連線、SSH 憑證）、更換 port 與 fail2ban 改不了的部分，依存取者身分、連線送達方式、收回方式選擇組合的依據，以及移除公開 IP 之後的出站路徑、切換順序、救援入口、組織護欄與稽核">伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口</a>：跳板機、VPN、代理程式反向連線、SSH 憑證這些 GCP 以外的做法，以及依存取者身分、代理把連線送進主機的方式、金鑰收回方式選擇組合的依據</li>
<li>→ <a href="/blog/devops/infra/03-network-foundation/openssh-ca-setup/" data-link-title="OpenSSH 內建 CA 的 SSH 憑證導入：CA 金鑰、sshd 設定、簽發、principal、撤銷與主機憑證" data-link-desc="用 OpenSSH 自帶的 ssh-keygen 與 sshd 建立 SSH 憑證的完整流程：使用者 CA 與主機 CA 的金鑰怎麼產生與保管、sshd 的 TrustedUserCAKeys 與 AuthorizedPrincipalsFile、簽發時的識別字串與有效期、憑證外洩時用 KRL 撤銷，以及用主機憑證取代逐台確認指紋，以及導入順序與兩個會讓所有人登不進去的設定：AuthorizedPrincipalsFile 的讀取權限、RevokedKeys 指的檔案不存在">OpenSSH 內建 CA 的 SSH 憑證導入</a>：主機不全在 GCP、OS Login 用不上時，改用 SSH 憑證讓登入權限自動到期</li>
<li>→ <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>：本篇的 tag、OS Login 與移除外部 IP 在 instance template 與 node pool 上的寫法，以及 GKE 控制平面的存取</li>
<li>→ <a href="/blog/devops/infra/03-network-foundation/vpc-subnet-security-group/" data-link-title="網路地基 — VPC、subnet 分層與 security group 設計" data-link-desc="VPC CIDR 規劃、public / private subnet 切分、route table 與 NAT 的可用性成本取捨、security group 最小開放設計，以及 NACL 的定位">網路地基 — VPC、subnet 分層與 security group 設計</a>：VPC 與 subnet 的分層與最小開放原則，本篇的防火牆規則是同一套原則在 GCP 上的寫法</li>
</ul>
]]></content:encoded></item><item><title>GCP 上由 MIG 與 GKE 管理的 VM 的管理存取：instance template、node pool、kubectl debug 與控制平面存取</title><link>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/gke-mig-management-access/</link><pubDate>Fri, 23 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/gke-mig-management-access/</guid><description>&lt;p>這篇處理 GCP 上兩種由 template 自動建立與重建 VM 的環境，其中 GKE 是 Google Kubernetes Engine，GCP 託管的 Kubernetes 服務：managed instance group（&lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/managed-instance-group/" data-link-title="Managed Instance Group（MIG，代管執行個體群組）" data-link-desc="VM 被自動重建之後設定不見、要決定一組 VM 的設定該寫在哪裡，或要判斷一台 VM 能不能直接刪掉重建時，查 GCP 的 MIG 與 instance template 怎麼建立、修復與更新 VM">MIG&lt;/a>，依 instance template 自動建立、修復與縮放一組 VM）與 GKE 的 node pool（一組共用同一份設定的叢集節點，GKE 在底下用 MIG 管理它們）。範圍是在這兩種環境裡套用 &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/knowledge-cards/identity-aware-proxy/" data-link-title="Identity-Aware Proxy（身分感知代理）" data-link-desc="要讓管理員或使用者不經公開入口連到內部主機與服務、評估把存取控制從來源 IP 改成帳號身分時，查身分感知代理的連線路徑、前提與代價">IAP&lt;/a>（Identity-Aware Proxy，GCP 依 IAM 權限轉送 SSH 連線的代理）、&lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/os-login/" data-link-title="OS Login" data-link-desc="GCP 專案要讓 SSH 登入權限跟著 Google 帳號與 IAM 走、啟用 OS Login 之後管理員或 CI 連不進 VM、或盤點專案 metadata 裡殘留的 SSH 公鑰時，查 OS Login 的運作方式、IAM role 與它和 metadata 金鑰的差別">OS Login&lt;/a>（讓 VM 的登入帳號與公鑰由 Google 帳號與 IAM 決定）與移除外部 IP：這些設定為什麼要寫在 template 與 node pool、怎麼判斷一台 VM 屬不屬於 MIG、MIG 的幾種救援各自適用的情形、GKE 的兩個存取面、&lt;code>kubectl debug node&lt;/code> 的權限邊界，以及 GKE 另外要管的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">控制平面（control plane）&lt;/a>存取。建新 template 與 canary 滾動更新、重建與主機金鑰的比對、除錯 namespace、node pool 設定與控制平面端點切換的逐步指令與實跑結果，在 &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/03-network-foundation/management-access-channel/" data-link-title="伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口" data-link-desc="與雲端廠商無關、適用於雲端 VM、地端主機與 VPS 的管理員登入通道設計：拆成網路可達性、通道授權與主機登入驗證的模型，各層的做法（來源白名單、跳板機、VPN、身分感知代理、代理程式反向連線、SSH 憑證）、更換 port 與 fail2ban 改不了的部分，依存取者身分、連線送達方式、收回方式選擇組合的依據，以及移除公開 IP 之後的出站路徑、切換順序、救援入口、組織護欄與稽核">伺服器管理通道的設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這篇處理 GCP 上兩種由 template 自動建立與重建 VM 的環境，其中 GKE 是 Google Kubernetes Engine，GCP 託管的 Kubernetes 服務：managed instance group（<a href="/blog/devops/infra/knowledge-cards/managed-instance-group/" data-link-title="Managed Instance Group（MIG，代管執行個體群組）" data-link-desc="VM 被自動重建之後設定不見、要決定一組 VM 的設定該寫在哪裡，或要判斷一台 VM 能不能直接刪掉重建時，查 GCP 的 MIG 與 instance template 怎麼建立、修復與更新 VM">MIG</a>，依 instance template 自動建立、修復與縮放一組 VM）與 GKE 的 node pool（一組共用同一份設定的叢集節點，GKE 在底下用 MIG 管理它們）。範圍是在這兩種環境裡套用 <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/knowledge-cards/identity-aware-proxy/" data-link-title="Identity-Aware Proxy（身分感知代理）" data-link-desc="要讓管理員或使用者不經公開入口連到內部主機與服務、評估把存取控制從來源 IP 改成帳號身分時，查身分感知代理的連線路徑、前提與代價">IAP</a>（Identity-Aware Proxy，GCP 依 IAM 權限轉送 SSH 連線的代理）、<a href="/blog/devops/infra/knowledge-cards/os-login/" data-link-title="OS Login" data-link-desc="GCP 專案要讓 SSH 登入權限跟著 Google 帳號與 IAM 走、啟用 OS Login 之後管理員或 CI 連不進 VM、或盤點專案 metadata 裡殘留的 SSH 公鑰時，查 OS Login 的運作方式、IAM role 與它和 metadata 金鑰的差別">OS Login</a>（讓 VM 的登入帳號與公鑰由 Google 帳號與 IAM 決定）與移除外部 IP：這些設定為什麼要寫在 template 與 node pool、怎麼判斷一台 VM 屬不屬於 MIG、MIG 的幾種救援各自適用的情形、GKE 的兩個存取面、<code>kubectl debug node</code> 的權限邊界，以及 GKE 另外要管的 <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">控制平面（control plane）</a>存取。建新 template 與 canary 滾動更新、重建與主機金鑰的比對、除錯 namespace、node pool 設定與控制平面端點切換的逐步指令與實跑結果，在 <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/03-network-foundation/management-access-channel/" data-link-title="伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口" data-link-desc="與雲端廠商無關、適用於雲端 VM、地端主機與 VPS 的管理員登入通道設計：拆成網路可達性、通道授權與主機登入驗證的模型，各層的做法（來源白名單、跳板機、VPN、身分感知代理、代理程式反向連線、SSH 憑證）、更換 port 與 fail2ban 改不了的部分，依存取者身分、連線送達方式、收回方式選擇組合的依據，以及移除公開 IP 之後的出站路徑、切換順序、救援入口、組織護欄與稽核">伺服器管理通道的設計</a>。</p>
<h2 id="autohealing滾動更新與擴增後消失的逐台設定network-tagos-login-metadata-與外部-ip">autohealing、滾動更新與擴增後消失的逐台設定：network tag、OS Login metadata 與外部 IP</h2>
<p>MIG 裡的每一台 VM 都由 instance template 建出來。有幾種時機 MIG 會依 template 重建或替換一台 VM，原本開機磁碟上的修改都不會留下：自動修復（autohealing，<a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">健康檢查</a>失敗時重建）、<a href="/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">滾動更新</a>（rolling update，換成新版 template），以及 GKE 的節點升級與節點自動修復；<a href="/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">autoscaler</a> 擴增台數時則直接依 template 新建。新建的 VM 只帶著 template 裡的設定，所以單台 VM 上做的修改——<code>gcloud compute instances add-tags</code> 加的 <a href="/blog/devops/infra/knowledge-cards/network-tag/" data-link-title="Network Tag（GCP 網路標籤）" data-link-desc="GCP 防火牆規則套不到某台 VM、新建的 VM 連不上，或要決定防火牆規則用 tag 還是服務帳號選 VM 時，查 network tag 怎麼讓防火牆規則選出要套用的 VM">network tag</a>（貼在 VM 上的標籤，防火牆規則用它選出要套用的 VM）、<code>add-metadata</code> 設的 <code>enable-oslogin</code>、登入之後 <code>useradd</code> 建的救援帳號、<code>delete-access-config</code> 移除的外部 IP——都只活到那台 VM 被重建為止，新加進來的 VM 則從一開始就沒有。</p>
<p>這種消失沒有任何通知。重建通常發生在半夜的自動修復或下一次擴增，而只放行 IAP 來源的防火牆規則靠 network tag 選 VM：新建的 VM 沒有那個 tag，這條規則就套不到它。收斂管理通道時通常也已經刪掉 <code>default-allow-ssh</code>（GCP 預設網路裡放行任何來源連 22 port 的規則），兩者加在一起，新 VM 上沒有任何一條規則放行 SSH，它就從管理通道上消失了。所以這兩種環境的做法是：<strong>管理通道的設定寫在建立 VM 的那一份設定裡</strong>（MIG 的 instance template、GKE 的 node pool 設定、或映像檔），逐台的 <code>gcloud compute instances</code> 指令只用在臨時查看。</p>
<p>instance template 建立之後不能修改，改設定的方式是建一份新的 template，再讓 MIG 滾動更新到新版；<code>instance-templates create</code> 從預設值開始建、不沿用舊 template 的任何設定，所以新 template 要把現行設定逐項帶上，漏掉的欄位會回到預設值。新版先讓一台 VM 換上（<a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a>），驗證通過再讓整組更新，template 寫錯時才只壞一台；建新 template 與 canary 的指令在 <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> 的〈建新的 instance template：現行設定照抄，再加 tag、OS Login 與不配外部 IP〉與〈先讓一台 VM 換成新 template（canary），再讓整組更新〉。MIG 與 GKE 多半由 Terraform 這類 <a href="/blog/devops/infra/knowledge-cards/iac/" data-link-title="Infrastructure as Code (IaC)" data-link-desc="用程式碼描述基礎設施的最終狀態，由工具負責收斂現實與描述的差異">IaC</a> 工具管理，這時 template 與 node pool 的修改都改在程式碼裡：在 CLI 上建的 template、改的 node pool 設定，下一次 apply 會依程式碼改回去，這種程式碼與實際狀態的落差叫 <a href="/blog/devops/infra/knowledge-cards/drift/" data-link-title="Drift（設定漂移）" data-link-desc="IaC 的 state 與雲端實際狀態之間的不一致，通常因為有人繞過 IaC 直接在 Console 改設定">drift</a>，過渡期的做法見<a href="/blog/devops/infra/takeover/partial-iac-dual-truth-operation/" data-link-title="兩套真相並存的過渡期操作" data-link-desc="部分資源在 IaC、部分在手動時，怎麼安全操作避免比全手動更危險，以及怎麼縮短這個過渡期">兩套真相並存的過渡期操作</a>。</p>
<p>要判斷一台 VM 屬不屬於 MIG，看它的 metadata（掛在 VM 上的鍵值設定，VM 內的程式與有讀取權限的帳號都讀得到）有沒有 <code>created-by</code> 這個鍵（值是那個 MIG 的資源路徑）。這個鍵由 MIG 在建立 VM 時寫入，VM 被移出 MIG（abandon）之後仍然留著，所以它標出的是建立這台 VM 的 MIG；要確認 VM 現在是否仍是那個 MIG 的成員，用 <code>gcloud compute instance-groups managed list-instances &lt;mig-name&gt; --zone=&lt;zone&gt;</code> 列出 MIG 目前的 VM 來對照。GKE 節點帶著 <code>goog-gke-node</code> 標籤。下面的指令用 <code>jq</code>（命令列的 JSON 處理工具）從 <code>gcloud</code> 的 JSON 輸出取出這個鍵，要先在本機裝好；<code>gcloud</code> 讀的是目前設定的專案，要查別的專案時加 <code>--project=&lt;project-id&gt;</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 列出 VM 與它所屬的 MIG（第二欄是 - 的就是單獨建立的 VM）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute instances list --format<span class="o">=</span>json <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  <span class="p">|</span> jq -r <span class="s1">&#39;.[] | [.name, (first((.metadata.items // [])[] | select(.key==&#34;created-by&#34;) | .value) // &#34;-&#34;)] | @tsv&#39;</span></span></span></code></pre></div><p>GKE 的節點同樣是 MIG 的成員，這條指令會把它們列成屬於某個名稱是 <code>gke-...-grp</code> 的 MIG。這類 MIG 由 GKE 管理：對它們建 template、滾動更新或 <code>recreate-instances</code>，改動會被 GKE 覆蓋或讓叢集狀態不一致，<code>recreate-instances</code> 也不會先把節點上的 Pod 移走。帶著 <code>goog-gke-node</code> 標籤的 VM 改走本篇 GKE 的幾節：設定改在 node pool，單一節點壞掉時用 <code>kubectl drain</code> 移走 Pod，再交給 GKE 的節點自動修復。</p>
<h2 id="mig-的救援重建退回舊版-template-與-serial-console-各自適用的情形">MIG 的救援：重建、退回舊版 template 與 Serial Console 各自適用的情形</h2>
<p>單台 VM 的救援入口是 Serial Console 加一個事先建好的本機救援帳號（<a href="/blog/devops/infra/knowledge-cards/serial-console/" data-link-title="Serial Console（序列主控台）" data-link-desc="SSH 因為防火牆、sshd 設定或磁碟滿而連不進主機，或要規劃管理通道斷掉時的備用路徑時，查序列主控台與頻外管理介面的用法與它本身的暴露面">Serial Console（序列主控台）</a>）。MIG 裡的 VM 多了兩條路，各自適用的情形不同：</p>
<ul>
<li><strong>依 template 重建那一台</strong>（<code>recreate-instances</code>）：一台 VM 連不上、而原因是那台 VM 自己壞了（sshd 設定改壞、磁碟滿）時，讓 MIG 依 template 重建它，比從 Serial Console 進去修快，也不必替每台 VM 維護救援密碼。重建的前提是 VM 上沒有只存在那一台的狀態：資料寫在外接的資料庫、物件儲存或另外掛載的磁碟上，VM 本身隨時可以丟。VM 上有不能丟的狀態時，MIG 的 stateful 設定把指定的磁碟、IP 與 metadata 標為要保留；狀態多到每台都不一樣時，它更適合當單獨的 VM 管理。</li>
<li><strong>退回上一版 template</strong>：重建修不好的是 template 本身的錯（tag 寫錯、啟動腳本有錯），重建出來的每一台都一樣壞，這種錯要整組退回上一版，所以舊版 template 留到新版驗證完之後再刪。</li>
<li><strong>Serial Console</strong>：需要登入一台壞掉的 VM 查原因、保留現場做事後分析時仍然可用。救援帳號的帳號與密碼雜湊要在建立<a href="/blog/backend/knowledge-cards/immutable-image/" data-link-title="Immutable Image（不可變映像）" data-link-desc="想「修改 / 升級一個已經 build 好的 image」、或搞不懂為什麼同一個 tag 隔天內容變了時回來讀 — image 是不可變的 build 產物、改了就是新 image">映像檔</a>時寫進去（用 Packer 這類建映像檔的工具，或在一台 VM 上建好帳號再把磁碟做成自訂映像檔），而不是寫進 template 的 startup-script，因為 startup-script 存在 metadata 裡，任何有讀取 metadata 權限的人都看得到，把密碼寫在那裡等於公開。寫進映像檔的代價是整組 VM 共用同一組密碼雜湊，要輪替就要重做映像檔、再滾動更新一次。</li>
</ul>
<p>重建之後要知道的一件事是主機金鑰換了：重建在原地換掉開機磁碟，VM 沿用原本的名稱與執行個體 ID，而 <code>gcloud compute ssh</code> 在本機用 <code>compute.&lt;instance-id&gt;</code> 記錄主機金鑰，所以第一次連線會出現 <code>WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!</code> 並拒絕連線。這個警告也是中間人攻擊時會看到的訊息，處理方式是先用不經過這條 SSH 連線的來源（guest attributes 回報的<a href="/blog/devops/infra/knowledge-cards/ssh-host-key/" data-link-title="SSH Host Key 與 known_hosts（主機金鑰）" data-link-desc="ssh 第一次連線問要不要信任指紋、出現 REMOTE HOST IDENTIFICATION HAS CHANGED 警告，或主機重建之後連不上時，查 host key 怎麼證明主機身分、known_hosts 記了什麼，以及首次信任的風險">主機金鑰</a>；guest attributes 是 VM 寫回 metadata server、可以經由 GCP API 讀取的鍵值，要事先設 <code>enable-guest-attributes=TRUE</code>）確認新的金鑰確實來自這台 VM，再移除舊紀錄，不關掉主機金鑰檢查。重建與退回的指令、重建做完的確認方式與主機金鑰的比對，在 <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> 的〈重建一台壞掉的 VM：recreate-instances 與重建之後的主機金鑰〉與〈template 本身寫錯時退回上一版〉；Serial Console 的開啟與救援帳號的登入，在 <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> 的〈準備救援入口：Serial Console 與本機救援帳號〉。</p>
<h2 id="gke-的存取面控制平面與節點">GKE 的存取面：控制平面與節點</h2>
<p>GKE 叢集有兩個不同的存取面，設計時要分開處理：</p>
<ul>
<li><strong>控制平面</strong>：Kubernetes 的 API server，<code>kubectl</code> 連的就是它。日常的部署、查 log、進 <a href="/blog/backend/knowledge-cards/pod/" data-link-title="Pod" data-link-desc="在 Kubernetes 的輸出或設定裡看到 Pod，要知道它和容器的關係、為什麼它的 IP 與名稱會變、以及誰負責替補它時">Pod</a> 執行指令都經過它，這是 GKE 上的主要管理通道。</li>
<li><strong>節點（node）</strong>：跑工作負載的 VM。節點的作業系統要登入的情形很少：GKE 管理節點的映像檔、升級與修復，節點本身也是隨時可以丟的。</li>
</ul>
<p>多數團隊在 GKE 上不需要節點的 SSH：查 Pod 的狀況用 <code>kubectl logs</code>、<code>kubectl exec</code>，查節點的狀況先看 Cloud Logging 裡的節點紀錄，需要看到節點作業系統時用 <code>kubectl debug node</code>。所以在 GKE 上收斂管理通道時，工作集中在控制平面的存取，節點的 SSH 盡量關掉。</p>
<p>GKE 有兩種叢集模式：Standard 由使用者管理 node pool 與節點設定；Autopilot 由 GKE 管理節點，使用者只管工作負載。〈kubectl debug node 的權限邊界：RBAC 與 Pod Security Admission〉裡建專用 namespace 與節點 SSH 的做法只適用於 Standard。</p>
<h2 id="kubectl-debug-node-的權限邊界rbac-與-pod-security-admission">kubectl debug node 的權限邊界：RBAC 與 Pod Security Admission</h2>
<p><code>kubectl debug node/&lt;node-name&gt;</code> 在指定的節點上建一個 Pod，把節點的根目錄掛在 Pod 的 <code>/host</code>，讓管理員經由 Kubernetes API 看到節點的作業系統，不需要節點開 SSH；進到 Pod 之後 <code>chroot /host</code>（把行程的根目錄換成 <code>/host</code>），之後執行的指令看到的是節點的檔案系統。不加 <code>--profile</code> 時，較新的 kubectl 用預設的 <code>general</code> profile（舊版的預設是 <code>legacy</code>），除錯 Pod 已經以 root 執行、共用節點的行程、網路與 IPC 的 Linux namespace，讀寫節點的檔案、用 <code>crictl</code> 列出節點上的容器都做得到；<code>--profile=sysadmin</code> 以<a href="/blog/backend/knowledge-cards/privileged-container/" data-link-title="Privileged Container（特權容器）" data-link-desc="在 Pod 設定裡看到 securityContext.privileged=true、要判斷某個除錯或系統元件為什麼需要它，或評估誰能建這種 Pod 等於持有節點 root 權限時，查特權容器能做什麼與它的權限邊界">特權容器（privileged container）</a>執行，多給的是需要額外 Linux capability（核心把 root 權限拆成的個別權限項）的操作。</p>
<p>這個做法的權限邊界有兩道。<a href="/blog/backend/knowledge-cards/kubernetes-rbac/" data-link-title="Kubernetes RBAC（角色型存取控制）" data-link-desc="要決定誰能對叢集裡的哪些物件做什麼、或查某個帳號為什麼被拒絕或為什麼權限過大時，查 Kubernetes RBAC 的 Role、ClusterRole、binding 與動詞資源的組成">Kubernetes RBAC</a> 決定誰能用它：在除錯 Pod 所在的 <a href="/blog/backend/knowledge-cards/namespace/" data-link-title="Namespace（命名空間）" data-link-desc="看到 kubectl 的 -n、kube-system、default，或跨 namespace 的 Service 名稱連不到時：namespace 劃分物件名稱的範圍、預設用哪一個、對 DNS 名稱與權限的影響">namespace</a> 要有 <code>pods</code> 的 <code>create</code>（建 Pod）、<code>list</code> 與 <code>watch</code>（kubectl 用 watch 等 Pod 進入執行狀態）、<code>delete</code>（用完刪掉），以及 <code>pods/attach</code> 的 <code>create</code>（<code>-it</code> 在 Pod 建好之後附加到它的終端機），這幾項綁在那個 namespace 的 RoleBinding；另外要有 <code>nodes</code> 的 <code>get</code>，節點不屬於任何 namespace，這一項要用 ClusterRoleBinding 授予。少了 <code>list</code> 或 <code>watch</code> 時 Pod 已經建好，kubectl 卻停在等待；少了 <code>pods/attach</code> 時 Pod 建得出來卻進不去；兩種情形都會留下一個在跑的除錯 Pod。</p>
<p>能在節點上建除錯 Pod 的帳號，用預設的 <code>general</code> profile 就能以 root 讀寫節點的檔案，所以這組權限只給負責維運的群組。<a href="/blog/backend/knowledge-cards/pod-security-admission/" data-link-title="Pod Security Admission（Pod 安全准入）" data-link-desc="Pod 因為安全設定被拒絕建立、要限制某個 namespace 不能跑 privileged 或掛載主機目錄的 Pod，或評估 kubectl debug 這類需要高權限的工具在叢集裡能不能用時，查 Pod Security Admission 的三個等級與 namespace label">Pod Security Admission</a> 決定那個 namespace 是否接受這類 Pod：<code>kubectl debug node</code> 用 <code>general</code> 或 <code>sysadmin</code> profile 建的 Pod 帶著主機的網路、行程與 IPC 的 Linux namespace（hostNetwork、hostPID、hostIPC）與主機目錄掛載（hostPath），namespace 設了 <code>pod-security.kubernetes.io/enforce=baseline</code> 或 <code>restricted</code> 時，這幾項都會讓除錯 Pod 被拒絕；用 <code>--profile=baseline</code> 或 <code>restricted</code> 建的除錯 Pod 不帶這些設定，建得進這類 namespace，但看不到節點的檔案。反過來，沒有設這個 label 的 namespace（包括 <code>default</code>）在 GKE Standard 上預設接受特權 Pod，所以任何能在 <code>default</code> 建 Pod 的帳號，現在就能用這個方法以 root 操作節點。做法是把一般工作負載的 namespace 都設成 <code>baseline</code> 以上，節點除錯集中到一個刻意保留為 <code>privileged</code> 等級、而且只有維運群組能建 Pod 的 namespace，由叢集管理員事先建好；建法與使用的指令在 <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> 的〈GKE 節點除錯：建專用的 namespace，用 kubectl debug node 查看節點〉。GKE Autopilot 預設拒絕特權 Pod（只有列入允許清單的合作夥伴工作負載例外），也不提供節點 SSH，查節點層的狀況靠 Cloud Logging 裡的節點紀錄與 GKE 主控台的節點狀態。</p>
<p>節點仍要 SSH 時（GKE Standard），單台 VM 的三項設定搬到 node pool 上：節點不配外部 IP 的 private nodes（先要有 Cloud NAT，換成內部 IP 之後節點的出站都經過它）、node pool 的 network tag（<code>node-pools update --tags</code> 整組取代使用者設定的 tag，GKE 自建的 tag 不受影響），以及 OS Login。Autopilot 叢集不能用 OS Login，因為節點由 GKE 管理。專案層級的 <code>enable-oslogin=TRUE</code> 是否套用到 GKE Standard 的節點，GKE 自己的兩份文件寫法相反：OS Login 的設定頁說專案層的設定套用到專案裡所有既有與新建的 VM，node pool 的管理頁則寫 OS Login 在 GKE 節點上被明確停用，所以啟用之後經由 IAP 實際登入一個節點，以登入後的帳號判斷。三項設定的指令在 <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> 的〈node pool：private nodes、network tag 與 OS Login〉。</p>
<h2 id="gke-控制平面的存取authorized-networksdns-based-endpoint-與-private-endpoint">GKE 控制平面的存取：authorized networks、DNS-based endpoint 與 private endpoint</h2>
<p>控制平面的 API server 預設有一個公開的 IP 端點，任何地方都連得到，靠 Kubernetes 的認證（通道授權）與 <a href="/blog/backend/knowledge-cards/kubernetes-rbac/" data-link-title="Kubernetes RBAC（角色型存取控制）" data-link-desc="要決定誰能對叢集裡的哪些物件做什麼、或查某個帳號為什麼被拒絕或為什麼權限過大時，查 Kubernetes RBAC 的 Role、ClusterRole、binding 與動詞資源的組成">Kubernetes RBAC</a>（主機登入驗證那一層的對應）擋下沒有權限的請求——在管理通道的模型裡，這等於網路可達性全開，與 <code>0.0.0.0/0</code> 開 22 port 是同一種狀態。收窄網路可達性的做法有三種：</p>
<ul>
<li><strong>authorized networks</strong>：只允許列出的來源 IP 範圍連到 API server 的 IP 端點，相當於來源 IP 白名單，代價與白名單相同（遠端工作、IP 會變動時要一直改，而且分辨不出個人）。清單每次都整組取代，執行指令的人與 CI 的出口 IP 漏列時 <code>kubectl</code> 當場連不上，而 <code>gcloud</code> 不經過這個端點，連不上時仍可以改回。設了 authorized networks 之後，GCP 上其他客戶的 VM 的外部 IP 是否放行，由叢集的 Google Cloud 外部 IP 存取設定決定（<code>gcpPublicCidrsAccessEnabled</code>，要明確設定過一次才會出現在 <code>describe</code> 的輸出裡）。</li>
<li><strong>DNS-based endpoint</strong>：叢集多一個專屬的網域名稱端點，請求先經過 Google Cloud、以 IAM 判斷這個帳號能不能連到這個叢集（<code>container.clusters.connect</code> 權限，<code>roles/container.developer</code> 這類角色裡有），通過之後才轉到 API server。它相當於控制平面前面的身分感知代理：網路可達性與通道授權改由 IAM 承擔，不必維護 IP 清單，GitHub Actions 這類外部 CI 也能用 <a href="/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">workload identity federation</a> 換到的身分連進來，不必另架<a href="/blog/devops/infra/knowledge-cards/bastion-host/" data-link-title="Bastion Host（跳板機）" data-link-desc="內部主機不想各自對外開管理 port、要設定 ProxyJump 經由跳板機連線、判斷 agent forwarding 的風險，或要評估自建跳板機與改用身分感知代理的取捨時，查跳板機的連線方式、暴露面與維運責任">跳板機</a>。啟用之後 IP 端點仍然開著，每個人與每條 CI 都改連網域名稱端點之後才關掉 IP 端點；兩步分開是因為關掉 IP 端點的那一刻，還在用它的人與 CI 當場連不上。</li>
<li><strong>private endpoint</strong>：控制平面只留內部 IP 端點（<code>--enable-private-endpoint</code>），只有 VPC 內與經 VPN 或 Interconnect 接進 VPC 的來源連得到，可以再加 <code>--enable-authorized-networks-on-private-endpoint</code> 收窄到 VPC 內的特定範圍。既有的 private cluster 多半是這種設定，從 VPC 外管理時要經過 VPN 或跳板機；改用 DNS-based endpoint 之後這一層可以拿掉。</li>
</ul>
<p>三種做法的指令、兩步切換的確認方式與實跑結果，在 <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> 的〈控制平面：authorized networks，或改連 DNS-based endpoint 再關掉 IP 端點〉。</p>
<p>一個 <code>kubectl</code> 請求要通過兩次檢查。連進叢集的檢查是 IAM 的 <code>container.clusters.connect</code>：經 DNS-based endpoint 連線時缺了它，請求在進到 API server 之前就被 Google 的前端以 <code>403 Forbidden</code> 擋下。請求進到 API server 之後，API server 再判斷這個帳號是否有權限執行這個請求（例如列出節點），這項授權檢查在 GKE 上由 IAM 與 Kubernetes RBAC 共同決定，兩者任一個允許就放行：IAM 的 Kubernetes 物件權限（例如 <code>container.nodes.list</code>，<code>roles/container.developer</code> 這類角色裡有）或 RBAC 的 Role 與 RoleBinding。只拿到 <code>container.clusters.connect</code>（例如 <code>roles/container.clusterViewer</code>）的帳號連得進叢集，列出節點時會收到 <code>nodes is forbidden: User &quot;&lt;email&gt;&quot; cannot list resource &quot;nodes&quot;</code>，訊息後面寫著它要的 IAM 權限名稱。IAM 與 RBAC 的授權要一起設計，原則與 <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>〈IAP 的 IAM 授權範圍：誰拿得到通道〉相同：綁給負責維運的群組，不綁給整個網域。</p>
<h2 id="延伸閱讀逐步的實作單台-vm-的做法管理通道模型與-iac-的落差">延伸閱讀：逐步的實作、單台 VM 的做法、管理通道模型與 IaC 的落差</h2>
<ul>
<li>→ <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>：新 template 與 canary、重建與主機金鑰、退回 template、除錯 namespace、node pool 設定與控制平面端點切換的逐步指令、實跑結果，以及每一步換一種做法時的結果</li>
<li>→ <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>：本篇沿用的 IAP 規則、IAM 權限、OS Login 與 Cloud NAT 在單台 VM 上的機制與切換順序</li>
<li>→ <a href="/blog/devops/infra/03-network-foundation/management-access-channel/" data-link-title="伺服器管理通道的設計：網路可達性、通道授權、主機登入驗證三層的做法與選擇，以及救援入口" data-link-desc="與雲端廠商無關、適用於雲端 VM、地端主機與 VPS 的管理員登入通道設計：拆成網路可達性、通道授權與主機登入驗證的模型，各層的做法（來源白名單、跳板機、VPN、身分感知代理、代理程式反向連線、SSH 憑證）、更換 port 與 fail2ban 改不了的部分，依存取者身分、連線送達方式、收回方式選擇組合的依據，以及移除公開 IP 之後的出站路徑、切換順序、救援入口、組織護欄與稽核">伺服器管理通道的設計</a>：網路可達性、通道授權、主機登入驗證三層的模型，以及不可變基礎設施下平時關閉通道的做法</li>
<li>→ <a href="/blog/devops/infra/takeover/partial-iac-dual-truth-operation/" data-link-title="兩套真相並存的過渡期操作" data-link-desc="部分資源在 IaC、部分在手動時，怎麼安全操作避免比全手動更危險，以及怎麼縮短這個過渡期">兩套真相並存的過渡期操作</a>：MIG 與 GKE 多半由 IaC 管理，在 CLI 上建的 template 與改的 node pool 設定要寫回程式碼</li>
</ul>
]]></content:encoded></item><item><title>GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷</title><link>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/</link><pubDate>Sat, 24 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/</guid><description>&lt;p>這篇整理在 GCP 上為費用失控設防的機制與判斷依據，範圍從專案已經連結到一個帳單帳戶開始。&lt;a href="https://tarrragon.github.io/blog/devops/operations/08-cost-management/dev-environment-cost/" data-link-title="開發環境成本控制" data-link-desc="壓非 production 環境的隱形浪費時，用排程關機、用完即拆的 ephemeral 環境、CI 跑 spot、以及 per-team 成本上限">開發環境成本控制&lt;/a>的〈Per-team 成本上限〉說明了為什麼要給團隊與環境設一個費用的上限；這一篇處理那個上限在 GCP 上由哪些機制執行、每一個機制在費用超過門檻時實際做了什麼，以及從費用發生到它動作之間隔了多久。三個機制分別是預算警示（花費達門檻時寄通知）、強制執行支出上限（暫停指定服務的新用量），以及由預算通知觸發的處置（由程式把專案從帳單帳戶解除連結，或只停止花錢的資源）；另外寫停損開關放在另一個專案的理由、驗收為什麼分兩段，以及停用之後的恢復順序。建預算、主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與恢復的逐步指令與實跑結果，在 &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>。&lt;/p>
&lt;p>文中的觀察來自 2026 年 10 月在一個 GCP 測試專案上的實跑；標「未實跑」的做法沒有在實跑中執行；引用 Cloud Billing 文件或 &lt;code>gcloud&lt;/code> 說明的行為，出處寫在句子裡。主控台的選項與服務清單會變動，以操作當下看到的為準。&lt;/p>
&lt;h2 id="費用失控的來源與反應時間">費用失控的來源與反應時間&lt;/h2>
&lt;p>費用失控指的是費用增加的速度快過人發現它、處理它的速度。來源分成兩類，兩類的處置方式不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>服務自己出錯&lt;/strong>：程式對失敗的請求無限重試、一個事件觸發的函式又產生同一種事件而形成遞迴、自動擴縮允許的最大執行個體數設得太高。費用花在自己的服務上，修好程式之後費用就停止增加。&lt;/li>
&lt;li>&lt;strong>資源被別人濫用&lt;/strong>：外洩的 API 金鑰（API key）被拿去呼叫付費 API、被入侵的 VM 被拿去挖礦。費用花在攻擊者的用途上，處理的人要先切斷攻擊者能用的資源，再處理入侵。&lt;/li>
&lt;/ul>
&lt;p>兩類的共同難處在時間：費用從發生到任何機制動作，中間隔著一段延遲。一個防護機制從費用發生到動作，要經過三段：雲端把用量算成費用、寫進帳單資料；帳單資料觸發通知；通知觸發處置（人或程式）。把用量算成費用那一段的延遲由雲端決定，GCP 的帳單主控台寫著費用資料要數小時後才會顯示、有時超過 24 小時。實測時一個專案已經跑了幾個小時的 VM 與 GKE 叢集，那段期間送到的預算通知裡 &lt;code>costAmount&lt;/code>（本期累計花費）仍是 &lt;code>0.0&lt;/code>；那個預算用預設的抵免設定，所以這次分不出 &lt;code>0.0&lt;/code> 來自延遲還是抵免。&lt;/p>
&lt;p>預算警示與由預算通知觸發的處置讀的是帳單資料，反應時間至少是這段延遲。強制執行支出上限依的是 GCP 估算的費用，Cloud Billing 文件說這份估算比帳單報表上的費用快得多，但仍不是即時，延遲期間超出上限的部分照常計費。這三個機制的費用最後停在哪裡也不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>強制執行支出上限與解除帳單連結&lt;/strong>：費用停止增加，總額是觸發的金額再加上延遲期間已經發生的費用。&lt;/li>
&lt;li>&lt;strong>只停止花錢的資源&lt;/strong>：被停掉的資源不再計費，磁碟與儲存照常計費。&lt;/li>
&lt;li>&lt;strong>預算警示&lt;/strong>：不停止任何用量，費用停在哪裡取決於收到通知的人多快處理。&lt;/li>
&lt;/ul>
&lt;p>要在費用發生的當下限制它，得靠不經過帳單資料的限制，它們也決定了延遲期間最多能多花多少：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>能用多少&lt;/strong>：API 的配額、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">速率限制（rate limit）&lt;/a>、服務允許的最大執行個體數（例如 Cloud Run 的 &lt;code>--max-instances&lt;/code>、MIG 的最大台數），以及 Compute Engine 的區域 CPU 與 GPU 配額。配額調到實際用量附近，被入侵的帳號能開的 VM 台數就有上限。&lt;/li>
&lt;li>&lt;strong>能建什麼&lt;/strong>：組織政策（Organization Policy，GCP 繁體中文介面寫作「機構政策」）可以限制專案能啟用的 API，搭配 Compute Engine 的自訂限制條件可以限制能用的機型，前提是專案屬於一個組織（未實跑：測試帳號底下沒有組織）。&lt;/li>
&lt;li>&lt;strong>金鑰能做什麼&lt;/strong>：API 金鑰的使用範圍限定可呼叫的 API 與來源；實測 &lt;code>gcloud services api-keys create&lt;/code> 不給 &lt;code>--api-target&lt;/code> 時被拒，新建的金鑰一定帶 API 範圍。金鑰被盜用時，先撤銷或限制那一把金鑰，影響範圍比停用整個專案的帳單小。&lt;/li>
&lt;/ul>
&lt;h2 id="預算警示門檻依據範圍抵免與通知對象">預算警示：門檻依據、範圍、抵免與通知對象&lt;/h2>
&lt;p>預算（budget）是建在帳單帳戶上的一組門檻：本期花費達到預算的某個比例時，GCP 寄通知給帳單帳戶的管理員。門檻有兩種依據，&lt;strong>實際花費&lt;/strong>（已經寫進帳單資料的費用）與&lt;strong>預測花費&lt;/strong>（GCP 依目前的速度推估到期末的費用）；預測花費的門檻通常比實際花費的門檻早觸發，代價是一次短暫的尖峰也可能觸發預測花費的門檻。預算只寄通知，扣款照常進行。建立指令（&lt;code>gcloud billing budgets create&lt;/code> 加 &lt;code>--filter-projects&lt;/code>、&lt;code>--threshold-rule&lt;/code>）與它的兩道權限檢查，在 &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> 的〈建立限定一個專案的預算〉。&lt;/p>
&lt;p>決定一個預算會不會在對的時候通知對的人，有三件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>範圍&lt;/strong>：預算用 &lt;code>--filter-projects&lt;/code> 限定要計算的專案。&lt;code>gcloud&lt;/code> 說明寫著不給這個旗標時涵蓋所有專案，通知裡的 &lt;code>costAmount&lt;/code> 就是帳單帳戶下所有專案的合計：別的專案的花費也會讓它越過門檻，而這個專案自己的超支被合計稀釋。&lt;/li>
&lt;li>&lt;strong>抵免&lt;/strong>：&lt;code>gcloud&lt;/code> 說明寫著預設把帳戶上的抵免扣掉之後才算花費。帳戶上有大額抵免（新創方案、試用額度）時，&lt;code>costAmount&lt;/code> 在抵免用完之前都接近 0，門檻在抵免被燒光的整段期間都不會觸發。抵免本身也是要保護的錢時，建立預算時加 &lt;code>--credit-types-treatment=exclude-all-credits&lt;/code>，花費以抵免前的金額計算，代價是免費方案的用量也會以原價算進去（推論：&lt;code>FREE_TIER&lt;/code> 是預算接受的抵免類型，這次沒有量）；只想讓部分抵免照常扣掉時，改用 &lt;code>include-specified-credits&lt;/code> 搭配 &lt;code>--filter-credit-types&lt;/code>。試用額度用完或試用期結束之後，帳單帳戶與資源會被停用、停止還是開始計費，GCP 與 Oracle Cloud、AWS 的規則見 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/pricing-comparison/" data-link-title="雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目" data-link-desc="Oracle Cloud、Google Cloud 與 AWS 的免費方案分別由哪幾種額度組成，比較時要換算的計費單位，免費主機能放在哪些區域，試用期結束後帳號與資源會怎樣，以及付費主機的價格該去哪裡查">雲端免費額度與小型主機的收費比較&lt;/a> 的〈試用結束後帳號與資源的處置〉。&lt;/li>
&lt;li>&lt;strong>通知對象&lt;/strong>：通知預設寄給帳單帳戶的管理員與帳單使用者，建立預算時先確認這兩個角色的成員信箱有人在看。值班的人通常不在這兩個角色裡，要讓他們收到，把通知另外寄到 Cloud Monitoring 的電子郵件通知管道（Cloud Billing 文件寫明預算只支援電子郵件類型的管道）；要送到簡訊、聊天工具或 webhook，改由 Pub/Sub 主題接一段轉送程式。&lt;/li>
&lt;/ul>
&lt;p>預算另外可以把通知送到一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">Pub/Sub&lt;/a> &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">主題（topic）&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> 的〈建接收通知的主題並把預算接上〉。預算警示沒有辦法手動送出一封測試用的門檻通知，第一封真實的門檻通知寄到之前，「通知送得到收的人」這一段沒有驗證過；寄到時確認收到的是誰、多久後有人看到。&lt;/p>
&lt;h2 id="強制執行支出上限涵蓋的服務範圍與暫停期間">強制執行支出上限：涵蓋的服務、範圍與暫停期間&lt;/h2>
&lt;p>在帳單主控台的「Budgets &amp;amp; caps」頁面（2026 年 10 月主控台顯示的名稱）建立預算時，表單裡有一個「強制執行支出上限（適用於某些服務）」的選項：花費達到上限時，GCP 暫停表單裡選定的那個專案、那一項服務的新用量，直到這個預算期間結束（以月為期的預算是到月底，下一期自動重設）或有人手動解除為止。它是帳單功能裡不必自己寫程式就能暫停用量的機制，範圍有四個限制：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>一個上限只涵蓋一個專案裡的一項服務&lt;/strong>：要保護多項服務，就每項各建一個。&lt;/li>
&lt;li>&lt;strong>只涵蓋清單上的服務&lt;/strong>：2026 年 10 月建立表單時列出的是 Cloud Run、Cloud Run functions、Gemini API 與 Vertex AI 四項，Compute Engine 與 GKE 不在清單上。表單的服務下拉選單就是涵蓋清單，建立之前打開看一次。&lt;/li>
&lt;li>&lt;strong>持續性資源的固定費用不暫停&lt;/strong>：Cloud Billing 文件說明，與持續性資源綁在一起的固定用量不會被暫停，執行也有延遲，超出的部分照常計費。&lt;/li>
&lt;li>&lt;strong>以抵免前的金額計算&lt;/strong>：Cloud Billing 文件寫明上限的花費不扣抵免與折扣，與預算的預設抵免設定相反。帳戶有抵免、而上限與預算的金額相近時，上限會先達到。&lt;/li>
&lt;/ul>
&lt;p>工作負載跑在清單上的服務時，這個選項不必寫程式；它暫停的是這項服務的新用量，所以對正式服務同樣是一次停機，只是範圍限於這一項服務。是否建立，依〈由預算通知觸發的處置：解除帳單連結與只停止花錢的資源〉比較的兩種損失（多付的費用，與服務停止、資料可能遺失）來判斷。一個專案同時有清單上的服務與不在清單上的服務時，兩種做法可以並用：清單上的服務各建一個支出上限，在估算費用達到上限時先暫停那一項服務；不在清單上的資源由只停止花錢的資源那一種處置接手。改用解除帳單連結的話，停掉的是整個專案，清單上的服務也一起停。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理在 GCP 上為費用失控設防的機制與判斷依據，範圍從專案已經連結到一個帳單帳戶開始。<a href="/blog/devops/operations/08-cost-management/dev-environment-cost/" data-link-title="開發環境成本控制" data-link-desc="壓非 production 環境的隱形浪費時，用排程關機、用完即拆的 ephemeral 環境、CI 跑 spot、以及 per-team 成本上限">開發環境成本控制</a>的〈Per-team 成本上限〉說明了為什麼要給團隊與環境設一個費用的上限；這一篇處理那個上限在 GCP 上由哪些機制執行、每一個機制在費用超過門檻時實際做了什麼，以及從費用發生到它動作之間隔了多久。三個機制分別是預算警示（花費達門檻時寄通知）、強制執行支出上限（暫停指定服務的新用量），以及由預算通知觸發的處置（由程式把專案從帳單帳戶解除連結，或只停止花錢的資源）；另外寫停損開關放在另一個專案的理由、驗收為什麼分兩段，以及停用之後的恢復順序。建預算、主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與恢復的逐步指令與實跑結果，在 <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>。</p>
<p>文中的觀察來自 2026 年 10 月在一個 GCP 測試專案上的實跑；標「未實跑」的做法沒有在實跑中執行；引用 Cloud Billing 文件或 <code>gcloud</code> 說明的行為，出處寫在句子裡。主控台的選項與服務清單會變動，以操作當下看到的為準。</p>
<h2 id="費用失控的來源與反應時間">費用失控的來源與反應時間</h2>
<p>費用失控指的是費用增加的速度快過人發現它、處理它的速度。來源分成兩類，兩類的處置方式不同：</p>
<ul>
<li><strong>服務自己出錯</strong>：程式對失敗的請求無限重試、一個事件觸發的函式又產生同一種事件而形成遞迴、自動擴縮允許的最大執行個體數設得太高。費用花在自己的服務上，修好程式之後費用就停止增加。</li>
<li><strong>資源被別人濫用</strong>：外洩的 API 金鑰（API key）被拿去呼叫付費 API、被入侵的 VM 被拿去挖礦。費用花在攻擊者的用途上，處理的人要先切斷攻擊者能用的資源，再處理入侵。</li>
</ul>
<p>兩類的共同難處在時間：費用從發生到任何機制動作，中間隔著一段延遲。一個防護機制從費用發生到動作，要經過三段：雲端把用量算成費用、寫進帳單資料；帳單資料觸發通知；通知觸發處置（人或程式）。把用量算成費用那一段的延遲由雲端決定，GCP 的帳單主控台寫著費用資料要數小時後才會顯示、有時超過 24 小時。實測時一個專案已經跑了幾個小時的 VM 與 GKE 叢集，那段期間送到的預算通知裡 <code>costAmount</code>（本期累計花費）仍是 <code>0.0</code>；那個預算用預設的抵免設定，所以這次分不出 <code>0.0</code> 來自延遲還是抵免。</p>
<p>預算警示與由預算通知觸發的處置讀的是帳單資料，反應時間至少是這段延遲。強制執行支出上限依的是 GCP 估算的費用，Cloud Billing 文件說這份估算比帳單報表上的費用快得多，但仍不是即時，延遲期間超出上限的部分照常計費。這三個機制的費用最後停在哪裡也不同：</p>
<ul>
<li><strong>強制執行支出上限與解除帳單連結</strong>：費用停止增加，總額是觸發的金額再加上延遲期間已經發生的費用。</li>
<li><strong>只停止花錢的資源</strong>：被停掉的資源不再計費，磁碟與儲存照常計費。</li>
<li><strong>預算警示</strong>：不停止任何用量，費用停在哪裡取決於收到通知的人多快處理。</li>
</ul>
<p>要在費用發生的當下限制它，得靠不經過帳單資料的限制，它們也決定了延遲期間最多能多花多少：</p>
<ul>
<li><strong>能用多少</strong>：API 的配額、<a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">速率限制（rate limit）</a>、服務允許的最大執行個體數（例如 Cloud Run 的 <code>--max-instances</code>、MIG 的最大台數），以及 Compute Engine 的區域 CPU 與 GPU 配額。配額調到實際用量附近，被入侵的帳號能開的 VM 台數就有上限。</li>
<li><strong>能建什麼</strong>：組織政策（Organization Policy，GCP 繁體中文介面寫作「機構政策」）可以限制專案能啟用的 API，搭配 Compute Engine 的自訂限制條件可以限制能用的機型，前提是專案屬於一個組織（未實跑：測試帳號底下沒有組織）。</li>
<li><strong>金鑰能做什麼</strong>：API 金鑰的使用範圍限定可呼叫的 API 與來源；實測 <code>gcloud services api-keys create</code> 不給 <code>--api-target</code> 時被拒，新建的金鑰一定帶 API 範圍。金鑰被盜用時，先撤銷或限制那一把金鑰，影響範圍比停用整個專案的帳單小。</li>
</ul>
<h2 id="預算警示門檻依據範圍抵免與通知對象">預算警示：門檻依據、範圍、抵免與通知對象</h2>
<p>預算（budget）是建在帳單帳戶上的一組門檻：本期花費達到預算的某個比例時，GCP 寄通知給帳單帳戶的管理員。門檻有兩種依據，<strong>實際花費</strong>（已經寫進帳單資料的費用）與<strong>預測花費</strong>（GCP 依目前的速度推估到期末的費用）；預測花費的門檻通常比實際花費的門檻早觸發，代價是一次短暫的尖峰也可能觸發預測花費的門檻。預算只寄通知，扣款照常進行。建立指令（<code>gcloud billing budgets create</code> 加 <code>--filter-projects</code>、<code>--threshold-rule</code>）與它的兩道權限檢查，在 <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> 的〈建立限定一個專案的預算〉。</p>
<p>決定一個預算會不會在對的時候通知對的人，有三件事：</p>
<ul>
<li><strong>範圍</strong>：預算用 <code>--filter-projects</code> 限定要計算的專案。<code>gcloud</code> 說明寫著不給這個旗標時涵蓋所有專案，通知裡的 <code>costAmount</code> 就是帳單帳戶下所有專案的合計：別的專案的花費也會讓它越過門檻，而這個專案自己的超支被合計稀釋。</li>
<li><strong>抵免</strong>：<code>gcloud</code> 說明寫著預設把帳戶上的抵免扣掉之後才算花費。帳戶上有大額抵免（新創方案、試用額度）時，<code>costAmount</code> 在抵免用完之前都接近 0，門檻在抵免被燒光的整段期間都不會觸發。抵免本身也是要保護的錢時，建立預算時加 <code>--credit-types-treatment=exclude-all-credits</code>，花費以抵免前的金額計算，代價是免費方案的用量也會以原價算進去（推論：<code>FREE_TIER</code> 是預算接受的抵免類型，這次沒有量）；只想讓部分抵免照常扣掉時，改用 <code>include-specified-credits</code> 搭配 <code>--filter-credit-types</code>。試用額度用完或試用期結束之後，帳單帳戶與資源會被停用、停止還是開始計費，GCP 與 Oracle Cloud、AWS 的規則見 <a href="/blog/devops/infra/cloud-providers/pricing-comparison/" data-link-title="雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目" data-link-desc="Oracle Cloud、Google Cloud 與 AWS 的免費方案分別由哪幾種額度組成，比較時要換算的計費單位，免費主機能放在哪些區域，試用期結束後帳號與資源會怎樣，以及付費主機的價格該去哪裡查">雲端免費額度與小型主機的收費比較</a> 的〈試用結束後帳號與資源的處置〉。</li>
<li><strong>通知對象</strong>：通知預設寄給帳單帳戶的管理員與帳單使用者，建立預算時先確認這兩個角色的成員信箱有人在看。值班的人通常不在這兩個角色裡，要讓他們收到，把通知另外寄到 Cloud Monitoring 的電子郵件通知管道（Cloud Billing 文件寫明預算只支援電子郵件類型的管道）；要送到簡訊、聊天工具或 webhook，改由 Pub/Sub 主題接一段轉送程式。</li>
</ul>
<p>預算另外可以把通知送到一個 <a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">Pub/Sub</a> <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">主題（topic）</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> 的〈建接收通知的主題並把預算接上〉。預算警示沒有辦法手動送出一封測試用的門檻通知，第一封真實的門檻通知寄到之前，「通知送得到收的人」這一段沒有驗證過；寄到時確認收到的是誰、多久後有人看到。</p>
<h2 id="強制執行支出上限涵蓋的服務範圍與暫停期間">強制執行支出上限：涵蓋的服務、範圍與暫停期間</h2>
<p>在帳單主控台的「Budgets &amp; caps」頁面（2026 年 10 月主控台顯示的名稱）建立預算時，表單裡有一個「強制執行支出上限（適用於某些服務）」的選項：花費達到上限時，GCP 暫停表單裡選定的那個專案、那一項服務的新用量，直到這個預算期間結束（以月為期的預算是到月底，下一期自動重設）或有人手動解除為止。它是帳單功能裡不必自己寫程式就能暫停用量的機制，範圍有四個限制：</p>
<ul>
<li><strong>一個上限只涵蓋一個專案裡的一項服務</strong>：要保護多項服務，就每項各建一個。</li>
<li><strong>只涵蓋清單上的服務</strong>：2026 年 10 月建立表單時列出的是 Cloud Run、Cloud Run functions、Gemini API 與 Vertex AI 四項，Compute Engine 與 GKE 不在清單上。表單的服務下拉選單就是涵蓋清單，建立之前打開看一次。</li>
<li><strong>持續性資源的固定費用不暫停</strong>：Cloud Billing 文件說明，與持續性資源綁在一起的固定用量不會被暫停，執行也有延遲，超出的部分照常計費。</li>
<li><strong>以抵免前的金額計算</strong>：Cloud Billing 文件寫明上限的花費不扣抵免與折扣，與預算的預設抵免設定相反。帳戶有抵免、而上限與預算的金額相近時，上限會先達到。</li>
</ul>
<p>工作負載跑在清單上的服務時，這個選項不必寫程式；它暫停的是這項服務的新用量，所以對正式服務同樣是一次停機，只是範圍限於這一項服務。是否建立，依〈由預算通知觸發的處置：解除帳單連結與只停止花錢的資源〉比較的兩種損失（多付的費用，與服務停止、資料可能遺失）來判斷。一個專案同時有清單上的服務與不在清單上的服務時，兩種做法可以並用：清單上的服務各建一個支出上限，在估算費用達到上限時先暫停那一項服務；不在清單上的資源由只停止花錢的資源那一種處置接手。改用解除帳單連結的話，停掉的是整個專案，清單上的服務也一起停。</p>
<h2 id="由預算通知觸發的處置解除帳單連結與只停止花錢的資源">由預算通知觸發的處置：解除帳單連結與只停止花錢的資源</h2>
<p>這一節的做法是讓預算通知觸發一段程式，由程式在花費達到預算時停止用量。停止用量有兩種做法，差別在停掉的範圍：</p>
<ul>
<li><strong>解除帳單連結</strong>：程式呼叫 Cloud Billing API 把專案從帳單帳戶解除連結。專案沒有連結帳單帳戶時，GCP 停止專案裡所有的服務（包括免費方案的服務），費用也就停止增加。這段程式在下文稱為<strong>停損開關</strong>（kill switch）。</li>
<li><strong>只停止花錢的資源</strong>：程式改成停止專案裡執行中的 VM、把 MIG 的目標台數設成 0、把 GKE node pool 縮到 0 個節點。Cloud Billing 文件的〈Control resource usage with notifications〉就是這種做法，範例是逐台停止一個 zone 裡執行中的 VM。實測改用 <code>instances().aggregatedList</code> 跨全部 zone 列出 <code>RUNNING</code> 的 VM 再逐台 <code>stop</code>，服務帳號只給 <code>roles/compute.instanceAdmin.v1</code>：訊息發出後函式立刻送出停止，VM 約兩分鐘後變成 <code>TERMINATED</code>，帳單仍是啟用、開機磁碟仍在。資料留在磁碟與儲存裡、那部分照常計費，所以費用不會歸零；代價是程式只停它列得出來的資源，漏掉一種資源型別，或攻擊者另外開的新資源，那部分照常計費。</li>
</ul>
<p>呼叫鏈上的 Eventarc 是 GCP 把事件（這裡是主題裡的訊息）轉成對函式的呼叫的服務；第二代的 Cloud Run functions 以 Eventarc 觸發器接主題。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">預算（帳單帳戶）── 通知 ──&gt; Pub/Sub 主題 ── 推送 ──&gt; Eventarc 觸發器 ── 呼叫 ──&gt; Cloud Run functions 函式 ──&gt; 解除帳單連結，或停止花錢的資源</span></span></code></pre></div><p>兩種做法的預算、主題、觸發器與重試設定相同，差別在函式本體、服務帳號的角色與驗收的終點：改成只停止花錢的資源時，函式改成呼叫 Compute Engine 與 GKE 的 API，服務帳號改給能修改那些資源的角色（VM 與 MIG 例如 <code>roles/compute.instanceAdmin.v1</code>，GKE node pool 的縮放要 container 的權限，以 <code>gcloud iam roles describe</code> 確認），驗收的終點改成超出預算的訊息之後 <code>gcloud compute instances list</code> 的狀態變成 <code>TERMINATED</code>、MIG 的目標台數與 node pool 的節點數變成 0，每一個 zone 與資源型別各確認一次（MIG 與 node pool 的部分未實跑）。</p>
<p>建之前先決定要不要建、建哪一種，因為停損開關的效果是一次自己造成的全面停機：VM、資料庫、函式、Kubernetes 叢集一起停擺，停損開關本身跑在同一個專案時也會停。Cloud Billing 文件另外警告，停用帳單時資源可能被永久刪除，也不保證服務能恢復，所以這是一次可能遺失資料的停機。判斷方式是比較兩種損失：「被濫用或程式出錯而多付一筆費用」與「這個專案的服務停止、資料可能遺失」，哪一種比較大。</p>
<p>判斷的單位是專案，因為停損開關停的是整個專案。混合了正式服務與沙箱的專案照正式服務判斷；想對沙箱建停損開關，就先把沙箱搬到自己的專案。逐個專案套下面三條：</p>
<ul>
<li><strong>停機時沒有人受影響、而且專案裡的資料都能從別處重建</strong>（只跑實驗的專案、個人的沙箱多半是這樣）：多付的費用沒有上限，建停損開關。</li>
<li><strong>停機可以接受、而資料不能遺失</strong>（團隊共用的 staging、有持久資料的實驗專案）：建只停止花錢的資源的那一種。</li>
<li><strong>停機會影響使用者或同事</strong>（正式服務）：全面停機的代價通常高於一段時間的超支，改用預算警示，寄到值班的人手上（見〈預算警示：門檻依據、範圍、抵免與通知對象〉的通知對象），並把〈費用失控的來源與反應時間〉列的即時限制設上，讓帳單資料延遲期間的花費有上限。沒有人能值班時，處理時間就是自己下一次看到通知的時間，配額要依這段時間能承受的花費來設。風險集中的功能可以拆出去，例如 API 金鑰寫在網頁或 App 裡、任何人都拿得到那把金鑰的功能：拆到自己的專案再對那個專案建停損開關。停損開關停掉那個專案時，正式服務裡用這把金鑰的功能也會一起停，正式服務的其餘部分與正式服務的資料要放在另一個專案。</li>
</ul>
<p>接停損開關的預算要另外建、範圍限定在要停用的那一個專案，金額設成這個專案本期能承受的最高花費減掉帳單資料延遲期間多出的費用。延遲費用的上限約是「每小時最多能花的費用 × 延遲的時數」，每小時最多能花多少由〈費用失控的來源與反應時間〉列的配額決定，所以先把配額壓到實際需要的量，才估得出這個上限。一個主題只接一個預算：函式不分辨通知來自哪一個預算，好幾個預算送到同一個主題時，任何一個超支都會讓它停掉同一個專案。</p>
<p>停損開關有三項設定，任何一項設錯，部署指令都回報成功、函式狀態也都顯示 <code>ACTIVE</code>，停損開關卻不會解除帳單連結：服務帳號要另外有讀取帳單狀態的 <code>roles/browser</code>（<code>roles/billing.projectManager</code> 只能建立與刪除帳單連結）、部署要加 <code>--retry</code>（不加時，函式在中途拋出例外而 Cloud Run 回 200，那則超出預算的通知被確認收訊、不再重送）、觸發器的服務帳號要有呼叫函式的 <code>roles/run.invoker</code>（缺它時每一次推送都被 403 擋下，函式一次都不執行）。三項的指令與實測時看到的錯誤，在 <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> 的〈建服務帳號並授予兩個角色〉〈部署函式：兩個服務帳號、執行個體上限與重試〉〈授予觸發器呼叫函式的權限〉。</p>
<h2 id="停損開關放在另一個專案">停損開關放在另一個專案</h2>
<p>專案裡跑的工作負載可能被入侵時，攻擊者拿到的身分若有專案的編輯權限，也能刪掉同一個專案裡的函式與主題。主題、函式與服務帳號可以放在一個專門放帳單工具、不受停用影響的專案（Cloud Billing 文件稱為 FinOps administration project，下文稱管理專案；文件建議把帳單相關的主題集中在這種專案），被保護的專案只綁服務帳號的兩個角色。這樣放有三個效果：解除連結之後函式仍在運作，能記錄並通知人「哪個專案被停了」；被保護的專案裡被入侵的身分刪不掉它；驗收時函式不會跟著停，所以兩則測試訊息不必照順序發。代價是管理專案成了所有停損開關的共同依賴：它的帳單停了或權限被收掉，所有被保護的專案一起失去停損開關，所以監看停損開關有沒有持續收到通知的心跳監看要放在管理專案以外。分開放時要改的指令在 <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> 的〈停損開關放在另一個專案時要改的指令〉。</p>
<h2 id="驗收真實通知確認預算到-pubsub-主題的一段測試訊息確認主題之後的一段">驗收：真實通知確認預算到 Pub/Sub 主題的一段，測試訊息確認主題之後的一段</h2>
<p>停損開關平常不動作，壞掉與「還沒輪到它動作」在任何狀態欄上都長得一樣；〈由預算通知觸發的處置：解除帳單連結與只停止花錢的資源〉最後一段列的三項設定，實測時各自讓它在狀態顯示正常時失效過一次。所以部署成功不算完成，驗收看的是呼叫鏈最後一段的結果：專案的帳單狀態。這是<a href="/blog/report/protection-proves-itself-by-firing/" data-link-title="平常不動作的防護機制的部署驗證：設定被接受、元件狀態正常、執行失敗被吞掉與測試從中途注入這些提早出現的成功訊號，驗證查詢與處置指令的假陰性，以及合成觸發本身有損害時的分段驗證" data-link-desc="部署停損開關、稽核紀錄、告警或自動處置這類平常不動作的防護機制之後，要判斷它算不算部署完成時讀；也用在驗收紀錄寫著「部署成功」「狀態 ACTIVE」「設定已套用」、或驗證查詢回傳零筆就被當成結論的時候">平常不動作的防護機制的部署驗證</a>的一個應用。</p>
<p>驗收分成兩段，因為測試訊息是從主題送進去的：主題之後的每一段（觸發器、呼叫權限、函式、角色、重試）都會被行使到，主題之前的那一段（預算的範圍、金額、抵免設定、發布到主題的權限）不會。主題之前的那一段，用第一則真實的預算通知確認：通知裡的 <code>costAmount</code> 對照帳單報表上這個專案本期的費用，接近 0 而報表不是，就是抵免設定或範圍設錯了。主題之後的那一段，用一則低於預算、一則超出預算的測試訊息確認，兩則走的是函式的兩條分支；只有超出預算那一則會行使解除連結要用到的兩個權限，結果看 <code>billingEnabled</code> 有沒有變成 <code>false</code>。</p>
<p>這次驗收會實際讓專案停擺。專案有停用之後不能遺失的資料時，改用拆開驗證：在可拋棄的專案上用一份換了名稱與主題的複本完整觸發一次，在正式專案上只用 Policy Troubleshooter 確認服務帳號的權限；拆開驗證仍沒有行使正式專案上那一次解除連結，驗收紀錄要寫明缺了這一段。部署之後別人調整了角色或鎖定了帳單連結時，部署的人不會知道要重驗，所以另設一個<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>：函式的紀錄長時間沒有出現就通知，讓函式停止收到通知這件事本身也成為訊號。兩段的訊息、查詢指令、拆開驗證的複本與心跳監看的設法，在 <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> 的〈兩段驗收：第一則真實通知，再發兩則測試訊息〉與〈心跳監看：函式停止收到通知時告警〉。</p>
<h2 id="解除連結之後恢復服務的順序">解除連結之後恢復服務的順序</h2>
<p>實際因為超支而停用之後，<code>costAmount</code> 是本期累計值，整個預算期間都不會降回預算以下；只重新連結的話，下一則預算通知（一天會來好幾則）會讓函式再解除一次。恢復的順序依停用的原因而不同：</p>
<ul>
<li><strong>服務自己出錯</strong>：修好程式之後，先調高預算金額或刪掉函式，再重新連結。</li>
<li><strong>資源被別人濫用</strong>：重新連結之前，把預算金額調成目前的 <code>costAmount</code> 加上清理期間能承受的金額，讓停損開關在攻擊者再開資源時仍會動作，又不會在清理途中被已經累計的花費再觸發一次；並先撤銷外洩的 API 金鑰與服務帳號金鑰。重新連結之後列出並刪除不屬於自己的資源，清單要在重新連結之後列：實測停用期間 <code>gcloud compute routers list</code> 回傳零筆，而重新連結之後 Cloud Router 仍在，所以停用期間的空清單不代表資源已經刪除。確認乾淨之後再把預算調回原本的金額。</li>
</ul>
<p>指令與實測時資源在停用前後的狀態，在 <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> 的〈超支停用之後的恢復〉。</p>
<h2 id="延伸閱讀逐步的實作上限的理由費用的歸因防護機制的驗收與服務帳號的兩邊權限">延伸閱讀：逐步的實作、上限的理由、費用的歸因、防護機制的驗收與服務帳號的兩邊權限</h2>
<ul>
<li>→ <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>：建預算、主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與恢復的逐步指令、實跑結果，以及每一步漏掉或換一種做法時的結果</li>
<li>→ <a href="/blog/devops/operations/08-cost-management/dev-environment-cost/" data-link-title="開發環境成本控制" data-link-desc="壓非 production 環境的隱形浪費時，用排程關機、用完即拆的 ephemeral 環境、CI 跑 spot、以及 per-team 成本上限">開發環境成本控制</a>：〈Per-team 成本上限〉說明給每個團隊與環境設上限的理由，本篇是那個上限在 GCP 上的執行方式</li>
<li>→ <a href="/blog/devops/operations/08-cost-management/cost-monitoring/" data-link-title="成本監控與告警" data-link-desc="要讓雲端帳單不失控時，靠歸因把每筆花費追到擁有者、用異常告警抓突增、以及選 showback 或 chargeback 讓成本有人負責">成本監控與告警</a>：收到預算警示之後，靠資源標記定位是哪個團隊的哪類資源在漲；停損開關解除連結之前，要依歸因的結果選定是哪一個專案</li>
<li>→ <a href="/blog/report/protection-proves-itself-by-firing/" data-link-title="平常不動作的防護機制的部署驗證：設定被接受、元件狀態正常、執行失敗被吞掉與測試從中途注入這些提早出現的成功訊號，驗證查詢與處置指令的假陰性，以及合成觸發本身有損害時的分段驗證" data-link-desc="部署停損開關、稽核紀錄、告警或自動處置這類平常不動作的防護機制之後，要判斷它算不算部署完成時讀；也用在驗收紀錄寫著「部署成功」「狀態 ACTIVE」「設定已套用」、或驗證查詢回傳零筆就被當成結論的時候">平常不動作的防護機制的部署驗證</a>：停損開關、稽核紀錄與告警共用的驗收方法，以及「部署成功、狀態正常」為什麼不算驗收</li>
<li>→ <a href="/blog/devops/infra/knowledge-cards/service-account/" data-link-title="Service Account（服務帳號）" data-link-desc="程式、排程或雲端服務要以自己的身分呼叫雲端 API、要決定給它哪些角色、遇到「狀態正常卻呼叫失敗」的權限問題、或看到 IAM 綁定裡的 serviceAccount: 成員時，查服務帳號是什麼、它的權限要從「它能做什麼」與「誰能以它的身分行事」兩邊設，以及它和人的帳號在管理上的差別">Service Account（服務帳號）</a>：服務帳號的權限分成「它能做什麼」與「誰能以它的身分行事」兩邊，停損開關的角色與部署者的 <code>serviceAccountUser</code> 各屬一邊</li>
</ul>
]]></content:encoded></item></channel></rss>