<?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>Rdp on Tarragon</title><link>https://tarrragon.github.io/blog/tags/rdp/</link><description>Recent content in Rdp on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sun, 25 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/rdp/index.xml" rel="self" type="application/rss+xml"/><item><title>GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄：盤點、救援入口、IAP 規則與 IAM、停用舊規則驗證、OS Login 啟用與移除外部 IP</title><link>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/iap-ssh-migration-walkthrough/</link><pubDate>Sun, 25 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/iap-ssh-migration-walkthrough/</guid><description>&lt;p>實測日期是 2026-10-24，在 GCP 測試專案 &lt;code>blog-verify-20261024&lt;/code>（zone &lt;code>asia-east1-b&lt;/code>，&lt;code>default&lt;/code> 網路）上，對一台 Debian 12 的 e2-micro 與一台 Windows Server 2022 的 e2-medium，用本機的 &lt;code>gcloud&lt;/code> 照這一篇的順序操作；IAM 條件與 OS Login 角色另外用第二個 Google 帳號登入驗證。&lt;/p>
&lt;p>這篇記錄把 VM 的管理員登入從防火牆對 &lt;code>0.0.0.0/0&lt;/code> 開放 22 與 3389 port，改成走 IAP TCP forwarding 的實際操作。內容依操作的順序是：切換前盤點依賴舊規則的連線；準備並實際登入一次救援入口（Serial Console 與本機救援帳號）；啟用 IAP API、開啟 IAP 的稽核紀錄、建立只放行 IAP 來源的防火牆規則與 network tag；授予開通道的 IAM 權限；停用舊規則、用 IAP 驗證新規則；刪除對外開放的規則；收斂 &lt;code>default-allow-internal&lt;/code> 與 &lt;code>default-allow-icmp&lt;/code>；啟用 OS Login；建 Cloud NAT 並移除外部 IP。每一步寫出這次的取值與實測結果，以及沒做或換一種做法時的結果；替代做法與沒做時的結果沒有另外標註的是這次實測看到的，照官方文件推出的寫出文件，推論的寫明沒有實跑。IAP 的連線路徑、預設規則為什麼要換、切換的順序為什麼這樣排、IAM 授權的範圍、連不上時的依層判讀、OS Login 與 Cloud NAT 的機制，寫在 &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 管理通道：從預設的全開規則改走 IAP TCP forwarding&lt;/a>（下文稱概念篇），這裡不重寫。&lt;/p>
&lt;p>這一篇的指令以單台 VM 為對象。由 MIG 或 GKE 管理的 VM，同樣的設定改寫在 instance template 與 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>；Shared VPC 的 service project、用 Terraform 管理的專案，指令要改寫在別的位置；新專案從零開始則可以跳過盤點與停用驗證。這幾種環境的調整見概念篇開場之後「另外四種環境要先調整做法再往下讀」那一段。&lt;/p>
&lt;h2 id="切換前的盤點依賴舊規則的連線">切換前的盤點：依賴舊規則的連線&lt;/h2>
&lt;p>&lt;code>default-allow-ssh&lt;/code> 放行的不只是管理員本人的 SSH：CI 或部署腳本用外部 IP 直連 VM、Ansible 這類&lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/configuration-management/" data-link-title="Configuration Management（組態管理）" data-link-desc="要讓好幾台主機的套件、設定檔與服務維持同一個狀態、讀到 Ansible 的 inventory 與 group_vars、或要判斷主機上的設定該交給 Ansible 這類工具還是 Terraform 這類 IaC 管理時">組態管理&lt;/a>工具、監控程式的探測、第三方廠商的維護連線，都可能經過這條規則，而預設規則沒有開 logging，手上沒有紀錄可以查出它們。盤點分兩步。先對舊規則開啟 logging，列出經過它的候選來源：&lt;/p></description><content:encoded><![CDATA[<p>實測日期是 2026-10-24，在 GCP 測試專案 <code>blog-verify-20261024</code>（zone <code>asia-east1-b</code>，<code>default</code> 網路）上，對一台 Debian 12 的 e2-micro 與一台 Windows Server 2022 的 e2-medium，用本機的 <code>gcloud</code> 照這一篇的順序操作；IAM 條件與 OS Login 角色另外用第二個 Google 帳號登入驗證。</p>
<p>這篇記錄把 VM 的管理員登入從防火牆對 <code>0.0.0.0/0</code> 開放 22 與 3389 port，改成走 IAP TCP forwarding 的實際操作。內容依操作的順序是：切換前盤點依賴舊規則的連線；準備並實際登入一次救援入口（Serial Console 與本機救援帳號）；啟用 IAP API、開啟 IAP 的稽核紀錄、建立只放行 IAP 來源的防火牆規則與 network tag；授予開通道的 IAM 權限；停用舊規則、用 IAP 驗證新規則；刪除對外開放的規則；收斂 <code>default-allow-internal</code> 與 <code>default-allow-icmp</code>；啟用 OS Login；建 Cloud NAT 並移除外部 IP。每一步寫出這次的取值與實測結果，以及沒做或換一種做法時的結果；替代做法與沒做時的結果沒有另外標註的是這次實測看到的，照官方文件推出的寫出文件，推論的寫明沒有實跑。IAP 的連線路徑、預設規則為什麼要換、切換的順序為什麼這樣排、IAM 授權的範圍、連不上時的依層判讀、OS Login 與 Cloud NAT 的機制，寫在 <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 管理通道：從預設的全開規則改走 IAP TCP forwarding</a>（下文稱概念篇），這裡不重寫。</p>
<p>這一篇的指令以單台 VM 為對象。由 MIG 或 GKE 管理的 VM，同樣的設定改寫在 instance template 與 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>；Shared VPC 的 service project、用 Terraform 管理的專案，指令要改寫在別的位置；新專案從零開始則可以跳過盤點與停用驗證。這幾種環境的調整見概念篇開場之後「另外四種環境要先調整做法再往下讀」那一段。</p>
<h2 id="切換前的盤點依賴舊規則的連線">切換前的盤點：依賴舊規則的連線</h2>
<p><code>default-allow-ssh</code> 放行的不只是管理員本人的 SSH：CI 或部署腳本用外部 IP 直連 VM、Ansible 這類<a href="/blog/devops/infra/knowledge-cards/configuration-management/" data-link-title="Configuration Management（組態管理）" data-link-desc="要讓好幾台主機的套件、設定檔與服務維持同一個狀態、讀到 Ansible 的 inventory 與 group_vars、或要判斷主機上的設定該交給 Ansible 這類工具還是 Terraform 這類 IaC 管理時">組態管理</a>工具、監控程式的探測、第三方廠商的維護連線，都可能經過這條規則，而預設規則沒有開 logging，手上沒有紀錄可以查出它們。盤點分兩步。先對舊規則開啟 logging，列出經過它的候選來源：</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"># 對舊規則開啟 logging，之後每一次放行都會寫進 Cloud Logging</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute firewall-rules update default-allow-ssh --enable-logging
</span></span><span class="line"><span class="ln">3</span><span class="cl">gcloud compute firewall-rules update default-allow-rdp --enable-logging
</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"># 觀察一段時間後，列出經由舊規則連進來的來源 IP 與次數（RDP 把規則名稱換成 default-allow-rdp）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">gcloud logging <span class="nb">read</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  <span class="s1">&#39;logName:&#34;compute.googleapis.com%2Ffirewall&#34; AND jsonPayload.rule_details.reference:&#34;default-allow-ssh&#34;&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --freshness<span class="o">=</span>7d --format<span class="o">=</span><span class="s2">&#34;value(jsonPayload.connection.src_ip)&#34;</span> <span class="p">|</span> sort <span class="p">|</span> uniq -c</span></span></code></pre></div><p><code>--freshness=7d</code> 是查詢往回看的時間長度，依自己的部署週期調整，要涵蓋到一次完整的部署與排程工作。實際依賴這條規則的連線，要從 VM 上的成功登入紀錄找（理由見概念篇〈切換到 IAP 的順序〉）：</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 VM 上列出這段期間成功的 SSH 登入：帳號、來源 IP 與金鑰指紋</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo journalctl -u ssh -u sshd --since <span class="s2">&#34;7 days ago&#34;</span> <span class="p">|</span> grep <span class="s2">&#34;Accepted publickey&#34;</span></span></span></code></pre></div><p>每一行的格式是 <code>Accepted publickey for &lt;user&gt; from &lt;ip&gt; ... &lt;金鑰指紋&gt;</code>，用帳號與金鑰指紋就認得出是哪個人或哪個自動化工具。Windows VM 看事件檢視器的安全性紀錄裡事件 ID 4624、登入類型 10（遠端互動，也就是 RDP）的事件。找出來的非人員來源要先改走 IAP 才能停用舊規則：CI 與自動化工具用服務帳號開 IAP 通道，一般 <code>ssh</code> 的工具照概念篇〈IAP TCP forwarding 的連線路徑與來源範圍〉的 <code>ProxyCommand</code> 改設定。</p>
<ul>
<li><strong>沒做時</strong>：停用舊規則的那一刻，依賴它的 CI、監控與廠商連線跟管理員的一起被拒；這些失敗出現在部署或排程執行時，不一定在切換當下被看到（從規則的作用推論，這次沒有實跑）。</li>
<li><strong>替代做法</strong>：觀察期縮短或省略，換到的是切換提早，付出的是只盤點到觀察期內發生過的連線，一個月跑一次的排程會漏掉；只看防火牆紀錄、不看 VM 上的登入紀錄，換到的是不必登入每一台 VM，付出的是 CI 這類來源 IP 每次不同的連線認不出來（兩者都從紀錄的形狀推論，這次沒有實跑）。</li>
</ul>
<h2 id="準備救援入口serial-console-與本機救援帳號">準備救援入口：Serial Console 與本機救援帳號</h2>
<p><a href="/blog/devops/infra/knowledge-cards/serial-console/" data-link-title="Serial Console（序列主控台）" data-link-desc="SSH 因為防火牆、sshd 設定或磁碟滿而連不進主機，或要規劃管理通道斷掉時的備用路徑時，查序列主控台與頻外管理介面的用法與它本身的暴露面">Serial Console</a> 透過 VM 的虛擬序列埠提供登入，不經過 VM 的網路設定與 sshd，在防火牆 tag 漏加、sshd 設定改壞、OS Login 設錯、磁碟滿導致 sshd 起不來時當作救援入口。它用 VM 內的本機帳號密碼登入，與 metadata 金鑰、OS Login 都無關，所以救援帳號要在 VM 還正常時就建好：</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 VM 上建一個設了密碼的本機救援帳號（RHEL 系把 sudo 群組換成 wheel）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo useradd -m rescue <span class="o">&amp;&amp;</span> sudo passwd rescue <span class="o">&amp;&amp;</span> sudo usermod -aG sudo rescue
</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"># 確認 sshd 仍然只收金鑰，救援帳號的密碼只在 Serial Console 上有效</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">sudo sshd -T <span class="p">|</span> grep -i passwordauthentication</span></span></code></pre></div><p><code>rescue</code> 是自己取的帳號名稱。密碼用密碼管理工具逐台隨機產生、存在團隊的密碼庫，不同 VM 不共用同一組；持有救援密碼的人離開團隊時，逐台輪替。<code>sshd -T</code> 印出 sshd 實際生效的設定，那一行要是 <code>passwordauthentication no</code>，否則救援帳號的密碼也能從 SSH 登入。Windows VM 的序列主控台（Special Administration Console）用 Windows 的本機管理員帳號登入，要事先確認那個帳號的密碼在手上。</p>
<p>Serial Console 的互動登入預設關閉，平時保持關閉，救援時才對單一 VM 開啟：</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 開啟 Serial Console 並連上</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute instances add-metadata &lt;instance-name&gt; --zone<span class="o">=</span>&lt;zone&gt; --metadata serial-port-enable<span class="o">=</span>TRUE
</span></span><span class="line"><span class="ln">3</span><span class="cl">gcloud compute connect-to-serial-port &lt;instance-name&gt; --zone<span class="o">=</span>&lt;zone&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"># 救援結束後關回去</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">gcloud compute instances add-metadata &lt;instance-name&gt; --zone<span class="o">=</span>&lt;zone&gt; --metadata serial-port-enable<span class="o">=</span>FALSE</span></span></code></pre></div><p><code>&lt;instance-name&gt;</code> 與 <code>&lt;zone&gt;</code> 用 <code>gcloud compute instances list</code> 查，第一欄是 VM 名稱、第二欄是 zone。建好救援帳號之後，對一台 VM 照上面的指令開啟、用 <code>rescue</code> 登入並執行一次 <code>sudo -v</code> 確認 sudo 可用，再關回去；這次在 Debian 12 的 VM 上實跑，登入與 sudo 都成功。Windows VM 的 SAC 這次沒有實跑。</p>
<ul>
<li><strong>沒做時</strong>：停用舊規則、或之後啟用 OS Login 時任何一項設錯，VM 沒有不經過網路與 sshd 的入口；剩下的路是在 metadata 設 <code>startup-script</code>（Linux）或 <code>windows-startup-script-ps1</code>（Windows）、重開機以 root 或 SYSTEM 執行來建帳號或改回設定，或把開機磁碟卸下掛到另一台 VM 修改，兩條都要重開機（Compute Engine 的啟動腳本與救援文件，這次沒有實跑）。</li>
<li><strong>替代做法</strong>：
<ul>
<li>救援帳號寫進自訂映像檔，換到的是新建的 VM 一開始就有，付出的是密碼雜湊在每台 VM 上相同、要輪替就要重做映像（推論，這次沒有實跑）。</li>
<li>靠 startup-script 臨時建帳號，換到的是平時不留密碼，付出的是救援時要重開機，而且 startup-script 存在 metadata 裡，有讀取權限的人都看得到內容（Compute Engine 的 metadata 文件，這次沒有實跑）。</li>
<li>組織強制了 <code>compute.disableSerialPortAccess</code> 時，救援名單要有能對專案解除這條政策的人，見 <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> 的〈組織層級的護欄：Organization Policy 與階層式防火牆〉。</li>
</ul>
</li>
</ul>
<h2 id="啟用-iap-api開啟稽核紀錄並建立只放行-iap-來源的防火牆規則">啟用 IAP API、開啟稽核紀錄並建立只放行 IAP 來源的防火牆規則</h2>
<p>IAP 端點以 API 的形式在專案上開關，沒有啟用時，<code>--tunnel-through-iap</code> 的連線在 IAP 端點那一步就開不起來：</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"># 啟用 IAP API（每個專案一次）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud services <span class="nb">enable</span> iap.googleapis.com</span></span></code></pre></div><p>同一步在 Google Cloud 控制台的「IAM 與管理 &gt; 稽核記錄」為 Cloud Identity-Aware Proxy 開啟 Data Access 紀錄裡的「管理員讀取」（<code>ADMIN_READ</code>）類型。開通道時 IAP 檢查的權限 <code>iap.tunnelInstances.accessViaIAP</code> 歸在這個類型，每一次開通道會留下一筆方法名稱為 <code>AuthorizeUser</code> 的紀錄。開啟之後經由 IAP 連一次，查到這次連線的紀錄才算開對了類型（<code>protoPayload.resourceName</code> 是 VM 的執行個體 ID）：</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">gcloud logging <span class="nb">read</span> <span class="s1">&#39;protoPayload.serviceName=&#34;iap.googleapis.com&#34; AND protoPayload.methodName=&#34;AuthorizeUser&#34;&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --freshness<span class="o">=</span>1h --format<span class="o">=</span><span class="s2">&#34;value(timestamp,protoPayload.authenticationInfo.principalEmail,protoPayload.resourceName)&#34;</span></span></span></code></pre></div><p>也在同一步確認所有要開通道的 Google 帳號都已強制兩步驟驗證，理由見概念篇〈IAP 的 IAM 授權範圍：誰拿得到通道〉最後兩段。</p>
<ul>
<li><strong>沒做時</strong>（不開 <code>ADMIN_READ</code>）：開通道的帳號與目標 VM 沒有 VM 以外的紀錄，事後調查只剩 Admin Activity 裡的設定變更與 VM 上的 sshd 紀錄；開啟之前的通道紀錄查不回來（Cloud Audit Logs 文件，這次沒有實跑）。</li>
<li><strong>替代做法</strong>：只開「資料讀取」（<code>DATA_READ</code>）實測過：連了十次左右，紀錄零筆。兩種都開，換到的是不必判斷權限歸在哪一類，付出的是紀錄量與費用增加（推論，這次沒有量）。</li>
</ul>
<p>新規則只開需要的 port，並用 target tag 限定作用的 VM：</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"># 放行 IAP 的 IPv4 來源連到帶指定 tag 的 Linux VM 的 22 port</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud compute firewall-rules create allow-ssh-from-iap <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --network<span class="o">=</span>&lt;network-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --direction<span class="o">=</span>INGRESS <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --action<span class="o">=</span>allow <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --rules<span class="o">=</span>tcp:22 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --source-ranges<span class="o">=</span>35.235.240.0/20 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --target-tags<span class="o">=</span>&lt;ssh-via-iap-tag&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --enable-logging
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 有 Windows VM 時，另建一條放行 3389 port 的規則，用另一個 tag</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">gcloud compute firewall-rules create allow-rdp-from-iap <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --network<span class="o">=</span>&lt;network-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --direction<span class="o">=</span>INGRESS <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>  --action<span class="o">=</span>allow <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="se"></span>  --rules<span class="o">=</span>tcp:3389 <span class="se">\
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="se"></span>  --source-ranges<span class="o">=</span>35.235.240.0/20 <span class="se">\
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="se"></span>  --target-tags<span class="o">=</span>&lt;rdp-via-iap-tag&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="se"></span>  --enable-logging</span></span></code></pre></div><ul>
<li><code>allow-ssh-from-iap</code>、<code>allow-rdp-from-iap</code>：規則名稱，自己取，同一個專案裡不能重複。</li>
<li><code>&lt;network-name&gt;</code>：VM 所在的 VPC 名稱，預設網路就是 <code>default</code>，自建網路用 <code>gcloud compute networks list</code> 查。</li>
<li><code>&lt;ssh-via-iap-tag&gt;</code>、<code>&lt;rdp-via-iap-tag&gt;</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> 名稱，例如 <code>ssh-iap</code>、<code>rdp-iap</code>。SSH 與 RDP 用不同的 tag，Linux VM 就不會被順便開了 3389。</li>
<li><code>--enable-logging</code>：讓防火牆把這條規則的每一次放行寫進 Cloud Logging，驗證新規則與之後稽核都要用到。</li>
<li><code>--source-ranges=35.235.240.0/20</code>：Google 公開的 IAP 來源位址段，每個專案都相同；<code>--rules</code> 依通道填要開的 port（SSH 是 <code>tcp:22</code>、RDP 是 <code>tcp:3389</code>）。這兩條規則沒有寫 <code>--priority</code>，預設是 1000（實測），比 <code>default-allow-ssh</code> 的 65534 先被評估。</li>
</ul>
<p>自建的 custom-mode VPC 有 IPv6 VM 時，再建一條名稱不同（例如 <code>allow-ssh-from-iap-v6</code>）、<code>--source-ranges=2600:2d00:1:7::/64</code>、其餘引數相同的規則。接著對每台 VM 加上對應的 tag，VM 分布在不同 zone 時逐台以各自的 <code>--zone</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">gcloud compute instances add-tags &lt;instance-name&gt; --zone<span class="o">=</span>&lt;zone&gt; --tags<span class="o">=</span>&lt;ssh-via-iap-tag&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute instances add-tags &lt;windows-instance-name&gt; --zone<span class="o">=</span>&lt;zone&gt; --tags<span class="o">=</span>&lt;rdp-via-iap-tag&gt;</span></span></code></pre></div><ul>
<li><strong>沒做時</strong>（漏加 tag）：停用舊規則之後，IAP 回 <code>4003</code>（failed to connect to backend），Connectivity Tests 指出擋下它的是 <code>default-deny-ingress</code>。</li>
<li><strong>替代做法</strong>：
<ul>
<li>省略 <code>--target-tags</code>，換到的是不必逐台加 tag，付出的是 IAP 來源連得到網路裡每一台 VM 的 22 port（照規則的作用推論，這次沒有實跑）。</li>
<li>把 <code>--rules</code> 寫成 <code>all</code>，換到的是不必列 port，付出的是 IAP 來源連得到每一台 VM 的每一個 port，等於把 <code>default-allow-internal</code> 的寬度複製給 IAP（照規則的作用推論，這次沒有實跑）。</li>
<li>改用 <code>--target-service-accounts</code> 以服務帳號選 VM，換到的是 tag 不能被持有 VM 修改權限的人隨手加上（能編輯 VM 的人改得了它的 tag），付出的是同一個服務帳號的 VM 全部一起套用，而且 VM 換服務帳號要停機（VPC 防火牆規則文件，這次沒有實跑）。</li>
</ul>
</li>
</ul>
<h2 id="授予開通道的-iam-權限">授予開通道的 IAM 權限</h2>
<p>開通道的權限是 IAM role <code>roles/iap.tunnelResourceAccessor</code>。下面的指令把它綁在專案層級，並用 IAM Conditions（在角色綁定上附加條件，條件成立時這個綁定才生效）限定可以連的 port 或期限；條件的寫法依環境選一種：</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 VM：條件限定只能連 22 port</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud projects add-iam-policy-binding &lt;project-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --member<span class="o">=</span>user:&lt;admin-email&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --role<span class="o">=</span>roles/iap.tunnelResourceAccessor <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --condition<span class="o">=</span><span class="s1">&#39;expression=destination.port == 22,title=iap-ssh-only&#39;</span>
</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：條件放行 22 與 3389</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">gcloud projects add-iam-policy-binding &lt;project-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --member<span class="o">=</span>user:&lt;admin-email&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --role<span class="o">=</span>roles/iap.tunnelResourceAccessor <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --condition<span class="o">=</span><span class="s1">&#39;expression=destination.port == 22 || destination.port == 3389,title=iap-ssh-rdp&#39;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 只允許連特定一台 VM 的 22 port</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">gcloud projects add-iam-policy-binding &lt;project-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>  --member<span class="o">=</span>user:&lt;admin-email&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="se"></span>  --role<span class="o">=</span>roles/iap.tunnelResourceAccessor <span class="se">\
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="se"></span>  --condition<span class="o">=</span><span class="s1">&#39;expression=destination.port == 22 &amp;&amp; destination.ip == &#34;&lt;vm-internal-ip&gt;&#34;,title=iap-ssh-one-vm&#39;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># 外包或臨時存取：到期之後綁定自動失效</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">gcloud projects add-iam-policy-binding &lt;project-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="se"></span>  --member<span class="o">=</span>user:&lt;contractor-email&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="se"></span>  --role<span class="o">=</span>roles/iap.tunnelResourceAccessor <span class="se">\
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="se"></span>  --condition<span class="o">=</span><span class="s1">&#39;expression=destination.port == 22 &amp;&amp; request.time &lt; timestamp(&#34;&lt;expiry-time&gt;&#34;),title=iap-ssh-until-expiry&#39;</span></span></span></code></pre></div><ul>
<li><code>&lt;project-id&gt;</code>：VM 所在的專案 ID，用 <code>gcloud config get-value project</code> 查目前設定的是哪一個。</li>
<li><code>&lt;admin-email&gt;</code>、<code>&lt;contractor-email&gt;</code>：要給權限的人的 Google 帳號；給一群人時把 <code>user:</code> 換成 <code>group:</code> 加 Google Group 的位址，CI 這類機器身分換成 <code>serviceAccount:</code> 加服務帳號的位址。</li>
<li><code>&lt;vm-internal-ip&gt;</code>：VM 的內部 IP，<code>gcloud compute instances list</code> 的 <code>INTERNAL_IP</code> 欄查得到。</li>
<li><code>&lt;expiry-time&gt;</code>：綁定失效的時間，RFC 3339 格式，例如 <code>2027-03-31T00:00:00Z</code>。</li>
<li><code>title</code>：這個條件的顯示名稱，自己取，會出現在 IAM 頁面上。</li>
</ul>
<p>這次用第二個帳號實測：沒有這個角色時開通道回 <code>4033</code>；條件只放行 22 時連 3389 回 <code>4033</code>（Owner 帳號對照回 <code>4003</code>，代表 Owner 不受條件限制、而是被防火牆擋下）；限定單一 VM 與過期的條件連續三分鐘回 <code>4033</code>，正對照在 75 秒內放行。IAM 綁定從送出到生效約 60 到 120 秒，這段時間內的 <code>4033</code> 不代表條件寫錯。</p>
<p>Linux VM 的 <code>gcloud compute ssh</code> 另外要 VM 端的登入權限：還沒啟用 OS Login 時，<code>gcloud compute ssh</code> 把公鑰寫進專案或 VM 的 metadata（實測寫進專案 metadata），所以非 Owner 的帳號要有修改 metadata 的權限（例如 <code>roles/compute.instanceAdmin.v1</code>），VM 掛著服務帳號時另外要那個服務帳號的 <code>roles/iam.serviceAccountUser</code>；已經啟用 OS Login 時，改由〈啟用 OS Login：先授角色、單台先行、再做專案層級〉的登入角色決定。兩種情形都要讀取 VM 與專案資訊的權限（例如 <code>roles/compute.viewer</code>）。</p>
<ul>
<li><strong>沒做時</strong>：非 Owner 的管理員開通道回 <code>4033</code>。Owner 不需要這一步，所以團隊全員都是 Owner 時，這一步看起來不必做，而 IAM Conditions 對 Owner 不生效，條件寫了也限不住任何人（Owner 連 3389 回 <code>4003</code> 而不是 <code>4033</code>）。</li>
<li><strong>替代做法</strong>：不加條件，換到的是指令短、不必維護條件，付出的是持有角色的人連得到專案裡每台 VM 的每個 port（只要防火牆放行）；綁給 <code>domain:</code> 或大型群組，換到的是不必逐人授權，付出的是整個組織的帳號都能開通道（兩者都從角色的作用推論，這次沒有實跑）。取捨的判斷見概念篇〈IAP 的 IAM 授權範圍：誰拿得到通道〉。</li>
</ul>
<h2 id="停用舊規則用-iap-驗證新規則">停用舊規則，用 IAP 驗證新規則</h2>
<p>先在舊規則還在時，每一位管理員各自用自己的帳號連一次，確認 IAM 權限：</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"># Windows：把 VM 的 3389 port 經 IAP 轉到本機的 13389，再用 RDP 用戶端連 localhost:13389</span>
</span></span><span class="line"><span class="ln">5</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>加上 <code>--tunnel-through-iap</code> 時，<code>gcloud</code> 改走 IAP、不連 VM 的外部 IP；VM 還有外部 IP 時，少了這個參數的連線走的是舊路徑。這一次連線驗證的是 IAM：IAP 端點會拒絕沒有權限的帳號。防火牆規則與 tag 這一次驗不到，因為舊規則的 <code>0.0.0.0/0</code> 也放行 IAP 的位址段。接著停用舊規則（規則仍在，只是不生效），由一位管理員對 Linux 與 Windows 各一台 VM 再連一次：</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">gcloud compute firewall-rules update default-allow-ssh --disabled
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute firewall-rules update default-allow-rdp --disabled
</span></span><span class="line"><span class="ln">3</span><span class="cl">gcloud compute ssh &lt;instance-name&gt; --zone<span class="o">=</span>&lt;zone&gt; --tunnel-through-iap</span></span></code></pre></div><p>這一次連得上，代表放行連線的是新規則；前提是專案裡沒有其他來源涵蓋 <code>35.235.240.0/20</code> 的允許規則，用概念篇〈預設網路自帶的防火牆規則〉的列出規則指令確認。這次實測停用之後經由 IAP 連得上、不加 <code>--tunnel-through-iap</code> 的直連被拒，Windows 經 IAP 的 3389 回了 X.224 Connection Confirm。新規則建立時加了 <code>--enable-logging</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"># 查最近一小時經由 IAP 規則放行的連線</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud logging <span class="nb">read</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  <span class="s1">&#39;logName:&#34;compute.googleapis.com%2Ffirewall&#34; AND jsonPayload.rule_details.reference:&#34;allow-ssh-from-iap&#34;&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --freshness<span class="o">=</span>1h --limit<span class="o">=</span><span class="m">10</span></span></span></code></pre></div><p>規則剛開啟 logging 的最初幾分鐘，紀錄裡的 <code>rule_details</code> 可能是空的（實測前兩筆沒有、約五分鐘後的三筆有），照本節的順序正好落在這段時間，所以上面這條查不到不代表新規則沒有命中。這時改用來源 IP 查，結果與 <code>rule_details</code> 是否已經填入無關；驗 RDP 的規則時把 <code>22</code> 換成 <code>3389</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">gcloud logging <span class="nb">read</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  <span class="s1">&#39;logName:&#34;compute.googleapis.com%2Ffirewall&#34; AND ip_in_net(jsonPayload.connection.src_ip, &#34;35.235.240.0/20&#34;) AND jsonPayload.connection.dest_port=22&#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>1h --limit<span class="o">=</span><span class="m">10</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --format<span class="o">=</span><span class="s2">&#34;value(timestamp,jsonPayload.connection.src_ip,jsonPayload.rule_details.reference)&#34;</span></span></span></code></pre></div><p>連不上時，用下面的指令把舊規則恢復，再照概念篇〈IAP 通道連不上時的依層判讀〉找出是哪一層；防火牆規則透過 GCP 的 API 修改，SSH 連不上也改得回來：</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">gcloud compute firewall-rules update default-allow-ssh --no-disabled
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute firewall-rules update default-allow-rdp --no-disabled</span></span></code></pre></div><p><code>4003</code> 時要用 Connectivity Tests 查是哪一條規則或政策擋下：<code>gcloud network-management connectivity-tests create &lt;test-name&gt; --source-ip-address=35.235.240.1 --destination-instance=projects/&lt;project-id&gt;/zones/&lt;zone&gt;/instances/&lt;instance-name&gt; --destination-port=22 --protocol=TCP</code>，<code>&lt;test-name&gt;</code> 自己取，<code>--source-ip-address</code> 只收單一 IP，取 IAP 位址段裡的任一個。</p>
<ul>
<li><strong>主控台的 SSH 按鈕</strong>（SSH-in-browser）：官方〈Connect to Linux VMs using IAP〉寫明它只在 VM 沒有外部 IP 時才走 IAP。所以 VM 還帶外部 IP 時，停用舊規則之後，用主控台按鈕的人連不上，而 <code>--tunnel-through-iap</code> 驗起來是通的，這不代表新規則壞了；外部 IP 移除之後，主控台按鈕改走 IAP（照官方文件推出，這次沒有實跑）。盤點時把用主控台按鈕登入的人一起找出來，切換期間請他們改用 <code>--tunnel-through-iap</code>。</li>
<li><strong>沒做時</strong>（只在舊規則還在時驗證）：連得上的那一次可能是舊規則放行的，tag 漏加或規則寫錯都測不出來，要到刪除舊規則之後才發現（照規則的作用推論，這次沒有實跑）。</li>
<li><strong>替代做法</strong>：
<ul>
<li>不停用、只查防火牆紀錄裡命中的規則名稱，換到的是驗證期間外部來源照常連得到，付出的是要等 <code>rule_details</code> 填入，而且兩條規則都放行時紀錄記的是先被評估的那一條（推論，這次沒有在兩條並存時查）。</li>
<li>直接刪除舊規則再驗證，換到的是少一步，付出的是出錯時要重建規則，重建時可能寫錯；停用的規則用 <code>--no-disabled</code> 一行恢復原樣。</li>
</ul>
</li>
</ul>
<h2 id="刪除對外開放的規則">刪除對外開放的規則</h2>
<p>刪除之前，列出還沒帶 tag 的 VM，兩份清單都要是空的（只有 Linux 或只有 Windows 時，查對應的那一份）：</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">gcloud compute instances list --filter<span class="o">=</span><span class="s2">&#34;NOT tags.items=&lt;ssh-via-iap-tag&gt;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute instances list --filter<span class="o">=</span><span class="s2">&#34;NOT tags.items=&lt;rdp-via-iap-tag&gt;&#34;</span></span></span></code></pre></div><p>清單裡出現的是另一種作業系統的 VM 時合理（Linux VM 本來就不帶 RDP 的 tag），其餘的要先補上 tag 再刪規則。專案裡還沒有任何 VM 帶 tag 時，<code>gcloud</code> 會先印一行 <code>WARNING: The following filter keys were not present in any resource : tags.items</code>，下面列出的 VM 清單仍然正確（實測）。</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">gcloud compute firewall-rules delete default-allow-ssh default-allow-rdp</span></span></code></pre></div><p>刪除之後才發現有漏掉的連線來源時，照原樣把規則建回來，再回頭處理（實測建得回來）：</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">gcloud compute firewall-rules create default-allow-ssh --network<span class="o">=</span>default <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --direction<span class="o">=</span>INGRESS --action<span class="o">=</span>allow --rules<span class="o">=</span>tcp:22 --source-ranges<span class="o">=</span>0.0.0.0/0 --priority<span class="o">=</span><span class="m">65534</span> --enable-logging
</span></span><span class="line"><span class="ln">3</span><span class="cl">gcloud compute firewall-rules create default-allow-rdp --network<span class="o">=</span>default <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --direction<span class="o">=</span>INGRESS --action<span class="o">=</span>allow --rules<span class="o">=</span>tcp:3389 --source-ranges<span class="o">=</span>0.0.0.0/0 --priority<span class="o">=</span><span class="m">65534</span> --enable-logging</span></span></code></pre></div><ul>
<li><strong>沒做時</strong>（停用之後不刪）：停用的規則不放行任何連線，效果與刪除相同；留著的代價是有人用 <code>--no-disabled</code> 一行就恢復全開（照規則的作用推論，這次沒有實跑）。Security Command Center 的 <code>OPEN_SSH_PORT</code> finding 是否把停用的規則算進去，這次沒有確認。</li>
<li><strong>替代做法</strong>：用 <code>gcloud compute firewall-rules update default-allow-ssh --source-ranges=&lt;固定的管理網段&gt;</code> 把來源收窄、不刪除，換到的是保留一條直連的路，付出的是這條路的授權仍然是來源 IP，分辨不出人（取捨見 <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 對誰開放〉，這次沒有實跑）。</li>
</ul>
<h2 id="收斂-default-allow-internal-與-default-allow-icmp">收斂 default-allow-internal 與 default-allow-icmp</h2>
<p><code>default-allow-internal</code> 要先確認 VM 之間實際需要哪些 port（應用連資料庫、節點之間的叢集通訊）再改成具體的規則。改法是為每一組需要的連線各建一條規則，例如應用連 PostgreSQL：</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">gcloud compute firewall-rules create allow-app-to-db --network<span class="o">=</span>default <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --direction<span class="o">=</span>INGRESS --action<span class="o">=</span>allow --rules<span class="o">=</span>tcp:5432 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --source-tags<span class="o">=</span>&lt;app-tag&gt; --target-tags<span class="o">=</span>&lt;db-tag&gt;</span></span></code></pre></div><p><code>&lt;app-tag&gt;</code>、<code>&lt;db-tag&gt;</code> 是自己取的 tag，加在應用與資料庫 VM 上；<code>tcp:5432</code> 換成實際的服務 port。每一組需要的連線都建好之後，再刪 <code>default-allow-internal</code>。<code>default-allow-icmp</code> 的處理看用途：監控或除錯流程會用 <code>ping</code> 確認 VM 存活時，保留並把來源縮到內部網段與監控主機（<code>gcloud compute firewall-rules update default-allow-icmp --source-ranges=10.128.0.0/9,&lt;monitoring-host-ip&gt;/32</code>，<code>&lt;monitoring-host-ip&gt;</code> 是發 ping 的監控主機的 IP）；沒有這種用途就一起刪除。這一節的指令這次沒有實跑。</p>
<ul>
<li><strong>沒做時</strong>：同一個網路裡的 VM 之間不受 port 限制，一台 VM 被入侵之後能直接連到其他 VM 的資料庫與管理 port；管理通道本身不受影響，所以這一步可以排進另一次變更（照規則的作用推論，這次沒有實跑）。</li>
<li><strong>替代做法</strong>：直接刪除 <code>default-allow-internal</code>，換到的是一步收完，付出的是 VM 之間的服務連線當場中斷；整個網路改成自己規劃的 custom-mode VPC，換到的是預設規則一次清掉，付出的是 VM 要搬網路，見概念篇〈預設網路自帶的防火牆規則〉（兩者都是推論，這次沒有實跑）。</li>
</ul>
<h2 id="啟用-os-login先授角色單台先行再做專案層級">啟用 OS Login：先授角色、單台先行、再做專案層級</h2>
<p>啟用 OS Login 之後，VM 不再接受 metadata 裡的金鑰（實測：metadata 金鑰被拒），順序為什麼是先授角色、單台先行，見 <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> 的〈SSH 金鑰在 GCP 上的管理位置：metadata 金鑰與 OS Login 的啟用條件〉。先把登入角色授予每一位管理員與每一個要登入 VM 的服務帳號；VM 掛著服務帳號時，每一個登入者另外需要那個服務帳號的 <code>roles/iam.serviceAccountUser</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"># 授予管理員 OS Login 的登入角色：一般登入與含 sudo 的兩種擇一</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud projects add-iam-policy-binding &lt;project-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --member<span class="o">=</span>user:&lt;admin-email&gt; --role<span class="o">=</span>roles/compute.osLogin
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">gcloud projects add-iam-policy-binding &lt;project-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --member<span class="o">=</span>user:&lt;admin-email&gt; --role<span class="o">=</span>roles/compute.osAdminLogin
</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"># CI 或自動化工具的服務帳號同樣要授予（需要 sudo 時改用 osAdminLogin）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">gcloud projects add-iam-policy-binding &lt;project-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --member<span class="o">=</span>serviceAccount:&lt;ci-service-account-email&gt; --role<span class="o">=</span>roles/compute.osLogin
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 查 VM 掛的服務帳號，並授予登入者使用它的權限</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">gcloud compute instances describe &lt;instance-name&gt; --zone<span class="o">=</span>&lt;zone&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --format<span class="o">=</span><span class="s2">&#34;get(serviceAccounts[0].email)&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">gcloud iam service-accounts add-iam-policy-binding &lt;service-account-email&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>  --member<span class="o">=</span>user:&lt;admin-email&gt; --role<span class="o">=</span>roles/iam.serviceAccountUser</span></span></code></pre></div><p><code>&lt;ci-service-account-email&gt;</code> 是 CI 或自動化工具開通道用的服務帳號，<code>&lt;service-account-email&gt;</code> 是上一條 <code>describe</code> 查出來、VM 掛的那個服務帳號，兩者不同。這次用第二個帳號實測：缺 <code>serviceAccountUser</code> 時 <code>Permission denied (publickey)</code>，補上之後 20 秒內登入；<code>osLogin</code> 不能 sudo、<code>osAdminLogin</code> 可以。</p>
<p>接著先對一台 VM 啟用，讓每一位非 Owner 的管理員各自用自己的帳號連通並確認 sudo 可用，CI 也對這台跑一次部署，再做專案層級：</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 啟用，連通確認</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute instances add-metadata &lt;instance-name&gt; --zone<span class="o">=</span>&lt;zone&gt; --metadata enable-oslogin<span class="o">=</span>TRUE
</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"># 確認之後在專案層級啟用（VM 層級的設定會蓋過專案層級）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">gcloud compute project-info add-metadata --metadata enable-oslogin<span class="o">=</span>TRUE
</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"># 連不上時還原（修改 metadata 走 API，不需要登入 VM）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">gcloud compute instances add-metadata &lt;instance-name&gt; --zone<span class="o">=</span>&lt;zone&gt; --metadata enable-oslogin<span class="o">=</span>FALSE
</span></span><span class="line"><span class="ln">9</span><span class="cl">gcloud compute project-info add-metadata --metadata enable-oslogin<span class="o">=</span>FALSE</span></span></code></pre></div><p>專案層級啟用之後再盤點兩件事。VM 層級設了 <code>enable-oslogin=FALSE</code> 的 VM 會蓋過專案設定、仍然讀 metadata 金鑰（實測），用下面這條列出它們的名稱，逐台改成 <code>TRUE</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">gcloud compute instances list --format<span class="o">=</span>json <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  <span class="p">|</span> jq -r <span class="s1">&#39;.[] | select(any(.metadata.items[]?; .key==&#34;enable-oslogin&#34; and (.value|ascii_downcase)==&#34;false&#34;)) | .name&#39;</span></span></span></code></pre></div><p>專案 metadata 的 <code>ssh-keys</code> 仍然留著前任與現任成員的公鑰，OS Login 一旦還原成 <code>FALSE</code> 它們就又能用，確認全部 VM 都啟用之後用 <code>gcloud compute project-info remove-metadata --keys=ssh-keys</code> 清掉（實測）。</p>
<ul>
<li><strong>沒做時</strong>（不啟用 OS Login）：登入驗證仍是 metadata 裡的公鑰，寫在專案 metadata 的公鑰對專案裡每台 VM 都生效，成員離職時漏刪一把，那把金鑰就持續能登入整個專案（OS Login 文件，這次沒有實跑）。</li>
<li><strong>替代做法</strong>：
<ul>
<li>直接在專案層級啟用、不經過單台，換到的是少一輪驗證，付出的是角色漏授時鎖住的是整個專案的每一台 VM；只由 Owner 驗證，看不出漏授，因為 Owner 持有全部權限（推論，這次沒有實跑）。</li>
<li>改用 SSH 憑證（OpenSSH CA），換到的是主機不全在 GCP 時也能用同一套，付出的是要維運 CA，見 <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>。</li>
<li>再加 <code>enable-oslogin-2fa=TRUE</code> 要求每次登入多一次第二因素，同樣先對單一 VM 設定驗證，連不上時改回 <code>FALSE</code>。付出的是每一位登入者的 Google 帳號都要先啟用兩步驟驗證，沒有啟用的會登不進去；而 OS Login 文件寫明兩步驟驗證不套用在服務帳號上，CI 這類登入者不受它約束（OS Login 文件，這次沒有實跑）。換到的是帳號密碼或本機的 refresh token 外洩時，登入還要第二因素（推論，這次沒有實跑）。</li>
</ul>
</li>
</ul>
<h2 id="建-cloud-nat-並移除外部-ip">建 Cloud NAT 並移除外部 IP</h2>
<p>Cloud NAT 承接沒有外部 IP 的 VM 往外的連線，建在 Cloud Router 上，以 region 為單位生效。哪些出站連線改由 Cloud NAT 承接、哪些改走 Private Google Access，以及出站的驗證為什麼要在移除外部 IP 之後做，見概念篇〈拿掉外部 IP 之後的出站路徑：Cloud NAT 與 Private Google Access〉。</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 所在的 region 建 Cloud Router，再在上面建 Cloud NAT（出站 IP 自動配發）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud compute routers create &lt;router-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --network<span class="o">=</span>&lt;network-name&gt; --region<span class="o">=</span>&lt;region&gt;
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">gcloud compute routers nats create &lt;nat-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --router<span class="o">=</span>&lt;router-name&gt; --region<span class="o">=</span>&lt;region&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --auto-allocate-nat-external-ips <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --nat-all-subnet-ip-ranges
</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"># 外部服務要求來源 IP 白名單時：先預留靜態 IP，NAT 改用它出站</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">gcloud compute addresses create &lt;nat-ip-name&gt; --region<span class="o">=</span>&lt;region&gt;
</span></span><span class="line"><span class="ln">11</span><span class="cl">gcloud compute routers nats create &lt;nat-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --router<span class="o">=</span>&lt;router-name&gt; --region<span class="o">=</span>&lt;region&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --nat-external-ip-pool<span class="o">=</span>&lt;nat-ip-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --nat-all-subnet-ip-ranges</span></span></code></pre></div><ul>
<li><code>&lt;router-name&gt;</code>、<code>&lt;nat-name&gt;</code>、<code>&lt;nat-ip-name&gt;</code>：自己取的資源名稱。</li>
<li><code>&lt;region&gt;</code>：VM 所在 zone 去掉最後的字母那一段，例如 zone <code>asia-east1-b</code> 對應 region <code>asia-east1</code>；VM 分布在幾個 region 就要各建一組。</li>
<li><code>--auto-allocate-nat-external-ips</code> 讓 GCP 自動配發出站用的外部 IP，這些 IP 可能變動；<code>--nat-external-ip-pool</code> 改用預留的靜態 IP。</li>
<li><code>--nat-all-subnet-ip-ranges</code> 讓這個 region 裡所有 subnet 都走這個 NAT，只想讓部分 subnet 出站時改用 <code>--nat-custom-subnet-ip-ranges</code> 逐一列出。</li>
</ul>
<p>VM 要存取 Cloud Storage、Artifact Registry 這類 Google API 時，subnet 開啟 Private Google Access，就能用內部 IP 連到 Google API，不經過 Cloud NAT：</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">gcloud compute networks subnets update &lt;subnet-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --region<span class="o">=</span>&lt;region&gt; --enable-private-ip-google-access</span></span></code></pre></div><p><code>&lt;subnet-name&gt;</code> 用 <code>gcloud compute networks subnets list --filter=&quot;region:&lt;region&gt;&quot;</code> 查；<code>default</code> 網路在每個 region 各有一個名為 <code>default</code> 的 subnet。</p>
<p>外部 IP 設定在 VM 網卡的 access config 上，<code>delete-access-config</code> 要靠名稱指定移除哪一個，而這個名稱依 VM 建立的方式不同，所以先查出來。對外提供服務的 VM 移除之前，要先把流量入口換到負載平衡器；只做管理用途的 VM 可以直接移除。先在一台 VM 上移除、確認，再做其餘的 VM：</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 第一張網卡的 access config 名稱</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud compute instances describe &lt;instance-name&gt; --zone<span class="o">=</span>&lt;zone&gt; <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;get(networkInterfaces[0].accessConfigs[0].name)&#34;</span>
</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"># 用查到的名稱移除外部 IP</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">gcloud compute instances delete-access-config &lt;instance-name&gt; --zone<span class="o">=</span>&lt;zone&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --access-config-name<span class="o">=</span><span class="s2">&#34;&lt;access-config-name&gt;&#34;</span></span></span></code></pre></div><p>移除之後經由 IAP 連進這台 VM，執行 <code>curl -s https://ifconfig.me</code> 印出出站時對方看到的來源 IP；那個 IP 列在 <code>gcloud compute routers get-nat-ip-info &lt;router-name&gt; --region=&lt;region&gt;</code> 的清單裡，就代表出站走了 Cloud NAT。這次實測 <code>--auto-allocate-nat-external-ips</code> 配到的 IP 正好是這台 VM 剛釋放的外部 IP，移除前後 <code>curl</code> 印出同一個 IP，<code>gcloud compute addresses list</code> 的 <code>PURPOSE</code> 欄是 <code>NAT_AUTO</code>，所以判斷依據是那份清單，而不是移除前後的 IP 有沒有變。連不到網際網路時，用 <code>gcloud compute instances add-access-config &lt;instance-name&gt; --zone=&lt;zone&gt;</code> 把外部 IP 加回去，再回頭查 Cloud NAT 的 region 與 subnet 設定。</p>
<ul>
<li><strong>沒做時</strong>（保留外部 IP）：管理通道已經只放行 IAP，SSH 照樣安全；外部 IP 仍讓 VM 的其他 port 只靠防火牆擋，而防火牆規則一寫寬，那台 VM 就直接暴露（照規則的作用推論，這次沒有實跑）。Security Command Center 有標出帶公開 IP 的資源的 finding 類別，VM 不需要對外時移除之後少一類要處理的 finding（Security Health Analytics 文件）。</li>
<li><strong>替代做法</strong>：
<ul>
<li>反過來先移除 IP、再建 NAT，換到的是驗證不受外部 IP 干擾，付出的是移除到 NAT 建好之間 VM 連不到網際網路，<code>apt update</code> 這類操作會失敗（推論，這次沒有實跑）。</li>
<li>不建 Cloud NAT、只開 Private Google Access，換到的是不經過 NAT、不產生 NAT 的閘道時數與資料處理費，付出的是 VM 只連得到 Google API，連不到外部的套件來源與第三方 API（Cloud NAT 定價與 Private Google Access 文件，這次沒有實跑）。</li>
<li>用 <code>--nat-external-ip-pool</code> 改用預留的靜態 IP，換到的是外部服務可以把這個固定的 IP 列進白名單；Cloud NAT 定價頁寫明 NAT 使用的外部 IP 不論靜態或自動配發都按 IP 計費，差別在靜態 IP 從 NAT 拿下來之後仍然保留在專案裡、要另外釋放（Cloud NAT 定價與 IP 位址文件，這次沒有實跑）。</li>
</ul>
</li>
</ul>
<p>移除之後，這台 VM 只剩 IAP 一條管理路徑，連線一律加 <code>--tunnel-through-iap</code>；救援入口是〈準備救援入口：Serial Console 與本機救援帳號〉建好的那一條。</p>
]]></content:encoded></item><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></channel></rss>