<?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>斷網環境的 infra：沒有網路時怎麼做 on Tarragon</title><link>https://tarrragon.github.io/blog/infra/air-gapped/</link><description>Recent content in 斷網環境的 infra：沒有網路時怎麼做 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 26 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/infra/air-gapped/index.xml" rel="self" type="application/rss+xml"/><item><title>斷網環境的通用原則</title><link>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-principles/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-principles/</guid><description>&lt;p>斷網環境的 infra 原則跟連網環境相同——可重建、可追蹤、可審查。差別在於連網環境用網路解決的事情（下載套件、推送 code、拉取映像、發送告警），斷網環境要用替代路徑解決。這些替代路徑有一個共通模式：把內容在有網路的環境準備好，經過安全審查後搬進隔離網路。本篇建立這個共通模式的操作框架，後續的 IaC、容器、監控各篇在這個框架上展開各自的細節。&lt;/p>
&lt;h2 id="內容搬運模式content-ferry">內容搬運模式（Content Ferry）&lt;/h2>
&lt;p>斷網環境裡的所有外部依賴（套件、映像、工具、更新）都要經過一條可控的搬運路徑進入。這條路徑的設計決定了環境的安全性和維護效率。&lt;/p>
&lt;h3 id="搬運路徑的三種形態">搬運路徑的三種形態&lt;/h3>
&lt;p>&lt;strong>離線媒介搬運&lt;/strong>：用 USB 隨身碟、外接硬碟或光碟把檔案從有網路的工作站搬進隔離網路。適合高安全環境（軍事、政府機密網路），搬運頻率通常是週或月級。每次搬運的內容要經過掃毒和完整性驗證。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部工作站：準備搬運包&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">mkdir -p ferry/&lt;span class="k">$(&lt;/span>date +%Y%m%d&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># 把需要的套件、映像、工具複製進去&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">cp -r packages/ images/ tools/ ferry/&lt;span class="k">$(&lt;/span>date +%Y%m%d&lt;span class="k">)&lt;/span>/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 產生 checksum&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">find ferry/&lt;span class="k">$(&lt;/span>date +%Y%m%d&lt;span class="k">)&lt;/span> -type f -exec sha256sum &lt;span class="o">{}&lt;/span> &lt;span class="se">\;&lt;/span> &amp;gt; ferry/&lt;span class="k">$(&lt;/span>date +%Y%m%d&lt;span class="k">)&lt;/span>/manifest.sha256&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 隔離網路內：驗證搬運包完整性&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /mnt/usb/ferry/20260626
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">sha256sum -c manifest.sha256&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>跨網段閘道搬運&lt;/strong>：在隔離網路的邊界放一台 staging gateway（跳板機），它有兩張網卡——一張連外部網路（或 DMZ）、一張連內部隔離網路。外部的內容先傳到閘道、經過掃描和審查後再推進內部。適合金融和工控環境，搬運頻率可以是日級。&lt;/p>
&lt;p>閘道的安全約束：只允許特定的檔案類型通過、所有傳入的檔案經過掃毒、傳輸記錄要保留 audit log、閘道本身定期更新安全軟體。&lt;/p>
&lt;p>&lt;strong>單向資料二極體（Data Diode）&lt;/strong>：硬體層面只允許資料單向流動（外 → 內），物理上無法從內部網路傳資料出去。用在最高安全等級的環境。搬運頻率和內容由二極體的設定決定。&lt;/p>
&lt;h3 id="搬運的操作紀律">搬運的操作紀律&lt;/h3>
&lt;p>每次搬運都要記錄：日期、搬運者、搬運內容清單（檔名 + 版本 + checksum）、搬運理由。這份紀錄存在內部網路的版本控制裡，讓「這個套件是誰、什麼時候、為什麼帶進來的」事後可追溯。&lt;/p>
&lt;p>搬運內容的安全審查至少包含：掃毒（ClamAV 或商業掃毒）、checksum 驗證（確認搬運過程沒有被竄改）、版本確認（確認搬進來的版本跟預期的一致、不是被降級的舊版）。&lt;/p>
&lt;p>時程參考：建立搬運流程（含閘道設定、掃描工具安裝、紀錄模板）約需 2-3 天。之後每次搬運操作約 1-2 小時（含準備、掃描、驗證、紀錄）。&lt;/p>
&lt;h2 id="離線套件管理">離線套件管理&lt;/h2>
&lt;p>連網環境的 &lt;code>apt install&lt;/code>、&lt;code>yum install&lt;/code>、&lt;code>npm install&lt;/code> 背後都在連線到公開的套件倉庫。斷網環境需要在內部建立這些倉庫的離線鏡像。&lt;/p>
&lt;h3 id="作業系統套件">作業系統套件&lt;/h3>
&lt;p>&lt;strong>Debian/Ubuntu&lt;/strong>：用 &lt;code>apt-mirror&lt;/code> 或 &lt;code>aptly&lt;/code> 在有網路的環境建立 mirror，把整個 mirror 搬進內部網路，內部機器的 &lt;code>/etc/apt/sources.list&lt;/code> 指向內部 mirror。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：建立 mirror（首次約 50-200GB，後續增量）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">apt-mirror /etc/apt/mirror.list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 內部：設定 sources.list 指向內部 mirror&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;deb http://internal-mirror.local/ubuntu jammy main restricted&amp;#34;&lt;/span> &amp;gt; /etc/apt/sources.list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">apt update&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>RHEL/CentOS&lt;/strong>：用 &lt;code>reposync&lt;/code> 把 yum repo 同步到本地，搬進內部後用 &lt;code>createrepo&lt;/code> 建立 repo metadata。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：同步 repo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">reposync --repoid&lt;span class="o">=&lt;/span>baseos --download-metadata -p /path/to/mirror/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 內部：建立 repo 並設定&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">createrepo /path/to/mirror/baseos&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="應用層套件">應用層套件&lt;/h3>
&lt;p>&lt;strong>Node.js（npm）&lt;/strong>：&lt;code>npm pack&lt;/code> 把每個依賴打包成 .tgz，搬進內部後用 &lt;code>npm install --offline&lt;/code> 或建立 Verdaccio private registry。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：打包所有依賴&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">npm pack --pack-destination ./offline-packages/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># 或用 npm-offline-mirror&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">npm install --prefer-offline --cache ./npm-cache&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Python（pip）&lt;/strong>：&lt;code>pip download&lt;/code> 把依賴下載成 wheel 或 tarball，搬進內部後 &lt;code>pip install --no-index --find-links=./packages/&lt;/code>。&lt;/p>
&lt;p>&lt;strong>PHP（Composer）&lt;/strong>：&lt;code>composer install&lt;/code> 後整個 &lt;code>vendor/&lt;/code> 目錄打包搬進去。或建立 Satis 作為 private Packagist mirror。&lt;/p>
&lt;h3 id="套件鏡像的維護節奏">套件鏡像的維護節奏&lt;/h3>
&lt;p>離線 mirror 需要定期更新——安全補丁、版本升級都要透過搬運流程進入。更新頻率取決於安全需求：高安全環境至少月更（安全補丁）、一般環境季更可接受。每次更新都是一次搬運操作，要走完整的審查流程。&lt;/p>
&lt;h3 id="多格式統一nexus-repository">多格式統一：Nexus Repository&lt;/h3>
&lt;p>上面的做法是每個套件生態各自建 mirror（apt-mirror + Verdaccio + Satis + pip local index）。Nexus Repository 是多格式統一的 artifact proxy，同時支援 apt / yum / npm / Maven / PyPI / Docker / Helm——在企業級斷網環境裡，用一個 Nexus 實例取代多個獨立的離線 repo mirror，維護成本較低。代價是 Nexus 本身的安裝和維運（Java 應用、需要磁碟空間和記憶體），小團隊各自建 mirror 可能反而更簡單。&lt;/p></description><content:encoded><![CDATA[<p>斷網環境的 infra 原則跟連網環境相同——可重建、可追蹤、可審查。差別在於連網環境用網路解決的事情（下載套件、推送 code、拉取映像、發送告警），斷網環境要用替代路徑解決。這些替代路徑有一個共通模式：把內容在有網路的環境準備好，經過安全審查後搬進隔離網路。本篇建立這個共通模式的操作框架，後續的 IaC、容器、監控各篇在這個框架上展開各自的細節。</p>
<h2 id="內容搬運模式content-ferry">內容搬運模式（Content Ferry）</h2>
<p>斷網環境裡的所有外部依賴（套件、映像、工具、更新）都要經過一條可控的搬運路徑進入。這條路徑的設計決定了環境的安全性和維護效率。</p>
<h3 id="搬運路徑的三種形態">搬運路徑的三種形態</h3>
<p><strong>離線媒介搬運</strong>：用 USB 隨身碟、外接硬碟或光碟把檔案從有網路的工作站搬進隔離網路。適合高安全環境（軍事、政府機密網路），搬運頻率通常是週或月級。每次搬運的內容要經過掃毒和完整性驗證。</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"># 外部工作站：準備搬運包</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mkdir -p ferry/<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 把需要的套件、映像、工具複製進去</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">cp -r packages/ images/ tools/ ferry/<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>/
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 產生 checksum</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">find ferry/<span class="k">$(</span>date +%Y%m%d<span class="k">)</span> -type f -exec sha256sum <span class="o">{}</span> <span class="se">\;</span> &gt; ferry/<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>/manifest.sha256</span></span></code></pre></div>




<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"><span class="nb">cd</span> /mnt/usb/ferry/20260626
</span></span><span class="line"><span class="ln">3</span><span class="cl">sha256sum -c manifest.sha256</span></span></code></pre></div><p><strong>跨網段閘道搬運</strong>：在隔離網路的邊界放一台 staging gateway（跳板機），它有兩張網卡——一張連外部網路（或 DMZ）、一張連內部隔離網路。外部的內容先傳到閘道、經過掃描和審查後再推進內部。適合金融和工控環境，搬運頻率可以是日級。</p>
<p>閘道的安全約束：只允許特定的檔案類型通過、所有傳入的檔案經過掃毒、傳輸記錄要保留 audit log、閘道本身定期更新安全軟體。</p>
<p><strong>單向資料二極體（Data Diode）</strong>：硬體層面只允許資料單向流動（外 → 內），物理上無法從內部網路傳資料出去。用在最高安全等級的環境。搬運頻率和內容由二極體的設定決定。</p>
<h3 id="搬運的操作紀律">搬運的操作紀律</h3>
<p>每次搬運都要記錄：日期、搬運者、搬運內容清單（檔名 + 版本 + checksum）、搬運理由。這份紀錄存在內部網路的版本控制裡，讓「這個套件是誰、什麼時候、為什麼帶進來的」事後可追溯。</p>
<p>搬運內容的安全審查至少包含：掃毒（ClamAV 或商業掃毒）、checksum 驗證（確認搬運過程沒有被竄改）、版本確認（確認搬進來的版本跟預期的一致、不是被降級的舊版）。</p>
<p>時程參考：建立搬運流程（含閘道設定、掃描工具安裝、紀錄模板）約需 2-3 天。之後每次搬運操作約 1-2 小時（含準備、掃描、驗證、紀錄）。</p>
<h2 id="離線套件管理">離線套件管理</h2>
<p>連網環境的 <code>apt install</code>、<code>yum install</code>、<code>npm install</code> 背後都在連線到公開的套件倉庫。斷網環境需要在內部建立這些倉庫的離線鏡像。</p>
<h3 id="作業系統套件">作業系統套件</h3>
<p><strong>Debian/Ubuntu</strong>：用 <code>apt-mirror</code> 或 <code>aptly</code> 在有網路的環境建立 mirror，把整個 mirror 搬進內部網路，內部機器的 <code>/etc/apt/sources.list</code> 指向內部 mirror。</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"># 外部：建立 mirror（首次約 50-200GB，後續增量）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">apt-mirror /etc/apt/mirror.list
</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"># 內部：設定 sources.list 指向內部 mirror</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;deb http://internal-mirror.local/ubuntu jammy main restricted&#34;</span> &gt; /etc/apt/sources.list
</span></span><span class="line"><span class="ln">6</span><span class="cl">apt update</span></span></code></pre></div><p><strong>RHEL/CentOS</strong>：用 <code>reposync</code> 把 yum repo 同步到本地，搬進內部後用 <code>createrepo</code> 建立 repo metadata。</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"># 外部：同步 repo</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">reposync --repoid<span class="o">=</span>baseos --download-metadata -p /path/to/mirror/
</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"># 內部：建立 repo 並設定</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">createrepo /path/to/mirror/baseos</span></span></code></pre></div><h3 id="應用層套件">應用層套件</h3>
<p><strong>Node.js（npm）</strong>：<code>npm pack</code> 把每個依賴打包成 .tgz，搬進內部後用 <code>npm install --offline</code> 或建立 Verdaccio private registry。</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"># 外部：打包所有依賴</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm pack --pack-destination ./offline-packages/
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 或用 npm-offline-mirror</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">npm install --prefer-offline --cache ./npm-cache</span></span></code></pre></div><p><strong>Python（pip）</strong>：<code>pip download</code> 把依賴下載成 wheel 或 tarball，搬進內部後 <code>pip install --no-index --find-links=./packages/</code>。</p>
<p><strong>PHP（Composer）</strong>：<code>composer install</code> 後整個 <code>vendor/</code> 目錄打包搬進去。或建立 Satis 作為 private Packagist mirror。</p>
<h3 id="套件鏡像的維護節奏">套件鏡像的維護節奏</h3>
<p>離線 mirror 需要定期更新——安全補丁、版本升級都要透過搬運流程進入。更新頻率取決於安全需求：高安全環境至少月更（安全補丁）、一般環境季更可接受。每次更新都是一次搬運操作，要走完整的審查流程。</p>
<h3 id="多格式統一nexus-repository">多格式統一：Nexus Repository</h3>
<p>上面的做法是每個套件生態各自建 mirror（apt-mirror + Verdaccio + Satis + pip local index）。Nexus Repository 是多格式統一的 artifact proxy，同時支援 apt / yum / npm / Maven / PyPI / Docker / Helm——在企業級斷網環境裡，用一個 Nexus 實例取代多個獨立的離線 repo mirror，維護成本較低。代價是 Nexus 本身的安裝和維運（Java 應用、需要磁碟空間和記憶體），小團隊各自建 mirror 可能反而更簡單。</p>
<h3 id="離線-configuration-managementansible">離線 Configuration Management：Ansible</h3>
<p>斷網環境的 OS 設定、套件安裝、服務啟動等 configuration management 需求，Ansible 是運作良好的工具——它不需要在目標機器安裝 agent、透過 SSH 推送 playbook 執行，playbook 本身是 YAML 可版本控制。在沒有雲端 IaC（Terraform 管的是雲端資源 API）的地端斷網環境裡，Ansible 負責 configuration management 層。Ansible 自身的安裝只需要 Python，控制端安裝後即可透過 SSH 管理內部所有機器。</p>
<h2 id="變更追蹤沒有-github-怎麼辦">變更追蹤：沒有 GitHub 怎麼辦</h2>
<p>斷網環境不能 push 到 GitHub、不能開 PR、不能用 GitHub Actions。但 git 本身是離線工具——git 的所有操作（commit、branch、merge、log、diff）都不需要網路。</p>
<h3 id="內部-git-server">內部 Git Server</h3>
<p>在隔離網路內架設 git server：Gitea（輕量、單一二進位、適合小團隊）、GitLab CE（功能完整、含 CI/CD runner、適合中大團隊）、或最簡單的 bare repo on NFS。</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"># 最簡單的方式：bare repo on 共用檔案系統</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git init --bare /shared/repos/infra.git
</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"># 開發者 clone</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">git clone /shared/repos/infra.git</span></span></code></pre></div><h3 id="git-bundle-跨網段傳遞">Git Bundle 跨網段傳遞</h3>
<p>如果需要在有網路的環境開發、完成後搬進隔離網路，用 <code>git bundle</code> 把 commit 打包成單一檔案：</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"># 外部：把 main branch 的所有 commit 打包</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git bundle create infra-<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>.bundle main
</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"># 搬運後，在內部 clone 或 pull</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">git clone infra-20260626.bundle infra-repo
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 或增量更新</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">git pull infra-20260626.bundle main</span></span></code></pre></div><p>bundle 檔案可以用 <code>git bundle verify</code> 驗證完整性。增量 bundle（只包含某個 tag 之後的 commit）可以減少搬運的資料量：</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">git bundle create incremental.bundle last-imported-tag..main</span></span></code></pre></div><h3 id="code-review-的替代方案">Code Review 的替代方案</h3>
<p>沒有 GitHub PR，code review 可以用：</p>
<ul>
<li>GitLab CE / Gitea 的內建 merge request（如果架了內部 git server）</li>
<li><code>git format-patch</code> 產出 patch 檔 + email review（傳統做法、不需要 web UI）</li>
<li><code>git diff main..feature | less</code> 直接在終端機 review（最簡陋但可行）</li>
</ul>
<h2 id="staging-gateway-的設計">Staging Gateway 的設計</h2>
<p>staging gateway 是搬運路徑的關鍵節點——它決定了什麼能進、什麼不能進。設計要點：</p>
<p><strong>最小安裝</strong>：閘道上只裝搬運需要的工具（scp、rsync、掃毒軟體、checksum 工具），不裝開發工具、不跑應用服務。攻擊面越小越好。</p>
<p><strong>雙網卡隔離</strong>：一張網卡連外部（或 DMZ）、一張連內部。兩張網卡之間沒有自動路由——檔案必須經過人工或腳本從外部目錄搬到內部目錄，中間經過掃描。</p>
<p><strong>審計紀錄</strong>：閘道上的所有檔案操作（建立、複製、刪除）都要記錄。<code>auditd</code> 或等價工具提供核心層級的操作追蹤。</p>
<p><strong>定期輪替</strong>：閘道本身的 OS 和掃毒軟體需要更新。這是一個遞迴問題（用什麼搬運閘道的更新？）——通常用離線媒介搬運閘道自身的更新，或用另一台更上游的閘道。</p>
<p>時程參考：閘道的初次設定（含 OS 安裝、雙網卡配置、掃描工具、審計設定）約需 1-2 天。搬運流程文件化約需半天。</p>
<h2 id="安全審查什麼能跨越隔離邊界">安全審查：什麼能跨越隔離邊界</h2>
<p>每一筆跨越隔離邊界的內容都是潛在的攻擊向量。審查的原則是：預設拒絕，逐項允許。</p>
<p>審查清單：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>檢查方式</th>
          <th>通過條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>掃毒</td>
          <td>ClamAV / 商業掃毒</td>
          <td>0 偵測</td>
      </tr>
      <tr>
          <td>完整性</td>
          <td>sha256sum 比對</td>
          <td>checksum 與外部記錄一致</td>
      </tr>
      <tr>
          <td>版本</td>
          <td>比對預期版本號</td>
          <td>跟申請單的版本一致</td>
      </tr>
      <tr>
          <td>來源</td>
          <td>驗證下載來源</td>
          <td>來自官方 repo 或已知 mirror</td>
      </tr>
      <tr>
          <td>必要性</td>
          <td>申請理由審查</td>
          <td>有明確的使用場景</td>
      </tr>
  </tbody>
</table>
<p>對決策者的重點：斷網環境的安全不是「隔離就安全」——搬運路徑是唯一的攻擊面，這條路徑的安全審查品質決定了整個隔離環境的安全水位。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-iac/" data-link-title="斷網環境的 IaC" data-link-desc="Terraform provider mirror、離線 plugin cache、本地 state backend、沒有雲端時的 plan/apply 流程與內網 CI">斷網環境的 IaC</a>：Terraform provider 和 module 的離線管理</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">斷網環境的容器管理</a>：映像搬運用的是本篇的 content ferry 模式</li>
<li>→ <a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>：斷網環境的搬運紀錄是治理的一部分</li>
</ul>
]]></content:encoded></item><item><title>斷網環境的 IaC</title><link>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-iac/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-iac/</guid><description>&lt;p>Terraform 在連網環境執行 &lt;code>init&lt;/code> 時會自動從 HashiCorp 的 registry 下載 provider plugin 和 module。斷網環境沒有這個路徑——provider、module、&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/state/" data-link-title="State（IaC 狀態檔）" data-link-desc="IaC 工具用來記錄每個納管資源在雲端真實樣貌的快照，是比對差異與排定操作順序的依據">state&lt;/a> backend 全部要用離線替代。&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/iac/" data-link-title="Infrastructure as Code (IaC)" data-link-desc="用程式碼描述基礎設施的最終狀態，由工具負責收斂現實與描述的差異">IaC&lt;/a> 的核心價值（宣告式描述 + state 追蹤 + &lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/terraform-plan-apply/" data-link-title="terraform plan / apply" data-link-desc="IaC 的兩個核心操作：plan 只看不動（產出差異報告）、apply 真的動（執行差異）">plan 預覽&lt;/a>）不因斷網而改變，改變的只是依賴的取得方式和 state 的存放位置。&lt;/p>
&lt;h2 id="provider-離線管理">Provider 離線管理&lt;/h2>
&lt;h3 id="provider-mirror">Provider Mirror&lt;/h3>
&lt;p>Terraform 的 &lt;code>providers mirror&lt;/code> 指令在有網路的環境把指定 provider 的二進位檔下載到本地目錄，產出符合 filesystem mirror 結構的檔案：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 在有網路的工作站執行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">mkdir -p /path/to/mirror
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">terraform providers mirror -platform&lt;span class="o">=&lt;/span>linux_amd64 /path/to/mirror
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># mirror 目錄結構&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># /path/to/mirror/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1"># └── registry.terraform.io/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1"># └── hashicorp/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># └── aws/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># └── 5.50.0/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># └── terraform-provider-aws_5.50.0_linux_amd64.zip&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把整個 mirror 目錄搬進隔離網路後，在 Terraform 設定裡指定 filesystem mirror：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># ~/.terraformrc 或 terraform.rc（Windows）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">provider_installation&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">filesystem_mirror&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n"> path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/opt/terraform/providers&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="n"> include&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;registry.terraform.io/*/*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">direct&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n"> exclude&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;registry.terraform.io/*/*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>direct&lt;/code> 區塊的 &lt;code>exclude&lt;/code> 確保 Terraform 不會嘗試連網下載——如果 mirror 裡沒有某個 provider，init 會直接報錯而非 hang 在網路連線。&lt;/p>
&lt;h3 id="plugin-cache">Plugin Cache&lt;/h3>
&lt;p>替代 mirror 的另一個做法是 plugin cache directory。在有網路的環境跑過 &lt;code>init&lt;/code> 後，&lt;code>.terraform/providers/&lt;/code> 裡會有已下載的 plugin。把這整個目錄搬進隔離網路，用 &lt;code>TF_PLUGIN_CACHE_DIR&lt;/code> 環境變數指向它：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">TF_PLUGIN_CACHE_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/opt/terraform/plugin-cache&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">terraform init&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>mirror 跟 plugin cache 的差別：mirror 是正式的離線分發機制（有版本結構、支援多平台）、plugin cache 是快取機制（省重複下載、但目錄結構跟 mirror 不同）。長期運作用 mirror，臨時驗證用 cache。&lt;/p>
&lt;h3 id="provider-版本鎖定">Provider 版本鎖定&lt;/h3>
&lt;p>斷網環境的 provider 版本管理比連網更嚴格——升級一個 provider 代表要重新搬運整個 provider binary。在 &lt;code>versions.tf&lt;/code> 裡鎖定精確版本（&lt;code>= 5.50.0&lt;/code> 而非 &lt;code>~&amp;gt; 5.50&lt;/code>），避免 &lt;code>init&lt;/code> 期待一個 mirror 裡沒有的版本：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">terraform&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">required_providers&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="n"> aws&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n"> source&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;hashicorp/aws&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="n"> version&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="n"> &amp;#34;&lt;/span>&lt;span class="o">=&lt;/span> &lt;span class="m">5&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="m">50&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="err">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="module-離線來源">Module 離線來源&lt;/h2>
&lt;p>連網環境的 module source 常指向 Terraform Registry 或 GitHub：&lt;code>source = &amp;quot;terraform-aws-modules/vpc/aws&amp;quot;&lt;/code>。斷網環境要改成本地路徑或內部 git server。&lt;/p></description><content:encoded><![CDATA[<p>Terraform 在連網環境執行 <code>init</code> 時會自動從 HashiCorp 的 registry 下載 provider plugin 和 module。斷網環境沒有這個路徑——provider、module、<a href="/blog/infra/knowledge-cards/state/" data-link-title="State（IaC 狀態檔）" data-link-desc="IaC 工具用來記錄每個納管資源在雲端真實樣貌的快照，是比對差異與排定操作順序的依據">state</a> backend 全部要用離線替代。<a href="/blog/infra/knowledge-cards/iac/" data-link-title="Infrastructure as Code (IaC)" data-link-desc="用程式碼描述基礎設施的最終狀態，由工具負責收斂現實與描述的差異">IaC</a> 的核心價值（宣告式描述 + state 追蹤 + <a href="/blog/infra/knowledge-cards/terraform-plan-apply/" data-link-title="terraform plan / apply" data-link-desc="IaC 的兩個核心操作：plan 只看不動（產出差異報告）、apply 真的動（執行差異）">plan 預覽</a>）不因斷網而改變，改變的只是依賴的取得方式和 state 的存放位置。</p>
<h2 id="provider-離線管理">Provider 離線管理</h2>
<h3 id="provider-mirror">Provider Mirror</h3>
<p>Terraform 的 <code>providers mirror</code> 指令在有網路的環境把指定 provider 的二進位檔下載到本地目錄，產出符合 filesystem mirror 結構的檔案：</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"># 在有網路的工作站執行</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">mkdir -p /path/to/mirror
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">terraform providers mirror -platform<span class="o">=</span>linux_amd64 /path/to/mirror
</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"># mirror 目錄結構</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># /path/to/mirror/</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># └── registry.terraform.io/</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">#     └── hashicorp/</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">#         └── aws/</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">#             └── 5.50.0/</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">#                 └── terraform-provider-aws_5.50.0_linux_amd64.zip</span></span></span></code></pre></div><p>把整個 mirror 目錄搬進隔離網路後，在 Terraform 設定裡指定 filesystem mirror：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># ~/.terraformrc 或 terraform.rc（Windows）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">provider_installation</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">filesystem_mirror</span> {
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">    path</span>    <span class="o">=</span> <span class="s2">&#34;/opt/terraform/providers&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">    include</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;registry.terraform.io/*/*&#34;</span><span class="p">]</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="k">direct</span> {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">    exclude</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;registry.terraform.io/*/*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  }
</span></span><span class="line"><span class="ln">10</span><span class="cl">}</span></span></code></pre></div><p><code>direct</code> 區塊的 <code>exclude</code> 確保 Terraform 不會嘗試連網下載——如果 mirror 裡沒有某個 provider，init 會直接報錯而非 hang 在網路連線。</p>
<h3 id="plugin-cache">Plugin Cache</h3>
<p>替代 mirror 的另一個做法是 plugin cache directory。在有網路的環境跑過 <code>init</code> 後，<code>.terraform/providers/</code> 裡會有已下載的 plugin。把這整個目錄搬進隔離網路，用 <code>TF_PLUGIN_CACHE_DIR</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="nb">export</span> <span class="nv">TF_PLUGIN_CACHE_DIR</span><span class="o">=</span><span class="s2">&#34;/opt/terraform/plugin-cache&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform init</span></span></code></pre></div><p>mirror 跟 plugin cache 的差別：mirror 是正式的離線分發機制（有版本結構、支援多平台）、plugin cache 是快取機制（省重複下載、但目錄結構跟 mirror 不同）。長期運作用 mirror，臨時驗證用 cache。</p>
<h3 id="provider-版本鎖定">Provider 版本鎖定</h3>
<p>斷網環境的 provider 版本管理比連網更嚴格——升級一個 provider 代表要重新搬運整個 provider binary。在 <code>versions.tf</code> 裡鎖定精確版本（<code>= 5.50.0</code> 而非 <code>~&gt; 5.50</code>），避免 <code>init</code> 期待一個 mirror 裡沒有的版本：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">terraform</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">required_providers</span> {
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">    aws</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">      source</span>  <span class="o">=</span> <span class="s2">&#34;hashicorp/aws&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">      version</span> <span class="o">=</span><span class="n"> &#34;</span><span class="o">=</span> <span class="m">5</span><span class="p">.</span><span class="m">50</span><span class="p">.</span><span class="m">0</span><span class="err">&#34;</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></span><span class="line"><span class="ln">8</span><span class="cl">}</span></span></code></pre></div><h2 id="module-離線來源">Module 離線來源</h2>
<p>連網環境的 module source 常指向 Terraform Registry 或 GitHub：<code>source = &quot;terraform-aws-modules/vpc/aws&quot;</code>。斷網環境要改成本地路徑或內部 git server。</p>
<h3 id="本地路徑">本地路徑</h3>
<p>最簡單——module 放在同一個 repo 或共用檔案系統的目錄裡：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">module</span> <span class="s2">&#34;network&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  source</span> <span class="o">=</span> <span class="s2">&#34;../../modules/network&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">}</span></span></code></pre></div><h3 id="內部-git-server">內部 Git Server</h3>
<p>如果有架 Gitea 或 GitLab CE（見<a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網通用原則</a>），module 可以指向內部的 git repo：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">module</span> <span class="s2">&#34;network&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  source</span> <span class="o">=</span><span class="n"> &#34;git::http://gitea.internal/infra/modules.git//network?ref</span><span class="o">=</span><span class="k">v1</span><span class="p">.</span><span class="m">2</span><span class="p">.</span><span class="m">0</span><span class="err">&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">}</span></span></code></pre></div><p><code>ref=v1.2.0</code> 鎖定版本。內部 git server 的 module repo 用 git bundle 從外部搬運更新。</p>
<h2 id="state-backend沒有-s3-時的替代">State Backend：沒有 S3 時的替代</h2>
<p>連網環境的 state 通常放 S3 + DynamoDB lock。斷網環境如果沒有 AWS（地端機房或隔離網路），state backend 的替代選項：</p>
<table>
  <thead>
      <tr>
          <th>Backend</th>
          <th>適用情境</th>
          <th>Lock 機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>本地檔案 + 共用磁碟</td>
          <td>小團隊、單人操作</td>
          <td>無（靠紀律避免並行 apply）</td>
      </tr>
      <tr>
          <td>Consul</td>
          <td>內網有 Consul cluster</td>
          <td>內建 lock</td>
      </tr>
      <tr>
          <td>PostgreSQL</td>
          <td>內網有 PostgreSQL</td>
          <td>內建 lock</td>
      </tr>
      <tr>
          <td>GitLab managed state</td>
          <td>內網有 GitLab CE</td>
          <td>內建 lock</td>
      </tr>
      <tr>
          <td>HTTP backend</td>
          <td>自建簡易 API</td>
          <td>自建 lock</td>
      </tr>
  </tbody>
