<?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>Secrets on Tarragon</title><link>https://tarrragon.github.io/blog/tags/secrets/</link><description>Recent content in Secrets 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/secrets/index.xml" rel="self" type="application/rss+xml"/><item><title>Tagging 規範與 Secrets 不進 code</title><link>https://tarrragon.github.io/blog/infra/08-governance-habits/tagging-secrets/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/08-governance-habits/tagging-secrets/</guid><description>&lt;p>每一個治理習慣單獨看都很小：在資源上多打三個 tag、把一段連線字串挪去別的地方。但少了這些習慣，半年後的代價是另一個量級 — 翻著一頁兩百筆沒有歸屬的資源猜哪個能砍、為了輪替一把外洩的密鑰回頭 grep 整個 repo。Tagging 與 secret 管理是治理習慣裡補救成本最陡的兩項：tag 一旦缺席就得回頭考古幾百個資源，密鑰一旦進了 git 歷史就無法清除。它們共同的特性是 day-1 建立的成本接近零，事後補的代價隨資源數量與時間複利。&lt;/p>
&lt;h2 id="tagging-規範查帳與清資源的依據">Tagging 規範：查帳與清資源的依據&lt;/h2>
&lt;p>Tag 是貼在每個資源上的結構化標籤，承擔「讓資源能被機器查詢與分群」的責任。沒有 tag 的資源在 console 裡只剩一個隨機後綴的名字，人能勉強認得幾個，但一旦數量過百，任何「列出所有 staging 的資源」「算出 team-a 這個月花多少」的問題都無法用查詢回答，只能逐筆翻。Tag 把這些問題從人工考古變成一行 filter。&lt;/p>
&lt;h3 id="最小-tag-集合">最小 tag 集合&lt;/h3>
&lt;p>值得從第一天就強制的最小 tag 集合是三個維度，各自回答一個治理問題：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Tag&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;code>env&lt;/code>&lt;/td>
 &lt;td>這是哪個環境&lt;/td>
 &lt;td>&lt;code>prod&lt;/code> / &lt;code>staging&lt;/code> / &lt;code>dev&lt;/code>&lt;/td>
 &lt;td>清資源時不敢動、怕誤刪生產&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>owner&lt;/code>&lt;/td>
 &lt;td>出事找誰&lt;/td>
 &lt;td>&lt;code>team-payments&lt;/code> / &lt;code>platform&lt;/code>&lt;/td>
 &lt;td>資源孤兒化、沒人認領也沒人敢回收&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>cost-center&lt;/code>&lt;/td>
 &lt;td>這筆錢算誰的&lt;/td>
 &lt;td>&lt;code>cc-1024&lt;/code> / &lt;code>growth&lt;/code>&lt;/td>
 &lt;td>帳單無法拆分、成本變成一筆沒人負責的公共支出&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>env&lt;/code> 是清資源時的安全護欄。回收動作最大的恐懼是誤刪生產資源。當每個資源都標了 &lt;code>env&lt;/code>，「列出所有 &lt;code>env=dev&lt;/code> 且 30 天無流量的資源」就是一條可以放心執行的清理查詢，而 &lt;code>env=prod&lt;/code> 的資源自動被排除在批次刪除之外。沒有這個 tag，任何自動化清理都因為怕誤傷而不敢落地，最後退回人工逐筆確認，於是根本沒人去清。&lt;/p>
&lt;p>&lt;code>owner&lt;/code> 解決資源孤兒化。服務出狀況、或是看到一個用途不明的資源時，第一個問題是「這誰的」。標了 owner，告警可以自動路由、清理前可以自動通知認領；沒標，這個資源就停在「沒人敢動、因為不知道砍了會不會弄壞什麼」的狀態，永久占用配額與費用。團隊命名比個人名好 — 人會離職，團隊邊界相對穩定。&lt;/p>
&lt;p>&lt;code>cost-center&lt;/code> 是成本歸屬的地基，把帳單從「一筆公共支出」拆成「每個團隊各自負責的花費」。這個維度的後續應用在&lt;a href="https://tarrragon.github.io/blog/infra/08-governance-habits/cost-visibility-rhythm/" data-link-title="成本可見性與最小可行治理節奏" data-link-desc="用 tag 驅動的成本分攤讓帳單有人負責，以及判斷什麼治理該 day-1 就立、什麼等規模逼出來再加">成本可見性與最小可行治理節奏&lt;/a>展開。&lt;/p>
&lt;h3 id="附加-tag-的合理時機">附加 tag 的合理時機&lt;/h3>
&lt;p>三個必填之外，隨著團隊規模增長，幾個常見的附加維度會自然浮現：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Tag&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;th>加入時機&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>managed-by&lt;/code>&lt;/td>
 &lt;td>區分 IaC 管理 vs 手動建立&lt;/td>
 &lt;td>導入 IaC 第一天就加&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>project&lt;/code>&lt;/td>
 &lt;td>區分同一團隊下不同產品線&lt;/td>
 &lt;td>團隊負責超過一個產品時&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>ttl&lt;/code>&lt;/td>
 &lt;td>資源預定存活時間（如 &lt;code>7d&lt;/code>）&lt;/td>
 &lt;td>開始有大量開發 / 測試用臨時資源時&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>compliance&lt;/code>&lt;/td>
 &lt;td>標記受法規約束的資源（如 &lt;code>pci&lt;/code> / &lt;code>hipaa&lt;/code>）&lt;/td>
 &lt;td>開始有合規稽核需求時&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>managed-by = terraform&lt;/code> 搭配 &lt;code>env&lt;/code>，可以快速找出「不在 IaC 管理下的生產資源」 — 這些就是 Console 唯讀紀律（&lt;a href="https://tarrragon.github.io/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一&lt;/a>）鬆動的痕跡。附加 tag 不需要一次規劃完，但一旦加入就要跟必填 tag 一起走自動護欄。&lt;/p>
&lt;h3 id="用-iac-自動標記">用 IaC 自動標記&lt;/h3>
&lt;p>Tag 必須在資源建立時就由 IaC 寫進去，而不是事後補。Terraform 的 &lt;code>default_tags&lt;/code> 讓一個 provider 區塊內的所有資源自動繼承一組 tag，避免逐個資源手動標、也避免漏標：&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">provider&lt;/span> &lt;span class="s2">&amp;#34;aws&amp;#34;&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"> region&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;ap-northeast-1&amp;#34;&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="k">default_tags&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"> tags&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="n"> env&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">env&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="n"> owner&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">team&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"> cost-center&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">cost_center&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n"> managed-by&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;terraform&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>用 &lt;code>var&lt;/code> 取代寫死的值，讓同一套 provider 設定跨環境複用 — 每個環境的 &lt;code>terraform.tfvars&lt;/code> 填入自己的值。這和&lt;a href="https://tarrragon.github.io/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化&lt;/a>的參數化設計一致。&lt;/p></description><content:encoded><![CDATA[<p>每一個治理習慣單獨看都很小：在資源上多打三個 tag、把一段連線字串挪去別的地方。但少了這些習慣，半年後的代價是另一個量級 — 翻著一頁兩百筆沒有歸屬的資源猜哪個能砍、為了輪替一把外洩的密鑰回頭 grep 整個 repo。Tagging 與 secret 管理是治理習慣裡補救成本最陡的兩項：tag 一旦缺席就得回頭考古幾百個資源，密鑰一旦進了 git 歷史就無法清除。它們共同的特性是 day-1 建立的成本接近零，事後補的代價隨資源數量與時間複利。</p>
<h2 id="tagging-規範查帳與清資源的依據">Tagging 規範：查帳與清資源的依據</h2>
<p>Tag 是貼在每個資源上的結構化標籤，承擔「讓資源能被機器查詢與分群」的責任。沒有 tag 的資源在 console 裡只剩一個隨機後綴的名字，人能勉強認得幾個，但一旦數量過百，任何「列出所有 staging 的資源」「算出 team-a 這個月花多少」的問題都無法用查詢回答，只能逐筆翻。Tag 把這些問題從人工考古變成一行 filter。</p>
<h3 id="最小-tag-集合">最小 tag 集合</h3>
<p>值得從第一天就強制的最小 tag 集合是三個維度，各自回答一個治理問題：</p>
<table>
  <thead>
      <tr>
          <th>Tag</th>
          <th>回答的問題</th>
          <th>典型值</th>
          <th>缺了會怎樣</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>env</code></td>
          <td>這是哪個環境</td>
          <td><code>prod</code> / <code>staging</code> / <code>dev</code></td>
          <td>清資源時不敢動、怕誤刪生產</td>
      </tr>
      <tr>
          <td><code>owner</code></td>
          <td>出事找誰</td>
          <td><code>team-payments</code> / <code>platform</code></td>
          <td>資源孤兒化、沒人認領也沒人敢回收</td>
      </tr>
      <tr>
          <td><code>cost-center</code></td>
          <td>這筆錢算誰的</td>
          <td><code>cc-1024</code> / <code>growth</code></td>
          <td>帳單無法拆分、成本變成一筆沒人負責的公共支出</td>
      </tr>
  </tbody>
