<?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>Ntp on Tarragon</title><link>https://tarrragon.github.io/blog/tags/ntp/</link><description>Recent content in Ntp 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/tags/ntp/index.xml" rel="self" type="application/rss+xml"/><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></channel></rss>