</table>
<p>最常見的組合是 <strong>PostgreSQL backend</strong>——多數環境已經有 PostgreSQL，不需要額外裝服務：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">terraform</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">backend</span> <span class="s2">&#34;pg&#34;</span> {
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">    conn_str</span> <span class="o">=</span><span class="n"> &#34;postgres://terraform:password@db.internal/terraform_state?sslmode</span><span class="o">=</span><span class="k">disable</span><span class="err">&#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></span></code></pre></div><p>PostgreSQL backend 的 lock 機制用 PostgreSQL 的 advisory lock，多人同時 apply 時第二個人會被擋住。</p>
<p>state 的備份紀律不變——定期 <code>terraform state pull &gt; backup.json</code>，backup 存在版本控制或另一台機器上。</p>
<h2 id="plan--apply-流程">Plan / Apply 流程</h2>
<p>斷網不影響 plan 和 apply 的執行——它們操作的是本地 provider 和目標基礎設施（地端伺服器、內部雲、VMware vSphere 等）。影響的是 provider 初始化和 module 取得，這些在前面幾節已處理。</p>
<h3 id="沒有雲端-api-的情境">沒有雲端 API 的情境</h3>
<p>如果基礎設施不是雲端（地端 VMware、OpenStack、裸機），Terraform 有對應的 provider：</p>
<ul>
<li>VMware vSphere：<code>hashicorp/vsphere</code></li>
<li>OpenStack：<code>terraform-provider-openstack/openstack</code></li>
<li>Proxmox：<code>telmate/proxmox</code>（社群維護）</li>
<li>裸機管理：用 <code>null_resource</code> + <code>local-exec</code> 呼叫 Ansible 或 shell script</li>
</ul>
<p>provider 的離線管理方式相同——mirror 或 plugin cache。</p>
<h3 id="plan-輸出的離線-review">Plan 輸出的離線 Review</h3>
<p>沒有 GitHub PR 的環境，plan 輸出用檔案分享 review：</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"># 產出 plan 並存成可讀格式</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">terraform plan -out<span class="o">=</span>plan.tfplan
</span></span><span class="line"><span class="ln">3</span><span class="cl">terraform show plan.tfplan &gt; plan-review-<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>.txt
</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"># 把 review 檔放到內部共用位置供 reviewer 閱讀</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">cp plan-review-*.txt /shared/reviews/</span></span></code></pre></div><p>reviewer 讀完後以 email、內部 chat、或直接在 review 檔旁邊放一個 <code>approved-by-alice-20260626.txt</code> 標記核准。不優雅但可追溯。</p>
<h2 id="內網-cicd">內網 CI/CD</h2>
<p>斷網環境的 CI/CD 用自架的 CI server：</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>特性</th>
          <th>適用規模</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GitLab CE + Runner</td>
          <td>完整的 git + CI + review，功能最豐富</td>
          <td>中大團隊</td>
      </tr>
      <tr>
          <td>Gitea + Drone / Woodpecker</td>
          <td>輕量 git + 輕量 CI</td>
          <td>小團隊</td>
      </tr>
      <tr>
          <td>Jenkins</td>
          <td>老牌 CI、plugin 生態豐富</td>
          <td>任何規模（但維護成本高）</td>
      </tr>
  </tbody>