</table>
<p><code>env</code> 是清資源時的安全護欄。回收動作最大的恐懼是誤刪生產資源。當每個資源都標了 <code>env</code>，「列出所有 <code>env=dev</code> 且 30 天無流量的資源」就是一條可以放心執行的清理查詢，而 <code>env=prod</code> 的資源自動被排除在批次刪除之外。沒有這個 tag，任何自動化清理都因為怕誤傷而不敢落地，最後退回人工逐筆確認，於是根本沒人去清。</p>
<p><code>owner</code> 解決資源孤兒化。服務出狀況、或是看到一個用途不明的資源時，第一個問題是「這誰的」。標了 owner，告警可以自動路由、清理前可以自動通知認領；沒標，這個資源就停在「沒人敢動、因為不知道砍了會不會弄壞什麼」的狀態，永久占用配額與費用。團隊命名比個人名好 — 人會離職，團隊邊界相對穩定。</p>
<p><code>cost-center</code> 是成本歸屬的地基，把帳單從「一筆公共支出」拆成「每個團隊各自負責的花費」。這個維度的後續應用在<a href="/blog/infra/08-governance-habits/cost-visibility-rhythm/" data-link-title="成本可見性與最小可行治理節奏" data-link-desc="用 tag 驅動的成本分攤讓帳單有人負責，以及判斷什麼治理該 day-1 就立、什麼等規模逼出來再加">成本可見性與最小可行治理節奏</a>展開。</p>
<h3 id="附加-tag-的合理時機">附加 tag 的合理時機</h3>
<p>三個必填之外，隨著團隊規模增長，幾個常見的附加維度會自然浮現：</p>
<table>
  <thead>
      <tr>
          <th>Tag</th>
          <th>用途</th>
          <th>加入時機</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>managed-by</code></td>
          <td>區分 IaC 管理 vs 手動建立</td>
          <td>導入 IaC 第一天就加</td>
      </tr>
      <tr>
          <td><code>project</code></td>
          <td>區分同一團隊下不同產品線</td>
          <td>團隊負責超過一個產品時</td>
      </tr>
      <tr>
          <td><code>ttl</code></td>
          <td>資源預定存活時間（如 <code>7d</code>）</td>
          <td>開始有大量開發 / 測試用臨時資源時</td>
      </tr>
      <tr>
          <td><code>compliance</code></td>
          <td>標記受法規約束的資源（如 <code>pci</code> / <code>hipaa</code>）</td>
          <td>開始有合規稽核需求時</td>
      </tr>
  </tbody>
</table>
<p><code>managed-by = terraform</code> 搭配 <code>env</code>，可以快速找出「不在 IaC 管理下的生產資源」 — 這些就是 Console 唯讀紀律（<a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一</a>）鬆動的痕跡。附加 tag 不需要一次規劃完，但一旦加入就要跟必填 tag 一起走自動護欄。</p>
<h3 id="用-iac-自動標記">用 IaC 自動標記</h3>
<p>Tag 必須在資源建立時就由 IaC 寫進去，而不是事後補。Terraform 的 <code>default_tags</code> 讓一個 provider 區塊內的所有資源自動繼承一組 tag，避免逐個資源手動標、也避免漏標：</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">provider</span> <span class="s2">&#34;aws&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  region</span> <span class="o">=</span> <span class="s2">&#34;ap-northeast-1&#34;</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="k">default_tags</span> {
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">    tags</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">      env</span>         <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">env</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">      owner</span>       <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">team</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">      cost-center</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">cost_center</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">      managed-by</span>  <span class="o">=</span> <span class="s2">&#34;terraform&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    }
</span></span><span class="line"><span class="ln">11</span><span class="cl">  }
</span></span><span class="line"><span class="ln">12</span><span class="cl">}</span></span></code></pre></div><p>用 <code>var</code> 取代寫死的值，讓同一套 provider 設定跨環境複用 — 每個環境的 <code>terraform.tfvars</code> 填入自己的值。這和<a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>的參數化設計一致。</p>
<p>個別資源若需要額外 tag（例如 <code>ttl</code>），在資源自身的 <code>tags</code> block 裡寫，它會跟 <code>default_tags</code> 合併，不需要重複寫環境層的三個必填。兩者有相同 key 時資源層優先，所以某個特殊資源要覆蓋 owner 也行。</p>
<p>事後補 tag 是個會無限拖延的工作，因為它不影響任何功能、沒有 deadline、永遠排在 backlog 最後。</p>
<h3 id="tag-合規護欄">Tag 合規護欄</h3>
<p>判讀訊號很簡單：定期跑一條「列出缺少必填 tag 的資源」的查詢，數字若持續成長，代表有人繞過 IaC 手動開資源 — 這既是 tag 問題，也是模組一「Console 唯讀」紀律鬆動的徵兆。</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"># 列出沒有 env tag 的 EC2 instance</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws resourcegroupstaggingapi get-resources <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --resource-type-filters ec2:instance <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --tag-filters <span class="nv">Key</span><span class="o">=</span>env,Values<span class="o">=</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;ResourceTagMappingList[].ResourceARN&#39;</span></span></span></code></pre></div><p>手動查詢只是起點。更可靠的做法是用策略引擎在建立期或 PR 階段就擋住不合規的資源：</p>
<ul>
<li><strong>AWS Tag Policy</strong>（Organizations 層級）：定義必填 tag 與允許值的枚舉，不符合就阻止建立。適合整個組織統一推行。</li>
<li><strong>OPA / Sentinel</strong>（CI / PR 層級）：在 <code>terraform plan</code> 之後、<code>apply</code> 之前檢查 plan 輸出，缺 tag 就讓 CI fail。適合跟<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>整合。</li>
<li><strong>checkov / tfsec 自訂規則</strong>：靜態掃描 HCL，在 code push 時就擋。成本最低但只擋得住 IaC 管理的資源。</li>
</ul>
<p>三層護欄互補：靜態掃描擋寫 code 時的遺漏、plan 檢查擋執行前的偏差、tag policy 擋繞過 IaC 的手動操作。早期只做一層也有價值，三層都做時覆蓋最完整。定期跑 tag 覆蓋率報告（缺少必填 tag 的資源數 / 總資源數）可以作為治理進度的量化指標。覆蓋率從 40% 到 90% 的趨勢比單次數字更有意義，適合放進月報讓管理層追蹤。</p>
<p>Tagging 在合規驅動的基礎設施中還有另一層用途：用 tag 標記資料的地理歸屬，讓合規查詢可以機器化。Hard Rock Digital 的運動博彩平台受美國 Wire Act 約束，不同州的投注資料必須留在州內。它們用 CockroachDB 跨 AWS Outposts 部署，每個 Outpost 的資源用地理 tag 標記歸屬州，合規稽核時用 tag 過濾而非逐台盤查。這個案例的 infra 教訓是：tag 的維度設計在受地理或法規約束的服務中，要提前納入合規需求的維度，而非只做成本和歸屬。詳見 <a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital：Wire Act 合規</a>。</p>
<h2 id="secrets-不進-code機密值的儲存與引用">Secrets 不進 code：機密值的儲存與引用</h2>
<p>機密值 — 資料庫密碼、第三方 API key、簽章用的私鑰 — 要存在專用的密鑰管理服務裡，而 code 與 IaC 只持有指向它的參照，不持有值本身。這條規則承擔的責任是把「機密外洩的爆炸半徑」與「程式碼的散布範圍」脫鉤：一旦密碼寫進 repo，它就跟著每一次 clone、每一份 CI 快取、每一個 fork 擴散，輪替時無法保證所有副本都更新，git 歷史更是會把它永久留存，即使後來刪掉那一行。</p>
<h3 id="密鑰管理服務的選型">密鑰管理服務的選型</h3>
<p>密鑰管理服務提供的是一個有存取控制、有審計紀錄、可輪替的集中儲存。值放在這裡，誰讀過、什麼時候讀的都有 log，輪替時只改一個地方，所有引用方下次讀取就拿到新值。</p>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>定位</th>
          <th>適合的情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS Secrets Manager</td>
          <td>受管 secret、支援自動輪替</td>
          <td>資料庫密碼、需要自動輪替的 key</td>
      </tr>
      <tr>
          <td>AWS SSM Parameter Store</td>
          <td>輕量級 key-value、有免費額度</td>
          <td>設定值、不需要自動輪替的 secret</td>
      </tr>
      <tr>
          <td>HashiCorp Vault</td>
          <td>自管 / 託管、跨雲、動態 secret</td>
          <td>多雲或需要動態產生短期憑證的團隊</td>
      </tr>
      <tr>
          <td>GCP Secret Manager</td>
          <td>GCP 原生受管 secret</td>
          <td>GCP 生態</td>
      </tr>
  </tbody>