</table>
<p>CI server 本身也需要離線安裝——GitLab CE 有 offline 安裝指南（<code>.deb</code> / <code>.rpm</code> 包）、Gitea 是單一二進位。CI runner 執行 Terraform 時使用內部的 provider mirror 和 module source。</p>
<p>CI workflow 的離線版本跟連網版本結構相同（init → fmt → validate → plan → review → apply），差別在 init 用 <code>-plugin-dir</code> 而非連網下載。</p>
<p>時程參考：內網 CI server 的初次建置（含 git server + CI runner + Terraform 離線環境）約需 3-5 天。之後的維護主要是 provider 版本更新的搬運（每次 1-2 小時）。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網環境的通用原則</a>：provider 和 module 的搬運走 content ferry 模式</li>
<li>→ <a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一：最小可行 IaC</a>：連網環境的 IaC 選型和 state 管理</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>：連網環境的 CI pipeline 設定</li>
</ul>
]]></content:encoded></item><item><title>斷網環境的容器與映像管理</title><link>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-container/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-container/</guid><description>&lt;p>容器化應用在斷網環境的主要挑戰不是容器本身——Docker 和 containerd 不需要網路就能啟動容器。挑戰在映像的取得和更新：沒有 Docker Hub、沒有 ECR、沒有 ghcr.io，每一個 base image 和應用映像都要經過搬運路徑進入隔離網路。映像的管理在斷網環境裡需要一條完整的 pipeline：外部下載 → 安全掃描 → 搬運 → 推送到內部 registry → 各節點 pull。&lt;/p>
&lt;h2 id="private-registry">Private Registry&lt;/h2>
&lt;p>隔離網路裡需要一個容器映像倉庫，讓內部的 Docker host / Kubernetes 節點能 pull image。&lt;/p>
&lt;h3 id="harbor">Harbor&lt;/h3>
&lt;p>Harbor 是 VMware 開源的企業級 registry，功能包含：映像儲存、漏洞掃描（整合 Trivy）、存取控制（RBAC）、映像簽章（Cosign / Notary）、複製策略。適合中大規模的斷網環境。&lt;/p>
&lt;p>離線安裝：Harbor 提供 offline installer（&lt;code>.tgz&lt;/code>，約 600MB），包含所有需要的容器映像。搬進隔離網路後解壓、跑 &lt;code>install.sh&lt;/code>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：下載 offline installer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">wget https://github.com/goharbor/harbor/releases/download/v2.11.0/harbor-offline-installer-v2.11.0.tgz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運後，在內部解壓安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">tar xzf harbor-offline-installer-v2.11.0.tgz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> harbor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">cp harbor.yml.tmpl harbor.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 編輯 harbor.yml：設定 hostname、HTTPS 憑證、admin 密碼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">./install.sh&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="docker-registry官方輕量版">Docker Registry（官方輕量版）&lt;/h3>
&lt;p>如果不需要 Harbor 的進階功能（RBAC、掃描），官方的 Docker Registry 是單一容器、設定最簡單：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># registry image 也要先搬進來&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">docker load &amp;lt; registry-2.8.3.tar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">docker run -d -p 5000:5000 --restart&lt;span class="o">=&lt;/span>always --name registry &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -v /data/registry:/var/lib/registry &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> registry:2.8.3&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>內部機器的 Docker daemon 要設定信任這個 registry（如果是 HTTP 而非 HTTPS）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;insecure-registries&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;registry.internal:5000&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="映像搬運">映像搬運&lt;/h2>
&lt;h3 id="docker-save--load">docker save / load&lt;/h3>
&lt;p>最直接的搬運方式——把映像匯出成 tar 檔、搬運後匯入：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：匯出&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">docker pull nginx:1.25-alpine
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">docker save nginx:1.25-alpine -o nginx-1.25-alpine.tar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運後，內部匯入&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">docker load &amp;lt; nginx-1.25-alpine.tar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 重新 tag 指向內部 registry&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">docker tag nginx:1.25-alpine registry.internal:5000/nginx:1.25-alpine
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">docker push registry.internal:5000/nginx:1.25-alpine&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>多個映像可以打包成一個 tar：&lt;code>docker save img1 img2 img3 -o bundle.tar&lt;/code>。&lt;/p>
&lt;h3 id="skopeo-copy">skopeo copy&lt;/h3>
&lt;p>skopeo 是不需要 Docker daemon 的映像操作工具，適合 CI 環境或沒有裝 Docker 的工作站：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：從 Docker Hub 複製到本地目錄&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">skopeo copy docker://nginx:1.25-alpine dir:/path/to/export/nginx-1.25
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運後，從本地目錄推送到內部 registry&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">skopeo copy dir:/path/to/export/nginx-1.25 docker://registry.internal:5000/nginx:1.25-alpine&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>skopeo 的優勢是不需要 pull 整個映像到本地 Docker（省磁碟空間）、支援 OCI layout、且可以在沒有 root 權限的環境執行。&lt;/p>
&lt;h3 id="搬運清單管理">搬運清單管理&lt;/h3>
&lt;p>映像搬運容易變成「需要什麼才搬什麼」的臨時操作。建議維護一份搬運清單（manifest），列出所有需要的 base image 和版本：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c"># image-manifest.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">images&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tag&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1.25&lt;/span>-&lt;span class="l">alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">source&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker.io/library/nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tag&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;16.3&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">source&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker.io/library/postgres&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tag&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20&lt;/span>-&lt;span class="l">alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">source&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker.io/library/node&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>搬運腳本讀這份清單自動 pull + save，確保每次搬運的內容一致且可追蹤。&lt;/p></description><content:encoded><![CDATA[<p>容器化應用在斷網環境的主要挑戰不是容器本身——Docker 和 containerd 不需要網路就能啟動容器。挑戰在映像的取得和更新：沒有 Docker Hub、沒有 ECR、沒有 ghcr.io，每一個 base image 和應用映像都要經過搬運路徑進入隔離網路。映像的管理在斷網環境裡需要一條完整的 pipeline：外部下載 → 安全掃描 → 搬運 → 推送到內部 registry → 各節點 pull。</p>
<h2 id="private-registry">Private Registry</h2>
<p>隔離網路裡需要一個容器映像倉庫，讓內部的 Docker host / Kubernetes 節點能 pull image。</p>
<h3 id="harbor">Harbor</h3>
<p>Harbor 是 VMware 開源的企業級 registry，功能包含：映像儲存、漏洞掃描（整合 Trivy）、存取控制（RBAC）、映像簽章（Cosign / Notary）、複製策略。適合中大規模的斷網環境。</p>
<p>離線安裝：Harbor 提供 offline installer（<code>.tgz</code>，約 600MB），包含所有需要的容器映像。搬進隔離網路後解壓、跑 <code>install.sh</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"># 外部：下載 offline installer</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">wget https://github.com/goharbor/harbor/releases/download/v2.11.0/harbor-offline-installer-v2.11.0.tgz
</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">tar xzf harbor-offline-installer-v2.11.0.tgz
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">cd</span> harbor
</span></span><span class="line"><span class="ln">7</span><span class="cl">cp harbor.yml.tmpl harbor.yml
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># 編輯 harbor.yml：設定 hostname、HTTPS 憑證、admin 密碼</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">./install.sh</span></span></code></pre></div><h3 id="docker-registry官方輕量版">Docker Registry（官方輕量版）</h3>
<p>如果不需要 Harbor 的進階功能（RBAC、掃描），官方的 Docker Registry 是單一容器、設定最簡單：</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"># registry image 也要先搬進來</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker load &lt; registry-2.8.3.tar
</span></span><span class="line"><span class="ln">3</span><span class="cl">docker run -d -p 5000:5000 --restart<span class="o">=</span>always --name registry <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  -v /data/registry:/var/lib/registry <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  registry:2.8.3</span></span></code></pre></div><p>內部機器的 Docker daemon 要設定信任這個 registry（如果是 HTTP 而非 HTTPS）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;insecure-registries&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;registry.internal:5000&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="映像搬運">映像搬運</h2>
<h3 id="docker-save--load">docker save / load</h3>
<p>最直接的搬運方式——把映像匯出成 tar 檔、搬運後匯入：</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"># 外部：匯出</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker pull nginx:1.25-alpine
</span></span><span class="line"><span class="ln">3</span><span class="cl">docker save nginx:1.25-alpine -o nginx-1.25-alpine.tar
</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">docker load &lt; nginx-1.25-alpine.tar
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 重新 tag 指向內部 registry</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">docker tag nginx:1.25-alpine registry.internal:5000/nginx:1.25-alpine
</span></span><span class="line"><span class="ln">9</span><span class="cl">docker push registry.internal:5000/nginx:1.25-alpine</span></span></code></pre></div><p>多個映像可以打包成一個 tar：<code>docker save img1 img2 img3 -o bundle.tar</code>。</p>
<h3 id="skopeo-copy">skopeo copy</h3>
<p>skopeo 是不需要 Docker daemon 的映像操作工具，適合 CI 環境或沒有裝 Docker 的工作站：</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"># 外部：從 Docker Hub 複製到本地目錄</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">skopeo copy docker://nginx:1.25-alpine dir:/path/to/export/nginx-1.25
</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"># 搬運後，從本地目錄推送到內部 registry</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">skopeo copy dir:/path/to/export/nginx-1.25 docker://registry.internal:5000/nginx:1.25-alpine</span></span></code></pre></div><p>skopeo 的優勢是不需要 pull 整個映像到本地 Docker（省磁碟空間）、支援 OCI layout、且可以在沒有 root 權限的環境執行。</p>
<h3 id="搬運清單管理">搬運清單管理</h3>
<p>映像搬運容易變成「需要什麼才搬什麼」的臨時操作。建議維護一份搬運清單（manifest），列出所有需要的 base image 和版本：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># image-manifest.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">images</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">tag</span><span class="p">:</span><span class="w"> </span><span class="m">1.25</span>-<span class="l">alpine</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/library/nginx</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">postgres</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">tag</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;16.3&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/library/postgres</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">node</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">tag</span><span class="p">:</span><span class="w"> </span><span class="m">20</span>-<span class="l">alpine</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/library/node</span></span></span></code></pre></div><p>搬運腳本讀這份清單自動 pull + save，確保每次搬運的內容一致且可追蹤。</p>
<h2 id="base-image-更新週期">Base Image 更新週期</h2>
<p>斷網環境的 base image 不會自動更新——<code>nginx:1.25-alpine</code> 搬進去之後就是那個版本，裡面的 Alpine 套件不會收到安全補丁。需要定期用新版 base image 替換舊的。</p>
<h3 id="更新流程">更新流程</h3>
<ol>
<li><strong>外部</strong>：pull 最新版 base image</li>
<li><strong>外部</strong>：用 Trivy 掃描漏洞（見下一節）</li>
<li><strong>搬運</strong>：走 content ferry 帶進內部</li>
<li><strong>內部</strong>：push 到內部 registry、更新 tag</li>
<li><strong>內部</strong>：重新 build 所有依賴這個 base image 的應用映像</li>
<li><strong>內部</strong>：部署更新後的應用映像</li>
</ol>
<p>更新頻率：安全敏感環境月更、一般環境季更。每次更新都要記錄哪些 base image 換了、從哪個版本換到哪個版本。</p>
<h3 id="helm-chart-離線">Helm Chart 離線</h3>
<p>如果內部有 Kubernetes 且使用 Helm，chart 也要離線管理：</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"># 外部：下載 chart</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">helm pull bitnami/postgresql --version 15.5.0
</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">helm install pg ./postgresql-15.5.0.tgz -f values.yaml</span></span></code></pre></div><p>或架設 ChartMuseum 作為內部 Helm repo：chart 搬進來後 push 到 ChartMuseum，<code>helm repo add</code> 指向它。</p>
<h2 id="離線漏洞掃描">離線漏洞掃描</h2>
<p>連網環境的 Trivy 會自動下載漏洞資料庫（CVE DB）。斷網環境要先在外部下載 DB、搬進來。</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"># 外部：下載 Trivy 漏洞資料庫</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">trivy image --download-db-only --cache-dir /path/to/trivy-db/
</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"># 搬運 DB 檔案（~30MB）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># db.tar.gz 在 /path/to/trivy-db/db/ 裡</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"># 內部：用離線 DB 掃描</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">trivy image --skip-db-update --cache-dir /path/to/trivy-db/ registry.internal:5000/nginx:1.25-alpine</span></span></code></pre></div><p>掃描結果的處理方式跟連網環境相同——critical 和 high 的 CVE 要評估是否影響、是否有 base image 更新可修。差別是斷網環境的修復週期更長（要走搬運流程），所以掃描要更頻繁（至少跟 base image 更新同步）。</p>
<p>Harbor 整合 Trivy 後可以在 push 時自動掃描——Trivy DB 的更新同樣需要定期搬運。</p>
<p>時程參考：Private registry 建置（Harbor offline）約需 1 天。映像搬運流程建立約需半天。第一批 base image 搬運 + 掃描約需半天。之後每次更新約 2-4 小時。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網環境的通用原則</a>：映像搬運走 content ferry 模式</li>
<li>→ <a href="/blog/infra/05-core-services/compute-ecs-eks/" data-link-title="運算平台上 IaC — ECS 與 EKS" data-link-desc="容器運算平台的 IaC 描述：ECS 與 EKS 選型、task definition 與映像版本解耦、IAM task role 分離、auto-scaling 策略">模組五：核心服務上 IaC — 運算</a>：連網環境的容器部署</li>
<li>→ <a href="/blog/infra/knowledge-cards/ecs/" data-link-title="ECS" data-link-desc="AWS Elastic Container Service — 受管的容器編排服務，用 task definition 描述容器配置、由平台負責排程與健康管理">ECS 知識卡</a>：容器編排的基礎概念</li>
</ul>
]]></content:encoded></item><item><title>斷網環境的監控與可觀測性</title><link>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-monitoring/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-monitoring/</guid><description>&lt;p>斷網環境不能用 Datadog、New Relic、Sentry Cloud、PagerDuty Cloud 這些 SaaS 監控服務——它們全部需要往外發送資料。監控的三個核心能力（metric 收集、log 彙整、告警通知）全部要用 self-hosted 的開源工具在隔離網路內搭建。原則跟連網環境相同（metric 跟資源同生命週期、alarm 要連到動作），差別在工具的部署和儲存規劃要自己管。&lt;/p>
&lt;h2 id="metric-收集prometheus--grafana">Metric 收集：Prometheus + Grafana&lt;/h2>
&lt;p>Prometheus 是 pull-based 的 metric 收集系統——它主動去 scrape 各服務的 metric endpoint，不需要服務往外推資料。這個架構天然適合斷網：所有流量都在內網、不需要出站連線。&lt;/p>
&lt;h3 id="離線安裝">離線安裝&lt;/h3>
&lt;p>Prometheus 和 Grafana 都是單一二進位或容器映像，離線安裝跟&lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">映像搬運&lt;/a>相同的流程：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：下載 release binary&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">wget https://github.com/prometheus/prometheus/releases/download/v2.53.0/prometheus-2.53.0.linux-amd64.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">wget https://dl.grafana.com/oss/release/grafana-11.1.0.linux-amd64.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運後解壓、設定 systemd service&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">tar xzf prometheus-2.53.0.linux-amd64.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">sudo mv prometheus-2.53.0.linux-amd64 /opt/prometheus&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果用容器部署，先把映像搬進內部 registry 再 pull：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 內部：從內部 registry 啟動&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">docker run -d -p 9090:9090 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -v /etc/prometheus:/etc/prometheus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -v /data/prometheus:/prometheus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> registry.internal:5000/prometheus:v2.53.0&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="scrape-設定">Scrape 設定&lt;/h3>
&lt;p>Prometheus 的 &lt;code>prometheus.yml&lt;/code> 定義要 scrape 的目標。斷網環境通常用 static config（手動列出目標）而非 service discovery（需要雲端 API）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">scrape_configs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">job_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;node-exporter&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">static_configs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;server-01:9100&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;server-02:9100&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;db-01:9100&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">job_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;app&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">static_configs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;app-01:8080&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;app-02:8080&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metrics_path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;/metrics&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>新增機器時手動把它加進 targets 清單。如果用 Consul（內網 service discovery），Prometheus 支援 Consul SD、可以自動發現新服務。&lt;/p>
&lt;h3 id="node-exporter">Node Exporter&lt;/h3>
&lt;p>每台需要監控的 Linux 機器裝一個 node_exporter（單一二進位、無依賴），暴露 CPU、記憶體、磁碟、網路等系統 metric。離線安裝同理——下載 binary、搬運、解壓、設成 service。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運後安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">tar xzf node_exporter-1.8.1.linux-amd64.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">sudo cp node_exporter-1.8.1.linux-amd64/node_exporter /usr/local/bin/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">sudo useradd --no-create-home --shell /bin/false node_exporter
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 建立 systemd service（略）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="log-收集loki-或-elk">Log 收集：Loki 或 ELK&lt;/h2>
&lt;h3 id="grafana-loki輕量">Grafana Loki（輕量）&lt;/h3>
&lt;p>Loki 是 Grafana 生態的 log 彙整系統，架構類似 Prometheus（pull/push 都支援），但儲存的是 log stream 而非 metric。它不索引 log 內容（只索引 label），所以儲存成本遠低於 Elasticsearch。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c"># loki-config.yaml 基本設定&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">auth_enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">http_listen_port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">storage_config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filesystem&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">directory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/data/loki/chunks&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">schema_config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">configs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="ld">2024-01-01&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">store&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tsdb&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">object_store&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">filesystem&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">schema&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v13&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">index&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">index_&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">period&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">24h&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>搭配 Promtail（log 收集 agent）在每台機器上收集 log 並推送到 Loki：&lt;/p></description><content:encoded><![CDATA[<p>斷網環境不能用 Datadog、New Relic、Sentry Cloud、PagerDuty Cloud 這些 SaaS 監控服務——它們全部需要往外發送資料。監控的三個核心能力（metric 收集、log 彙整、告警通知）全部要用 self-hosted 的開源工具在隔離網路內搭建。原則跟連網環境相同（metric 跟資源同生命週期、alarm 要連到動作），差別在工具的部署和儲存規劃要自己管。</p>
<h2 id="metric-收集prometheus--grafana">Metric 收集：Prometheus + Grafana</h2>
<p>Prometheus 是 pull-based 的 metric 收集系統——它主動去 scrape 各服務的 metric endpoint，不需要服務往外推資料。這個架構天然適合斷網：所有流量都在內網、不需要出站連線。</p>
<h3 id="離線安裝">離線安裝</h3>
<p>Prometheus 和 Grafana 都是單一二進位或容器映像，離線安裝跟<a href="/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">映像搬運</a>相同的流程：</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"># 外部：下載 release binary</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">wget https://github.com/prometheus/prometheus/releases/download/v2.53.0/prometheus-2.53.0.linux-amd64.tar.gz
</span></span><span class="line"><span class="ln">3</span><span class="cl">wget https://dl.grafana.com/oss/release/grafana-11.1.0.linux-amd64.tar.gz
</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"># 搬運後解壓、設定 systemd service</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">tar xzf prometheus-2.53.0.linux-amd64.tar.gz
</span></span><span class="line"><span class="ln">7</span><span class="cl">sudo mv prometheus-2.53.0.linux-amd64 /opt/prometheus</span></span></code></pre></div><p>如果用容器部署，先把映像搬進內部 registry 再 pull：</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"># 內部：從內部 registry 啟動</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker run -d -p 9090:9090 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -v /etc/prometheus:/etc/prometheus <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  -v /data/prometheus:/prometheus <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  registry.internal:5000/prometheus:v2.53.0</span></span></code></pre></div><h3 id="scrape-設定">Scrape 設定</h3>
<p>Prometheus 的 <code>prometheus.yml</code> 定義要 scrape 的目標。斷網環境通常用 static config（手動列出目標）而非 service discovery（需要雲端 API）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">scrape_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;node-exporter&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">      </span>- <span class="nt">targets</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">          </span>- <span class="s1">&#39;server-01:9100&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">          </span>- <span class="s1">&#39;server-02:9100&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">          </span>- <span class="s1">&#39;db-01:9100&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;app&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span>- <span class="nt">targets</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">          </span>- <span class="s1">&#39;app-01:8080&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">          </span>- <span class="s1">&#39;app-02:8080&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">metrics_path</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;/metrics&#39;</span></span></span></code></pre></div><p>新增機器時手動把它加進 targets 清單。如果用 Consul（內網 service discovery），Prometheus 支援 Consul SD、可以自動發現新服務。</p>
<h3 id="node-exporter">Node Exporter</h3>
<p>每台需要監控的 Linux 機器裝一個 node_exporter（單一二進位、無依賴），暴露 CPU、記憶體、磁碟、網路等系統 metric。離線安裝同理——下載 binary、搬運、解壓、設成 service。</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"># 搬運後安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">tar xzf node_exporter-1.8.1.linux-amd64.tar.gz
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo cp node_exporter-1.8.1.linux-amd64/node_exporter /usr/local/bin/
</span></span><span class="line"><span class="ln">4</span><span class="cl">sudo useradd --no-create-home --shell /bin/false node_exporter
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 建立 systemd service（略）</span></span></span></code></pre></div><h2 id="log-收集loki-或-elk">Log 收集：Loki 或 ELK</h2>
<h3 id="grafana-loki輕量">Grafana Loki（輕量）</h3>
<p>Loki 是 Grafana 生態的 log 彙整系統，架構類似 Prometheus（pull/push 都支援），但儲存的是 log stream 而非 metric。它不索引 log 內容（只索引 label），所以儲存成本遠低於 Elasticsearch。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># loki-config.yaml 基本設定</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">auth_enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">server</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">http_listen_port</span><span class="p">:</span><span class="w"> </span><span class="m">3100</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="nt">storage_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">filesystem</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">directory</span><span class="p">:</span><span class="w"> </span><span class="l">/data/loki/chunks</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="nt">schema_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span>- <span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="ld">2024-01-01</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">store</span><span class="p">:</span><span class="w"> </span><span class="l">tsdb</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">object_store</span><span class="p">:</span><span class="w"> </span><span class="l">filesystem</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">schema</span><span class="p">:</span><span class="w"> </span><span class="l">v13</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span><span class="nt">index</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="nt">prefix</span><span class="p">:</span><span class="w"> </span><span class="l">index_</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span><span class="nt">period</span><span class="p">:</span><span class="w"> </span><span class="l">24h</span></span></span></code></pre></div><p>搭配 Promtail（log 收集 agent）在每台機器上收集 log 並推送到 Loki：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># promtail-config.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">clients</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">http://loki.internal:3100/loki/api/v1/push</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="nt">scrape_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="l">system</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span>- <span class="nt">targets</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">localhost]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">          </span><span class="nt">job</span><span class="p">:</span><span class="w"> </span><span class="l">syslog</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">          </span><span class="nt">__path__</span><span class="p">:</span><span class="w"> </span><span class="l">/var/log/*.log</span></span></span></code></pre></div><h3 id="elk-stack功能豐富">ELK Stack（功能豐富）</h3>
<p>Elasticsearch + Logstash + Kibana 是功能最完整的 log 平台，但資源消耗大（Elasticsearch 建議至少 4GB RAM 起跳）。適合需要全文搜索 log 內容的場景。</p>
<p>離線安裝：Elastic 提供離線安裝包（<code>.deb</code> / <code>.rpm</code>），或用 Docker 映像。三個組件都要搬運。</p>
<p>選型判準：5 台以下的小環境用 Loki（輕量、跟 Prometheus + Grafana 同一套 dashboard）。需要全文搜索、已有 ELK 經驗的團隊用 ELK。</p>
<h2 id="告警沒有外部-webhook-怎麼通知">告警：沒有外部 webhook 怎麼通知</h2>
<p>連網環境的告警通常發到 Slack webhook、PagerDuty API、或 email relay service。斷網環境這些路徑都不通。</p>
<h3 id="內部-smtp">內部 SMTP</h3>
<p>如果隔離網路內有 email server（很多企業內網有 Exchange 或 Postfix），Prometheus Alertmanager 可以發 email 告警：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># alertmanager.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">route</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">receiver</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;email-team&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="nt">receivers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;email-team&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">email_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span>- <span class="nt">to</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;oncall@internal.corp&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span><span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;alertmanager@internal.corp&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">smarthost</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;smtp.internal.corp:25&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="nt">require_tls</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span></span></span></code></pre></div><h3 id="內部即時通訊">內部即時通訊</h3>
<p>如果內網有 Mattermost（Slack 的 self-hosted 替代）或 Rocket.Chat，Alertmanager 可以用 webhook 發送到這些工具的 incoming webhook endpoint。</p>
<h3 id="實體告警">實體告警</h3>
<p>極端情境（沒有 email、沒有 chat）：Alertmanager 把告警寫到檔案或資料庫、搭配值班制度定期查看。或用 Grafana 的 dashboard + 控制室大螢幕，值班人員直接看板。</p>
<p>告警的設計原則跟連網環境相同——symptom-based（錯誤率、延遲）優先於 cause-based（CPU、記憶體），閾值設計避免告警疲勞。差別在通知的到達速度可能慢一些（email 比 Slack push 慢），所以閾值要稍微保守（提早告警）。</p>
<h2 id="metric-與-log-的儲存規劃">Metric 與 Log 的儲存規劃</h2>
<p>SaaS 監控的儲存是雲端自動擴展的。Self-hosted 的儲存要自己規劃——磁碟滿了 Prometheus 就停止收集、Loki 就停止寫入。</p>
<h3 id="容量估算">容量估算</h3>
<p>Prometheus 的儲存量取決於 series 數量 × scrape 間隔 × 保留天數。粗估公式：</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">每日儲存 ≈ active_series × sample_size(2B) × (86400 / scrape_interval) × compression_ratio(~0.1)</span></span></code></pre></div><p>1 萬個 active series、15 秒 scrape interval、保留 30 天 ≈ 約 5GB。保留 90 天 ≈ 約 15GB。</p>
<p>Loki 的儲存量取決於 log 流量。粗估：每天 10GB 的 raw log 在 Loki 壓縮後約 1-2GB，保留 30 天 ≈ 30-60GB。</p>
<h3 id="retention-設定">Retention 設定</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># prometheus.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">global</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">scrape_interval</span><span class="p">:</span><span class="w"> </span><span class="l">15s</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="nt">storage</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">tsdb</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="nt">retention.time</span><span class="p">:</span><span class="w"> </span><span class="l">30d</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="nt">retention.size</span><span class="p">:</span><span class="w"> </span><span class="l">10GB </span><span class="w"> </span><span class="c"># 以先到的為準</span></span></span></code></pre></div><p>超過容量時 Prometheus 自動刪除最舊的資料。設定 retention 前先確認磁碟空間足夠——斷網環境擴容磁碟的流程（採購 + 安裝）可能需要週到月級的時間。</p>
<h2 id="ntp-時間同步">NTP 時間同步</h2>
<p>斷網環境容易被忽略的一個問題是時間同步。沒有 NTP server（<code>pool.ntp.org</code>）可連的機器，時鐘會漂移——幾天後各台機器的時間差可能達到秒級。當 Prometheus 收到的 metric timestamp 跟 Loki 收到的 log timestamp 有幾秒落差，事故排查時 metric 跟 log 對不上。</p>
<p>解法是在隔離網路內架一台 NTP server，所有機器從它同步：</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"># 內部 NTP server（chrony）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># /etc/chrony/chrony.conf</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">local</span> stratum <span class="m">10</span>         <span class="c1"># 沒有外部來源時、自己當 stratum 10</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">allow 10.0.0.0/16        <span class="c1"># 允許內部網段同步</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 其他機器指向內部 NTP</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">server ntp.internal iburst</span></span></code></pre></div><p>如果隔離網路的閘道可以開 NTP（UDP 123），讓閘道從外部 NTP 同步、內部機器從閘道同步，時間精度可以維持在毫秒級。</p>
<p>時程參考：Prometheus + Grafana + Alertmanager 的初次建置約需 1-2 天。Loki + Promtail 約需半天到一天。NTP server 約需 2 小時。後續維護主要是 Prometheus/Loki 版本更新的搬運（每次 1-2 小時）和儲存容量監控。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網環境的通用原則</a>：監控工具的離線安裝走 content ferry 模式</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">斷網環境的容器管理</a>：Prometheus/Grafana/Loki 的容器映像搬運</li>
<li>→ <a href="/blog/infra/06-observability-logging/" data-link-title="模組六：可觀測性與 log 一併寫進 code" data-link-desc="log group、metric、alarm 跟基礎設施同生命週期管理，出事時追得到查得到">模組六：可觀測性與 log</a>：連網環境的可觀測性 IaC</li>
<li>→ <a href="/blog/infra/takeover/legacy-external-monitoring/" data-link-title="無 SSH 環境的監控與告警" data-link-desc="無 SSH 環境沒辦法裝 agent、沒辦法串 log pipeline，用外部 HTTP check、錯誤追蹤服務與效能基線建立最低成本的監控能力">無 SSH 環境的監控與告警</a>：另一個極端——完全外部監控</li>
<li>→ <a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">Monitoring 04：Collector 架構與部署</a>：SDK 和 Collector 的應用層監控，斷網環境需要把 Collector endpoint 指向 self-hosted backend</li>
<li>→ <a href="/blog/monitoring/06-commercial-comparison/self-hosted-vs-commercial/" data-link-title="自架 vs 商業的判斷決策表" data-link-desc="使用者數、網路範圍、功能需求、合規要求四個維度判斷該自架還是用商業方案">Monitoring 06：Self-hosted vs Commercial</a>：斷網環境只能走 self-hosted 路線</li>
</ul>
]]></content:encoded></item><item><title>斷網環境要自建的服務清單</title><link>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-self-hosted-services/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-self-hosted-services/</guid><description>&lt;p>連網環境的 infra 團隊消費數十個 SaaS 服務：程式碼放 GitHub、CI 用 GitHub Actions、套件從 npm 和 PyPI 拉、container image 從 Docker Hub pull、憑證用 Let&amp;rsquo;s Encrypt 自動簽、監控用 Datadog。這些服務的共同特性是「有人幫你維護」——infra 團隊只需要設定和使用，不需要部署、升級、備份。&lt;/p>
&lt;p>斷網環境裡這些服務全部要自建。每一個 SaaS 變成一個內部服務，infra 團隊承擔它的部署、設定、升級、備份、監控和使用者管理。這篇文章盤點完整的服務清單、推薦的自建工具、部署順序，以及容易被低估的維護成本。&lt;/p>
&lt;h2 id="服務清單與選型">服務清單與選型&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>服務類別&lt;/th>
 &lt;th>連網環境的 SaaS&lt;/th>
 &lt;th>自建替代&lt;/th>
 &lt;th>部署複雜度&lt;/th>
 &lt;th>維護頻率&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>版本控制&lt;/td>
 &lt;td>GitHub / GitLab.com&lt;/td>
 &lt;td>GitLab CE / Gitea&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>月級更新&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CI/CD&lt;/td>
 &lt;td>GitHub Actions&lt;/td>
 &lt;td>Jenkins / GitLab CI&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>週級維護&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>套件 registry&lt;/td>
 &lt;td>npm / PyPI / Maven / apt&lt;/td>
 &lt;td>Nexus Repository&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>月級更新&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容器 registry&lt;/td>
 &lt;td>Docker Hub / ECR&lt;/td>
 &lt;td>Harbor / Docker Registry&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>月級更新&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>內部 CA&lt;/td>
 &lt;td>Let&amp;rsquo;s Encrypt&lt;/td>
 &lt;td>step-ca / cfssl&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>季級輪替&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>內部 DNS&lt;/td>
 &lt;td>Route 53 / Cloud DNS&lt;/td>
 &lt;td>CoreDNS / BIND&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>變更時維護&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>時間同步&lt;/td>
 &lt;td>pool.ntp.org&lt;/td>
 &lt;td>chrony&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>部署後極少&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>監控&lt;/td>
 &lt;td>Datadog / New Relic&lt;/td>
 &lt;td>Prometheus + Grafana + Loki&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>週級維護&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>機密管理&lt;/td>
 &lt;td>AWS Secrets Manager&lt;/td>
 &lt;td>HashiCorp Vault&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>月級維護&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>IaC state 後端&lt;/td>
 &lt;td>S3 + DynamoDB&lt;/td>
 &lt;td>PostgreSQL / Consul&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>變更時維護&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「部署複雜度」指首次部署到可用狀態的工程量。「維護頻率」指部署完成後的持續性工作——安全更新、容量擴充、故障排查。&lt;/p>
&lt;h3 id="各服務的選型判斷">各服務的選型判斷&lt;/h3>
&lt;p>&lt;strong>版本控制&lt;/strong>：GitLab CE 功能完整（含 CI/CD、container registry、package registry），但資源消耗大（建議 4 核 / 8GB 以上）。Gitea 輕量（512MB 記憶體可跑），適合小團隊或只需要 Git hosting 的情境。如果選 GitLab CE，版控 + CI/CD + registry 可以用同一個實例，減少部署數量。&lt;/p>
&lt;p>&lt;strong>CI/CD&lt;/strong>：如果已部署 GitLab CE，內建的 GitLab CI 是最低成本的選擇——Runner 裝在同一網段的機器上即可。Jenkins 的生態更大（plugin 多），但 plugin 的離線安裝和更新需要額外的搬運流程。&lt;/p>
&lt;p>&lt;strong>套件 registry&lt;/strong>：Nexus Repository 是斷網環境的首選，因為它用一個實例同時支援 apt / yum / npm / Maven / PyPI / Docker / Helm——維護一個服務取代六個獨立的離線 repo mirror。Artifactory 是商業替代品，功能相似但需要授權費。&lt;/p>
&lt;p>&lt;strong>容器 registry&lt;/strong>：Harbor 提供映像掃描（整合 Trivy）、RBAC、複寫、稽核 log。如果只需要儲存和拉取映像、不需要掃描和稽核，Docker Registry（開源）足夠。&lt;/p>
&lt;p>&lt;strong>內部 CA&lt;/strong>：step-ca 支援 ACME 協定（跟 Let&amp;rsquo;s Encrypt 相同的自動簽發流程），內部服務可以用跟外部一樣的 certbot 工具自動續期。cfssl 是更輕量的選擇但沒有 ACME 支援、需要手動或腳本續期。&lt;/p></description><content:encoded><![CDATA[<p>連網環境的 infra 團隊消費數十個 SaaS 服務：程式碼放 GitHub、CI 用 GitHub Actions、套件從 npm 和 PyPI 拉、container image 從 Docker Hub pull、憑證用 Let&rsquo;s Encrypt 自動簽、監控用 Datadog。這些服務的共同特性是「有人幫你維護」——infra 團隊只需要設定和使用，不需要部署、升級、備份。</p>
<p>斷網環境裡這些服務全部要自建。每一個 SaaS 變成一個內部服務，infra 團隊承擔它的部署、設定、升級、備份、監控和使用者管理。這篇文章盤點完整的服務清單、推薦的自建工具、部署順序，以及容易被低估的維護成本。</p>
<h2 id="服務清單與選型">服務清單與選型</h2>
<table>
  <thead>
      <tr>
          <th>服務類別</th>
          <th>連網環境的 SaaS</th>
          <th>自建替代</th>
          <th>部署複雜度</th>
          <th>維護頻率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>版本控制</td>
          <td>GitHub / GitLab.com</td>
          <td>GitLab CE / Gitea</td>
          <td>中</td>
          <td>月級更新</td>
      </tr>
      <tr>
          <td>CI/CD</td>
          <td>GitHub Actions</td>
          <td>Jenkins / GitLab CI</td>
          <td>高</td>
          <td>週級維護</td>
      </tr>
      <tr>
          <td>套件 registry</td>
          <td>npm / PyPI / Maven / apt</td>
          <td>Nexus Repository</td>
          <td>中</td>
          <td>月級更新</td>
      </tr>
      <tr>
          <td>容器 registry</td>
          <td>Docker Hub / ECR</td>
          <td>Harbor / Docker Registry</td>
          <td>中</td>
          <td>月級更新</td>
      </tr>
      <tr>
          <td>內部 CA</td>
          <td>Let&rsquo;s Encrypt</td>
          <td>step-ca / cfssl</td>
          <td>低</td>
          <td>季級輪替</td>
      </tr>
      <tr>
          <td>內部 DNS</td>
          <td>Route 53 / Cloud DNS</td>
          <td>CoreDNS / BIND</td>
          <td>低</td>
          <td>變更時維護</td>
      </tr>
      <tr>
          <td>時間同步</td>
          <td>pool.ntp.org</td>
          <td>chrony</td>
          <td>低</td>
          <td>部署後極少</td>
      </tr>
      <tr>
          <td>監控</td>
          <td>Datadog / New Relic</td>
          <td>Prometheus + Grafana + Loki</td>
          <td>高</td>
          <td>週級維護</td>
      </tr>
      <tr>
          <td>機密管理</td>
          <td>AWS Secrets Manager</td>
          <td>HashiCorp Vault</td>
          <td>高</td>
          <td>月級維護</td>
      </tr>
      <tr>
          <td>IaC state 後端</td>
          <td>S3 + DynamoDB</td>
          <td>PostgreSQL / Consul</td>
          <td>低</td>
          <td>變更時維護</td>
      </tr>
  </tbody>
</table>
<p>「部署複雜度」指首次部署到可用狀態的工程量。「維護頻率」指部署完成後的持續性工作——安全更新、容量擴充、故障排查。</p>
<h3 id="各服務的選型判斷">各服務的選型判斷</h3>
<p><strong>版本控制</strong>：GitLab CE 功能完整（含 CI/CD、container registry、package registry），但資源消耗大（建議 4 核 / 8GB 以上）。Gitea 輕量（512MB 記憶體可跑），適合小團隊或只需要 Git hosting 的情境。如果選 GitLab CE，版控 + CI/CD + registry 可以用同一個實例，減少部署數量。</p>
<p><strong>CI/CD</strong>：如果已部署 GitLab CE，內建的 GitLab CI 是最低成本的選擇——Runner 裝在同一網段的機器上即可。Jenkins 的生態更大（plugin 多），但 plugin 的離線安裝和更新需要額外的搬運流程。</p>
<p><strong>套件 registry</strong>：Nexus Repository 是斷網環境的首選，因為它用一個實例同時支援 apt / yum / npm / Maven / PyPI / Docker / Helm——維護一個服務取代六個獨立的離線 repo mirror。Artifactory 是商業替代品，功能相似但需要授權費。</p>
<p><strong>容器 registry</strong>：Harbor 提供映像掃描（整合 Trivy）、RBAC、複寫、稽核 log。如果只需要儲存和拉取映像、不需要掃描和稽核，Docker Registry（開源）足夠。</p>
<p><strong>內部 CA</strong>：step-ca 支援 ACME 協定（跟 Let&rsquo;s Encrypt 相同的自動簽發流程），內部服務可以用跟外部一樣的 certbot 工具自動續期。cfssl 是更輕量的選擇但沒有 ACME 支援、需要手動或腳本續期。</p>
<p><strong>內部 DNS</strong>：CoreDNS 用設定檔驅動、輕量、適合 Kubernetes 環境。BIND 是傳統選擇、功能完整但設定複雜。多數斷網環境的 DNS 需求簡單（幾十筆 A record），CoreDNS 的 file plugin 足夠。</p>
<p><strong>時間同步</strong>：chrony 是 NTP 的現代替代——啟動快、適應性強、低資源。內網裡指定一台機器當 NTP server（stratum 1 如果有 GPS 時鐘、stratum 2 如果手動校時），其他機器指向它。時間不同步會讓 log correlation 失效、TLS 憑證驗證失敗、Kerberos 認證拒絕。</p>
<p><strong>監控</strong>：Prometheus（metric 收集）+ Grafana（視覺化）+ Loki（log 聚合）是最常見的 self-hosted 監控組合。三者都支援離線部署、不需要外部依賴。詳見<a href="/blog/infra/air-gapped/air-gapped-monitoring/" data-link-title="斷網環境的監控與可觀測性" data-link-desc="Self-hosted 監控（Prometheus &#43; Grafana）、離線 log 收集（Loki / ELK）、不能 phone home 的告警、NTP 時間同步">斷網環境的監控與可觀測性</a>。</p>
<p><strong>機密管理</strong>：HashiCorp Vault 提供 secret 儲存、動態 secret 產生、PKI、加密即服務。部署和維護複雜度高——Vault 本身需要 unseal、HA 需要 Raft 或 Consul 後端、稽核 log 需要儲存規劃。如果機密數量少且變更不頻繁，加密的 ansible-vault 或 git-crypt 是輕量替代。</p>
<p><strong>IaC state 後端</strong>：PostgreSQL 是 Terraform 支援的 state backend 之一（<code>backend &quot;pg&quot;</code>），斷網環境裡用既有的 PostgreSQL 實例存 state、用 PostgreSQL 的 advisory lock 防並行。比自建 S3 + DynamoDB 簡單得多。Consul 是另一個選擇（Terraform 原生支援），但引入 Consul 只為了存 state 的 ROI 通常不划算、除非環境裡已經有 Consul 跑 service discovery。</p>
<h2 id="部署順序">部署順序</h2>
<p>服務之間有依賴關係，部署順序由依賴方向決定：</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">第一層（基礎設施服務）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  DNS → 所有服務都需要名稱解析
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  NTP → 所有服務都需要時間同步
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  CA  → 所有服務都需要 TLS 憑證
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</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">  版本控制 → 程式碼要有地方存才能跑 CI
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  套件 + 容器 registry → build 需要依賴
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</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">  CI/CD → 依賴版控 + registry
</span></span><span class="line"><span class="ln">12</span><span class="cl">  IaC state backend → Terraform 需要 state 存放處
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">第四層（營運服務）
</span></span><span class="line"><span class="ln">15</span><span class="cl">  機密管理 → 其他服務的 secret 集中管理
</span></span><span class="line"><span class="ln">16</span><span class="cl">  監控 → 監控所有上述服務的健康</span></span></code></pre></div><p>第一層的三個服務可以平行部署——它們彼此不依賴。第四層的監控放最後是因為它要監控的對象都還沒就位時、設定 target 沒有意義。</p>
<p>每一層部署完成後做一次整體驗證（所有服務能互相連通、TLS 正常、時間同步），再進下一層。</p>
<h2 id="統一管理-vs-個別部署">統一管理 vs 個別部署</h2>
<p>GitLab CE 把版控、CI/CD、container registry、package registry 打包在一個實例裡。用 GitLab CE 取代四個獨立服務的優缺點：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>統一（GitLab CE）</th>
          <th>個別部署</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署成本</td>
          <td>部署 1 個服務</td>
          <td>部署 4 個服務</td>
      </tr>
      <tr>
          <td>維護</td>
          <td>升級 1 個服務</td>
          <td>各自升級週期</td>
      </tr>
      <tr>
          <td>資源消耗</td>
          <td>單機 8GB+ 記憶體</td>
          <td>分散在多台</td>
      </tr>
      <tr>
          <td>故障半徑</td>
          <td>GitLab 掛 = 版控 + CI + registry 全停</td>
          <td>某一個掛不影響其他</td>
      </tr>
      <tr>
          <td>靈活性</td>
          <td>綁 GitLab 生態</td>
          <td>各服務可獨立替換</td>
      </tr>
  </tbody>
</table>
<p>小團隊（5-15 人）的斷網環境，GitLab CE 統一管理的 ROI 通常較高——維護一個服務比維護四個省力，故障半徑的風險靠備份和 HA（GitLab 支援 Geo replication）緩解。</p>
<p>大團隊或高安全環境，個別部署的隔離性較好——CI runner 跟版控分開、registry 跟 CI 分開，每個服務的存取控制和稽核獨立。</p>
<p>同樣的邏輯適用於 Nexus：它用一個實例服務 6 種格式的套件，比為每種格式各建一個離線 mirror 省力。</p>
<h2 id="維護的隱藏成本">維護的隱藏成本</h2>
<p>自建服務的維護成本容易被低估，因為部署完成時感覺「已經做完了」，但持續性維護才剛開始。每個自建服務需要：</p>
<table>
  <thead>
      <tr>
          <th>維護項目</th>
          <th>頻率</th>
          <th>漏做的後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>安全更新</td>
          <td>月級</td>
          <td>已知漏洞暴露在內網（斷網不代表零風險）</td>
      </tr>
      <tr>
          <td>備份</td>
          <td>日級</td>
          <td>服務掛了資料沒了</td>
      </tr>
      <tr>
          <td>容量監控</td>
          <td>週級</td>
          <td>磁碟滿了服務停擺</td>
      </tr>
      <tr>
          <td>憑證續期</td>
          <td>季級</td>
          <td>TLS 過期、服務拒絕連線</td>
      </tr>
      <tr>
          <td>使用者管理</td>
          <td>變更時</td>
          <td>離職員工仍有存取權</td>
      </tr>
      <tr>
          <td>監控的監控</td>
          <td>持續</td>
          <td>監控系統本身掛了沒人知道</td>
      </tr>
  </tbody>
</table>
<p>10 個自建服務各自都有這六項維護需求。時程參考：每月的例行維護（安全更新 + 備份驗證 + 容量檢查）約需 2-3 天工程師時間。這筆時間是隱性的——不在任何 sprint 或 ticket 裡，但不做的後果是累積的。</p>
<p>管理層溝通時的關鍵數字：自建 10 個服務的維護成本約等於 0.3-0.5 個全職工程師。這筆人力投入是斷網環境的結構性成本，跟應用開發無關。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網環境的通用原則</a>：內容搬運、離線套件管理的共通模式</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-iac/" data-link-title="斷網環境的 IaC" data-link-desc="Terraform provider mirror、離線 plugin cache、本地 state backend、沒有雲端時的 plan/apply 流程與內網 CI">斷網環境的 IaC</a>：state backend（PostgreSQL）和 CI 的詳細設定</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">斷網環境的容器與映像管理</a>：Harbor 和映像搬運的詳細操作</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-monitoring/" data-link-title="斷網環境的監控與可觀測性" data-link-desc="Self-hosted 監控（Prometheus &#43; Grafana）、離線 log 收集（Loki / ELK）、不能 phone home 的告警、NTP 時間同步">斷網環境的監控與可觀測性</a>：Prometheus + Grafana + Loki 的部署</li>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：Vault 的身分管理與 infra IAM 的關係</li>
<li>→ <a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>：自建服務的 secret 管理與成本歸因</li>
</ul>
]]></content:encoded></item><item><title>斷網環境的版本控制與 CI/CD</title><link>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-vcs-ci/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-vcs-ci/</guid><description>&lt;p>版本控制和 CI/CD 是所有 infra 操作的前提——程式碼要有地方存、變更要能被 review、build 和 deploy 要自動化。正常環境裡這些由 GitHub + GitHub Actions 提供，斷網環境裡這兩個服務都不存在，需要在內網自建替代品。&lt;/p>
&lt;h2 id="gitlab-ce-vs-gitea選型判準">GitLab CE vs Gitea：選型判準&lt;/h2>
&lt;p>兩個主流的自建版本控制方案定位不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>GitLab CE&lt;/th>
 &lt;th>Gitea&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>定位&lt;/td>
 &lt;td>VCS + CI + Container Registry + Issue Tracker 一體&lt;/td>
 &lt;td>純 VCS（輕量 Git 伺服器）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資源需求&lt;/td>
 &lt;td>4GB+ RAM、推薦 8GB&lt;/td>
 &lt;td>512MB RAM 即可運作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CI 內建&lt;/td>
 &lt;td>GitLab CI（&lt;code>.gitlab-ci.yml&lt;/code>）&lt;/td>
 &lt;td>無（搭配 Drone / Woodpecker / Jenkins）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Container Registry&lt;/td>
 &lt;td>內建&lt;/td>
 &lt;td>無（搭配 Harbor）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>安裝複雜度&lt;/td>
 &lt;td>中（Omnibus 包裝簡化了安裝、但設定項多）&lt;/td>
 &lt;td>低（單一二進位檔、啟動即可用）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>維護負擔&lt;/td>
 &lt;td>高（PostgreSQL、Redis、Sidekiq 都在裡面）&lt;/td>
 &lt;td>低（SQLite 或 MySQL、無背景服務）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選型判準是團隊規模和需要的功能範圍。5 人以下、只需要 VCS + 輕量 CI 的團隊，Gitea + Drone 的組合維護成本低。10 人以上、需要 MR review + CI pipeline + Container Registry 一站到位的團隊，GitLab CE 的整合度值得它的資源消耗。&lt;/p>