</table>
<p>選型看的是團隊已有的生態與輪替需求。對已在 AWS 上的團隊，Secrets Manager 適合需要自動輪替的資料庫密碼，SSM Parameter Store 適合其餘設定值（免費額度通常夠用）。跨雲或對動態 secret 有需求的團隊會走 Vault。</p>
<h3 id="iac-怎麼引用-secret">IaC 怎麼引用 secret</h3>
<p>IaC 應該存的是密鑰的 ARN（或等價的資源識別碼）與「在執行期去讀它」的指令，而不是密鑰的明文：</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">data</span> <span class="s2">&#34;aws_secretsmanager_secret_version&#34; &#34;db&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  secret_id</span> <span class="o">=</span> <span class="s2">&#34;prod/payments/db-password&#34;</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></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_db_instance&#34; &#34;payments&#34;</span> {
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">  password</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_secretsmanager_secret_version</span><span class="p">.</span><span class="k">db</span><span class="p">.</span><span class="k">secret_string</span><span class="c1">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">  # ...
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span>}</span></span></code></pre></div><p>另一種做法是讓 IaC 只建立 secret 的「容器」（空的 Secrets Manager entry），值由人工或自動化流程在 IaC 之外寫入。這樣 state 裡只有 secret 的 metadata（ARN、名稱、版本 ID），完全不碰明文。適合密碼由安全團隊管理、IaC 只負責「確保 secret 存在且有正確的存取策略」的分工模式。</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">resource</span> <span class="s2">&#34;aws_secretsmanager_secret&#34; &#34;db&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;${var.env}/payments/db-password&#34;</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"><span class="c1">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 值不由 Terraform 管理 — 在 Console 或 CLI 手動設定
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="err">#</span> <span class="k">secret</span> <span class="k">version</span> <span class="k">生命週期在</span> <span class="k">IaC</span> <span class="k">之外</span></span></span></code></pre></div><h3 id="state-裡的機密邊界">state 裡的機密邊界</h3>
<p>Terraform 即使從 Secrets Manager 讀值，那個值仍然會以明文落進 state 檔。這是一個常被忽略的邊界。「不進 code」只是第一道，state 後端的加密與存取控制（<a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一的 state 地基</a>）是同等重要的第二道 — 否則密鑰只是從 repo 搬到了一個沒鎖好的 state bucket。</p>
<p>State 的保護措施是一道複合防線：</p>
<ul>
<li>S3 bucket 開 <code>encrypt = true</code>（AES-256 或 KMS）</li>
<li>Bucket 的 IAM policy 只允許跑 <code>apply</code> 的 role 讀寫</li>
<li>Bucket 開 versioning，誤寫或損壞時可以回捲</li>
<li>DynamoDB lock table 防止並行 apply 覆蓋</li>
</ul>
<p>這些措施在<a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一</a>的 remote state backend 段已經詳述，這裡提醒的是：state 的安全程度決定了 secret 引用策略的上限。state 沒鎖好時，把 secret 值拉進 state 的做法等於把密碼從 repo 搬到了另一個不設防的地方。</p>
<h3 id="secret-掃描">Secret 掃描</h3>
<p>判讀訊號：定期用 secret 掃描工具掃 repo 與 CI log，任何命中都當成需要輪替的外洩事件處理，而不是刪掉那行就算了 — 因為 git 歷史與既有 clone 已經保不住了。</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"># gitleaks：掃描整個 git 歷史</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gitleaks detect --source . --report-format json --report-path gitleaks-report.json
</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"># trufflehog：掃描 git、filesystem、CI</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">trufflehog git file://. --json</span></span></code></pre></div><p>兩個工具覆蓋面不同（gitleaks 用 regex pattern、trufflehog 用 detector + entropy），搭配用覆蓋更完整。放進 CI pipeline 讓每個 PR 自動掃，比人工定期跑更可靠。命中後的處理流程：先輪替被洩露的 secret，再從 repo 清除（<code>git filter-repo</code>），最後通知所有可能受影響的服務。</p>
<h3 id="secret-命名規範">Secret 命名規範</h3>
<p>機密的命名也值得約定。用 <code>{env}/{service}/{purpose}</code> 這類有結構的路徑（如 <code>prod/payments/db-password</code>），讓存取策略可以用前綴授權：</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"># 給 payments service 的 role 只能讀自己的 secret
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">data</span> <span class="s2">&#34;aws_iam_policy_document&#34; &#34;payments_secrets&#34;</span> {
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">statement</span> {
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">    actions</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;secretsmanager:GetSecretValue&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">    resources</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;arn:aws:secretsmanager:*:*:secret:${var.env}/payments/*&#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></span></code></pre></div><p>前綴授權的好處是新增 secret 時不需要改 IAM policy — 只要命名落在同一個前綴下，存取權限自動繼承。跟<a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二</a>的最小權限設計一致：service A 的 role 只看得到 <code>payments/*</code>，看不到 <code>auth/*</code>，即使它們存在同一個帳號的 Secrets Manager 裡。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<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>：state 後端的加密與存取控制是 secret 引用策略的安全地基</li>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：誰能讀哪些 secret 的 IAM 權限設計</li>
<li>→ <a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>：tag 的環境值與 module 參數化的對齊</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>：tag 合規與 secret 掃描整合進 CI pipeline</li>
<li>→ <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>：密鑰生命週期、輪替策略與資料保護的完整討論</li>
</ul>
]]></content:encoded></item><item><title>模組八：治理好習慣 — 規模長大後不失控的最小節奏</title><link>https://tarrragon.github.io/blog/infra/08-governance-habits/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/08-governance-habits/</guid><description>&lt;p>每一個治理習慣單獨看都很小：在資源上多打三個 tag、把一段連線字串挪去別的地方、給帳單欄位填個用途。但少了這些習慣，半年後的代價是另一個量級 — 翻著一頁兩百筆沒有歸屬的資源猜哪個能砍、為了輪替一把外洩的密鑰回頭 grep 整個 repo、對著一張看不出誰花的雲帳單開跨部門會議。這一章談的就是這組「現在花幾分鐘、未來省幾天」的最小節奏。&lt;/p>
&lt;p>治理習慣的責任是讓基礎設施在規模長大後仍然可被盤點、可被追責、可被回收。資源數量從幾十個長到幾百個時，「這是誰的、為什麼存在、花了多少」這三個問題若沒有預先在資源上留下答案，就只能靠人腦記憶與口頭考古，而記憶會隨著人員流動蒸發。&lt;/p>
&lt;p>先界定這一章的邊界。身分與憑證本身怎麼設計 — IAM role、OIDC、最小權限 — 是模組二「身分與憑證地基」的範圍，這一章只談 secret 的儲存與引用：機密值放在哪、IaC 怎麼安全地指到它。成本這一塊也分兩層：把資源歸屬到擁有者與用途的地基（tagging、chargeback 的依據）在這一章，運行期怎麼用 reserved instance、spot、rightsizing 去壓低帳單，是 &lt;a href="https://tarrragon.github.io/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理&lt;/a> 的範圍。&lt;/p>
&lt;h2 id="tagging-規範查帳與清資源的依據">Tagging 規範：查帳與清資源的依據&lt;/h2>
&lt;p>Tag 是貼在每個資源上的結構化標籤，承擔「讓資源能被機器查詢與分群」的責任。沒有 tag 的資源在 console 裡只剩一個隨機後綴的名字，人能勉強認得幾個，但一旦數量過百，任何「列出所有 staging 的資源」「算出 team-a 這個月花多少」的問題都無法用查詢回答，只能逐筆翻。Tag 把這些問題從人工考古變成一行 filter。&lt;/p>
&lt;p>值得從第一天就強制的最小 tag 集合是三個維度，各自回答一個治理問題：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Tag&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;code>env&lt;/code>&lt;/td>
 &lt;td>這是哪個環境&lt;/td>
 &lt;td>&lt;code>prod&lt;/code> / &lt;code>staging&lt;/code> / &lt;code>dev&lt;/code>&lt;/td>
 &lt;td>清資源時不敢動、怕誤刪生產&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>owner&lt;/code>&lt;/td>
 &lt;td>出事找誰&lt;/td>
 &lt;td>&lt;code>team-payments&lt;/code> / &lt;code>platform&lt;/code>&lt;/td>
 &lt;td>資源孤兒化、沒人認領也沒人敢回收&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>cost-center&lt;/code>&lt;/td>
 &lt;td>這筆錢算誰的&lt;/td>
 &lt;td>&lt;code>cc-1024&lt;/code> / &lt;code>growth&lt;/code>&lt;/td>
 &lt;td>帳單無法拆分、成本變成一筆沒人負責的公共支出&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>env&lt;/code> 是清資源時的安全護欄。回收動作最大的恐懼是誤刪生產資源，當每個資源都標了 &lt;code>env&lt;/code>，「列出所有 &lt;code>env=dev&lt;/code> 且 30 天無流量的資源」就是一條可以放心執行的清理查詢，而 &lt;code>env=prod&lt;/code> 的資源自動被排除在批次刪除之外。沒有這個 tag，任何自動化清理都因為怕誤傷而不敢落地，最後退回人工逐筆確認，於是根本沒人去清。&lt;/p>
&lt;p>&lt;code>owner&lt;/code> 解決資源孤兒化。服務出狀況、或是看到一個用途不明的資源時，第一個問題是「這誰的」。標了 owner，告警可以自動路由、清理前可以自動通知認領；沒標，這個資源就停在「沒人敢動、因為不知道砍了會不會弄壞什麼」的狀態，永久占用配額與費用。團隊命名比個人名好 — 人會離職，團隊邊界相對穩定。&lt;/p>
&lt;p>&lt;code>cost-center&lt;/code> 是成本歸屬的地基，下一節展開。&lt;/p>
&lt;p>關鍵在於 tag 必須在資源建立時就由 IaC 寫進去，而不是事後補。Terraform 的 &lt;code>default_tags&lt;/code> 讓一個 provider 區塊內的所有資源自動繼承一組 tag，避免逐個資源手動標、也避免漏標：&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">provider&lt;/span> &lt;span class="s2">&amp;#34;aws&amp;#34;&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"> region&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;ap-northeast-1&amp;#34;&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="k">default_tags&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"> tags&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="n"> env&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;staging&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="n"> owner&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;team-payments&amp;#34;&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"> cost-center&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;cc-1024&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n"> managed-by&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;terraform&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>事後補 tag 是個會無限拖延的工作，因為它不影響任何功能、沒有 deadline、永遠排在 backlog 最後。判讀訊號很簡單：定期跑一條「列出缺少必填 tag 的資源」的查詢，數字若持續成長，代表有人繞過 IaC 手動開資源 — 這既是 tag 問題，也是模組一「Console 唯讀」紀律鬆動的徵兆。可以用 AWS 的 tag policy 或 OPA 這類策略引擎把「缺 tag 的資源」擋在 PR 階段，讓規範變成自動護欄而不是靠人自律。&lt;/p>
&lt;h2 id="secrets-不進-code機密值的儲存與引用">Secrets 不進 code：機密值的儲存與引用&lt;/h2>
&lt;p>機密值 — 資料庫密碼、第三方 API key、簽章用的私鑰 — 要存在專用的密鑰管理服務裡，而 code 與 IaC 只持有指向它的參照，不持有值本身。這條規則承擔的責任是把「機密外洩的爆炸半徑」與「程式碼的散布範圍」脫鉤：一旦密碼寫進 repo，它就跟著每一次 clone、每一份 CI 快取、每一個 fork 擴散，輪替時無法保證所有副本都更新，git 歷史更是會把它永久留存，即使後來刪掉那一行。&lt;/p>
&lt;p>密鑰管理服務 — AWS Secrets Manager、SSM Parameter Store、HashiCorp Vault、GCP Secret Manager — 提供的是一個有存取控制、有審計紀錄、可輪替的集中儲存。值放在這裡，誰讀過、什麼時候讀的都有 log，輪替時只改一個地方，所有引用方下次讀取就拿到新值。&lt;/p>
&lt;p>關鍵在 IaC 怎麼引用。IaC 應該存的是密鑰的 ARN（或等價的資源識別碼）與「在執行期去讀它」的指令，而不是密鑰的明文。下面這段把 RDS 密碼從 Secrets Manager 引用進來，state 與 plan 裡出現的是 secret 的 reference，不是密碼字串：&lt;/p></description><content:encoded><![CDATA[<p>每一個治理習慣單獨看都很小：在資源上多打三個 tag、把一段連線字串挪去別的地方、給帳單欄位填個用途。但少了這些習慣，半年後的代價是另一個量級 — 翻著一頁兩百筆沒有歸屬的資源猜哪個能砍、為了輪替一把外洩的密鑰回頭 grep 整個 repo、對著一張看不出誰花的雲帳單開跨部門會議。這一章談的就是這組「現在花幾分鐘、未來省幾天」的最小節奏。</p>
<p>治理習慣的責任是讓基礎設施在規模長大後仍然可被盤點、可被追責、可被回收。資源數量從幾十個長到幾百個時，「這是誰的、為什麼存在、花了多少」這三個問題若沒有預先在資源上留下答案，就只能靠人腦記憶與口頭考古，而記憶會隨著人員流動蒸發。</p>
<p>先界定這一章的邊界。身分與憑證本身怎麼設計 — IAM role、OIDC、最小權限 — 是模組二「身分與憑證地基」的範圍，這一章只談 secret 的儲存與引用：機密值放在哪、IaC 怎麼安全地指到它。成本這一塊也分兩層：把資源歸屬到擁有者與用途的地基（tagging、chargeback 的依據）在這一章，運行期怎麼用 reserved instance、spot、rightsizing 去壓低帳單，是 <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a> 的範圍。</p>
<h2 id="tagging-規範查帳與清資源的依據">Tagging 規範：查帳與清資源的依據</h2>
<p>Tag 是貼在每個資源上的結構化標籤，承擔「讓資源能被機器查詢與分群」的責任。沒有 tag 的資源在 console 裡只剩一個隨機後綴的名字，人能勉強認得幾個，但一旦數量過百，任何「列出所有 staging 的資源」「算出 team-a 這個月花多少」的問題都無法用查詢回答，只能逐筆翻。Tag 把這些問題從人工考古變成一行 filter。</p>
<p>值得從第一天就強制的最小 tag 集合是三個維度，各自回答一個治理問題：</p>
<table>
  <thead>
      <tr>
          <th>Tag</th>
          <th>回答的問題</th>
          <th>典型值</th>
          <th>缺了會怎樣</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>env</code></td>
          <td>這是哪個環境</td>
          <td><code>prod</code> / <code>staging</code> / <code>dev</code></td>
          <td>清資源時不敢動、怕誤刪生產</td>
      </tr>
      <tr>
          <td><code>owner</code></td>
          <td>出事找誰</td>
          <td><code>team-payments</code> / <code>platform</code></td>
          <td>資源孤兒化、沒人認領也沒人敢回收</td>
      </tr>
      <tr>
          <td><code>cost-center</code></td>
          <td>這筆錢算誰的</td>
          <td><code>cc-1024</code> / <code>growth</code></td>
          <td>帳單無法拆分、成本變成一筆沒人負責的公共支出</td>
      </tr>
  </tbody>
</table>
<p><code>env</code> 是清資源時的安全護欄。回收動作最大的恐懼是誤刪生產資源，當每個資源都標了 <code>env</code>，「列出所有 <code>env=dev</code> 且 30 天無流量的資源」就是一條可以放心執行的清理查詢，而 <code>env=prod</code> 的資源自動被排除在批次刪除之外。沒有這個 tag，任何自動化清理都因為怕誤傷而不敢落地，最後退回人工逐筆確認，於是根本沒人去清。</p>
<p><code>owner</code> 解決資源孤兒化。服務出狀況、或是看到一個用途不明的資源時，第一個問題是「這誰的」。標了 owner，告警可以自動路由、清理前可以自動通知認領；沒標，這個資源就停在「沒人敢動、因為不知道砍了會不會弄壞什麼」的狀態，永久占用配額與費用。團隊命名比個人名好 — 人會離職，團隊邊界相對穩定。</p>
<p><code>cost-center</code> 是成本歸屬的地基，下一節展開。</p>
<p>關鍵在於 tag 必須在資源建立時就由 IaC 寫進去，而不是事後補。Terraform 的 <code>default_tags</code> 讓一個 provider 區塊內的所有資源自動繼承一組 tag，避免逐個資源手動標、也避免漏標：</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">provider</span> <span class="s2">&#34;aws&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  region</span> <span class="o">=</span> <span class="s2">&#34;ap-northeast-1&#34;</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="k">default_tags</span> {
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">    tags</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">      env</span>         <span class="o">=</span> <span class="s2">&#34;staging&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">      owner</span>       <span class="o">=</span> <span class="s2">&#34;team-payments&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">      cost-center</span> <span class="o">=</span> <span class="s2">&#34;cc-1024&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">      managed-by</span>  <span class="o">=</span> <span class="s2">&#34;terraform&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    }
</span></span><span class="line"><span class="ln">11</span><span class="cl">  }
</span></span><span class="line"><span class="ln">12</span><span class="cl">}</span></span></code></pre></div><p>事後補 tag 是個會無限拖延的工作，因為它不影響任何功能、沒有 deadline、永遠排在 backlog 最後。判讀訊號很簡單：定期跑一條「列出缺少必填 tag 的資源」的查詢，數字若持續成長，代表有人繞過 IaC 手動開資源 — 這既是 tag 問題，也是模組一「Console 唯讀」紀律鬆動的徵兆。可以用 AWS 的 tag policy 或 OPA 這類策略引擎把「缺 tag 的資源」擋在 PR 階段，讓規範變成自動護欄而不是靠人自律。</p>
<h2 id="secrets-不進-code機密值的儲存與引用">Secrets 不進 code：機密值的儲存與引用</h2>
<p>機密值 — 資料庫密碼、第三方 API key、簽章用的私鑰 — 要存在專用的密鑰管理服務裡，而 code 與 IaC 只持有指向它的參照，不持有值本身。這條規則承擔的責任是把「機密外洩的爆炸半徑」與「程式碼的散布範圍」脫鉤：一旦密碼寫進 repo，它就跟著每一次 clone、每一份 CI 快取、每一個 fork 擴散，輪替時無法保證所有副本都更新，git 歷史更是會把它永久留存，即使後來刪掉那一行。</p>
<p>密鑰管理服務 — AWS Secrets Manager、SSM Parameter Store、HashiCorp Vault、GCP Secret Manager — 提供的是一個有存取控制、有審計紀錄、可輪替的集中儲存。值放在這裡，誰讀過、什麼時候讀的都有 log，輪替時只改一個地方，所有引用方下次讀取就拿到新值。</p>
<p>關鍵在 IaC 怎麼引用。IaC 應該存的是密鑰的 ARN（或等價的資源識別碼）與「在執行期去讀它」的指令，而不是密鑰的明文。下面這段把 RDS 密碼從 Secrets Manager 引用進來，state 與 plan 裡出現的是 secret 的 reference，不是密碼字串：</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">data</span> <span class="s2">&#34;aws_secretsmanager_secret&#34; &#34;db&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;prod/payments/db-password&#34;</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></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">data</span> <span class="s2">&#34;aws_secretsmanager_secret_version&#34; &#34;db&#34;</span> {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">  secret_id</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_secretsmanager_secret</span><span class="p">.</span><span class="k">db</span><span class="p">.</span><span class="k">id</span>
</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><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_db_instance&#34; &#34;payments&#34;</span> {<span class="c1">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">  # 引用 secret 的值、但這個值不是寫在 code 裡
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="n">  password</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_secretsmanager_secret_version</span><span class="p">.</span><span class="k">db</span><span class="p">.</span><span class="k">secret_string</span><span class="c1">
</span></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"><span class="c1"></span>}</span></span></code></pre></div><p>這裡有一個常被忽略的邊界：Terraform 即使從 Secrets Manager 讀值，那個值仍然會以明文落進 state file。所以「不進 code」只是第一道，state 後端的加密與存取控制（模組一的 state 地基）是同等重要的第二道 — 否則密鑰只是從 repo 搬到了一個沒鎖好的 state bucket。判讀訊號：定期用 secret 掃描工具（gitleaks、trufflehog）掃 repo 與 CI log，任何命中都當成需要輪替的外洩事件處理，而不是刪掉那行就算了，因為 git 歷史與既有 clone 已經保不住了。</p>
<p>機密的命名也值得約定。用 <code>env/service/purpose</code> 這類有結構的路徑（如 <code>prod/payments/db-password</code>），讓存取策略可以用前綴授權 — 給某個 service 的 role 只能讀 <code>prod/payments/*</code>，自然落實最小權限。誰能讀哪些 secret 的權限設計屬於模組二，更完整的密鑰生命週期、輪替策略與資料保護在 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>。</p>
<h2 id="成本可見性每筆花費都對得到擁有者與用途">成本可見性：每筆花費都對得到擁有者與用途</h2>
<p>成本可見性的目標是讓帳單上的每一筆花費都能回答「這是誰的、為了什麼」。雲帳單預設是一筆按服務類型加總的數字 — EC2 多少、RDS 多少 — 這個視角能告訴你花在哪類資源，卻答不出花在哪個團隊、哪個產品線、哪個功能。當這個問題答不出來，成本就變成一筆沒人負責的公共支出，沒有人有動機去優化自己看不到的帳。</p>
<p>把成本拆解到擁有者的地基，正是前面的 tagging。雲廠商的成本分攤工具（AWS Cost Explorer、Cost Allocation Tags、GCP 的 billing label）能用 tag 當分群維度，前提是那些 tag 要先在 billing 後台啟用為「成本分攤標籤」。啟用後，<code>cost-center</code> 和 <code>owner</code> 就從單純的標籤升級成帳單的可查詢維度，於是「team-payments 這個月花多少」「staging 環境占總成本幾成」變成一張報表而不是一場會議。</p>
<p>可見性先於優化，這個順序不能反。看不見的成本無法被歸屬，無法歸屬就無法問責，沒有問責就沒有人去做優化。所以這一章把地基鋪好 — 資源有 tag、tag 進了 billing 維度、報表能拆到團隊 — 之後運行期那些真正省錢的手段才有施力點。判讀訊號：設一條成本異常告警（如日均花費超過基線某個百分比就通知），當告警觸發時，因為有 tag，你能立刻定位是哪個團隊的哪類資源在漲，而不是面對一個總數乾瞪眼。</p>
<p>到了「知道誰花多少、接下來怎麼省」這一步 — reserved instance 的承諾折扣、spot 的可中斷算力、閒置資源的 rightsizing 與排程關機 — 就進入 <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a> 的運行期優化範圍。這一章負責的是讓那些優化「有帳可查、有人可問」。</p>
<h2 id="最小可行節奏先把地基跑起來再逐步加">最小可行節奏：先把地基跑起來，再逐步加</h2>
<p>治理的最小可行節奏，是早期只立「拔掉就會痛、補起來很貴」的那幾條規範，其餘留到規模逼出需求時再加。治理機制本身有維護成本 — 每一條策略規則、每一個審批關卡、每一套標籤分類法都要有人維護、有人解釋、有人在它擋錯東西時來救。在團隊還小、資源還少時堆滿企業級治理框架，付出的是當下的速度，換來的是一套還用不到的複雜度。</p>
<p>判斷一條治理規範該不該現在就立，看它的「補救成本曲線」。有些規範越晚補越貴，因為它要改的是既有資源的既成事實：</p>
<ul>
<li><strong>Tagging</strong>：越晚補越貴。幾百個沒 tag 的資源要回頭逐個考古歸屬，而當初建立時順手標只要幾秒。屬於 day-1 該立。</li>
<li><strong>Secrets 不進 code</strong>：幾乎無法事後補救。一旦密鑰進了 git 歷史就回不去，只能輪替所有外洩的密鑰。屬於 day-1 鐵律。</li>
<li><strong>成本分攤維度</strong>：依賴 tagging，tag 立了它就近乎免費啟用。地基屬於早期，細緻的 chargeback 報表可以晚點做。</li>
<li><strong>細緻的審批流程 / 多層級策略引擎</strong>：補救成本低、可以隨時加。早期硬上反而拖慢交付。屬於規模逼出需求再做。</li>
</ul>
<p>這個曲線給出的節奏是：補救成本陡的（tagging、secrets）從第一天就用 IaC 強制進去，因為它們事後補的代價是逐筆考古或全面輪替；補救成本平的（複雜審批、精細策略）等到痛點真的出現 — 開始有人手滑誤刪、開始有跨團隊的權限爭議 — 再有針對性地加，那時你也才知道該往哪個方向加。</p>
<p>這個節奏跟模組零的成熟度階梯是同一套思路：基礎設施的治理跟基礎設施本身一樣，是逐級長出來的，不是一次到位設計完的。過度設計的治理框架跟過度設計的架構一樣，會在還沒帶來價值之前就先收走團隊的速度。把規範變成自動護欄的工程（PR 階段擋缺 tag、CI 掃 secret）值得早投入，因為自動化的護欄維護成本低、且越早接管越省人力 — 這部分怎麼落地在 <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 模組七：infra 走 PR 流程</a> 展開。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/infra/08-governance-habits/tagging-secrets/" data-link-title="Tagging 規範與 Secrets 不進 code" data-link-desc="tag 讓資源可盤點、可清理、可歸屬；密鑰存在專用服務裡而非 code 或 state，兩者都屬於 day-1 就該立的治理地基">Tagging 規範與 Secrets 不進 code</a></td>
          <td>tag 讓資源可盤點可歸屬；密鑰存在專用服務裡而非 code 或 state，兩者都屬於 day-1 治理地基</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/08-governance-habits/cost-visibility-rhythm/" data-link-title="成本可見性與最小可行治理節奏" data-link-desc="用 tag 驅動的成本分攤讓帳單有人負責，以及判斷什麼治理該 day-1 就立、什麼等規模逼出來再加">成本可見性與最小可行治理節奏</a></td>
          <td>用 tag 驅動的成本分攤讓帳單有人負責，以及判斷什麼治理該 day-1 就立、什麼等規模逼出來再加</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/08-governance-habits/handover-design/" data-link-title="職務交接與存取撤銷設計" data-link-desc="人員異動時的存取撤銷順序、credential rotation、最小交接清單，以及讓交接成本結構性降低的 infra 設計原則">職務交接與存取撤銷設計</a></td>
          <td>人員異動時的存取撤銷清單、credential rotation、IaC 降低交接成本、最小交接清單與結構性設計</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>：secret 管理的更完整討論</li>
<li>→ <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a>：運行期的成本控制</li>
</ul>
]]></content:encoded></item><item><title>HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 Vault 在 secrets / credentials 治理譜系的定位（跟 cloud-native secrets manager / cert-manager 的取捨）、本文聚焦 &lt;em>dynamic credential engine&lt;/em> 的實作層：怎麼配 database engine、application 怎麼 renew lease、production 踩過哪些坑、跟 cloud-native vault 跟 vault-agent injector 怎麼整合。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Long-lived database credential 寫進 application config 是 production 環境最常見的 secret hygiene 失敗：credential 一旦外洩、輪替成本是 &lt;em>跨團隊協調 + 多服務同步重啟&lt;/em>、實務上半年才換一次、credential 在 git history / log / dump file 留下軌跡。動態憑證（dynamic credential）的核心承諾是 &lt;em>credential 生命週期跟 application session 對齊&lt;/em>、用完就 revoke、外洩窗口從幾個月縮到幾分鐘。&lt;/p>
&lt;p>但 dynamic credential 不是「換個 SDK 就好」、它把 &lt;em>credential 治理&lt;/em> 從 secret rotation 問題轉成 &lt;em>lease lifecycle&lt;/em> 問題。lease TTL 設多久、renewal 怎麼跑、DB 端 user 創建會不會撞 max_connections、Vault sealed 時 application 怎麼降級 — 每個都是 production-grade 議題、無法靠 vendor doc 預設值直接上線。&lt;/p>
&lt;h2 id="核心概念lease-lifecycle-跟-secrets-engine-模型">核心概念：lease lifecycle 跟 secrets engine 模型&lt;/h2>
&lt;p>Vault dynamic credential 由三個元件協作：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>元件&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Secrets engine&lt;/strong>&lt;/td>
 &lt;td>後端執行 credential 創建跟 revoke、每個 engine 對應一個 datastore（database / aws / ssh）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Role&lt;/strong>&lt;/td>
 &lt;td>創建 credential 的範本：DB 連線 + creation SQL + default / max TTL + allowed_roles&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Lease&lt;/strong>&lt;/td>
 &lt;td>每次 credential 發放都對應一個 lease ID、由 Vault 管 TTL / renew / revoke&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跟 static secret（K/V store）對照、dynamic credential 的關鍵差異是 &lt;em>credential 在 read 時才產生&lt;/em>、且 Vault 追蹤每個 outstanding lease；application 必須 &lt;em>主動 renew&lt;/em> 或接受 credential 失效。&lt;/p>
&lt;p>Lease 的兩個 TTL：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>default_ttl&lt;/strong>：credential 初始有效期、application 不 renew 就到期&lt;/li>
&lt;li>&lt;strong>max_ttl&lt;/strong>：credential 最長有效期、不管 renew 幾次都不能超過&lt;/li>
&lt;/ul>
&lt;p>實務 default 配置：&lt;code>default_ttl: 1h&lt;/code> + &lt;code>max_ttl: 24h&lt;/code>、application 每 30-45 分鐘 renew 一次、credential 最多活 24 小時必換新的。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a> overview 的 implementation-layer deep article。Overview 已說明 Vault 在 secrets / credentials 治理譜系的定位（跟 cloud-native secrets manager / cert-manager 的取捨）、本文聚焦 <em>dynamic credential engine</em> 的實作層：怎麼配 database engine、application 怎麼 renew lease、production 踩過哪些坑、跟 cloud-native vault 跟 vault-agent injector 怎麼整合。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Long-lived database credential 寫進 application config 是 production 環境最常見的 secret hygiene 失敗：credential 一旦外洩、輪替成本是 <em>跨團隊協調 + 多服務同步重啟</em>、實務上半年才換一次、credential 在 git history / log / dump file 留下軌跡。動態憑證（dynamic credential）的核心承諾是 <em>credential 生命週期跟 application session 對齊</em>、用完就 revoke、外洩窗口從幾個月縮到幾分鐘。</p>
<p>但 dynamic credential 不是「換個 SDK 就好」、它把 <em>credential 治理</em> 從 secret rotation 問題轉成 <em>lease lifecycle</em> 問題。lease TTL 設多久、renewal 怎麼跑、DB 端 user 創建會不會撞 max_connections、Vault sealed 時 application 怎麼降級 — 每個都是 production-grade 議題、無法靠 vendor doc 預設值直接上線。</p>
<h2 id="核心概念lease-lifecycle-跟-secrets-engine-模型">核心概念：lease lifecycle 跟 secrets engine 模型</h2>
<p>Vault dynamic credential 由三個元件協作：</p>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Secrets engine</strong></td>
          <td>後端執行 credential 創建跟 revoke、每個 engine 對應一個 datastore（database / aws / ssh）</td>
      </tr>
      <tr>
          <td><strong>Role</strong></td>
          <td>創建 credential 的範本：DB 連線 + creation SQL + default / max TTL + allowed_roles</td>
      </tr>
      <tr>
          <td><strong>Lease</strong></td>
          <td>每次 credential 發放都對應一個 lease ID、由 Vault 管 TTL / renew / revoke</td>
      </tr>
  </tbody>
</table>
<p>跟 static secret（K/V store）對照、dynamic credential 的關鍵差異是 <em>credential 在 read 時才產生</em>、且 Vault 追蹤每個 outstanding lease；application 必須 <em>主動 renew</em> 或接受 credential 失效。</p>
<p>Lease 的兩個 TTL：</p>
<ul>
<li><strong>default_ttl</strong>：credential 初始有效期、application 不 renew 就到期</li>
<li><strong>max_ttl</strong>：credential 最長有效期、不管 renew 幾次都不能超過</li>
</ul>
<p>實務 default 配置：<code>default_ttl: 1h</code> + <code>max_ttl: 24h</code>、application 每 30-45 分鐘 renew 一次、credential 最多活 24 小時必換新的。</p>
<h2 id="step-by-step-配置">Step-by-step 配置</h2>
<h3 id="vault-server-啟用-database-secrets-engine">Vault server 啟用 database secrets engine</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"># 1. enable secrets engine</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">vault secrets <span class="nb">enable</span> -path<span class="o">=</span>database database
</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"># 2. 配置 PostgreSQL connection</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">vault write database/config/myapp-prod <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  <span class="nv">plugin_name</span><span class="o">=</span>postgresql-database-plugin <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  <span class="nv">allowed_roles</span><span class="o">=</span><span class="s2">&#34;myapp-reader,myapp-writer&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  <span class="nv">connection_url</span><span class="o">=</span><span class="s2">&#34;postgresql://{{username}}:{{password}}@db.internal:5432/myapp?sslmode=require&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  <span class="nv">username</span><span class="o">=</span><span class="s2">&#34;vault_root&#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="nv">password</span><span class="o">=</span><span class="s2">&#34;&lt;vault_root_pw&gt;&#34;</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"># 3. 創建 role</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">vault write database/roles/myapp-reader <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  <span class="nv">db_name</span><span class="o">=</span>myapp-prod <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>  <span class="nv">creation_statements</span><span class="o">=</span><span class="s2">&#34;CREATE ROLE \&#34;{{name}}\&#34; WITH LOGIN PASSWORD &#39;{{password}}&#39; VALID UNTIL &#39;{{expiration}}&#39;; \
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s2">                       GRANT SELECT ON ALL TABLES IN SCHEMA public TO \&#34;{{name}}\&#34;;&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="se"></span>  <span class="nv">default_ttl</span><span class="o">=</span><span class="s2">&#34;1h&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="se"></span>  <span class="nv">max_ttl</span><span class="o">=</span><span class="s2">&#34;24h&#34;</span></span></span></code></pre></div><p>關鍵：<code>vault_root</code> 是 Vault 用來創建其他 user 的 <em>bootstrapping account</em>、權限要含 <code>CREATEROLE</code>、但不需要 SUPERUSER；creation_statements 必須含 <code>VALID UNTIL '{{expiration}}'</code>、否則 DB 端 user 不會自動過期、Vault revoke 失敗時會留 zombie account。</p>
<h3 id="application-取得-credential">Application 取得 credential</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"># Read 動態 credential（每次 read 都產生新 user）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">vault <span class="nb">read</span> database/creds/myapp-reader
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># Key                Value</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># lease_id           database/creds/myapp-reader/abc123</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># lease_duration     1h</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># username           v-myapp-reader-x7y8z9-1747512345</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># password           A1b2C3d4E5f6...</span></span></span></code></pre></div><p>Application 從 response 拿三個值：<code>lease_id</code>（用來 renew / revoke）、<code>username</code> + <code>password</code>（DB 連線）、<code>lease_duration</code>（決定何時 renew）。</p>
<h3 id="renew-lease">Renew lease</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"># 在 lease 到期前 renew（推薦在 50-70% TTL 跑）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">vault lease renew database/creds/myapp-reader/abc123
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># Key                Value</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># lease_id           database/creds/myapp-reader/abc123</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># lease_duration     1h    # renew 後重置回 default_ttl</span></span></span></code></pre></div><p><code>lease_duration</code> 在 renew 後 <em>重置回 default_ttl</em>、但 <em>不會超過 max_ttl</em>。例：default 1h / max 24h、application 連 renew 23 小時後、第 24 次 renew Vault 拒絕、application 必須拿新 credential。</p>
<h3 id="revoke-leaseapplication-shutdown-時">Revoke lease（application shutdown 時）</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"># Graceful shutdown 時主動 revoke</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">vault lease revoke database/creds/myapp-reader/abc123</span></span></code></pre></div><p>Application 結束時 revoke 是 <em>credential hygiene 的最後一道閘門</em> — 即使 lease 還有時間、主動 revoke 讓 DB 端 user 立刻消失、避免 credential 在 application crash dump / log 內被翻出時還能用。</p>
<h2 id="故障演練--邊界-case">故障演練 / 邊界 case</h2>
<h3 id="case-1lease-renewal-racecredential-中途失效">Case 1：Lease renewal race，credential 中途失效</h3>
<p><strong>徵兆</strong>：application log 突然出現 <code>FATAL: role &quot;v-myapp-reader-x7y8z9-...&quot; does not exist</code>、且時間點接近某個整點 / 半點。</p>
<p><strong>根因</strong>：application 用 lease_duration 推算 renew 時機、但用了 <em>系統時間</em> 而非 <em>lease 簽發時間</em>；application 啟動晚於 lease 簽發 30 秒、renew 跑在 lease 過期後 5 秒、Vault 已 revoke credential、DB 端 user 已刪除。</p>
<p><strong>修法</strong>：用 <em>server 回傳的 lease_duration</em> 反推 renew 時機、留 <em>20-30% buffer</em>。例：lease_duration 3600 秒、application 在 2400-2520 秒（66-70%）開始 renew、不要拖到 3500 秒。Vault SDK 多數有 LifetimeWatcher（Go SDK）或 Renewer（Python hvac）這類 helper、優先用 SDK 不要自管 ticker。</p>
<h3 id="case-2db-max_connections-撞牆">Case 2：DB max_connections 撞牆</h3>
<p><strong>徵兆</strong>：application 在流量高峰開始大量 <code>FATAL: too many connections for role</code>、Vault audit log 顯示新 credential 還在發、PostgreSQL <code>pg_stat_activity</code> 看到上百個 <code>v-myapp-...</code> user 同時連著。</p>
<p><strong>根因</strong>：每個 application instance / pod 在啟動時 read 一次 credential、credential lease 1h、但 <em>application 跑 30 分鐘就重啟</em>（K8s rolling update / OOM）；舊 user 還在 PostgreSQL 端連著（connection pool 沒釋放）、新 user 又被創建、累積到 max_connections。</p>
<p><strong>修法</strong>：兩層</p>
<ol>
<li>Application graceful shutdown 時 <code>vault lease revoke</code> + connection pool drain</li>
<li>PostgreSQL connection pool 加 <code>pool_lifetime_max</code> 跟 application instance lifetime 對齊、避免 connection leak 到 lease 失效後仍 holding</li>
</ol>
<h3 id="case-3vault-sealed-中existing-lease-仍可用但新-lease-拿不到">Case 3：Vault sealed 中、existing lease 仍可用但新 lease 拿不到</h3>
<p><strong>徵兆</strong>：deploy 新 version 時、新 pod 起不來、<code>vault read database/creds/...</code> 卡住或回 <code>Vault is sealed</code>；但 <em>舊 pod 持續運作正常</em>（因為已持有 lease）。</p>
<p><strong>根因</strong>：Vault sealed（master key 被 wrap、需要 unseal key 解封）時、existing lease 因為 <em>credential 已在 DB 端創建</em>、application 連線不需要 Vault 介入；但 <em>新 lease 創建需要 Vault</em> / <em>renew 也需要 Vault</em>。Sealed 期間 application 還能用、但無法擴容、無法 renew。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Vault HA cluster + auto-unseal（KMS / HSM auto-unseal）避免人工 unseal 鏈</li>
<li>Application 加 retry-with-backoff、Vault 短暫 unavailable 時不要立刻 crash</li>
<li>Lease 設長一點（default 4h、max 48h）給 unseal 流程留時間</li>
</ol>
<h3 id="case-4application-vault-token-expirelease-orphan">Case 4：Application Vault token expire、lease orphan</h3>
<p><strong>徵兆</strong>：application 在連續跑 1-2 週後突然開始 <code>Permission denied</code> on <code>vault lease renew</code>、credential 在 max_ttl 後失效但 application 不知道。</p>
<p><strong>根因</strong>：application 的 Vault token（不是 DB credential 的 lease）也有 TTL；token 過期後 application 無法 renew lease、但 application 可能還沒到 <em>自己拿新 token</em> 的循環。Lease 變 orphan（沒人能 renew）、TTL 到就被 revoke。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Application 用 vault-agent injector / sidecar pattern、由 sidecar 維護 token + lease；application 只讀 file</li>
<li>不用 sidecar 時、application token 用 <em>renewable token</em> + 跟 lease 同 lifecycle 管</li>
<li>AppRole auth method 的 secret_id 跟 token TTL 都要納入 application reload 流程</li>
</ol>
<h3 id="case-5circleci-2023-incident-對照--secret_id-scope-過寬">Case 5：<a href="/blog/backend/07-security-data-protection/cases/" data-link-title="模組七案例正文" data-link-desc="資安控制面與控制平面轉換案例入口。">CircleCI 2023 incident</a> 對照 — secret_id scope 過寬</h3>
<p><strong>徵兆</strong>：CircleCI 2023 1 月事件、攻擊者拿到開發者 endpoint session token、進而拿到 Vault AppRole 的 secret_id；secret_id 對應的 policy 含 <em>跨環境跨資料庫 read</em>、攻擊者用 secret_id 拿到大量動態 credential。</p>
<p><strong>根因</strong>：AppRole secret_id 的 policy scope 設成 <em>single AppRole 服務所有環境</em>、而不是 <em>per-environment AppRole</em>；secret_id 外洩等於拿到全公司 dynamic credential 發放權。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Per-environment AppRole：dev / staging / prod 各有獨立 AppRole + secret_id、policy 只允許該環境的 database engine path</li>
<li>Secret_id TTL 短化（&lt; 24h）、用 <em>response wrapping</em> 傳遞、拿到後立刻 unwrap、減少 secret_id 在 build pipeline log 留軌跡</li>
<li>Vault audit log 接 SIEM、<code>approle/login</code> 異常 location / IP 即刻 alert</li>
</ol>
<h2 id="容量規劃">容量規劃</h2>
<p>Dynamic credential 的容量設計圍繞 <em>lease churn rate</em> — 每秒多少新 lease 創建、多少 renew、多少 revoke。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算方式</th>
          <th>警戒值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新 lease / s</td>
          <td><code>應用 instance 數 × (1 / lease_duration)</code></td>
          <td>單 Vault node ~50/s、HA cluster ~200/s</td>
      </tr>
      <tr>
          <td>Renew / s</td>
          <td><code>outstanding lease × renew_freq</code></td>
          <td>renew 跟 read 同 cost</td>
      </tr>
      <tr>
          <td>DB 端 user 數</td>
          <td><code>peak outstanding lease</code></td>
          <td>不能超過 DB max_roles 限制</td>
      </tr>
      <tr>
          <td>DB connection 數</td>
          <td><code>peak outstanding lease × avg connection per credential</code></td>
          <td>不能超過 DB max_connections</td>
      </tr>
      <tr>
          <td>Vault audit log size</td>
          <td>每 lease 操作 ~500 byte、<code>(新+renew+revoke) × 500B</code></td>
          <td>100 lease/s → 50MB/s audit、SIEM 端要 sizing</td>
      </tr>
  </tbody>
</table>
<p>實務 sizing 範例：100 個 application pod、lease_duration 1h、renew at 50% TTL：</p>
<ul>
<li>新 lease：100 / 3600 ≈ 0.03/s（pod 重啟才有）</li>
<li>Renew：100 / 1800 ≈ 0.06/s</li>
<li>Outstanding lease：~100 個（每 pod 一個）</li>
<li>DB user 數：~100 個（peak ~150 含 grace period）</li>
<li>DB connection：100 × 5（pool size）= 500、需要 PostgreSQL <code>max_connections &gt;= 600</code></li>
</ul>
<p>超出單 Vault node 容量（~50 ops/s）時、走 Vault HA cluster + auto-unseal、或拆 namespace。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="vault-agent-injectork8s-環境推薦">vault-agent injector（K8s 環境推薦）</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"># pod annotation</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">metadata</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">annotations</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">vault.hashicorp.com/agent-inject</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</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">vault.hashicorp.com/role</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;myapp-reader&#34;</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">vault.hashicorp.com/agent-inject-secret-db-creds</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;database/creds/myapp-reader&#34;</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">vault.hashicorp.com/agent-inject-template-db-creds</span><span class="p">:</span><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">      {{- with secret &#34;database/creds/myapp-reader&#34; -}}
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="sd">      DB_USER={{ .Data.username }}
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="sd">      DB_PASSWORD={{ .Data.password }}
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="sd">      {{- end }}</span></span></span></code></pre></div><p>Sidecar 自動 renew lease、credential 寫進 pod shared volume、application 讀 file。Application code 不需要 Vault SDK、降低 dependency。</p>
<h3 id="sdk-pattern非-k8s-環境">SDK pattern（非 K8s 環境）</h3>
<p>Go：<code>hashicorp/vault/api</code> + <code>LifetimeWatcher</code>、Java：spring-cloud-vault、Python：hvac + Renewer。SDK 已處理 renew timing / retry / token rotation、不要自寫 ticker。</p>
<h3 id="跟-cloud-native-secret-manager-的混搭">跟 cloud-native secret manager 的混搭</h3>
<p><a href="/blog/backend/07-security-data-protection/vendors/aws-secrets-manager/" data-link-title="AWS Secrets Manager" data-link-desc="AWS 原生 secret store &#43; 內建 RDS / Redshift rotation Lambda、Resource Policy 跨帳號共享、KMS 加密">AWS Secrets Manager</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-secret-manager/" data-link-title="Google Secret Manager" data-link-desc="GCP 原生 secret store、CMEK &#43; Workload Identity Federation 整合、rotation 走自寫 Cloud Function 而非 built-in Lambda">Google Secret Manager</a> 也有 dynamic credential rotation（每 30 天輪替）、但 <em>cadence 是按時間</em>、不是 <em>按 application session</em>。混搭 pattern：</p>
<ul>
<li>Cloud-native：infrastructure-level credential（RDS master / k8s service account）、long TTL（30-90 天）</li>
<li>Vault dynamic：application-level credential、short TTL（1-24 小時）</li>
<li>Vault root credential 存 cloud-native secret manager、Vault auto-unseal 也用 cloud KMS</li>
</ul>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Database snapshot 跟 dynamic credential 衝突</strong>：PostgreSQL <code>pg_dump</code> 用 long-lived credential、不適用 dynamic；snapshot user 用 static + scoped policy、跟 application user 分離</li>
<li><strong>Connection pool 端的 dynamic credential 支援</strong>：<a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PgBouncer</a> 不支援 per-connection credential rotation、需要 connection 整個 lifecycle 跟 lease 對齊</li>
<li><strong>多 region Vault replication</strong>：performance replication 跟 disaster recovery replication 對 lease 的處理不同、跨 region application 要 sticky 同一 region 的 Vault primary</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/" data-link-title="HashiCorp Vault" data-link-desc="Self-hosted secret management 與 dynamic credential / encryption-as-a-service / PKI engine、跨雲跨環境的 secret 控制面">HashiCorp Vault</a></li>
<li>對照案例：<a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></li>
<li>對照案例：<a href="/blog/backend/07-security-data-protection/cases/" data-link-title="模組七案例正文" data-link-desc="資安控制面與控制平面轉換案例入口。">CircleCI 2023 AppRole 事件</a> — Cross-vendor mapping</li>
<li>上游 chapter：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer 配置</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item></channel></rss>