&lt;p>接下來以 GitLab CE 為主線說明（功能最完整），Gitea 的差異在各段附註。&lt;/p>
&lt;h2 id="gitlab-ce-離線安裝">GitLab CE 離線安裝&lt;/h2>
&lt;p>GitLab Omnibus 包把所有依賴打包成單一安裝檔，不需要在目標機器上 &lt;code>apt install&lt;/code> 任何前置套件。&lt;/p>
&lt;h3 id="在外網機器下載安裝包">在外網機器下載安裝包&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># Ubuntu/Debian&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">wget https://packages.gitlab.com/gitlab/gitlab-ce/packages/ubuntu/jammy/gitlab-ce_17.0.0-ce.0_amd64.deb/download.deb
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># RHEL/CentOS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">wget https://packages.gitlab.com/gitlab/gitlab-ce/packages/el/9/gitlab-ce-17.0.0-ce.0.el9.x86_64.rpm/download.rpm&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把下載的 &lt;code>.deb&lt;/code> 或 &lt;code>.rpm&lt;/code> 透過&lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">內容搬運機制&lt;/a>（USB、光碟、跨邊界傳輸站）帶進斷網環境。&lt;/p>
&lt;h3 id="在斷網機器安裝">在斷網機器安裝&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># Ubuntu/Debian&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">sudo dpkg -i gitlab-ce_17.0.0-ce.0_amd64.deb
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># RHEL/CentOS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">sudo yum localinstall gitlab-ce-17.0.0-ce.0.el9.x86_64.rpm&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="離線設定">離線設定&lt;/h3>
&lt;p>安裝後編輯 &lt;code>/etc/gitlab/gitlab.rb&lt;/code>，把所有外部連線關掉：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ruby" data-lang="ruby">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 設定內部域名（不是公網域名）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">external_url&lt;/span> &lt;span class="s1">&amp;#39;https://gitlab.internal.example.com&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 關閉 Gravatar（頭像服務、需要外網）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="n">gitlab_rails&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="s1">&amp;#39;gravatar_enabled&amp;#39;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kp">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 關閉 usage ping（回報使用統計到 GitLab Inc）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n">gitlab_rails&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="s1">&amp;#39;usage_ping_enabled&amp;#39;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kp">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># 關閉 version check&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="n">gitlab_rails&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="s1">&amp;#39;gitlab_check_on_connect&amp;#39;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kp">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1"># 如果沒有內部 SMTP，用 sendmail 或關閉 email&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="n">gitlab_rails&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="s1">&amp;#39;smtp_enable&amp;#39;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kp">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="c1"># TLS 憑證用內部 CA 簽發&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="n">nginx&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="s1">&amp;#39;ssl_certificate&amp;#39;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/etc/gitlab/ssl/gitlab.crt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="n">nginx&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="s1">&amp;#39;ssl_certificate_key&amp;#39;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/etc/gitlab/ssl/gitlab.key&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">sudo gitlab-ctl reconfigure&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Gitea 的離線安裝更簡單：下載單一二進位檔 &lt;code>gitea&lt;/code>、設定 &lt;code>app.ini&lt;/code>、用 systemd 管理即可。&lt;/p>
&lt;h3 id="升級策略">升級策略&lt;/h3>
&lt;p>GitLab CE 的升級包也要從外部下載帶進來。升級前先備份（&lt;code>gitlab-backup create&lt;/code>），升級路徑要按 GitLab 的&lt;a href="https://docs.gitlab.com/ee/update/index.html#upgrade-paths">版本跳級規則&lt;/a>——不能任意跳版、某些大版本之間需要中繼版本。在斷網環境裡，每次升級要預先規劃中繼版本、一次帶進所有需要的安裝包。&lt;/p></description><content:encoded><![CDATA[<p>版本控制和 CI/CD 是所有 infra 操作的前提——程式碼要有地方存、變更要能被 review、build 和 deploy 要自動化。正常環境裡這些由 GitHub + GitHub Actions 提供，斷網環境裡這兩個服務都不存在，需要在內網自建替代品。</p>
<h2 id="gitlab-ce-vs-gitea選型判準">GitLab CE vs Gitea：選型判準</h2>
<p>兩個主流的自建版本控制方案定位不同：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>GitLab CE</th>
          <th>Gitea</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>定位</td>
          <td>VCS + CI + Container Registry + Issue Tracker 一體</td>
          <td>純 VCS（輕量 Git 伺服器）</td>
      </tr>
      <tr>
          <td>資源需求</td>
          <td>4GB+ RAM、推薦 8GB</td>
          <td>512MB RAM 即可運作</td>
      </tr>
      <tr>
          <td>CI 內建</td>
          <td>GitLab CI（<code>.gitlab-ci.yml</code>）</td>
          <td>無（搭配 Drone / Woodpecker / Jenkins）</td>
      </tr>
      <tr>
          <td>Container Registry</td>
          <td>內建</td>
          <td>無（搭配 Harbor）</td>
      </tr>
      <tr>
          <td>安裝複雜度</td>
          <td>中（Omnibus 包裝簡化了安裝、但設定項多）</td>
          <td>低（單一二進位檔、啟動即可用）</td>
      </tr>
      <tr>
          <td>維護負擔</td>
          <td>高（PostgreSQL、Redis、Sidekiq 都在裡面）</td>
          <td>低（SQLite 或 MySQL、無背景服務）</td>
      </tr>
  </tbody>
</table>
<p>選型判準是團隊規模和需要的功能範圍。5 人以下、只需要 VCS + 輕量 CI 的團隊，Gitea + Drone 的組合維護成本低。10 人以上、需要 MR review + CI pipeline + Container Registry 一站到位的團隊，GitLab CE 的整合度值得它的資源消耗。</p>
<p>接下來以 GitLab CE 為主線說明（功能最完整），Gitea 的差異在各段附註。</p>
<h2 id="gitlab-ce-離線安裝">GitLab CE 離線安裝</h2>
<p>GitLab Omnibus 包把所有依賴打包成單一安裝檔，不需要在目標機器上 <code>apt install</code> 任何前置套件。</p>
<h3 id="在外網機器下載安裝包">在外網機器下載安裝包</h3>





<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"># Ubuntu/Debian</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">wget https://packages.gitlab.com/gitlab/gitlab-ce/packages/ubuntu/jammy/gitlab-ce_17.0.0-ce.0_amd64.deb/download.deb
</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"># RHEL/CentOS</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">wget https://packages.gitlab.com/gitlab/gitlab-ce/packages/el/9/gitlab-ce-17.0.0-ce.0.el9.x86_64.rpm/download.rpm</span></span></code></pre></div><p>把下載的 <code>.deb</code> 或 <code>.rpm</code> 透過<a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">內容搬運機制</a>（USB、光碟、跨邊界傳輸站）帶進斷網環境。</p>
<h3 id="在斷網機器安裝">在斷網機器安裝</h3>





<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"># Ubuntu/Debian</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo dpkg -i gitlab-ce_17.0.0-ce.0_amd64.deb
</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"># RHEL/CentOS</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">sudo yum localinstall gitlab-ce-17.0.0-ce.0.el9.x86_64.rpm</span></span></code></pre></div><h3 id="離線設定">離線設定</h3>
<p>安裝後編輯 <code>/etc/gitlab/gitlab.rb</code>，把所有外部連線關掉：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><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"><span class="n">external_url</span> <span class="s1">&#39;https://gitlab.internal.example.com&#39;</span>
</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"># 關閉 Gravatar（頭像服務、需要外網）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;gravatar_enabled&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="kp">false</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"># 關閉 usage ping（回報使用統計到 GitLab Inc）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;usage_ping_enabled&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="kp">false</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 關閉 version check</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;gitlab_check_on_connect&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="kp">false</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"># 如果沒有內部 SMTP，用 sendmail 或關閉 email</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">gitlab_rails</span><span class="o">[</span><span class="s1">&#39;smtp_enable&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="kp">false</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># TLS 憑證用內部 CA 簽發</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">nginx</span><span class="o">[</span><span class="s1">&#39;ssl_certificate&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="s2">&#34;/etc/gitlab/ssl/gitlab.crt&#34;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="n">nginx</span><span class="o">[</span><span class="s1">&#39;ssl_certificate_key&#39;</span><span class="o">]</span> <span class="o">=</span> <span class="s2">&#34;/etc/gitlab/ssl/gitlab.key&#34;</span></span></span></code></pre></div>




<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">sudo gitlab-ctl reconfigure</span></span></code></pre></div><p>Gitea 的離線安裝更簡單：下載單一二進位檔 <code>gitea</code>、設定 <code>app.ini</code>、用 systemd 管理即可。</p>
<h3 id="升級策略">升級策略</h3>
<p>GitLab CE 的升級包也要從外部下載帶進來。升級前先備份（<code>gitlab-backup create</code>），升級路徑要按 GitLab 的<a href="https://docs.gitlab.com/ee/update/index.html#upgrade-paths">版本跳級規則</a>——不能任意跳版、某些大版本之間需要中繼版本。在斷網環境裡，每次升級要預先規劃中繼版本、一次帶進所有需要的安裝包。</p>
<h2 id="ci-runner-離線設定">CI Runner 離線設定</h2>
<p>CI pipeline 在斷網環境裡跑的最大差異是 runner 不能即時拉依賴。</p>
<h3 id="runner-安裝與註冊">Runner 安裝與註冊</h3>





<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"># 下載 runner 二進位檔（外網下載、帶進來）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># https://docs.gitlab.com/runner/install/linux-manually.html</span>
</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">sudo gitlab-runner register <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --url https://gitlab.internal.example.com <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --token <span class="nv">$RUNNER_TOKEN</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --executor docker <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --docker-image alpine:3.20</span></span></code></pre></div><h3 id="executor-選擇">Executor 選擇</h3>
<table>
  <thead>
      <tr>
          <th>Executor</th>
          <th>隔離性</th>
          <th>前置條件</th>
          <th>斷網適用度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>shell</td>
          <td>低（直接跑在 runner 機器上）</td>
          <td>無</td>
          <td>高（最簡單）</td>
      </tr>
      <tr>
          <td>docker</td>
          <td>高（每個 job 一個容器）</td>
          <td>需要 Docker + 預拉 image</td>
          <td>中（image 管理成本）</td>
      </tr>
      <tr>
          <td>kubernetes</td>
          <td>高（每個 job 一個 pod）</td>
          <td>需要 K8s cluster</td>
          <td>低（斷網 K8s 維護重）</td>
      </tr>
  </tbody>
</table>
<p>斷網環境推薦 shell executor（最少依賴）或 docker executor 搭配預拉好的 image。</p>
<h3 id="docker-executor-的-image-管理">Docker executor 的 image 管理</h3>
<p>Docker executor 的每個 job 都基於一個 base image。斷網環境裡這些 image 必須預先存在於內網的 <a href="/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">private registry</a>：</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"># runner 的 /etc/docker/daemon.json 指向內部 registry</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="s2">&#34;insecure-registries&#34;</span>: <span class="o">[</span><span class="s2">&#34;registry.internal:5000&#34;</span><span class="o">]</span>,
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="s2">&#34;registry-mirrors&#34;</span>: <span class="o">[</span><span class="s2">&#34;https://registry.internal:5000&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="o">}</span></span></span></code></pre></div><p>CI pipeline 裡用到的每個 image（build 用的 golang/node/php、lint 用的 tflint/checkov、deploy 用的 awscli）都要事先搬進內部 registry。</p>
<h3 id="依賴快取">依賴快取</h3>
<p>沒有 npm registry / PyPI / Maven Central 可以拉，CI job 的依賴安裝必須用本地來源：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># .gitlab-ci.yml — 使用內部 Nexus 作為套件來源</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">variables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">NPM_CONFIG_REGISTRY</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://nexus.internal/repository/npm-proxy/&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">PIP_INDEX_URL</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://nexus.internal/repository/pypi-proxy/simple/&#34;</span></span></span></code></pre></div><p>或者把 <code>node_modules</code> / <code>vendor</code> 打包成 CI artifact 快取，避免每次 job 都重新安裝。</p>
<h2 id="git-bundle-跨邊界傳輸">Git Bundle 跨邊界傳輸</h2>
<p>某些斷網環境不允許直接 <code>git push</code> 到內網 GitLab（例如開發在外網、部署在內網）。Git bundle 是把 commit 歷史打包成單一檔案的機制：</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"># 外網開發機：打包最近的 commit</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git bundle create changes.bundle main~5..main
</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">git bundle verify changes.bundle
</span></span><span class="line"><span class="ln">6</span><span class="cl">git fetch changes.bundle main:incoming
</span></span><span class="line"><span class="ln">7</span><span class="cl">git merge incoming</span></span></code></pre></div><p>bundle 檔案包含完整的 Git 物件（commit、tree、blob），可以通過任何檔案傳輸方式帶過邊界——USB、光碟、審批後的檔案傳輸閘道。</p>
<p>跨邊界傳輸的安全考量：bundle 的內容應該在傳入前被掃描（至少 <code>git bundle verify</code>），確認不包含預期外的分支或異常大的物件。某些高安全環境要求所有跨邊界檔案經過人工審批。</p>
<h2 id="mr-review-流程">MR Review 流程</h2>
<p>斷網環境的 MR（Merge Request）review 流程跟<a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>的原則相同——變更走 MR → CI 跑 plan → reviewer 看 diff + plan 輸出 → 合併 → apply。差別在於所有環節都在內網：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># .gitlab-ci.yml — Terraform plan 貼回 MR comment</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">plan</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">plan</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span>- <span class="l">terraform init -plugin-dir=/opt/terraform/plugins</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="l">terraform plan -no-color -out=plan.tfplan | tee plan.txt</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span>- <span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="sd">      curl --request POST \
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="sd">        --header &#34;PRIVATE-TOKEN: $GITLAB_TOKEN&#34; \
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="sd">        --data-urlencode &#34;body=$(cat plan.txt)&#34; \
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="sd">        &#34;https://gitlab.internal/api/v4/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">only</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span>- <span class="l">merge_requests</span></span></span></code></pre></div><p>GitLab CI 的 <code>merge_requests</code> trigger 跟 GitHub Actions 的 <code>pull_request</code> 等價——MR 開啟或更新時自動跑 pipeline。</p>
<p>reviewer 在 GitLab 的 MR 頁面看 code diff + plan 輸出 comment，approve 後合併，合併觸發 apply pipeline。流程跟有網路時完全相同，只是所有元件（GitLab、runner、Terraform、provider plugin）都在內網。</p>
<h2 id="時程與維護">時程與維護</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>初始設定</th>
          <th>持續維護</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GitLab CE 安裝 + 設定</td>
          <td>1 天</td>
          <td>每季升級（含帶包 + 備份 + 升級 + 驗證）~半天</td>
      </tr>
      <tr>
          <td>CI runner 設定</td>
          <td>半天</td>
          <td>image 更新隨 registry 同步</td>
      </tr>
      <tr>
          <td>Gitea + Drone（替代方案）</td>
          <td>半天</td>
          <td>極低（二進位更新即可）</td>
      </tr>
      <tr>
          <td>Git bundle 流程建立</td>
          <td>2 小時</td>
          <td>按需（有跨邊界需求時）</td>
      </tr>
  </tbody>
</table>
<p>GitLab CE 的主要維護成本在升級——斷網環境的升級不能一鍵 <code>apt upgrade</code>，要預先下載正確版本的安裝包帶進來。跳版規則讓這個過程比正常環境多一層規劃。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網環境的通用原則</a>：內容搬運、離線套件管理的共通模式</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">斷網環境的容器與映像管理</a>：CI runner 的 Docker image 管理</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>：MR review 流程的原則與護欄</li>
</ul>
]]></content:encoded></item><item><title>斷網環境的套件與容器映像 Registry</title><link>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-package-registry/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-package-registry/</guid><description>&lt;p>連網環境的套件安裝和映像拉取，背後都有一個公開的 registry 在服務：apt 走 archive.ubuntu.com、npm 走 registry.npmjs.org、Docker 走 Docker Hub。斷網環境裡這些 endpoint 全部不可達，每一條 &lt;code>apt install&lt;/code>、&lt;code>npm install&lt;/code>、&lt;code>pip install&lt;/code>、&lt;code>docker pull&lt;/code> 都會失敗。替代做法是在內網部署自己的 registry，把需要的套件和映像從外部下載、經過安全審查後搬進來。&lt;/p>
&lt;p>本篇涵蓋兩個 registry 的部署與操作：Nexus Repository（多格式套件）和 Harbor（容器映像）。兩者可以獨立運作，也可以搭配使用——Nexus 管套件依賴、Harbor 管容器映像，各自負責不同的 artifact 類型。&lt;/p>
&lt;h2 id="nexus-repository統一的離線套件-proxy">Nexus Repository：統一的離線套件 proxy&lt;/h2>
&lt;p>Nexus Repository OSS（開源版）支援 apt、yum、npm、PyPI、Maven、NuGet、Go modules 等多種格式，用一個實例取代多個獨立的離線 repo mirror。部署在內網後，所有開發機器和 CI runner 把套件 source 指向 Nexus。&lt;/p>
&lt;h3 id="部署">部署&lt;/h3>
&lt;p>Nexus 本身是一個 Java 應用，用 Docker 部署最簡單。映像需要事先從外部搬進來：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部機器下載映像&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">docker pull sonatype/nexus3:latest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">docker save sonatype/nexus3:latest -o nexus3.tar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運到內網後載入&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">docker load -i nexus3.tar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">docker run -d -p 8081:8081 --name nexus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -v nexus-data:/nexus-data &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> sonatype/nexus3:latest&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>初始管理員密碼在容器內 &lt;code>/nexus-data/admin.password&lt;/code>，首次登入後強制修改。&lt;/p>
&lt;h3 id="hosted-repo-模式">Hosted repo 模式&lt;/h3>
&lt;p>連網環境的 Nexus 通常用 proxy repo（代理公開 registry、快取下載過的套件）。斷網環境 proxy 模式無法運作，改用 hosted repo——手動上傳套件到 Nexus，Nexus 作為唯一的分發來源。&lt;/p>
&lt;p>以 npm 為例，workflow 是在外部機器打包、搬運、上傳：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部機器：打包專案的所有依賴&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">npm pack --pack-destination ./npm-packages/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1"># 或用 npm-offline-packager 批次下載整棵依賴樹&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">npx npm-offline-packager --package ./package.json --output ./npm-packages/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運到內網後上傳到 Nexus&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> pkg in ./npm-packages/*.tgz&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> curl -u admin:password &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --upload-file &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$pkg&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s2">&amp;#34;http://nexus.internal:8081/repository/npm-hosted/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>apt 和 yum 的做法類似：外部機器用 &lt;code>apt-get download&lt;/code> 或 &lt;code>yumdownloader&lt;/code> 抓 .deb / .rpm 檔案，搬進來後上傳到 Nexus 的 hosted repo。&lt;/p>
&lt;h3 id="客戶端設定">客戶端設定&lt;/h3>
&lt;p>開發機器和 CI runner 的套件 source 指向 Nexus：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># npm&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">npm config &lt;span class="nb">set&lt;/span> registry http://nexus.internal:8081/repository/npm-hosted/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># pip&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">pip install --index-url http://nexus.internal:8081/repository/pypi-hosted/simple/ package-name
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># apt（在 /etc/apt/sources.list.d/ 加一份）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">deb http://nexus.internal:8081/repository/apt-hosted/ focal main&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="harbor容器映像的-private-registry">Harbor：容器映像的 private registry&lt;/h2>
&lt;p>Harbor 是 CNCF 畢業專案的企業級容器 registry，支援映像簽章、漏洞掃描（Trivy）、存取控制、映像複製。在斷網環境裡它是 Docker Hub 和 ECR 的替代品。&lt;/p></description><content:encoded><![CDATA[<p>連網環境的套件安裝和映像拉取，背後都有一個公開的 registry 在服務：apt 走 archive.ubuntu.com、npm 走 registry.npmjs.org、Docker 走 Docker Hub。斷網環境裡這些 endpoint 全部不可達，每一條 <code>apt install</code>、<code>npm install</code>、<code>pip install</code>、<code>docker pull</code> 都會失敗。替代做法是在內網部署自己的 registry，把需要的套件和映像從外部下載、經過安全審查後搬進來。</p>
<p>本篇涵蓋兩個 registry 的部署與操作：Nexus Repository（多格式套件）和 Harbor（容器映像）。兩者可以獨立運作，也可以搭配使用——Nexus 管套件依賴、Harbor 管容器映像，各自負責不同的 artifact 類型。</p>
<h2 id="nexus-repository統一的離線套件-proxy">Nexus Repository：統一的離線套件 proxy</h2>
<p>Nexus Repository OSS（開源版）支援 apt、yum、npm、PyPI、Maven、NuGet、Go modules 等多種格式，用一個實例取代多個獨立的離線 repo mirror。部署在內網後，所有開發機器和 CI runner 把套件 source 指向 Nexus。</p>
<h3 id="部署">部署</h3>
<p>Nexus 本身是一個 Java 應用，用 Docker 部署最簡單。映像需要事先從外部搬進來：</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"># 外部機器下載映像</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker pull sonatype/nexus3:latest
</span></span><span class="line"><span class="ln">3</span><span class="cl">docker save sonatype/nexus3:latest -o nexus3.tar
</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">docker load -i nexus3.tar
</span></span><span class="line"><span class="ln">7</span><span class="cl">docker run -d -p 8081:8081 --name nexus <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  -v nexus-data:/nexus-data <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  sonatype/nexus3:latest</span></span></code></pre></div><p>初始管理員密碼在容器內 <code>/nexus-data/admin.password</code>，首次登入後強制修改。</p>
<h3 id="hosted-repo-模式">Hosted repo 模式</h3>
<p>連網環境的 Nexus 通常用 proxy repo（代理公開 registry、快取下載過的套件）。斷網環境 proxy 模式無法運作，改用 hosted repo——手動上傳套件到 Nexus，Nexus 作為唯一的分發來源。</p>
<p>以 npm 為例，workflow 是在外部機器打包、搬運、上傳：</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"># 外部機器：打包專案的所有依賴</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">npm pack --pack-destination ./npm-packages/
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 或用 npm-offline-packager 批次下載整棵依賴樹</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">npx npm-offline-packager --package ./package.json --output ./npm-packages/
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 搬運到內網後上傳到 Nexus</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">for</span> pkg in ./npm-packages/*.tgz<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  curl -u admin:password <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>    --upload-file <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>    <span class="s2">&#34;http://nexus.internal:8081/repository/npm-hosted/&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>apt 和 yum 的做法類似：外部機器用 <code>apt-get download</code> 或 <code>yumdownloader</code> 抓 .deb / .rpm 檔案，搬進來後上傳到 Nexus 的 hosted repo。</p>
<h3 id="客戶端設定">客戶端設定</h3>
<p>開發機器和 CI runner 的套件 source 指向 Nexus：</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"># npm</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm config <span class="nb">set</span> registry http://nexus.internal:8081/repository/npm-hosted/
</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"># pip</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">pip install --index-url http://nexus.internal:8081/repository/pypi-hosted/simple/ package-name
</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"># apt（在 /etc/apt/sources.list.d/ 加一份）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">deb http://nexus.internal:8081/repository/apt-hosted/ focal main</span></span></code></pre></div><h2 id="harbor容器映像的-private-registry">Harbor：容器映像的 private registry</h2>
<p>Harbor 是 CNCF 畢業專案的企業級容器 registry，支援映像簽章、漏洞掃描（Trivy）、存取控制、映像複製。在斷網環境裡它是 Docker Hub 和 ECR 的替代品。</p>
<h3 id="部署-1">部署</h3>
<p>Harbor 用 Docker Compose 部署。安裝包需要從外部下載後搬進來：</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"># 外部機器下載離線安裝包</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">wget https://github.com/goharbor/harbor/releases/download/v2.11.0/harbor-offline-installer-v2.11.0.tgz
</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">tar xzf harbor-offline-installer-v2.11.0.tgz
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nb">cd</span> harbor
</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"><span class="c1"># 複製並編輯設定</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">cp harbor.yml.tmpl harbor.yml
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 修改 hostname、storage 路徑、HTTPS 憑證（內部 CA 簽發）</span>
</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"><span class="c1"># 安裝</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">./install.sh --with-trivy</span></span></code></pre></div><p><code>--with-trivy</code> 啟用內建的漏洞掃描。Trivy 的漏洞資料庫需要離線更新——從外部下載 DB 檔案、搬進來放到指定路徑。</p>
<h3 id="專案與存取控制">專案與存取控制</h3>
<p>Harbor 用「專案」（project）組織映像。每個專案可以設定獨立的存取控制：</p>
<ul>
<li><code>library</code>：公開專案、所有使用者可 pull</li>
<li><code>platform</code>：平台團隊專用、限定成員可 push</li>
<li><code>vendor</code>：第三方 base image、由 infra 團隊管理更新</li>
</ul>
<p>robot account 提供 CI/CD 用的非互動式認證（限定 pull / push 權限、可設定到期時間）。</p>
<h2 id="映像搬運-sop">映像搬運 SOP</h2>
<p>映像從外部搬進斷網環境是一個需要標準化的操作，涉及格式、大小、多架構支援：</p>
<h3 id="搬運工具比較">搬運工具比較</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>優點</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>docker save/load</code></td>
          <td>最直覺、不需要額外安裝</td>
          <td>只能處理本地已 pull 的映像、不支援跨 registry 直接搬</td>
      </tr>
      <tr>
          <td><code>skopeo copy</code></td>
          <td>不需要 Docker daemon、支援跨 registry、支援 manifest list</td>
          <td>需要安裝 skopeo</td>
      </tr>
      <tr>
          <td><code>crane</code></td>
          <td>輕量 CLI、支援 manifest 操作</td>
          <td>功能比 skopeo 少</td>
      </tr>
  </tbody>
</table>
<p>skopeo 的操作流程：</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"># 外部機器：從 Docker Hub 複製到本地目錄</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">skopeo copy docker://nginx:1.25-alpine dir:./images/nginx-1.25-alpine
</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"># 搬運到內網後：從本地目錄推到 Harbor</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">skopeo copy dir:./images/nginx-1.25-alpine <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  docker://harbor.internal/library/nginx:1.25-alpine <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --dest-tls-verify<span class="o">=</span><span class="nb">false</span>  <span class="c1"># 如果 Harbor 用內部 CA</span></span></span></code></pre></div><h3 id="多架構映像">多架構映像</h3>
<p>如果環境同時有 amd64 和 arm64 的機器，搬運時要帶整個 manifest list：</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"># 外部：複製所有架構</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">skopeo copy --all docker://nginx:1.25-alpine <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  dir:./images/nginx-1.25-alpine-multiarch
</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">skopeo copy --all dir:./images/nginx-1.25-alpine-multiarch <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  docker://harbor.internal/library/nginx:1.25-alpine</span></span></code></pre></div><p><code>--all</code> flag 確保 manifest list 裡的每個架構都被複製，而非只複製本機架構。</p>
<h2 id="套件與映像的更新週期">套件與映像的更新週期</h2>
<p>斷網環境的套件和映像不會自動更新——每一次更新都是一次有意識的搬運操作。更新週期的頻率由安全需求決定：</p>
<table>
  <thead>
      <tr>
          <th>安全等級</th>
          <th>更新頻率</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一般</td>
          <td>每月一次</td>
          <td>開發工具、非直接面對外部的服務</td>
      </tr>
      <tr>
          <td>中等</td>
          <td>每兩週</td>
          <td>有外部接口的服務、包含網路元件</td>
      </tr>
      <tr>
          <td>高</td>
          <td>每週或 CVE 驅動</td>
          <td>安全敏感環境、合規要求</td>
      </tr>
  </tbody>
</table>
<p>每次更新的標準流程：</p>
<ol>
<li><strong>外部機器下載</strong>：按清單下載指定版本的套件和映像</li>
<li><strong>安全掃描</strong>：在外部（或 staging gateway）跑 Trivy / Snyk 掃描，確認沒有已知的高風險 CVE</li>
<li><strong>審查核准</strong>：掃描報告給安全團隊或負責人簽核</li>
<li><strong>搬運</strong>：核准的 artifact 寫入唯讀媒體或加密通道搬進內網</li>
<li><strong>上傳到 registry</strong>：推到 Nexus 和 Harbor</li>
<li><strong>通知團隊</strong>：哪些套件/映像有新版本可用</li>
</ol>
<p>這個流程的產出是一份更新清單（什麼版本、掃描結果、核准人），存進版控作為稽核紀錄。</p>
<h2 id="helm-chart-離線管理">Helm chart 離線管理</h2>
<p>Kubernetes 環境用 Helm 部署應用。斷網時 Helm chart 需要離線管理：</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"># 外部機器：下載 chart</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">helm repo add bitnami https://charts.bitnami.com/bitnami
</span></span><span class="line"><span class="ln">3</span><span class="cl">helm pull bitnami/postgresql --version 15.5.0
</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></code></pre></div><p><strong>選項一：Harbor 內建 chart 支援</strong>。Harbor 2.0+ 支援 OCI artifact，Helm chart 可以直接推到 Harbor：</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">helm push postgresql-15.5.0.tgz oci://harbor.internal/charts</span></span></code></pre></div><p><strong>選項二：ChartMuseum</strong>。獨立的 chart repository server：</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"># 上傳 chart</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">curl --data-binary <span class="s2">&#34;@postgresql-15.5.0.tgz&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  http://chartmuseum.internal:8080/api/charts</span></span></code></pre></div><p>Harbor 的 OCI 方式較簡單（不需要額外維護 ChartMuseum），但需要 Helm 3.8+ 的 OCI 支援。</p>
<h2 id="時程與管理層溝通">時程與管理層溝通</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>初次部署時間</th>
          <th>持續維護</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Nexus Repository</td>
          <td>1 天（部署 + 初始套件上傳）</td>
          <td>每次更新週期 2-4 小時</td>
      </tr>
      <tr>
          <td>Harbor</td>
          <td>1 天（部署 + 初始映像搬運）</td>
          <td>每次更新週期 2-4 小時</td>
      </tr>
      <tr>
          <td>搬運 SOP 建立</td>
          <td>半天（腳本化 + 文件）</td>
          <td>每次執行 1-2 小時</td>
      </tr>
      <tr>
          <td>Trivy 離線 DB 更新</td>
          <td>含在 Harbor 部署內</td>
          <td>每次更新週期 30 分鐘</td>
      </tr>
  </tbody>
</table>
<p>管理層需要知道的成本：registry 的維護不是一次性投入，每個更新週期都需要工程師時間執行搬運和掃描。這筆成本在連網環境裡由公開 registry 和自動更新吸收，斷網環境裡由團隊承擔。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網環境的通用原則</a>：content ferry pattern 和安全審查流程</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">斷網環境的容器與映像管理</a>：映像搬運的更完整討論（本篇聚焦 registry 部署、該篇聚焦映像生命週期）</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-iac/" data-link-title="斷網環境的 IaC" data-link-desc="Terraform provider mirror、離線 plugin cache、本地 state backend、沒有雲端時的 plan/apply 流程與內網 CI">斷網環境的 IaC</a>：Terraform provider 也需要離線 mirror、可用 Nexus 的 raw hosted repo 存放</li>
</ul>
]]></content:encoded></item><item><title>斷網環境的基礎服務：DNS、NTP、CA 與 Secret Management</title><link>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-infrastructure-services/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-infrastructure-services/</guid><description>&lt;p>斷網環境裡的 GitLab、&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/harbor/" data-link-title="Harbor" data-link-desc="開源的 container image registry，支援映像掃描、RBAC、複製，斷網環境取代 Docker Hub 的方案">Harbor&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/prometheus/" data-link-title="Prometheus" data-link-desc="開源的 metrics 收集與告警系統，用 pull 模式從 target 拉取指標，斷網環境的預設監控方案">Prometheus&lt;/a>、Nexus 都有一個共同前提：它們需要名稱解析（&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/dns/" data-link-title="DNS" data-link-desc="Domain Name System — 把域名轉成 IP 位址的系統，以及 A record、CNAME、NS、TTL 的角色">DNS&lt;/a>）才能互相找到、需要時間同步（NTP）才能讓 log 和憑證有效、需要 &lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/ssl-tls/" data-link-title="SSL / TLS" data-link-desc="加密 client 與 server 之間通訊的協定，讓 HTTPS 成為可能。TLS 是 SSL 的後繼者，但 SSL 憑證的稱呼仍廣泛使用">TLS&lt;/a> 憑證（CA）才能走 HTTPS、需要機密儲存（&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/vault/" data-link-title="HashiCorp Vault" data-link-desc="機密管理系統，集中存放密碼、API key、TLS 私鑰，提供存取控制、稽核和自動輪替">Vault&lt;/a>）才能安全管理密碼和 token。這四個是「服務的服務」——沒有它們，其他自建服務要麼無法啟動、要麼只能用不安全的 HTTP 明文通訊。&lt;/p>
&lt;h2 id="internal-dns內部名稱解析">Internal DNS：內部名稱解析&lt;/h2>
&lt;p>斷網環境沒有公開 DNS 可用。內部服務之間的互相引用（GitLab 連 PostgreSQL、Harbor 連 storage backend）如果靠 IP 位址，每次 IP 變動都要改一輪設定。內部 DNS 讓服務用 hostname（&lt;code>gitlab.internal&lt;/code>、&lt;code>harbor.internal&lt;/code>）互相引用，IP 變動只改 DNS zone 一處。&lt;/p>
&lt;h3 id="coredns-vs-bind">CoreDNS vs BIND&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>CoreDNS&lt;/th>
 &lt;th>BIND&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>設定方式&lt;/td>
 &lt;td>Corefile（宣告式、短）&lt;/td>
 &lt;td>named.conf（傳統、長）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>部署方式&lt;/td>
 &lt;td>單一 binary / container&lt;/td>
 &lt;td>系統套件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適合情境&lt;/td>
 &lt;td>Kubernetes 原生整合、輕量&lt;/td>
 &lt;td>複雜 DNS 需求（split-horizon、DNSSEC）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>學習曲線&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>中高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>多數斷網環境用 CoreDNS 就夠——zone 檔案放在磁碟上、Corefile 幾行就能啟動。&lt;/p>
&lt;h3 id="最小設定">最小設定&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl"># Corefile
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">internal:53 {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> file /etc/coredns/zones/internal.zone
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> log
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> errors
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">.:53 {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> forward . /dev/null
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> log
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第一個 block 處理 &lt;code>internal&lt;/code> 域名的查詢、從 zone 檔案回應。第二個 block 攔截所有其他查詢——斷網環境不能轉發到上游 DNS，&lt;code>forward . /dev/null&lt;/code> 讓非內部域名直接返回 NXDOMAIN 而非 timeout。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">; /etc/coredns/zones/internal.zone
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">$ORIGIN internal.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">@ IN SOA ns1.internal. admin.internal. (
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> 2026062601 ; serial
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> 3600 ; refresh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> 600 ; retry
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> 86400 ; expire
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> 60 ; minimum
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> IN NS ns1.internal.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">ns1 IN A 10.0.1.10
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">gitlab IN A 10.0.1.20
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">harbor IN A 10.0.1.21
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">vault IN A 10.0.1.22
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">nexus IN A 10.0.1.23
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">prom IN A 10.0.1.24
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">grafana IN A 10.0.1.25
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">ntp IN A 10.0.1.11&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>新增服務時加一行 A record、重載 CoreDNS（&lt;code>kill -SIGUSR1 $(pidof coredns)&lt;/code> 或重啟 container）。serial 號遞增讓變更可追蹤。&lt;/p></description><content:encoded><![CDATA[<p>斷網環境裡的 GitLab、<a href="/blog/infra/knowledge-cards/harbor/" data-link-title="Harbor" data-link-desc="開源的 container image registry，支援映像掃描、RBAC、複製，斷網環境取代 Docker Hub 的方案">Harbor</a>、<a href="/blog/infra/knowledge-cards/prometheus/" data-link-title="Prometheus" data-link-desc="開源的 metrics 收集與告警系統，用 pull 模式從 target 拉取指標，斷網環境的預設監控方案">Prometheus</a>、Nexus 都有一個共同前提：它們需要名稱解析（<a href="/blog/infra/knowledge-cards/dns/" data-link-title="DNS" data-link-desc="Domain Name System — 把域名轉成 IP 位址的系統，以及 A record、CNAME、NS、TTL 的角色">DNS</a>）才能互相找到、需要時間同步（NTP）才能讓 log 和憑證有效、需要 <a href="/blog/infra/knowledge-cards/ssl-tls/" data-link-title="SSL / TLS" data-link-desc="加密 client 與 server 之間通訊的協定，讓 HTTPS 成為可能。TLS 是 SSL 的後繼者，但 SSL 憑證的稱呼仍廣泛使用">TLS</a> 憑證（CA）才能走 HTTPS、需要機密儲存（<a href="/blog/infra/knowledge-cards/vault/" data-link-title="HashiCorp Vault" data-link-desc="機密管理系統，集中存放密碼、API key、TLS 私鑰，提供存取控制、稽核和自動輪替">Vault</a>）才能安全管理密碼和 token。這四個是「服務的服務」——沒有它們，其他自建服務要麼無法啟動、要麼只能用不安全的 HTTP 明文通訊。</p>
<h2 id="internal-dns內部名稱解析">Internal DNS：內部名稱解析</h2>
<p>斷網環境沒有公開 DNS 可用。內部服務之間的互相引用（GitLab 連 PostgreSQL、Harbor 連 storage backend）如果靠 IP 位址，每次 IP 變動都要改一輪設定。內部 DNS 讓服務用 hostname（<code>gitlab.internal</code>、<code>harbor.internal</code>）互相引用，IP 變動只改 DNS zone 一處。</p>
<h3 id="coredns-vs-bind">CoreDNS vs BIND</h3>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>CoreDNS</th>
          <th>BIND</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>設定方式</td>
          <td>Corefile（宣告式、短）</td>
          <td>named.conf（傳統、長）</td>
      </tr>
      <tr>
          <td>部署方式</td>
          <td>單一 binary / container</td>
          <td>系統套件</td>
      </tr>
      <tr>
          <td>適合情境</td>
          <td>Kubernetes 原生整合、輕量</td>
          <td>複雜 DNS 需求（split-horizon、DNSSEC）</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>低</td>
          <td>中高</td>
      </tr>
  </tbody>
</table>
<p>多數斷網環境用 CoreDNS 就夠——zone 檔案放在磁碟上、Corefile 幾行就能啟動。</p>
<h3 id="最小設定">最小設定</h3>





<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"># Corefile
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">internal:53 {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    file /etc/coredns/zones/internal.zone
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    log
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    errors
</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></span><span class="line"><span class="ln"> 8</span><span class="cl">.:53 {
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    forward . /dev/null
</span></span><span class="line"><span class="ln">10</span><span class="cl">    log
</span></span><span class="line"><span class="ln">11</span><span class="cl">}</span></span></code></pre></div><p>第一個 block 處理 <code>internal</code> 域名的查詢、從 zone 檔案回應。第二個 block 攔截所有其他查詢——斷網環境不能轉發到上游 DNS，<code>forward . /dev/null</code> 讓非內部域名直接返回 NXDOMAIN 而非 timeout。</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">; /etc/coredns/zones/internal.zone
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">$ORIGIN internal.
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">@       IN SOA  ns1.internal. admin.internal. (
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        2026062601 ; serial
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        3600       ; refresh
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        600        ; retry
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        86400      ; expire
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        60         ; minimum
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">)
</span></span><span class="line"><span class="ln">10</span><span class="cl">        IN NS   ns1.internal.
</span></span><span class="line"><span class="ln">11</span><span class="cl">ns1     IN A    10.0.1.10
</span></span><span class="line"><span class="ln">12</span><span class="cl">gitlab  IN A    10.0.1.20
</span></span><span class="line"><span class="ln">13</span><span class="cl">harbor  IN A    10.0.1.21
</span></span><span class="line"><span class="ln">14</span><span class="cl">vault   IN A    10.0.1.22
</span></span><span class="line"><span class="ln">15</span><span class="cl">nexus   IN A    10.0.1.23
</span></span><span class="line"><span class="ln">16</span><span class="cl">prom    IN A    10.0.1.24
</span></span><span class="line"><span class="ln">17</span><span class="cl">grafana IN A    10.0.1.25
</span></span><span class="line"><span class="ln">18</span><span class="cl">ntp     IN A    10.0.1.11</span></span></code></pre></div><p>新增服務時加一行 A record、重載 CoreDNS（<code>kill -SIGUSR1 $(pidof coredns)</code> 或重啟 container）。serial 號遞增讓變更可追蹤。</p>
<h3 id="客戶端設定">客戶端設定</h3>
<p>每台機器的 <code>/etc/resolv.conf</code> 指向 CoreDNS 的 IP：</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">nameserver 10.0.1.10
</span></span><span class="line"><span class="ln">2</span><span class="cl">search internal</span></span></code></pre></div><p>如果環境有 DHCP server，在 DHCP option 裡配 DNS server 位址，新加入的機器自動取得。沒有 DHCP 就靠 provisioning 腳本或 Ansible playbook 推送。</p>
<h2 id="ntp內部時間同步">NTP：內部時間同步</h2>
<p>時間不同步在斷網環境會引發三類問題：log 的時間戳錯亂讓事故排查無法跨機器對齊、TLS 憑證的有效期判斷出錯導致合法憑證被拒絕、以及 Kerberos 等時間敏感的認證協定直接失敗。正常環境從 <code>pool.ntp.org</code> 取得時間，斷網環境需要自己的時間源。</p>
<h3 id="chrony-作為-ntp-server">chrony 作為 NTP server</h3>
<p>chrony 比傳統的 ntpd 更適合網路不穩或隔離的環境——它的時鐘修正演算法在長時間無外部時間源時仍能保持較準確的漂移補償。</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"># /etc/chrony.conf（NTP server 端）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 斷網環境：沒有上游 NTP、用本機時鐘作為最後手段</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">local</span> stratum <span class="m">10</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">allow 10.0.0.0/8
</span></span><span class="line"><span class="ln">5</span><span class="cl">driftfile /var/lib/chrony/drift</span></span></code></pre></div><p><code>local stratum 10</code> 宣告「我自己是時間源、但 stratum 很低（精度不高）」。其他機器的 chrony 設定指向這台 server：</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"># /etc/chrony.conf（客戶端）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">server ntp.internal iburst
</span></span><span class="line"><span class="ln">3</span><span class="cl">makestep 1.0 <span class="m">3</span></span></span></code></pre></div><p><code>iburst</code> 讓開機時快速同步、<code>makestep 1.0 3</code> 允許前三次校正時跳大步（修正啟動時的大偏差）。</p>
<h3 id="高精度需求">高精度需求</h3>
<p>如果環境對時間精度有要求（金融交易、工控系統），NTP server 需要硬體時間源——GPS 接收器或原子鐘模組。GPS 天線不需要網路連線、只需要看得到衛星的位置（屋頂或窗邊）。chrony 支援 PPS（Pulse Per Second）輸入、可以達到微秒級精度。</p>
<p>多數斷網環境不需要這個精度——毫秒級一致（chrony 預設行為）對 log 對齊和 TLS 驗證已經足夠。</p>
<h2 id="internal-ca內部憑證簽發">Internal CA：內部憑證簽發</h2>
<p>斷網環境的每個內部 HTTPS 服務都需要 TLS 憑證。Let&rsquo;s Encrypt 的 ACME challenge 需要連網驗證，在斷網環境無法使用。替代方案是建立內部 CA（Certificate Authority），自己簽發憑證。</p>
<h3 id="step-casmallstep">step-ca（Smallstep）</h3>
<p>step-ca 是一個輕量的 CA server，支援 ACME 協定——內部服務可以用跟 Let&rsquo;s Encrypt 相同的流程自動申請和續期憑證，只是 ACME server 是內網的 step-ca 而非 Let&rsquo;s Encrypt。</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"># 初始化 CA</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">step ca init --name<span class="o">=</span><span class="s2">&#34;Internal CA&#34;</span> --dns<span class="o">=</span><span class="s2">&#34;ca.internal&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --address<span class="o">=</span><span class="s2">&#34;:443&#34;</span> --provisioner<span class="o">=</span><span class="s2">&#34;admin&#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"># 啟動 CA server</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">step-ca <span class="k">$(</span>step path<span class="k">)</span>/config/ca.json</span></span></code></pre></div><p>初始化會產生 root CA 和 intermediate CA 的 key pair。root CA 的私鑰是整個信任鏈的根——它的保護等級要最高（離線儲存、存取紀錄）。</p>
<h3 id="憑證簽發流程">憑證簽發流程</h3>
<p>服務用 ACME client 向 step-ca 申請憑證：</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"># 用 step CLI 申請憑證（手動方式）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">step ca certificate <span class="s2">&#34;gitlab.internal&#34;</span> gitlab.crt gitlab.key
</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"># 用 ACME 自動續期（搭配 certbot 或 step 的 renewal daemon）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">step ca renew --daemon gitlab.crt gitlab.key</span></span></code></pre></div><p>certbot 也能配合 step-ca 使用——把 ACME server URL 從 Let&rsquo;s Encrypt 改成 <code>https://ca.internal/acme/acme/directory</code>。已有 certbot 自動續期腳本的服務只要改一行設定。</p>
<h3 id="root-ca-分發">Root CA 分發</h3>
<p>每台機器和每個服務都要信任內部 CA 的 root certificate：</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"># Debian/Ubuntu</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">cp root_ca.crt /usr/local/share/ca-certificates/internal-ca.crt
</span></span><span class="line"><span class="ln">3</span><span class="cl">update-ca-certificates
</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"># RHEL/CentOS</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">cp root_ca.crt /etc/pki/ca-trust/source/anchors/internal-ca.crt
</span></span><span class="line"><span class="ln">7</span><span class="cl">update-ca-trust</span></span></code></pre></div><p>Docker daemon 也需要信任內部 CA（否則 <code>docker pull harbor.internal/image</code> 會報 TLS 錯誤）：</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">mkdir -p /etc/docker/certs.d/harbor.internal
</span></span><span class="line"><span class="ln">2</span><span class="cl">cp root_ca.crt /etc/docker/certs.d/harbor.internal/ca.crt
</span></span><span class="line"><span class="ln">3</span><span class="cl">systemctl restart docker</span></span></code></pre></div><p>Ansible playbook 批量推送 root CA 到所有機器，是初始部署的標準做法。</p>
<h3 id="cfssl-作為替代">cfssl 作為替代</h3>
<p>cfssl（Cloudflare 的 PKI 工具組）比 step-ca 更簡單但沒有 ACME 自動化——每張憑證要手動簽發。適合只有 5-10 個服務、不需要自動續期的小規模環境。</p>
<h2 id="secret-managementhashicorp-vault">Secret Management：HashiCorp Vault</h2>
<p>資料庫密碼、API token、TLS 私鑰這些機密值需要一個集中的安全儲存。斷網環境不能用 AWS Secrets Manager 或 GCP Secret Manager，HashiCorp Vault 是最常見的自建選項。</p>
<h3 id="斷網環境的-vault-初始化">斷網環境的 Vault 初始化</h3>
<p>Vault 的初始化（unsealing）在雲端環境通常用 AWS KMS 或 GCP Cloud KMS 自動 unseal。斷網環境沒有雲端 KMS，退回 Shamir&rsquo;s Secret Sharing——初始化時產生 N 個 unseal key、啟動時需要 M 個 key 才能解鎖（典型設定：5 個 key、3 個即可 unseal）。</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"># 初始化 Vault（5 key shares、3 threshold）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">vault operator init -key-shares<span class="o">=</span><span class="m">5</span> -key-threshold<span class="o">=</span><span class="m">3</span>
</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"># Unseal（需要 3 次、每次用不同的 key）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">vault operator unseal &lt;key-1&gt;
</span></span><span class="line"><span class="ln">6</span><span class="cl">vault operator unseal &lt;key-2&gt;
</span></span><span class="line"><span class="ln">7</span><span class="cl">vault operator unseal &lt;key-3&gt;</span></span></code></pre></div><p>5 個 unseal key 分別交給不同的人保管。任何單一個人都無法獨自解鎖 Vault——這是刻意的安全設計。Vault 重啟後需要重新 unseal，所以 unseal key 的保管和取用流程要事先演練。</p>
<h3 id="機器身分認證">機器身分認證</h3>
<p>服務從 Vault 讀取 secret 時需要認證自己的身分。雲端環境用 IAM role，斷網環境用 AppRole——每個服務拿到一組 role_id + secret_id、用它們換取短期 token。</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"># 建立 AppRole</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">vault auth <span class="nb">enable</span> approle
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">vault write auth/approle/role/gitlab <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  <span class="nv">token_ttl</span><span class="o">=</span>1h <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  <span class="nv">token_max_ttl</span><span class="o">=</span>4h <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  <span class="nv">policies</span><span class="o">=</span>gitlab-secrets
</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"><span class="c1"># 服務端取得 token</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">vault write auth/approle/login <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  <span class="nv">role_id</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$ROLE_ID</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  <span class="nv">secret_id</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$SECRET_ID</span><span class="s2">&#34;</span></span></span></code></pre></div><p>secret_id 本身也是 secret——初次部署時由 Vault admin 手動提供給服務、或透過 Ansible 的 encrypted variable 推送。</p>
<h3 id="儲存後端">儲存後端</h3>
<p>Vault 需要一個持久化的儲存後端。雲端用 DynamoDB 或 Consul，斷網環境用：</p>
<table>
  <thead>
      <tr>
          <th>後端</th>
          <th>適用情境</th>
          <th>特性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>檔案系統</td>
          <td>單節點、小規模</td>
          <td>最簡單、但沒有 HA</td>
      </tr>
      <tr>
          <td>PostgreSQL</td>
          <td>已有 PostgreSQL 的環境</td>
          <td>利用現有基礎設施</td>
      </tr>
      <tr>
          <td>Consul</td>
          <td>需要 HA 的環境</td>
          <td>Vault + Consul 是官方推薦的 HA 組合</td>
      </tr>
  </tbody>
</table>
<h2 id="部署順序的相互依賴">部署順序的相互依賴</h2>
<p>四個服務之間有依賴鏈：</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">DNS → NTP → CA → Vault
</span></span><span class="line"><span class="ln">2</span><span class="cl"> ↑_________________↓（Vault 的 FQDN 要 DNS 解析）</span></span></code></pre></div><p>DNS 先啟動（其他服務靠它解析 hostname）→ NTP 跟著（CA 簽發憑證時需要準確的時間、否則 notBefore/notAfter 判斷會出問題）→ CA 啟動（Vault 的 HTTPS 需要 TLS 憑證）→ Vault 最後（依賴 DNS 和 TLS）。</p>
<p>DNS 跟 CA 之間有一個循環依賴：CA 簽發憑證時需要 DNS 解析（ACME challenge 或 CSR 裡的 SAN），但 DNS server 本身要不要 TLS？解法是 DNS 第一次啟動時用明文（不走 HTTPS），CA 啟動後回頭替 DNS 簽一張憑證、再切到 DNS-over-TLS。多數內網環境 DNS 維持明文即可——DNS 查詢在內網不加密是常見做法，風險可控。</p>
<h2 id="時程與維護">時程與維護</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>初始部署</th>
          <th>持續維護</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CoreDNS</td>
          <td>2-4 小時</td>
          <td>新增服務時加 zone record（分鐘級）</td>
      </tr>
      <tr>
          <td>chrony</td>
          <td>1-2 小時</td>
          <td>幾乎不需要（漂移補償自動運作）</td>
      </tr>
      <tr>
          <td>step-ca</td>
          <td>3-4 小時</td>
          <td>憑證到期前的監控和續期（自動化後接近零）</td>
      </tr>
      <tr>
          <td>Vault</td>
          <td>4-8 小時</td>
          <td>unseal key 管理、policy 更新、備份</td>
      </tr>
  </tbody>
</table>
<p>四個服務合計約 1.5-2 個工作天完成初始部署。部署完成後的日常維護負擔集中在 Vault（unseal key 管理和 policy 維護）和 DNS zone 更新。CA 的憑證續期如果用 ACME 自動化就接近零維護。</p>
<p>向管理層溝通時的框架：「這四個服務是所有其他服務的地基——沒有它們，其他服務要麼找不到彼此（DNS）、時間對不上（NTP）、通訊不加密（CA）、密碼寫在設定檔裡（Vault）。部署一次、之後幾乎自動運作。」</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網環境的通用原則</a>：content ferry 和離線套件管理的通用操作模式</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-iac/" data-link-title="斷網環境的 IaC" data-link-desc="Terraform provider mirror、離線 plugin cache、本地 state backend、沒有雲端時的 plan/apply 流程與內網 CI">斷網環境的 IaC</a>：Vault 作為 Terraform 的 secret backend</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">斷網環境的容器與映像管理</a>：Harbor 依賴 DNS 和 TLS、映像拉取需要信任內部 CA</li>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：Vault 的角色跟雲端的 Secrets Manager 對應</li>
<li>→ <a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>：Secret 不進 code 的原則在斷網環境用 Vault 落地</li>
</ul>
]]></content:encoded></item><item><title>斷網環境的資安與權限控管</title><link>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-security-access/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-security-access/</guid><description>&lt;p>斷網環境的安全假設跟連網環境相反。連網環境的主要威脅是外部攻擊者透過網路入侵——防火牆、WAF、IDS 構成防禦層。斷網環境的實體隔離幾乎消除了遠端攻擊的可能，但威脅沒有消失，而是轉向兩個方向：有權限存取內部系統的人員（insider threat），以及透過合法管道跨越隔離邊界的內容（supply chain）。每一個刻意建立的橋樑——USB 隨身碟、資料搬運站、data diode——都是攻擊面。&lt;/p>
&lt;h2 id="威脅模型的轉變">威脅模型的轉變&lt;/h2>
&lt;p>連網環境的安全投資集中在邊界防禦：防火牆規則、DDoS 防護、入侵偵測、漏洞修補的速度。斷網環境的邊界是物理的——網路線沒有接上去，防火牆規則不是問題。威脅從「外面的人怎麼進來」變成「裡面的人怎麼把東西帶出去、或把有害的東西帶進來」。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>威脅類型&lt;/th>
 &lt;th>連網環境的可能性&lt;/th>
 &lt;th>斷網環境的可能性&lt;/th>
 &lt;th>斷網環境的主要載體&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>遠端漏洞利用&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>極低&lt;/td>
 &lt;td>—&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>釣魚 / 社交工程&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>低（無外部 email）&lt;/td>
 &lt;td>但內部通訊仍可能被利用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>USB / 可移除媒體&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>人員帶入的 USB、外接硬碟&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>供應鏈污染&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>搬運進來的套件、映像、更新檔&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>內部人員濫用權限&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>有實體存取權的操作人員&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料外洩&lt;/td>
 &lt;td>高（網路）&lt;/td>
 &lt;td>中（實體）&lt;/td>
 &lt;td>USB 複製、列印、手機拍照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>橫向移動&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>內部網路扁平時仍然可能&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>斷網環境的安全投資因此集中在三個面向：控制誰能碰什麼（存取控制）、記錄誰碰了什麼（稽核日誌）、審查什麼東西跨越邊界（傳輸審查）。&lt;/p>
&lt;h2 id="實體安全是-infra-的責任">實體安全是 infra 的責任&lt;/h2>
&lt;p>連網環境的實體安全通常歸 facility team——機房門禁、監視器、電力冗餘。infra 團隊負責的是邏輯層的安全（IAM、security group、加密）。斷網環境裡這條分界線消失了：「誰能帶 USB 進機房」直接等於「誰能把任意程式碼注入生產環境」，這是 infra 的安全邊界，不是 facility 的。&lt;/p>
&lt;p>需要 infra 團隊參與制定的實體安全政策：&lt;/p>
&lt;p>&lt;strong>可移除媒體管控&lt;/strong>：哪些人被授權攜帶 USB / 外接硬碟進入安全區域。媒體是否需要預先登記和加密。進入前是否要在掃描站過掃。政策的嚴格度依環境敏感度而定——最嚴格的環境禁止所有個人裝置、只使用登記在冊的專用搬運媒體。&lt;/p>
&lt;p>&lt;strong>機房存取控制&lt;/strong>：門禁卡 / 生物辨識的日誌要進入 infra 的稽核系統。每一次實體進出都要有記錄——誰、什麼時候、待了多久。伺服器機櫃如果有獨立的鎖，鎖的鑰匙管理也歸 infra。&lt;/p>
&lt;p>&lt;strong>Console 存取&lt;/strong>：能直接操作伺服器 console（KVM、IPMI、iLO）的人等於擁有最高權限——可以繞過所有 OS 層的認證。console 存取要限制到最小人數，每次使用要記錄。&lt;/p>
&lt;p>&lt;strong>螢幕與攝影裝置&lt;/strong>：敏感環境可能限制在安全區域內使用手機（防止拍攝螢幕上的資料）。這個政策的執行通常是 facility 負責，但政策的制定依據（什麼資料在螢幕上算敏感）是 infra 定義的。&lt;/p>
&lt;h2 id="身分與認證沒有雲端-iam">身分與認證（沒有雲端 IAM）&lt;/h2>
&lt;p>連網環境用 OIDC / SSO / 雲端 &lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/iam/" data-link-title="IAM（Identity and Access Management）" data-link-desc="雲端平台的授權系統，回答「某個身分能不能對某個資源做某件事」">IAM&lt;/a> 管理身分。斷網環境沒有這些——需要自建身分基礎設施。&lt;/p>
&lt;p>&lt;strong>集中身分管理&lt;/strong>：FreeIPA（整合 LDAP + Kerberos + DNS + CA）或 OpenLDAP 作為統一的使用者目錄。所有內部服務（GitLab、Nexus、Harbor、Vault、Grafana）都配置 LDAP 認證，避免每個服務各自管一套使用者帳號。FreeIPA 的優勢是把 LDAP、Kerberos、DNS 和 CA 整合在一個管理介面——在資源有限的斷網環境裡減少維運面。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># FreeIPA 安裝（CentOS/Rocky）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">sudo yum install -y ipa-server ipa-server-dns
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">sudo ipa-server-install --setup-dns --no-forwarders&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>MFA（沒有網路的情況下）&lt;/strong>：TOTP（如 Google Authenticator）完全在本地運作、不需要網路連線。硬體 token（YubiKey）支援 FIDO2 / PIV / TOTP，在高安全環境是標準做法。智慧卡（CAC / PIV card）在政府和軍事環境最常見。&lt;/p>
&lt;p>&lt;strong>服務帳號&lt;/strong>：機器對機器的認證用 Vault 的 AppRole（role_id + secret_id 換取短期 token）或本地 SSL client certificate。不使用長期密碼或寫死的 token。&lt;/p>
&lt;h2 id="稽核日誌沒有-cloudtrail">稽核日誌（沒有 CloudTrail）&lt;/h2>
&lt;p>連網環境用 CloudTrail / GCP Audit Log 自動記錄所有 API 操作。斷網環境要自建整條稽核鏈：收集 → 傳輸 → 儲存 → 查詢 → 告警。&lt;/p>
&lt;p>&lt;strong>OS 層級&lt;/strong>：Linux auditd 記錄 kernel 層的操作——誰執行了什麼指令、誰存取了什麼檔案、誰修改了什麼系統設定。規則用 &lt;code>auditctl&lt;/code> 或 &lt;code>/etc/audit/rules.d/&lt;/code> 設定。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 監控所有 sudo 操作&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">-a always,exit -F &lt;span class="nv">arch&lt;/span>&lt;span class="o">=&lt;/span>b64 -S execve -F &lt;span class="nv">euid&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span> -k root-commands
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># 監控 /etc/ 目錄的修改&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">-w /etc/ -p wa -k etc-changes&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>服務層級&lt;/strong>：每個自建服務都有自己的 audit log——GitLab 的 audit events、Vault 的 audit device（可設成 file 或 syslog）、Harbor 的 activity log。這些日誌要匯聚到中央 log server。&lt;/p></description><content:encoded><![CDATA[<p>斷網環境的安全假設跟連網環境相反。連網環境的主要威脅是外部攻擊者透過網路入侵——防火牆、WAF、IDS 構成防禦層。斷網環境的實體隔離幾乎消除了遠端攻擊的可能，但威脅沒有消失，而是轉向兩個方向：有權限存取內部系統的人員（insider threat），以及透過合法管道跨越隔離邊界的內容（supply chain）。每一個刻意建立的橋樑——USB 隨身碟、資料搬運站、data diode——都是攻擊面。</p>
<h2 id="威脅模型的轉變">威脅模型的轉變</h2>
<p>連網環境的安全投資集中在邊界防禦：防火牆規則、DDoS 防護、入侵偵測、漏洞修補的速度。斷網環境的邊界是物理的——網路線沒有接上去，防火牆規則不是問題。威脅從「外面的人怎麼進來」變成「裡面的人怎麼把東西帶出去、或把有害的東西帶進來」。</p>
<table>
  <thead>
      <tr>
          <th>威脅類型</th>
          <th>連網環境的可能性</th>
          <th>斷網環境的可能性</th>
          <th>斷網環境的主要載體</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>遠端漏洞利用</td>
          <td>高</td>
          <td>極低</td>
          <td>—</td>
      </tr>
      <tr>
          <td>釣魚 / 社交工程</td>
          <td>高</td>
          <td>低（無外部 email）</td>
          <td>但內部通訊仍可能被利用</td>
      </tr>
      <tr>
          <td>USB / 可移除媒體</td>
          <td>中</td>
          <td>高</td>
          <td>人員帶入的 USB、外接硬碟</td>
      </tr>
      <tr>
          <td>供應鏈污染</td>
          <td>中</td>
          <td>高</td>
          <td>搬運進來的套件、映像、更新檔</td>
      </tr>
      <tr>
          <td>內部人員濫用權限</td>
          <td>中</td>
          <td>高</td>
          <td>有實體存取權的操作人員</td>
      </tr>
      <tr>
          <td>資料外洩</td>
          <td>高（網路）</td>
          <td>中（實體）</td>
          <td>USB 複製、列印、手機拍照</td>
      </tr>
      <tr>
          <td>橫向移動</td>
          <td>高</td>
          <td>中</td>
          <td>內部網路扁平時仍然可能</td>
      </tr>
  </tbody>
</table>
<p>斷網環境的安全投資因此集中在三個面向：控制誰能碰什麼（存取控制）、記錄誰碰了什麼（稽核日誌）、審查什麼東西跨越邊界（傳輸審查）。</p>
<h2 id="實體安全是-infra-的責任">實體安全是 infra 的責任</h2>
<p>連網環境的實體安全通常歸 facility team——機房門禁、監視器、電力冗餘。infra 團隊負責的是邏輯層的安全（IAM、security group、加密）。斷網環境裡這條分界線消失了：「誰能帶 USB 進機房」直接等於「誰能把任意程式碼注入生產環境」，這是 infra 的安全邊界，不是 facility 的。</p>
<p>需要 infra 團隊參與制定的實體安全政策：</p>
<p><strong>可移除媒體管控</strong>：哪些人被授權攜帶 USB / 外接硬碟進入安全區域。媒體是否需要預先登記和加密。進入前是否要在掃描站過掃。政策的嚴格度依環境敏感度而定——最嚴格的環境禁止所有個人裝置、只使用登記在冊的專用搬運媒體。</p>
<p><strong>機房存取控制</strong>：門禁卡 / 生物辨識的日誌要進入 infra 的稽核系統。每一次實體進出都要有記錄——誰、什麼時候、待了多久。伺服器機櫃如果有獨立的鎖，鎖的鑰匙管理也歸 infra。</p>
<p><strong>Console 存取</strong>：能直接操作伺服器 console（KVM、IPMI、iLO）的人等於擁有最高權限——可以繞過所有 OS 層的認證。console 存取要限制到最小人數，每次使用要記錄。</p>
<p><strong>螢幕與攝影裝置</strong>：敏感環境可能限制在安全區域內使用手機（防止拍攝螢幕上的資料）。這個政策的執行通常是 facility 負責，但政策的制定依據（什麼資料在螢幕上算敏感）是 infra 定義的。</p>
<h2 id="身分與認證沒有雲端-iam">身分與認證（沒有雲端 IAM）</h2>
<p>連網環境用 OIDC / SSO / 雲端 <a href="/blog/infra/knowledge-cards/iam/" data-link-title="IAM（Identity and Access Management）" data-link-desc="雲端平台的授權系統，回答「某個身分能不能對某個資源做某件事」">IAM</a> 管理身分。斷網環境沒有這些——需要自建身分基礎設施。</p>
<p><strong>集中身分管理</strong>：FreeIPA（整合 LDAP + Kerberos + DNS + CA）或 OpenLDAP 作為統一的使用者目錄。所有內部服務（GitLab、Nexus、Harbor、Vault、Grafana）都配置 LDAP 認證，避免每個服務各自管一套使用者帳號。FreeIPA 的優勢是把 LDAP、Kerberos、DNS 和 CA 整合在一個管理介面——在資源有限的斷網環境裡減少維運面。</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"># FreeIPA 安裝（CentOS/Rocky）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo yum install -y ipa-server ipa-server-dns
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo ipa-server-install --setup-dns --no-forwarders</span></span></code></pre></div><p><strong>MFA（沒有網路的情況下）</strong>：TOTP（如 Google Authenticator）完全在本地運作、不需要網路連線。硬體 token（YubiKey）支援 FIDO2 / PIV / TOTP，在高安全環境是標準做法。智慧卡（CAC / PIV card）在政府和軍事環境最常見。</p>
<p><strong>服務帳號</strong>：機器對機器的認證用 Vault 的 AppRole（role_id + secret_id 換取短期 token）或本地 SSL client certificate。不使用長期密碼或寫死的 token。</p>
<h2 id="稽核日誌沒有-cloudtrail">稽核日誌（沒有 CloudTrail）</h2>
<p>連網環境用 CloudTrail / GCP Audit Log 自動記錄所有 API 操作。斷網環境要自建整條稽核鏈：收集 → 傳輸 → 儲存 → 查詢 → 告警。</p>
<p><strong>OS 層級</strong>：Linux auditd 記錄 kernel 層的操作——誰執行了什麼指令、誰存取了什麼檔案、誰修改了什麼系統設定。規則用 <code>auditctl</code> 或 <code>/etc/audit/rules.d/</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"># 監控所有 sudo 操作</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">-a always,exit -F <span class="nv">arch</span><span class="o">=</span>b64 -S execve -F <span class="nv">euid</span><span class="o">=</span><span class="m">0</span> -k root-commands
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 監控 /etc/ 目錄的修改</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">-w /etc/ -p wa -k etc-changes</span></span></code></pre></div><p><strong>服務層級</strong>：每個自建服務都有自己的 audit log——GitLab 的 audit events、Vault 的 audit device（可設成 file 或 syslog）、Harbor 的 activity log。這些日誌要匯聚到中央 log server。</p>
<p><strong>集中收集</strong>：rsyslog 或 syslog-ng 把各主機的 audit log 轉送到一台專用的 log server。log server 的儲存用 append-only 或 write-once 媒體（防止日誌被竄改）。</p>
<p><strong>日誌完整性</strong>：定期對日誌檔做 hash（<code>sha256sum</code>）並把 hash 存到獨立的位置。如果日誌內容被修改，hash 不匹配會被發現。在最高安全等級的環境裡，日誌會同時寫到光碟或 WORM（Write Once Read Many）儲存。</p>
<p><strong>審閱與告警</strong>：日誌收集了但沒人看等於沒有。定義哪些事件觸發主動通知（root 登入、非工作時段的操作、大量檔案存取）、哪些事件定期審閱（每週掃描異常模式）。</p>
<h2 id="更新的延遲窗口">更新的延遲窗口</h2>
<p>連網環境的 CVE 修補可以在小時到天的層級完成——<code>apt update &amp;&amp; apt upgrade</code>。斷網環境的修補從「得知漏洞」到「修補上線」之間有結構性的延遲。</p>
<p>典型的延遲鏈：外部公告 CVE → 安全團隊評估影響（1-2 天）→ 在外部環境下載修補（同日）→ 掃描修補本身的安全性（1 天）→ 審批跨邊界傳輸（1-3 天）→ 在斷網測試環境驗證（1-2 天）→ 部署到生產環境（同日）。總延遲 5-10 個工作天。</p>
<p>這個延遲窗口是已知的、可管理的風險。管理方式：</p>
<p><strong>風險接受文件</strong>：記錄哪些 CVE 在「已知但尚未修補」的窗口內，每條標註預計修補時間和暫時的補償控制。</p>
<p><strong>補償控制</strong>：在修補到位之前降低漏洞的可利用性——禁用受影響的服務功能、收緊網路分段、限制受影響服務的存取權限。</p>
<p><strong>分級修補</strong>：不是所有 CVE 都需要緊急處理。Critical（CVSS 9+）走加速通道（目標 3 天內修補）、High（CVSS 7-8.9）走正常通道（目標 10 天）、Medium 以下排進常規更新週期。</p>
<h2 id="跨邊界傳輸的安全審查">跨邊界傳輸的安全審查</h2>
<p>每一個跨越隔離邊界的物件都需要審查——套件、映像、設定檔、資料匯出。搬運的操作流程在<a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">通用原則篇</a>描述，這裡聚焦安全審查的部分。</p>
<p><strong>掃描站</strong>：在邊界設置一台專用的掃描機器，所有入境的媒體先在這裡過掃——防毒掃描、檔案類型驗證、hash 比對（確認下載的套件跟官方發布的 hash 一致）。掃描站本身的病毒定義也需要定期更新（走相同的搬運流程）。</p>
<p><strong>傳輸審批日誌</strong>：每次跨邊界傳輸記錄：搬運的內容清單、搬運者、審批者、搬運日期、每個檔案的 hash。這份日誌是稽核的依據——如果內部發現惡意軟體，可以回溯「它是什麼時候、由誰搬進來的」。</p>
<p><strong>Data diode（單向網路裝置）</strong>：在最高安全等級的環境裡，跨邊界的網路連線用 data diode——物理上只允許資料往一個方向流動（外部→內部，或反過來）。這比軟體防火牆更難繞過，因為它是硬體限制。data diode 的限制是不支援雙向協定（如 TCP handshake），需要用 UDP-based 的傳輸工具。</p>
<h2 id="主機層入侵偵測">主機層入侵偵測</h2>
<p>斷網環境的網路流量監控（NIDS）效果有限——內部網路通常扁平、流量加密後難以檢查。主機層入侵偵測（HIDS）是更適合斷網環境的選擇：在每台主機上監控檔案完整性、程序行為、登入模式，而非在網路層攔截。OSSEC 和 Wazuh（OSSEC 的積極維護分支）是開源的 HIDS 方案，agent 裝在每台主機、manager 集中收集告警，不需要連外。</p>
<h2 id="時程與管理層溝通">時程與管理層溝通</h2>
<p>斷網環境的安全管控初始建置時程：FreeIPA 部署 + 跟所有內部服務（GitLab、Nexus、Harbor、Vault）的 LDAP 整合約需 2-3 天。auditd 規則設定 + syslog 聚合到中央 log server 約需 1 天。掃描站建置（防毒 + hash 驗證 + 傳輸日誌）約需半天。HIDS 部署（Wazuh manager + 各主機 agent）約需 1-2 天。整體安全管控從零到運作約需 5-7 個工作天。</p>
<p>持續維護的主要工作是病毒定義更新搬運（跟隨套件更新週期）、稽核日誌的定期審閱（每週）、以及 CVE 修補的分級處理（依 CVSS 嚴重度排程）。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：連網環境的 IAM 設計，跟本篇的離線身分方案互補</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網環境的通用原則</a>：content ferry 模式的操作流程</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-infrastructure-services/" data-link-title="斷網環境的基礎服務：DNS、NTP、CA 與 Secret Management" data-link-desc="斷網環境裡其他所有服務的前提——內部 DNS 做名稱解析、NTP 做時間同步、內部 CA 簽發 TLS 憑證、Vault 管理機密值。這四個服務先部署、其他才能跟上。">斷網環境的基礎服務</a>：CA 和 Vault 是本篇認證和機密管理的技術基礎</li>
<li>→ <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>：應用層的安全措施</li>
</ul>
]]></content:encoded></item></channel></rss>