<?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>Audit on Tarragon</title><link>https://tarrragon.github.io/blog/tags/audit/</link><description>Recent content in Audit 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/audit/index.xml" rel="self" type="application/rss+xml"/><item><title>Security Group 稽核與清理</title><link>https://tarrragon.github.io/blog/infra/03-network-foundation/security-group-audit-cleanup/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/03-network-foundation/security-group-audit-cleanup/</guid><description>&lt;p>Security group 的規則會隨時間累積：某次救火加了一條 0.0.0.0/0、某個已下線的服務留下沒人認領的 SG、某條規則的用途只存在建立者的記憶裡。稽核的目標是把這些累積的規則攤開來，逐條回答「這條規則還有在用嗎、來源該這麼寬嗎」，然後安全地清理不需要的部分。&lt;/p>
&lt;h2 id="匯出所有-security-group-與規則">匯出所有 security group 與規則&lt;/h2>
&lt;p>稽核的第一步是把當前所有 SG 和它們的規則拉出來存成可查詢的 JSON。這份 JSON 是後續所有分析的輸入，也是「稽核那天環境長什麼樣」的快照。&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">aws ec2 describe-security-groups &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s1">&amp;#39;SecurityGroups[].{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="s1"> GroupId:GroupId,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="s1"> GroupName:GroupName,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s1"> VpcId:VpcId,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s1"> Description:Description,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s1"> IngressRules:IpPermissions,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s1"> EgressRules:IpPermissionsEgress,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s1"> Tags:Tags
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --output json &amp;gt; sg-inventory-&lt;span class="k">$(&lt;/span>date +%Y%m%d&lt;span class="k">)&lt;/span>.json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這份檔案通常幾百 KB 到幾 MB，存進 repo 的 &lt;code>inventory/&lt;/code> 目錄，方便日後比對變化。如果帳號有多個 region，每個 region 各跑一次並標明 region。&lt;/p>
&lt;p>用 jq 快速看有多少 SG 和總規則數：&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">jq &lt;span class="s1">&amp;#39;length&amp;#39;&lt;/span> sg-inventory-*.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">jq &lt;span class="s1">&amp;#39;[.[].IngressRules | length] | add&amp;#39;&lt;/span> sg-inventory-*.json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="找出-00000-全開的入站規則">找出 0.0.0.0/0 全開的入站規則&lt;/h2>
&lt;p>0.0.0.0/0 入站代表允許整個網際網路連到這個埠。對外 ALB 的 80/443 開 0.0.0.0/0 是設計意圖，但資料庫埠（5432、3306、6379）、SSH（22）或管理埠開 0.0.0.0/0 是需要收斂的目標。&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">jq -r &lt;span class="s1">&amp;#39;.[] | select(.IngressRules[]?.IpRanges[]?.CidrIp == &amp;#34;0.0.0.0/0&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="s1"> {GroupId, GroupName, OpenPorts: [.IngressRules[] |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="s1"> select(.IpRanges[]?.CidrIp == &amp;#34;0.0.0.0/0&amp;#34;) |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="s1"> &amp;#34;\(.FromPort // &amp;#34;all&amp;#34;)-\(.ToPort // &amp;#34;all&amp;#34;)/\(.IpProtocol)&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="s1"> ]}&amp;#39;&lt;/span> sg-inventory-*.json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>輸出會列出每個有全開規則的 SG 和對應的 port 範圍。對每一條命中，判斷：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>全開是否合規&lt;/th>
 &lt;th>處理方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>ALB 的 80/443&lt;/td>
 &lt;td>合規 — 負載平衡器的職責就是接收公開流量&lt;/td>
 &lt;td>保留，標記為已審查&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SSH (22) 或 RDP (3389)&lt;/td>
 &lt;td>需收斂 — 管理埠暴露在持續的暴力掃描下&lt;/td>
 &lt;td>改用 SSM Session Manager 或限縮到辦公室 IP&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料庫埠 (5432/3306/6379)&lt;/td>
 &lt;td>需收斂 — 資料庫不應從公網可達&lt;/td>
 &lt;td>改為只允許應用層 SG 來源&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>全埠 (0-65535 / -1)&lt;/td>
 &lt;td>需收斂 — 等於沒有防火牆&lt;/td>
 &lt;td>拆成具體需要的埠和來源&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>IPv6 的 &lt;code>::/0&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">jq -r &lt;span class="s1">&amp;#39;.[] | select(.IngressRules[]?.Ipv6Ranges[]?.CidrIpv6 == &amp;#34;::/0&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="s1"> .GroupId&amp;#39;&lt;/span> sg-inventory-*.json&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="找出未使用的-security-group">找出未使用的 security group&lt;/h2>
&lt;p>未使用的 SG 是沒有任何網路介面（ENI）掛載的 SG。它不影響任何正在運行的資源，但佔用 SG 配額（每個 VPC 預設上限 2500 個），而且它的規則會讓稽核清單更長、更難判讀。&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">aws ec2 describe-network-interfaces &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s1">&amp;#39;NetworkInterfaces[].Groups[].GroupId&amp;#39;&lt;/span> &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> --output text &lt;span class="p">|&lt;/span> tr &lt;span class="s1">&amp;#39;\t&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> sort -u &amp;gt; sg-in-use.txt
&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">jq -r &lt;span class="s1">&amp;#39;.[].GroupId&amp;#39;&lt;/span> sg-inventory-*.json &lt;span class="p">|&lt;/span> sort -u &amp;gt; sg-all.txt
&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">comm -23 sg-all.txt sg-in-use.txt &amp;gt; sg-unused.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">cat sg-unused.txt&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>sg-unused.txt&lt;/code> 裡列出的就是當前沒有任何 ENI 引用的 SG。注意幾個例外：&lt;/p></description><content:encoded><![CDATA[<p>Security group 的規則會隨時間累積：某次救火加了一條 0.0.0.0/0、某個已下線的服務留下沒人認領的 SG、某條規則的用途只存在建立者的記憶裡。稽核的目標是把這些累積的規則攤開來，逐條回答「這條規則還有在用嗎、來源該這麼寬嗎」，然後安全地清理不需要的部分。</p>
<h2 id="匯出所有-security-group-與規則">匯出所有 security group 與規則</h2>
<p>稽核的第一步是把當前所有 SG 和它們的規則拉出來存成可查詢的 JSON。這份 JSON 是後續所有分析的輸入，也是「稽核那天環境長什麼樣」的快照。</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">aws ec2 describe-security-groups <span class="se">\
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;SecurityGroups[].{
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s1">    GroupId:GroupId,
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s1">    GroupName:GroupName,
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s1">    VpcId:VpcId,
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s1">    Description:Description,
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s1">    IngressRules:IpPermissions,
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s1">    EgressRules:IpPermissionsEgress,
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s1">    Tags:Tags
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s1">  }&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --output json &gt; sg-inventory-<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>.json</span></span></code></pre></div><p>這份檔案通常幾百 KB 到幾 MB，存進 repo 的 <code>inventory/</code> 目錄，方便日後比對變化。如果帳號有多個 region，每個 region 各跑一次並標明 region。</p>
<p>用 jq 快速看有多少 SG 和總規則數：</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">jq <span class="s1">&#39;length&#39;</span> sg-inventory-*.json
</span></span><span class="line"><span class="ln">2</span><span class="cl">jq <span class="s1">&#39;[.[].IngressRules | length] | add&#39;</span> sg-inventory-*.json</span></span></code></pre></div><h2 id="找出-00000-全開的入站規則">找出 0.0.0.0/0 全開的入站規則</h2>
<p>0.0.0.0/0 入站代表允許整個網際網路連到這個埠。對外 ALB 的 80/443 開 0.0.0.0/0 是設計意圖，但資料庫埠（5432、3306、6379）、SSH（22）或管理埠開 0.0.0.0/0 是需要收斂的目標。</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">jq -r <span class="s1">&#39;.[] | select(.IngressRules[]?.IpRanges[]?.CidrIp == &#34;0.0.0.0/0&#34;) |
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s1">  {GroupId, GroupName, OpenPorts: [.IngressRules[] |
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s1">    select(.IpRanges[]?.CidrIp == &#34;0.0.0.0/0&#34;) |
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s1">    &#34;\(.FromPort // &#34;all&#34;)-\(.ToPort // &#34;all&#34;)/\(.IpProtocol)&#34;
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s1">  ]}&#39;</span> sg-inventory-*.json</span></span></code></pre></div><p>輸出會列出每個有全開規則的 SG 和對應的 port 範圍。對每一條命中，判斷：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>全開是否合規</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ALB 的 80/443</td>
          <td>合規 — 負載平衡器的職責就是接收公開流量</td>
          <td>保留，標記為已審查</td>
      </tr>
      <tr>
          <td>SSH (22) 或 RDP (3389)</td>
          <td>需收斂 — 管理埠暴露在持續的暴力掃描下</td>
          <td>改用 SSM Session Manager 或限縮到辦公室 IP</td>
      </tr>
      <tr>
          <td>資料庫埠 (5432/3306/6379)</td>
          <td>需收斂 — 資料庫不應從公網可達</td>
          <td>改為只允許應用層 SG 來源</td>
      </tr>
      <tr>
          <td>全埠 (0-65535 / -1)</td>
          <td>需收斂 — 等於沒有防火牆</td>
          <td>拆成具體需要的埠和來源</td>
      </tr>
  </tbody>
</table>
<p>IPv6 的 <code>::/0</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">jq -r <span class="s1">&#39;.[] | select(.IngressRules[]?.Ipv6Ranges[]?.CidrIpv6 == &#34;::/0&#34;) |
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s1">  .GroupId&#39;</span> sg-inventory-*.json</span></span></code></pre></div><h2 id="找出未使用的-security-group">找出未使用的 security group</h2>
<p>未使用的 SG 是沒有任何網路介面（ENI）掛載的 SG。它不影響任何正在運行的資源，但佔用 SG 配額（每個 VPC 預設上限 2500 個），而且它的規則會讓稽核清單更長、更難判讀。</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">aws ec2 describe-network-interfaces <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;NetworkInterfaces[].Groups[].GroupId&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --output text <span class="p">|</span> tr <span class="s1">&#39;\t&#39;</span> <span class="s1">&#39;\n&#39;</span> <span class="p">|</span> sort -u &gt; sg-in-use.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">jq -r <span class="s1">&#39;.[].GroupId&#39;</span> sg-inventory-*.json <span class="p">|</span> sort -u &gt; sg-all.txt
</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">comm -23 sg-all.txt sg-in-use.txt &gt; sg-unused.txt
</span></span><span class="line"><span class="ln">8</span><span class="cl">cat sg-unused.txt</span></span></code></pre></div><p><code>sg-unused.txt</code> 裡列出的就是當前沒有任何 ENI 引用的 SG。注意幾個例外：</p>
<ul>
<li><strong>default SG</strong>：每個 VPC 都有一個 default SG，即使未使用也無法刪除，可以跳過</li>
<li><strong>被其他 SG 引用</strong>：某個 SG 雖然沒有掛在任何 ENI 上，但被另一個 SG 的入站規則引用為 source — 刪除它會讓引用方的規則失效</li>
<li><strong>被 launch template 或 auto-scaling group 引用</strong>：新啟動的實例會套用這個 SG，刪了之後新實例啟動會失敗</li>
</ul>
<h2 id="依賴檢查刪除前確認沒有間接引用">依賴檢查：刪除前確認沒有間接引用</h2>
<p>直接刪一個 SG 之前，確認沒有其他資源引用它。AWS 在 SG 被引用時會擋住刪除（報 DependencyViolation），但提前知道引用方可以避免白跑一趟。</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="nv">SG_ID</span><span class="o">=</span><span class="s2">&#34;sg-0abc123&#34;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 哪些 SG 的入站規則引用了這個 SG 作為來源</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">jq -r --arg sg <span class="s2">&#34;</span><span class="nv">$SG_ID</span><span class="s2">&#34;</span> <span class="s1">&#39;.[] |
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s1">  select(.IngressRules[]?.UserIdGroupPairs[]?.GroupId == $sg) |
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s1">  &#34;\(.GroupId) (\(.GroupName)) 的入站規則引用了 \($sg)&#34;&#39;</span> sg-inventory-*.json
</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"># 哪些 ENI 掛了這個 SG</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">aws ec2 describe-network-interfaces <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --filters <span class="nv">Name</span><span class="o">=</span>group-id,Values<span class="o">=</span><span class="nv">$SG_ID</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;NetworkInterfaces[].{Id:NetworkInterfaceId,Desc:Description,Status:Status}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --output table
</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 class="c1"># 哪些 RDS instance 使用這個 SG</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">aws rds describe-db-instances <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;DBInstances[?VpcSecurityGroups[?VpcSecurityGroupId==&#39;</span><span class="nv">$SG_ID</span><span class="s2">&#39;]].[DBInstanceIdentifier]&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="se"></span>  --output text
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># 哪些 ELB 使用這個 SG</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">aws elbv2 describe-load-balancers <span class="se">\
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;LoadBalancers[?SecurityGroups[?contains(@,&#39;</span><span class="nv">$SG_ID</span><span class="s2">&#39;)]].[LoadBalancerName]&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="se"></span>  --output text</span></span></code></pre></div><p>如果所有查詢都回傳空，這個 SG 可以安全刪除。</p>
<h2 id="清理流程標記--通知--等待--刪除">清理流程：標記 → 通知 → 等待 → 刪除</h2>
<p>批量清理不是一次 <code>delete-security-group</code> 的事。安全的流程有四步：</p>
<h3 id="標記候選">標記候選</h3>
<p>對每個要清理的 SG 加一個 tag 標明狀態和預定刪除日期：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws ec2 create-tags <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --resources sg-0abc123 sg-0def456 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --tags <span class="nv">Key</span><span class="o">=</span>cleanup-status,Value<span class="o">=</span>pending-deletion <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>         <span class="nv">Key</span><span class="o">=</span>cleanup-date,Value<span class="o">=</span>2026-07-10 <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>         <span class="nv">Key</span><span class="o">=</span>cleanup-reason,Value<span class="o">=</span><span class="s2">&#34;unused-no-eni-no-reference&#34;</span></span></span></code></pre></div><h3 id="通知">通知</h3>
<p>如果 SG 有 <code>owner</code> tag，通知該 owner：「這個 SG 預計在 cleanup-date 刪除，如果仍在使用請回報」。如果沒有 owner tag（多數需要清理的 SG 都沒有），在團隊頻道公告清理清單。</p>
<h3 id="等待">等待</h3>
<p>給 7-14 天的寬限期。期間如果有人回報某個 SG 仍在使用，把 cleanup-status 改成 <code>retained</code> 並補上正確的 owner tag。</p>
<h3 id="刪除">刪除</h3>
<p>寬限期過後，對仍是 <code>pending-deletion</code> 的 SG 執行刪除：</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="k">for</span> sg in <span class="k">$(</span>aws ec2 describe-security-groups <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --filters <span class="nv">Name</span><span class="o">=</span>tag:cleanup-status,Values<span class="o">=</span>pending-deletion <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;SecurityGroups[].GroupId&#39;</span> --output text<span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Deleting </span><span class="nv">$sg</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  aws ec2 delete-security-group --group-id <span class="nv">$sg</span> 2&gt;<span class="p">&amp;</span><span class="m">1</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>DependencyViolation 代表有遺漏的引用，跳過該 SG 並重新調查。</p>
<h2 id="自動化持續治理">自動化持續治理</h2>
<p>手動稽核適合第一次清理，持續治理靠自動化：</p>
<h3 id="aws-config-規則">AWS Config 規則</h3>
<p><code>restricted-ssh</code> 和 <code>restricted-common-ports</code> 是 AWS Config 的 managed rule，啟用後會持續監控 SG 規則，新增的 0.0.0.0/0 規則會在幾分鐘內被標記為 non-compliant。</p>
<h3 id="prowler-定期掃描">Prowler 定期掃描</h3>
<p>在 CI 排程中定期跑 Prowler，掃描結果存進 repo 作為趨勢追蹤：</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">prowler aws --services ec2 --checks ec2_securitygroup_allow_ingress_from_internet_to_any_port <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  -M json-ocsf -o inventory/prowler/</span></span></code></pre></div><h3 id="pr-流程攔截">PR 流程攔截</h3>
<p><a href="/blog/infra/07-infra-as-pr/plan-review-apply-guardrails/" data-link-title="infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七的 checkov/tfsec 護欄</a>在 PR 階段攔截新增的 0.0.0.0/0 規則。這是把治理從「事後稽核」推到「事前攔截」的關鍵一步：稽核能發現已存在的問題，PR 護欄能阻止新問題被引入。</p>
<p>AWS Security Hub 啟用 Foundational Security Best Practices 標準後，會自動聚合 SG 相關的合規 finding 並提供統一 dashboard，適合作為管理層報告的來源。Security Hub 整合了 Config rules 和 Prowler 各自能發現的問題，提供單一窗口追蹤合規趨勢。</p>
<h2 id="稽核節奏">稽核節奏</h2>
<p>第一次稽核最花時間（半天到一天，取決於 SG 數量）。之後的節奏取決於環境變動速度：</p>
<table>
  <thead>
      <tr>
          <th>環境類型</th>
          <th>建議節奏</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>有 PR 流程 + checkov 的環境</td>
          <td>每季</td>
          <td>新規則已被 PR 攔截，稽核主要看 drift</td>
      </tr>
      <tr>
          <td>有 IaC 但沒有 PR 護欄</td>
          <td>每月</td>
          <td>手動 apply 可能繞過審查</td>
      </tr>
      <tr>
          <td>全手動環境</td>
          <td>每月或每次事故後</td>
          <td>沒有任何自動攔截機制</td>
      </tr>
  </tbody>
</table>
<p>稽核產出一份報告：SG 總數、0.0.0.0/0 規則數、未使用 SG 數、上次稽核以來的變化。這份報告可以作為治理進度的量化指標，納入月報。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/03-network-foundation/vpc-subnet-security-group/" data-link-title="網路地基 — VPC、subnet 分層與 security group 設計" data-link-desc="VPC CIDR 規劃、public / private subnet 切分、route table 與 NAT 的可用性成本取捨、security group 最小開放設計，以及 NACL 的定位">網路地基 — security group 設計</a>：SG 的設計原則（最小開放、group 互相引用）</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/plan-review-apply-guardrails/" data-link-title="infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">infra 走 PR 流程</a>：checkov/tfsec 在 PR 階段攔截 0.0.0.0/0</li>
<li>→ <a href="/blog/infra/08-governance-habits/tagging-secrets/" data-link-title="Tagging 規範與 Secrets 不進 code" data-link-desc="tag 讓資源可盤點、可清理、可歸屬；密鑰存在專用服務裡而非 code 或 state，兩者都屬於 day-1 就該立的治理地基">治理好習慣 — tagging</a>：tag 是識別 SG owner 和清理候選的依據</li>
</ul>
]]></content:encoded></item><item><title>查詢消費模式</title><link>https://tarrragon.github.io/blog/monitoring/04-collector/query-consumption-patterns/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/04-collector/query-consumption-patterns/</guid><description>&lt;p>事件的價值在於被查詢消費。設計事件時反過來想：查詢需要什麼欄位 → 事件需要帶什麼 data → 感測器需要在什麼時機觸發。從消費端反推設計，避免「收了一堆事件但查不到想要的答案」。&lt;/p>
&lt;p>五種查詢場景各自需要不同的事件類型、欄位和查詢模式。每種場景的查詢模式也決定了需要 SQLite 層還是 PostgreSQL 層（見 &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/feature-tier-boundary/" data-link-title="功能分層與 Backend 選擇" data-link-desc="SQLite 層和 PostgreSQL 層各自承載哪些功能 — 分界線是查詢模式而非資料量、觸發升級的是功能需求而非規模成長">功能分層與 Backend 選擇&lt;/a>）。&lt;/p>
&lt;h2 id="debug-查詢">Debug 查詢&lt;/h2>
&lt;p>Debug 查詢回答「問題出在哪」。觸發時機是使用者回報問題或 error alert 觸發後，開發者需要還原問題的 context。&lt;/p>
&lt;h3 id="查詢場景">查詢場景&lt;/h3>
&lt;h4 id="剛才使用者回報的問題">剛才使用者回報的問題&lt;/h4>
&lt;p>查詢模式：用 session_id 過濾，拉出該 session 的全部事件，按時間排序。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- SQLite
&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">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">type&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ts&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">data&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="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&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="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">session_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;abc-123&amp;#39;&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="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ts&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>需要的事件欄位：session_id（關聯同次使用的事件）、ts（排序）、error 的 stack trace 和 step（定位失敗點）。&lt;/p>
&lt;h4 id="這個-error-多常發生">這個 error 多常發生&lt;/h4>
&lt;p>查詢模式：按 error name 分群計數，看時間趨勢。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- SQLite
&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">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">as&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">count&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="n">strftime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;%Y-%m-%d&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ts&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">as&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">day&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="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&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="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;error&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="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ts&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;now&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;-7 days&amp;#39;&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="k">GROUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">day&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="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">day&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">count&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DESC&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>需要的事件欄位：type=&amp;lsquo;error&amp;rsquo;、name（分群鍵）、ts（時間分桶）。&lt;/p>
&lt;h3 id="需要的事件">需要的事件&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>事件類型&lt;/th>
 &lt;th>必要欄位&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>error&lt;/td>
 &lt;td>stack_trace, step, session_id&lt;/td>
 &lt;td>定位失敗點 + 關聯 session&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>event&lt;/td>
 &lt;td>name, session_id&lt;/td>
 &lt;td>還原使用者操作路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>lifecycle&lt;/td>
 &lt;td>name, session_id&lt;/td>
 &lt;td>還原系統狀態轉換&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="alerting-查詢">Alerting 查詢&lt;/h2>
&lt;p>Alerting 查詢回答「需要注意嗎」。分兩種機制：rule engine 的即時評估（事件到達時逐筆比對規則）和事後查詢的趨勢分析。&lt;/p>
&lt;h3 id="查詢場景-1">查詢場景&lt;/h3>
&lt;h4 id="error-數量突然上升">Error 數量突然上升&lt;/h4>
&lt;p>查詢模式：最近 1 小時的 error 計數 vs 前一天同時段，偏差超過閾值則告警。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- SQLite
&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">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">as&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">recent_count&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="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&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="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;error&amp;#39;&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="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ts&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;now&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;-1 hour&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Rule engine 的即時版：每收到一筆 error 事件，遞增計數器，計數器超過閾值觸發動作。&lt;/p>
&lt;h4 id="特定-error-首次出現">特定 error 首次出現&lt;/h4>
&lt;p>查詢模式：收到 error 時查是否有歷史記錄。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- SQLite
&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">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&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="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;error&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">?&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="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ts&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>結果為 0 代表首次出現 — 觸發「新 error 類型」告警。Sentry 的核心功能之一就是這個查詢。&lt;/p>
&lt;h3 id="rule-engine-vs-事後查詢">Rule engine vs 事後查詢&lt;/h3>
&lt;p>Rule engine 逐筆評估，延遲在毫秒級，適合「error 出現就通知」。事後查詢用 SQL 聚合，延遲在秒到分鐘級，適合「過去一小時的 error 趨勢」。兩者互補 — rule engine 做即時告警、SQL 查詢做事後分析。&lt;/p></description><content:encoded><![CDATA[<p>事件的價值在於被查詢消費。設計事件時反過來想：查詢需要什麼欄位 → 事件需要帶什麼 data → 感測器需要在什麼時機觸發。從消費端反推設計，避免「收了一堆事件但查不到想要的答案」。</p>
<p>五種查詢場景各自需要不同的事件類型、欄位和查詢模式。每種場景的查詢模式也決定了需要 SQLite 層還是 PostgreSQL 層（見 <a href="/blog/monitoring/04-collector/feature-tier-boundary/" data-link-title="功能分層與 Backend 選擇" data-link-desc="SQLite 層和 PostgreSQL 層各自承載哪些功能 — 分界線是查詢模式而非資料量、觸發升級的是功能需求而非規模成長">功能分層與 Backend 選擇</a>）。</p>
<h2 id="debug-查詢">Debug 查詢</h2>
<p>Debug 查詢回答「問題出在哪」。觸發時機是使用者回報問題或 error alert 觸發後，開發者需要還原問題的 context。</p>
<h3 id="查詢場景">查詢場景</h3>
<h4 id="剛才使用者回報的問題">剛才使用者回報的問題</h4>
<p>查詢模式：用 session_id 過濾，拉出該 session 的全部事件，按時間排序。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">type</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">ts</span><span class="p">,</span><span class="w"> </span><span class="k">data</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">session_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;abc-123&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">ts</span><span class="p">;</span></span></span></code></pre></div><p>需要的事件欄位：session_id（關聯同次使用的事件）、ts（排序）、error 的 stack trace 和 step（定位失敗點）。</p>
<h4 id="這個-error-多常發生">這個 error 多常發生</h4>
<p>查詢模式：按 error name 分群計數，看時間趨勢。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="k">count</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="n">strftime</span><span class="p">(</span><span class="s1">&#39;%Y-%m-%d&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">ts</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="k">day</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;error&#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="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">datetime</span><span class="p">(</span><span class="s1">&#39;now&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;-7 days&#39;</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="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="k">day</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">day</span><span class="p">,</span><span class="w"> </span><span class="k">count</span><span class="w"> </span><span class="k">DESC</span><span class="p">;</span></span></span></code></pre></div><p>需要的事件欄位：type=&lsquo;error&rsquo;、name（分群鍵）、ts（時間分桶）。</p>
<h3 id="需要的事件">需要的事件</h3>
<table>
  <thead>
      <tr>
          <th>事件類型</th>
          <th>必要欄位</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>error</td>
          <td>stack_trace, step, session_id</td>
          <td>定位失敗點 + 關聯 session</td>
      </tr>
      <tr>
          <td>event</td>
          <td>name, session_id</td>
          <td>還原使用者操作路徑</td>
      </tr>
      <tr>
          <td>lifecycle</td>
          <td>name, session_id</td>
          <td>還原系統狀態轉換</td>
      </tr>
  </tbody>
</table>
<h2 id="alerting-查詢">Alerting 查詢</h2>
<p>Alerting 查詢回答「需要注意嗎」。分兩種機制：rule engine 的即時評估（事件到達時逐筆比對規則）和事後查詢的趨勢分析。</p>
<h3 id="查詢場景-1">查詢場景</h3>
<h4 id="error-數量突然上升">Error 數量突然上升</h4>
<p>查詢模式：最近 1 小時的 error 計數 vs 前一天同時段，偏差超過閾值則告警。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">recent_count</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;error&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">datetime</span><span class="p">(</span><span class="s1">&#39;now&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;-1 hour&#39;</span><span class="p">);</span></span></span></code></pre></div><p>Rule engine 的即時版：每收到一筆 error 事件，遞增計數器，計數器超過閾值觸發動作。</p>
<h4 id="特定-error-首次出現">特定 error 首次出現</h4>
<p>查詢模式：收到 error 時查是否有歷史記錄。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;error&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">?</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="o">?</span><span class="p">;</span></span></span></code></pre></div><p>結果為 0 代表首次出現 — 觸發「新 error 類型」告警。Sentry 的核心功能之一就是這個查詢。</p>
<h3 id="rule-engine-vs-事後查詢">Rule engine vs 事後查詢</h3>
<p>Rule engine 逐筆評估，延遲在毫秒級，適合「error 出現就通知」。事後查詢用 SQL 聚合，延遲在秒到分鐘級，適合「過去一小時的 error 趨勢」。兩者互補 — rule engine 做即時告警、SQL 查詢做事後分析。</p>
<h3 id="需要的事件-1">需要的事件</h3>
<table>
  <thead>
      <tr>
          <th>事件類型</th>
          <th>必要欄位</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>error</td>
          <td>name, ts</td>
          <td>計數 + 時間趨勢</td>
      </tr>
      <tr>
          <td>error</td>
          <td>source.version</td>
          <td>按版本分群看是否新版本引入</td>
      </tr>
  </tbody>
</table>
<h2 id="產品決策查詢">產品決策查詢</h2>
<p>產品決策查詢回答「使用者怎麼用產品」。從簡單的功能使用率到複雜的 funnel 分析。</p>
<h3 id="查詢場景-2">查詢場景</h3>
<h4 id="新功能有多少人用">新功能有多少人用</h4>
<p>查詢模式：按 event name 計數。SQLite 層即可。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="k">count</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="k">COUNT</span><span class="p">(</span><span class="k">DISTINCT</span><span class="w"> </span><span class="n">session_id</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">unique_sessions</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;event&#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="k">AND</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;new_feature.%&#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="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">datetime</span><span class="p">(</span><span class="s1">&#39;now&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;-7 days&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">name</span><span class="p">;</span></span></span></code></pre></div><h4 id="註冊流程在哪流失">註冊流程在哪流失</h4>
<p>查詢模式：session 級 funnel JOIN。需要 PostgreSQL 層。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- PostgreSQL
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">WITH</span><span class="w"> </span><span class="n">session_steps</span><span class="w"> </span><span class="k">AS</span><span class="w"> </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="k">SELECT</span><span class="w"> </span><span class="n">session_id</span><span class="p">,</span><span class="w"> </span><span class="n">name</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="n">ROW_NUMBER</span><span class="p">()</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">session_id</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">ts</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">step_order</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="k">WHERE</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;signup.start&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;signup.email&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;signup.verify&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;signup.complete&#39;</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="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">NOW</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;30 days&#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="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="k">SELECT</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="k">DISTINCT</span><span class="w"> </span><span class="n">session_id</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">sessions</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">session_steps</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">name</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">MIN</span><span class="p">(</span><span class="n">step_order</span><span class="p">);</span></span></span></code></pre></div><p>完整的 funnel 分析方法論見 <a href="/blog/monitoring/08-business-analytics/self-hosted-funnel/" data-link-title="從 collector 資料做基礎 funnel 分析" data-link-desc="SQLite 層能做什麼程度的 funnel、PostgreSQL 層提供什麼進階能力、JSONL 匯出後的臨時分析">從 collector 資料做基礎 funnel 分析</a>。</p>
<h3 id="需要的事件-2">需要的事件</h3>
<table>
  <thead>
      <tr>
          <th>事件類型</th>
          <th>必要欄位</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>event</td>
          <td>name, session_id, ts</td>
          <td>漏斗步驟計數和排序</td>
      </tr>
      <tr>
          <td>lifecycle</td>
          <td>session.start, ts</td>
          <td>session 邊界定義</td>
      </tr>
  </tbody>
</table>
<h2 id="安全審計查詢">安全審計查詢</h2>
<p>安全審計查詢回答「有沒有非預期的存取」。重點是偵測異常模式而非單筆事件。</p>
<h3 id="查詢場景-3">查詢場景</h3>
<h4 id="有沒有異常登入">有沒有異常登入</h4>
<p>查詢模式：auth 失敗事件按 session 分群計數，短時間內大量失敗 = 暴力破解嘗試。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">session_id</span><span class="p">,</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">fail_count</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="k">MIN</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">first_attempt</span><span class="p">,</span><span class="w"> </span><span class="k">MAX</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">last_attempt</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;error&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;auth.login.failed&#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="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">datetime</span><span class="p">(</span><span class="s1">&#39;now&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;-1 hour&#39;</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="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">session_id</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">HAVING</span><span class="w"> </span><span class="n">fail_count</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">5</span><span class="p">;</span></span></span></code></pre></div><h4 id="誰存取了什麼敏感資料">誰存取了什麼敏感資料</h4>
<p>查詢模式：敏感操作的 audit trail — 按時間列出所有敏感操作事件。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">ts</span><span class="p">,</span><span class="w"> </span><span class="n">session_id</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="k">data</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;event&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;data.export&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;admin.user_lookup&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;config.secret_read&#39;</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="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="k">DESC</span><span class="p">;</span></span></span></code></pre></div><h3 id="需要的事件-3">需要的事件</h3>
<table>
  <thead>
      <tr>
          <th>事件類型</th>
          <th>必要欄位</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>error</td>
          <td>name=&lsquo;auth.*.failed&rsquo;, session_id</td>
          <td>偵測暴力破解</td>
      </tr>
      <tr>
          <td>event</td>
          <td>敏感操作的 name, session_id</td>
          <td>audit trail</td>
      </tr>
      <tr>
          <td>event</td>
          <td>data 中的操作目標（哪筆資料）</td>
          <td>存取範圍追溯</td>
      </tr>
  </tbody>
</table>
<p>安全事件的取樣率必須是 1.0（全收）— 取樣會讓攻擊嘗試在統計上隱形。見 <a href="/blog/monitoring/03-sdk-design/sensor-lifecycle-management/" data-link-title="感測器生命週期管理" data-link-desc="產品生命週期的五個階段各啟用什麼感測器 — feature flag 整合、取樣率動態調整、感測器開關的可觀察性">感測器生命週期管理</a> 的取樣率設計段。</p>
<h2 id="效能查詢">效能查詢</h2>
<p>效能查詢回答「系統有多快」和「哪裡變慢了」。</p>
<h3 id="查詢場景-4">查詢場景</h3>
<h4 id="p95-回應時間趨勢">P95 回應時間趨勢</h4>
<p>查詢模式：時間分桶 + percentile 聚合。需要 PostgreSQL 層。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- PostgreSQL
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">date_trunc</span><span class="p">(</span><span class="s1">&#39;hour&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">ts</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">hour</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="n">percentile_cont</span><span class="p">(</span><span class="mi">0</span><span class="p">.</span><span class="mi">95</span><span class="p">)</span><span class="w"> </span><span class="n">WITHIN</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="p">(</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="p">(</span><span class="k">data</span><span class="o">-&gt;&gt;</span><span class="s1">&#39;duration_ms&#39;</span><span class="p">)::</span><span class="nb">int</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">p95</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;metric&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;api.response.duration&#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="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">NOW</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;7 days&#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="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">hour</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">hour</span><span class="p">;</span></span></span></code></pre></div><p>SQLite 沒有內建 percentile 函數。SQLite 層的替代方案是排序後取第 95% 位置的值，但在大資料量時效能差。</p>
<h4 id="哪個版本變慢了">哪個版本變慢了</h4>
<p>查詢模式：按 source.version 分群比較效能。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- SQLite / PostgreSQL
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">source_version</span><span class="p">,</span><span class="w"> </span><span class="k">AVG</span><span class="p">((</span><span class="k">data</span><span class="o">-&gt;&gt;</span><span class="s1">&#39;duration_ms&#39;</span><span class="p">)::</span><span class="nb">int</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">avg_ms</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="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">sample_count</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;metric&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;api.response.duration&#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="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">datetime</span><span class="p">(</span><span class="s1">&#39;now&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;-7 days&#39;</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="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">source_version</span><span class="p">;</span></span></span></code></pre></div><h3 id="需要的事件-4">需要的事件</h3>
<table>
  <thead>
      <tr>
          <th>事件類型</th>
          <th>必要欄位</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>metric</td>
          <td>name, data.duration_ms, ts</td>
          <td>延遲趨勢</td>
      </tr>
      <tr>
          <td>metric</td>
          <td>source.version</td>
          <td>按版本比較</td>
      </tr>
      <tr>
          <td>metric</td>
          <td>data.memory_mb, data.cpu_percent</td>
          <td>資源使用趨勢</td>
      </tr>
  </tbody>
</table>
<h2 id="查詢--事件反推表">查詢 → 事件反推表</h2>
<p>設計事件時用這張表反向確認：每種查詢場景需要什麼事件、什麼欄位、什麼 storage 層級。</p>
<table>
  <thead>
      <tr>
          <th>查詢場景</th>
          <th>事件類型</th>
          <th>必要欄位</th>
          <th>Storage 層級</th>
          <th>保留需求</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Session 回放</td>
          <td>全部</td>
          <td>session_id, ts</td>
          <td>SQLite</td>
          <td>原始 7d</td>
      </tr>
      <tr>
          <td>Error 計數趨勢</td>
          <td>error</td>
          <td>name, ts</td>
          <td>SQLite</td>
          <td>小時聚合 90d</td>
      </tr>
      <tr>
          <td>功能使用率</td>
          <td>event</td>
          <td>name</td>
          <td>SQLite</td>
          <td>天聚合 365d</td>
      </tr>
      <tr>
          <td>Funnel 分析</td>
          <td>event</td>
          <td>name, session_id, ts</td>
          <td>PostgreSQL</td>
          <td>原始 30d</td>
      </tr>
      <tr>
          <td>暴力破解偵測</td>
          <td>error</td>
          <td>auth name, session_id</td>
          <td>SQLite</td>
          <td>原始 30d</td>
      </tr>
      <tr>
          <td>Audit trail</td>
          <td>event</td>
          <td>敏感操作 name, session_id</td>
          <td>SQLite</td>
          <td>原始 365d</td>
      </tr>
      <tr>
          <td>P95 趨勢</td>
          <td>metric</td>
          <td>duration_ms, ts</td>
          <td>PostgreSQL</td>
          <td>小時聚合 90d</td>
      </tr>
      <tr>
          <td>版本比較</td>
          <td>metric</td>
          <td>duration_ms, version</td>
          <td>SQLite</td>
          <td>天聚合 365d</td>
      </tr>
  </tbody>
</table>
<p>這張表和 <a href="/blog/monitoring/01-mental-model/event-enumeration-method/" data-link-title="事件枚舉與補齊檢查" data-link-desc="從操作盤點系統性地推導出完整的事件清單 — 四類補齊檢查確保沒有遺漏、粒度判準確保每個事件只記一個事實">事件枚舉與補齊檢查</a> 的事件表互補 — 事件枚舉從操作端正向推導「要收什麼」，本表從查詢端反向確認「收的夠不夠」。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>從操作端正向推導事件 → <a href="/blog/monitoring/01-mental-model/event-enumeration-method/" data-link-title="事件枚舉與補齊檢查" data-link-desc="從操作盤點系統性地推導出完整的事件清單 — 四類補齊檢查確保沒有遺漏、粒度判準確保每個事件只記一個事實">事件枚舉與補齊檢查</a></li>
<li>動機和事件的對應關係 → <a href="/blog/monitoring/01-mental-model/motivation-to-event-mapping/" data-link-title="動機驅動的事件設計" data-link-desc="Debug / 商業 / 資安 / 效能四個動機各自需要什麼事件 — 從「為什麼收」反推「收什麼」和「什麼階段啟用」">動機驅動的事件設計</a></li>
<li>SQLite vs PostgreSQL 的查詢能力分界 → <a href="/blog/monitoring/04-collector/feature-tier-boundary/" data-link-title="功能分層與 Backend 選擇" data-link-desc="SQLite 層和 PostgreSQL 層各自承載哪些功能 — 分界線是查詢模式而非資料量、觸發升級的是功能需求而非規模成長">功能分層與 Backend 選擇</a></li>
<li>Rule engine 的即時評估 → <a href="/blog/monitoring/04-collector/rule-engine/" data-link-title="Rule engine 設計" data-link-desc="條件 → 動作 → 模板的三段式規則結構 — 讓 collector 從被動儲存變成主動回應">Rule engine 設計</a></li>
</ul>
]]></content:encoded></item><item><title>CloudTrail</title><link>https://tarrragon.github.io/blog/infra/knowledge-cards/cloudtrail/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/knowledge-cards/cloudtrail/</guid><description>&lt;p>CloudTrail 的核心職責是把 AWS 帳號內每一個 API 呼叫記錄成可查詢的稽核日誌 — 哪個身分、在什麼時間、對哪個資源、呼叫了哪個 API、結果是成功還是拒絕。它是事故排查和合規稽核的事實來源。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>CloudTrail 在 infra 治理裡的角色是「發生了什麼」的最後防線。人工變更日誌記錄「為什麼改」，CloudTrail 記錄「改了什麼」— 兩者一起才能從事故回推到可回退的操作。&lt;/p>
&lt;p>CloudTrail 預設記錄 management event（建立、修改、刪除資源的 API 呼叫）並保留 90 天可查閱。要長期保存或記錄 data event（S3 物件存取、Lambda 呼叫等更細粒度的操作），需要建立 trail 並指定 S3 bucket 儲存。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>以下狀況指向 CloudTrail 的使用場景：&lt;/p>
&lt;ul>
&lt;li>事故排查需要回答「誰在過去 24 小時改過這個 security group」— CloudTrail 的 &lt;code>LookupEvents&lt;/code> API 可以按事件名稱、資源類型或使用者名稱查詢&lt;/li>
&lt;li>安全稽核要求提供「過去 90 天內所有 IAM policy 變更的紀錄」— CloudTrail 是標準的證據來源&lt;/li>
&lt;li>發現不預期的資源變更（drift），需要確認是人為操作還是自動化觸發 — CloudTrail 的 &lt;code>userIdentity&lt;/code> 欄位區分人類使用者和 assume-role 的服務&lt;/li>
&lt;/ul>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>使用 CloudTrail 時要決定：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>保留期限&lt;/strong>：預設 90 天免費查閱；超過需要建 trail 存到 S3，費用是 S3 儲存成本&lt;/li>
&lt;li>&lt;strong>事件範圍&lt;/strong>：management event 預設開啟；data event（S3 物件讀寫、Lambda invoke）要額外設定，且量大時儲存成本可觀&lt;/li>
&lt;li>&lt;strong>跨帳號整合&lt;/strong>：多帳號架構下，Organization trail 可以把所有帳號的事件集中到一個 S3 bucket&lt;/li>
&lt;li>&lt;strong>存取控制&lt;/strong>：CloudTrail 的 S3 bucket 本身要限制存取 — 能修改稽核日誌等於能掩蓋操作痕跡&lt;/li>
&lt;/ul>
&lt;h2 id="鄰卡">鄰卡&lt;/h2>
&lt;ul>
&lt;li>&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> — CloudTrail 記錄的是 IAM 身分的 API 呼叫&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/drift/" data-link-title="Drift（設定漂移）" data-link-desc="IaC 的 state 與雲端實際狀態之間的不一致，通常因為有人繞過 IaC 直接在 Console 改設定">Drift&lt;/a> — CloudTrail 是追查 drift 來源（誰手動改了什麼）的工具&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>CloudTrail 的核心職責是把 AWS 帳號內每一個 API 呼叫記錄成可查詢的稽核日誌 — 哪個身分、在什麼時間、對哪個資源、呼叫了哪個 API、結果是成功還是拒絕。它是事故排查和合規稽核的事實來源。</p>
<h2 id="概念位置">概念位置</h2>
<p>CloudTrail 在 infra 治理裡的角色是「發生了什麼」的最後防線。人工變更日誌記錄「為什麼改」，CloudTrail 記錄「改了什麼」— 兩者一起才能從事故回推到可回退的操作。</p>
<p>CloudTrail 預設記錄 management event（建立、修改、刪除資源的 API 呼叫）並保留 90 天可查閱。要長期保存或記錄 data event（S3 物件存取、Lambda 呼叫等更細粒度的操作），需要建立 trail 並指定 S3 bucket 儲存。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>以下狀況指向 CloudTrail 的使用場景：</p>
<ul>
<li>事故排查需要回答「誰在過去 24 小時改過這個 security group」— CloudTrail 的 <code>LookupEvents</code> API 可以按事件名稱、資源類型或使用者名稱查詢</li>
<li>安全稽核要求提供「過去 90 天內所有 IAM policy 變更的紀錄」— CloudTrail 是標準的證據來源</li>
<li>發現不預期的資源變更（drift），需要確認是人為操作還是自動化觸發 — CloudTrail 的 <code>userIdentity</code> 欄位區分人類使用者和 assume-role 的服務</li>
</ul>
<h2 id="設計責任">設計責任</h2>
<p>使用 CloudTrail 時要決定：</p>
<ul>
<li><strong>保留期限</strong>：預設 90 天免費查閱；超過需要建 trail 存到 S3，費用是 S3 儲存成本</li>
<li><strong>事件範圍</strong>：management event 預設開啟；data event（S3 物件讀寫、Lambda invoke）要額外設定，且量大時儲存成本可觀</li>
<li><strong>跨帳號整合</strong>：多帳號架構下，Organization trail 可以把所有帳號的事件集中到一個 S3 bucket</li>
<li><strong>存取控制</strong>：CloudTrail 的 S3 bucket 本身要限制存取 — 能修改稽核日誌等於能掩蓋操作痕跡</li>
</ul>
<h2 id="鄰卡">鄰卡</h2>
<ul>
<li><a href="/blog/infra/knowledge-cards/iam/" data-link-title="IAM（Identity and Access Management）" data-link-desc="雲端平台的授權系統，回答「某個身分能不能對某個資源做某件事」">IAM</a> — CloudTrail 記錄的是 IAM 身分的 API 呼叫</li>
<li><a href="/blog/infra/knowledge-cards/drift/" data-link-title="Drift（設定漂移）" data-link-desc="IaC 的 state 與雲端實際狀態之間的不一致，通常因為有人繞過 IaC 直接在 Console 改設定">Drift</a> — CloudTrail 是追查 drift 來源（誰手動改了什麼）的工具</li>
</ul>
]]></content:encoded></item><item><title>Legacy PHP 的安全盤點</title><link>https://tarrragon.github.io/blog/infra/takeover/legacy-php-security-audit/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/takeover/legacy-php-security-audit/</guid><description>&lt;p>接手的 legacy PHP 專案在做完&lt;a href="https://tarrragon.github.io/blog/infra/takeover/legacy-ftp-no-ssh/" data-link-title="無 SSH 的 FTP / 面板管理環境接管" data-link-desc="接手一個只有 FTP 和 phpMyAdmin（或 cPanel / Plesk）存取的 PHP 專案：沒有 SSH、沒有 CLI 時，怎麼盤點現況、建立本地開發環境、制定部署與資料庫變更紀律，以及找到升級路徑的切入點">程式碼與資料庫的現況快照&lt;/a>之後，下一步是安全盤點。安全狀態在盤點之前是未知的——前一位維護者可能所有表單都用 prepared statement，也可能每個查詢都直接拼接使用者輸入。盤點的範圍涵蓋 credential 散落、PHP 版本風險、程式碼層的漏洞模式、伺服器端的 .htaccess 與權限設定、以及外部依賴的已知漏洞。&lt;/p>
&lt;h2 id="credential-掃描與處理">Credential 掃描與處理&lt;/h2>
&lt;p>寫死在程式碼裡的 credential 是接手後最先要掌握的風險面。資料庫密碼、API key、SMTP 帳號這些值如果散落在多個 PHP 檔案裡，每一個都是外洩路徑。&lt;/p>
&lt;h3 id="掃描方式">掃描方式&lt;/h3>
&lt;p>用 grep 對整個 codebase 搜尋常見的 credential 關鍵字：&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">grep -rn &lt;span class="s2">&amp;#34;password\|passwd\|secret\|api_key\|app_key\|mysql_connect\|mysqli_connect\|PDO(&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --include&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;*.php&amp;#34;&lt;/span> .&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>常見的集中位置是 &lt;code>config.php&lt;/code>、&lt;code>wp-config.php&lt;/code>、&lt;code>database.php&lt;/code>、&lt;code>settings.php&lt;/code>，以及專案根目錄的 &lt;code>.env&lt;/code>。但 legacy 專案的 credential 經常散落在意想不到的地方——寫在某個 helper function 的預設參數裡、硬編碼在 cron job 的 PHP 檔案裡、或藏在某個很久沒改的 email 發送模組裡。grep 的涵蓋範圍應該是整個專案目錄，不只是已知的 config 檔案。&lt;/p>
&lt;p>如果專案已經在本地 Git repo（見&lt;a href="https://tarrragon.github.io/blog/infra/takeover/legacy-ftp-no-ssh/" data-link-title="無 SSH 的 FTP / 面板管理環境接管" data-link-desc="接手一個只有 FTP 和 phpMyAdmin（或 cPanel / Plesk）存取的 PHP 專案：沒有 SSH、沒有 CLI 時，怎麼盤點現況、建立本地開發環境、制定部署與資料庫變更紀律，以及找到升級路徑的切入點">主文&lt;/a>的快照步驟），檢查 Git 歷史裡有沒有曾經存在但後來被刪除的 credential：&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">git log --all -p -- &lt;span class="s1">&amp;#39;*.php&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> grep -i &lt;span class="s2">&amp;#34;password\|secret\|api_key&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> head -30&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>歷史裡的 credential 無法從 Git 裡真正移除（rewrite history 可以但成本高），所以找到的 credential 都要列入輪替清單。&lt;/p>
&lt;h3 id="處理方式">處理方式&lt;/h3>
&lt;p>掃描結果彙整成一張清單，每筆記錄：credential 類型、所在檔案、用途、是否可輪替。處理優先序：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &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;code>.env&lt;/code> 或 &lt;code>config.local.php&lt;/code>（gitignore）&lt;/td>
 &lt;td>立刻&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第三方 API key（金流、簡訊）&lt;/td>
 &lt;td>移到 config + 確認可輪替&lt;/td>
 &lt;td>立刻&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SMTP 密碼&lt;/td>
 &lt;td>移到 config&lt;/td>
 &lt;td>第二順位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>內部服務 token&lt;/td>
 &lt;td>移到 config + 確認對方端有沒有輪替機制&lt;/td>
 &lt;td>第二順位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>已停用的 credential&lt;/td>
 &lt;td>確認停用後從 code 移除&lt;/td>
 &lt;td>第三順位&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把 credential 從 code 移到 &lt;code>.env&lt;/code> 後，用 &lt;code>getenv('DB_PASSWORD')&lt;/code> 或框架的 config 機制讀取。&lt;code>.env&lt;/code> 加進 &lt;code>.gitignore&lt;/code>，prod 的 &lt;code>.env&lt;/code> 透過 FTP 單獨上傳、不進版本控制。&lt;/p>
&lt;h2 id="php-版本與已知漏洞">PHP 版本與已知漏洞&lt;/h2>
&lt;p>PHP 版本決定了這個專案暴露在什麼層級的平台風險下。已結束安全支援（EOL）的 PHP 版本不代表「馬上會被攻擊」，但代表任何未來被發現的漏洞都不會得到官方修補。&lt;/p>
&lt;h3 id="版本確認">版本確認&lt;/h3>
&lt;p>在站台放一個 &lt;code>phpinfo.php&lt;/code>，瀏覽後記錄版本號，完成後立刻刪除（&lt;code>phpinfo()&lt;/code> 輸出含伺服器路徑與配置細節，留在 prod 上是資訊外洩）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="o">&amp;lt;?&lt;/span>&lt;span class="nx">php&lt;/span> &lt;span class="nx">phpinfo&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="cp">?&amp;gt;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>或在 cPanel / Plesk 的 PHP 設定頁面直接查看。&lt;/p>
&lt;h3 id="版本風險對照">版本風險對照&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>版本&lt;/th>
 &lt;th>安全支援狀態（2026）&lt;/th>
 &lt;th>風險等級&lt;/th>
 &lt;th>行動&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>5.6 以下&lt;/td>
 &lt;td>已 EOL 超過 8 年&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>列入升級計畫、優先處理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>7.0 - 7.4&lt;/td>
 &lt;td>已 EOL&lt;/td>
 &lt;td>中高&lt;/td>
 &lt;td>排進季度 roadmap&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>8.0&lt;/td>
 &lt;td>已 EOL（2023-11）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>排進半年 roadmap&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>8.1&lt;/td>
 &lt;td>安全修補中（至 2025-12）&lt;/td>
 &lt;td>已接近 EOL&lt;/td>
 &lt;td>規劃升級到 8.2+&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>8.2+&lt;/td>
 &lt;td>活躍支援中&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>維持更新&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>版本升級是獨立的工程專案——可能會觸發函式棄用警告、行為變更、甚至語法不相容。盤點階段的任務是記錄版本和風險等級，升級規劃放在穩定維運之後。&lt;/p></description><content:encoded><![CDATA[<p>接手的 legacy PHP 專案在做完<a href="/blog/infra/takeover/legacy-ftp-no-ssh/" data-link-title="無 SSH 的 FTP / 面板管理環境接管" data-link-desc="接手一個只有 FTP 和 phpMyAdmin（或 cPanel / Plesk）存取的 PHP 專案：沒有 SSH、沒有 CLI 時，怎麼盤點現況、建立本地開發環境、制定部署與資料庫變更紀律，以及找到升級路徑的切入點">程式碼與資料庫的現況快照</a>之後，下一步是安全盤點。安全狀態在盤點之前是未知的——前一位維護者可能所有表單都用 prepared statement，也可能每個查詢都直接拼接使用者輸入。盤點的範圍涵蓋 credential 散落、PHP 版本風險、程式碼層的漏洞模式、伺服器端的 .htaccess 與權限設定、以及外部依賴的已知漏洞。</p>
<h2 id="credential-掃描與處理">Credential 掃描與處理</h2>
<p>寫死在程式碼裡的 credential 是接手後最先要掌握的風險面。資料庫密碼、API key、SMTP 帳號這些值如果散落在多個 PHP 檔案裡，每一個都是外洩路徑。</p>
<h3 id="掃描方式">掃描方式</h3>
<p>用 grep 對整個 codebase 搜尋常見的 credential 關鍵字：</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">grep -rn <span class="s2">&#34;password\|passwd\|secret\|api_key\|app_key\|mysql_connect\|mysqli_connect\|PDO(&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --include<span class="o">=</span><span class="s2">&#34;*.php&#34;</span> .</span></span></code></pre></div><p>常見的集中位置是 <code>config.php</code>、<code>wp-config.php</code>、<code>database.php</code>、<code>settings.php</code>，以及專案根目錄的 <code>.env</code>。但 legacy 專案的 credential 經常散落在意想不到的地方——寫在某個 helper function 的預設參數裡、硬編碼在 cron job 的 PHP 檔案裡、或藏在某個很久沒改的 email 發送模組裡。grep 的涵蓋範圍應該是整個專案目錄，不只是已知的 config 檔案。</p>
<p>如果專案已經在本地 Git repo（見<a href="/blog/infra/takeover/legacy-ftp-no-ssh/" data-link-title="無 SSH 的 FTP / 面板管理環境接管" data-link-desc="接手一個只有 FTP 和 phpMyAdmin（或 cPanel / Plesk）存取的 PHP 專案：沒有 SSH、沒有 CLI 時，怎麼盤點現況、建立本地開發環境、制定部署與資料庫變更紀律，以及找到升級路徑的切入點">主文</a>的快照步驟），檢查 Git 歷史裡有沒有曾經存在但後來被刪除的 credential：</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 log --all -p -- <span class="s1">&#39;*.php&#39;</span> <span class="p">|</span> grep -i <span class="s2">&#34;password\|secret\|api_key&#34;</span> <span class="p">|</span> head -30</span></span></code></pre></div><p>歷史裡的 credential 無法從 Git 裡真正移除（rewrite history 可以但成本高），所以找到的 credential 都要列入輪替清單。</p>
<h3 id="處理方式">處理方式</h3>
<p>掃描結果彙整成一張清單，每筆記錄：credential 類型、所在檔案、用途、是否可輪替。處理優先序：</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>處理方式</th>
          <th>優先級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料庫密碼</td>
          <td>移到 <code>.env</code> 或 <code>config.local.php</code>（gitignore）</td>
          <td>立刻</td>
      </tr>
      <tr>
          <td>第三方 API key（金流、簡訊）</td>
          <td>移到 config + 確認可輪替</td>
          <td>立刻</td>
      </tr>
      <tr>
          <td>SMTP 密碼</td>
          <td>移到 config</td>
          <td>第二順位</td>
      </tr>
      <tr>
          <td>內部服務 token</td>
          <td>移到 config + 確認對方端有沒有輪替機制</td>
          <td>第二順位</td>
      </tr>
      <tr>
          <td>已停用的 credential</td>
          <td>確認停用後從 code 移除</td>
          <td>第三順位</td>
      </tr>
  </tbody>
</table>
<p>把 credential 從 code 移到 <code>.env</code> 後，用 <code>getenv('DB_PASSWORD')</code> 或框架的 config 機制讀取。<code>.env</code> 加進 <code>.gitignore</code>，prod 的 <code>.env</code> 透過 FTP 單獨上傳、不進版本控制。</p>
<h2 id="php-版本與已知漏洞">PHP 版本與已知漏洞</h2>
<p>PHP 版本決定了這個專案暴露在什麼層級的平台風險下。已結束安全支援（EOL）的 PHP 版本不代表「馬上會被攻擊」，但代表任何未來被發現的漏洞都不會得到官方修補。</p>
<h3 id="版本確認">版本確認</h3>
<p>在站台放一個 <code>phpinfo.php</code>，瀏覽後記錄版本號，完成後立刻刪除（<code>phpinfo()</code> 輸出含伺服器路徑與配置細節，留在 prod 上是資訊外洩）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="ln">1</span><span class="cl"><span class="o">&lt;?</span><span class="nx">php</span> <span class="nx">phpinfo</span><span class="p">();</span> <span class="cp">?&gt;</span><span class="err">
</span></span></span></code></pre></div><p>或在 cPanel / Plesk 的 PHP 設定頁面直接查看。</p>
<h3 id="版本風險對照">版本風險對照</h3>
<table>
  <thead>
      <tr>
          <th>版本</th>
          <th>安全支援狀態（2026）</th>
          <th>風險等級</th>
          <th>行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>5.6 以下</td>
          <td>已 EOL 超過 8 年</td>
          <td>高</td>
          <td>列入升級計畫、優先處理</td>
      </tr>
      <tr>
          <td>7.0 - 7.4</td>
          <td>已 EOL</td>
          <td>中高</td>
          <td>排進季度 roadmap</td>
      </tr>
      <tr>
          <td>8.0</td>
          <td>已 EOL（2023-11）</td>
          <td>中</td>
          <td>排進半年 roadmap</td>
      </tr>
      <tr>
          <td>8.1</td>
          <td>安全修補中（至 2025-12）</td>
          <td>已接近 EOL</td>
          <td>規劃升級到 8.2+</td>
      </tr>
      <tr>
          <td>8.2+</td>
          <td>活躍支援中</td>
          <td>低</td>
          <td>維持更新</td>
      </tr>
  </tbody>
</table>
<p>版本升級是獨立的工程專案——可能會觸發函式棄用警告、行為變更、甚至語法不相容。盤點階段的任務是記錄版本和風險等級，升級規劃放在穩定維運之後。</p>
<h2 id="常見的-php-安全漏洞模式">常見的 PHP 安全漏洞模式</h2>
<p>Legacy PHP 專案最常見的四類漏洞都可以用 grep 做初步掃描。掃描結果是候選清單、不是確認的漏洞——每個命中都需要讀上下文確認是否有防護。</p>
<h3 id="sql-injection">SQL injection</h3>
<p>任何把使用者輸入直接拼接到 SQL 查詢裡的寫法都是 SQL injection 的候選：</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"># 找使用 mysql_query / mysqli_query 但沒有 prepare/bind 的查詢</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep -rn <span class="s2">&#34;mysql_query\|mysqli_query&#34;</span> --include<span class="o">=</span><span class="s2">&#34;*.php&#34;</span> . <span class="p">|</span> grep -v <span class="s2">&#34;prepare\|bind_param&#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="c1"># 找字串拼接的 SQL 查詢</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">grep -rn <span class="s2">&#34;query.*\\\$_GET\|query.*\\\$_POST\|query.*\\\$_REQUEST&#34;</span> --include<span class="o">=</span><span class="s2">&#34;*.php&#34;</span> .</span></span></code></pre></div><p>修法是改用 prepared statement（PDO 或 mysqli 的 <code>prepare</code> + <code>bind_param</code>）。如果 codebase 大量使用 <code>mysql_*</code> 函式（PHP 7.0 已移除），這本身就是版本升級的阻礙——需要同時處理。</p>
<h3 id="xss跨站腳本">XSS（跨站腳本）</h3>
<p>把使用者輸入直接輸出到 HTML 而沒有跳脫：</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"># 找直接 echo/print 使用者輸入的地方</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep -rn <span class="s2">&#34;echo.*\\\$_GET\|echo.*\\\$_POST\|echo.*\\\$_REQUEST\|echo.*\\\$_COOKIE&#34;</span> --include<span class="o">=</span><span class="s2">&#34;*.php&#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="c1"># 找 PHP 短標籤輸出</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">grep -rn <span class="s2">&#34;&lt;?=.*\\\$_&#34;</span> --include<span class="o">=</span><span class="s2">&#34;*.php&#34;</span> .</span></span></code></pre></div><p>修法是所有輸出都經過 <code>htmlspecialchars($var, ENT_QUOTES, 'UTF-8')</code>。模板引擎（如 Twig、Blade）預設會做跳脫，使用模板引擎的專案 XSS 風險較低。</p>
<h3 id="檔案包含file-inclusion">檔案包含（File Inclusion）</h3>
<p>把使用者輸入當作 <code>include</code> 或 <code>require</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">grep -rn <span class="s2">&#34;include.*\\\$_\|require.*\\\$_\|include_once.*\\\$_\|require_once.*\\\$_&#34;</span> --include<span class="o">=</span><span class="s2">&#34;*.php&#34;</span> .</span></span></code></pre></div><p>這類寫法讓攻擊者可以指定載入任意檔案（本地或遠端）。修法是用白名單限制可載入的檔案路徑。</p>
<h3 id="檔案上傳">檔案上傳</h3>
<p>檢查上傳處理的三個面向：副檔名驗證（只允許白名單）、上傳目錄是否可執行 PHP（不應該）、檔案大小限制。</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">grep -rn <span class="s2">&#34;move_uploaded_file\|\\\$_FILES&#34;</span> --include<span class="o">=</span><span class="s2">&#34;*.php&#34;</span> .</span></span></code></pre></div><p>每個命中的上傳處理都要確認：有沒有驗證副檔名（黑名單不夠、要白名單）、上傳目錄有沒有 <code>.htaccess</code> 禁止 PHP 執行（見下節）、有沒有重新命名上傳的檔案（避免覆寫攻擊）。</p>
<h3 id="session-管理">Session 管理</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"># 找 session 相關設定</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep -rn <span class="s2">&#34;session_start\|session_regenerate_id\|session\.cookie_httponly\|session\.cookie_secure&#34;</span> --include<span class="o">=</span><span class="s2">&#34;*.php&#34;</span> .</span></span></code></pre></div><p>確認：登入成功後有沒有呼叫 <code>session_regenerate_id(true)</code> 防止 session fixation、<code>session.cookie_httponly</code> 是否為 on（防止 JavaScript 讀取 session cookie）、<code>session.cookie_secure</code> 在 HTTPS 站台是否為 on。</p>
<h2 id="htaccess-安全設定">.htaccess 安全設定</h2>
<p>無 SSH 的 Apache 環境中 <code>.htaccess</code> 是可用的伺服器端安全防線。盤點時確認這些設定是否存在，缺少的補上。</p>
<h3 id="基礎安全設定">基礎安全設定</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-apache" data-lang="apache"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># 禁止目錄列表 — 防止瀏覽上傳目錄的檔案清單</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nb">Options</span> -Indexes
</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="c"># 阻擋敏感檔案的 HTTP 存取</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nt">&lt;FilesMatch</span> <span class="s">&#34;\.(env|local|bak|sql|log|ini|conf|yml|json|lock|md)$&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nb">Require</span> <span class="k">all</span> denied
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">&lt;/FilesMatch&gt;</span>
</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="c"># 阻擋隱藏檔案與目錄（.git、.env 等）</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nt">&lt;IfModule</span> <span class="s">mod_rewrite.c</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nb">RewriteEngine</span> <span class="k">On</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nb">RewriteRule</span> (^\.|/\.) - [F]
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nt">&lt;/IfModule&gt;</span>
</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"><span class="c"># 強制 HTTPS</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nt">&lt;IfModule</span> <span class="s">mod_rewrite.c</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="nb">RewriteCond</span> %{HTTPS} <span class="k">off</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nb">RewriteRule</span> ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="nt">&lt;/IfModule&gt;</span></span></span></code></pre></div><h3 id="上傳目錄的-php-執行禁令">上傳目錄的 PHP 執行禁令</h3>
<p>在上傳目錄（如 <code>uploads/</code>、<code>wp-content/uploads/</code>）放一個獨立的 <code>.htaccess</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-apache" data-lang="apache"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># 禁止此目錄下的 PHP 執行</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">php_flag</span> engine <span class="k">off</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="c"># 只允許靜態檔案類型</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nt">&lt;FilesMatch</span> <span class="s">&#34;\.(?!jpg|jpeg|png|gif|pdf|webp|svg|css|js)&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nb">Require</span> <span class="k">all</span> denied
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nt">&lt;/FilesMatch&gt;</span></span></span></code></pre></div><p>這條設定讓即使攻擊者成功上傳了 <code>.php</code> 檔案，也無法透過 HTTP 請求觸發執行。</p>
<h3 id="安全-header">安全 header</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-apache" data-lang="apache"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># 防止 MIME type sniffing</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nb">Header</span> set X-Content-Type-Options <span class="s2">&#34;nosniff&#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="c"># 防止 clickjacking</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nb">Header</span> set X-Frame-Options <span class="s2">&#34;SAMEORIGIN&#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 class="c"># XSS 防護（現代瀏覽器多已內建、但舊站加上無害）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nb">Header</span> set X-XSS-Protection <span class="s2">&#34;1; mode=block&#34;</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="c"># Referrer 資訊控制</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">Header</span> set Referrer-Policy <span class="s2">&#34;strict-origin-when-cross-origin&#34;</span></span></span></code></pre></div><h2 id="檔案權限">檔案權限</h2>
<p>無 SSH 環境的權限控制能力有限——多數情況下透過 FTP client 檢查和調整。</p>
<table>
  <thead>
      <tr>
          <th>對象</th>
          <th>建議權限</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>目錄</td>
          <td>755</td>
          <td>owner 可讀寫執行、group/other 可讀可執行（Apache 需要執行權才能進入目錄）</td>
      </tr>
      <tr>
          <td>PHP 檔案</td>
          <td>644</td>
          <td>owner 可讀寫、group/other 只讀</td>
      </tr>
      <tr>
          <td>Config 檔案（含 credential）</td>
          <td>640</td>
          <td>group 可讀（Apache 通常跟 owner 同 group）、other 不可讀</td>
      </tr>
      <tr>
          <td>上傳目錄</td>
          <td>755</td>
          <td>跟一般目錄相同，搭配 .htaccess 禁止 PHP 執行</td>
      </tr>
  </tbody>
</table>
<p>777 權限（所有人可讀寫執行）在多租戶主機上等於同一台伺服器的其他租戶也能讀寫這些檔案。如果發現任何目錄或檔案是 777，立刻改回 755/644。FileZilla 在檔案上按右鍵 → 「File permissions」可以查看和修改。</p>
<h2 id="外部依賴的安全性">外部依賴的安全性</h2>
<h3 id="composer-管理的依賴">Composer 管理的依賴</h3>
<p>如果專案使用 Composer，在本地跑一次已知漏洞檢查：</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">composer audit</span></span></code></pre></div><p>這條指令比對 <code>composer.lock</code> 裡的每個套件版本與 Packagist 的安全公告資料庫，列出有已知 CVE 的套件。</p>
<h3 id="手動管理的依賴">手動管理的依賴</h3>
<p>沒有 Composer 的 legacy 專案可能直接把第三方程式碼複製進專案目錄。常見的高風險依賴：</p>
<table>
  <thead>
      <tr>
          <th>依賴</th>
          <th>常見位置</th>
          <th>檢查方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PHPMailer</td>
          <td><code>class.phpmailer.php</code>、<code>PHPMailer/</code></td>
          <td>比對版本號與 GitHub releases 的安全公告</td>
      </tr>
      <tr>
          <td>jQuery</td>
          <td><code>js/jquery.min.js</code></td>
          <td>打開檔案看版本號、低於 3.5.0 有 XSS 漏洞</td>
      </tr>
      <tr>
          <td>CKEditor / TinyMCE</td>
          <td><code>editor/</code>、<code>tinymce/</code></td>
          <td>舊版有 XSS 漏洞、比對 CVE</td>
      </tr>
      <tr>
          <td>WordPress plugins</td>
          <td><code>wp-content/plugins/</code></td>
          <td>用 WPScan 掃描</td>
      </tr>
  </tbody>
</table>
<h3 id="javascript-cdn-引用">JavaScript CDN 引用</h3>
<p>檢查 HTML 裡引用的外部 JavaScript CDN 連結，確認：使用 <code>integrity</code> 屬性（Subresource Integrity）防止 CDN 被竄改、引用的 CDN 是否仍在維護。</p>
<h2 id="掃描工具">掃描工具</h2>
<p>除了手動 grep，可以用工具做自動化掃描。這些工具都從本地或外部執行，不需要在 prod 伺服器上安裝任何東西。</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>類型</th>
          <th>用途</th>
          <th>費用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PHP_CodeSniffer + Security Standard</td>
          <td>靜態分析</td>
          <td>掃描 PHP 程式碼的安全反模式</td>
          <td>免費</td>
      </tr>
      <tr>
          <td>PHPStan / Psalm</td>
          <td>靜態分析</td>
          <td>型別檢查間接發現不安全的資料流</td>
          <td>免費</td>
      </tr>
      <tr>
          <td>WPScan</td>
          <td>WordPress 專用</td>
          <td>掃描 WordPress 核心、plugin、theme 漏洞</td>
          <td>免費（API key 有額度限制）</td>
      </tr>
      <tr>
          <td>Nikto</td>
          <td>Web server 掃描</td>
          <td>從外部掃描 HTTP server 的已知弱點</td>
          <td>免費</td>
      </tr>
      <tr>
          <td>Mozilla Observatory</td>
          <td>線上掃描</td>
          <td>檢查 HTTP security header 設定</td>
          <td>免費</td>
      </tr>
      <tr>
          <td>Snyk</td>
          <td>依賴掃描</td>
          <td>類似 <code>composer audit</code> 但涵蓋更廣</td>
          <td>免費方案可用</td>
      </tr>
  </tbody>
</table>
<p>WordPress 站台的掃描指令：</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"># WPScan 掃描（從本地執行、掃描遠端站台）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">wpscan --url https://example.com --enumerate vp,vt,u
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># vp = vulnerable plugins, vt = vulnerable themes, u = users</span></span></span></code></pre></div><p>所有掃描結果存進 repo 的 <code>security-audit/</code> 目錄，標上日期。這份報告是後續修補計畫的輸入，也是向管理層說明安全狀態的依據。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/takeover/legacy-ftp-no-ssh/" data-link-title="無 SSH 的 FTP / 面板管理環境接管" data-link-desc="接手一個只有 FTP 和 phpMyAdmin（或 cPanel / Plesk）存取的 PHP 專案：沒有 SSH、沒有 CLI 時，怎麼盤點現況、建立本地開發環境、制定部署與資料庫變更紀律，以及找到升級路徑的切入點">無 SSH 的 FTP / 面板管理環境接管</a>：本文的前置步驟（程式碼與資料庫快照）</li>
<li>→ <a href="/blog/infra/takeover/legacy-database-backup-migration/" data-link-title="無 SSH 環境的資料庫備份與變更管理" data-link-desc="在只有 phpMyAdmin 或有限遠端連線的無 SSH 環境裡，怎麼建立可靠的資料庫備份策略、schema 變更紀律與還原演練流程">資料庫備份與變更管理</a>：SQL injection 修復前先備份，避免修補過程造成資料遺失</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/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：credential 管理的系統性設計</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/report/security-teaching-rigor-asymmetry/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/security-teaching-rigor-asymmetry/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資安教學內容的 audit 標準不該由「讀者讀不讀得懂」決定、該由「讀者照做後系統會不會出破口」決定。&lt;/strong> 讀懂是學習端的成本、破口是生產端的代價、兩者級數不同。&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>一般工程教學（layout / refactor / debug）&lt;/td>
 &lt;td>讀者學不會、要重學&lt;/td>
 &lt;td>學習端&lt;/td>
 &lt;td>可逆（再學一次）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資安教學（auth / crypto / 防護 / 標準引用）&lt;/td>
 &lt;td>讀者&lt;strong>以為&lt;/strong>學會、實作時留破口&lt;/td>
 &lt;td>生產端&lt;/td>
 &lt;td>&lt;strong>不可逆&lt;/strong>（破口被利用）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>級數不對稱的後果：一般教學的 audit bar 是「讀者能不能拿到 reasoning」、資安教學的 audit bar 必須升級為「讀者照做後的實作可不可被驗證為無破口」。預設讀者會 implement、不只 read。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>資安章節（&lt;code>backend/07-security-data-protection/&lt;/code>）的內容形態不是純概念說明、是「問題節點 + 判讀訊號 + 風險後果 + 前置控制面 + 交接路由」。讀者拿到的不只是知識、是&lt;strong>會拿去做防護設計的依據&lt;/strong>。&lt;/p>
&lt;p>寫作便利的選擇（在一般教學沒問題、在資安教學會出事）：&lt;/p>
&lt;ul>
&lt;li>用「能擋」「能防」「可以避免」這類動詞、沒寫適用 threat model&lt;/li>
&lt;li>給防護方法、沒寫「這方法擋不到什麼」&lt;/li>
&lt;li>引用 OWASP / RFC / NIST、沒寫版本 / 沒驗證引用句意&lt;/li>
&lt;li>描述判讀訊號、沒給訊號失效的 deployment 條件&lt;/li>
&lt;li>把 mitigation 寫得通用、沒拆 context-dependence（同 mitigation 在 SaaS / on-prem / 多租戶條件失效不同）&lt;/li>
&lt;/ul>
&lt;p>這些選擇在一般教學是「簡潔風格」、在資安教學是 &lt;strong>silent 破口&lt;/strong>——讀者照字面理解去實作、產生 false sense of security（見 &lt;a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security 是資安寫作的主要失敗模式&lt;/a>）。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>把資安內容的審查標準從 &lt;strong>readability-first&lt;/strong> 升級到 &lt;strong>verifiability-first&lt;/strong>：每個論述要回答「讀者照做後、實作的正確性能不能被反向驗證」。&lt;/p>
&lt;h3 id="三條-audit-bar">三條 audit bar&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Threat model 對稱性&lt;/strong>：講「防 X」必須寫「不防 Y」、形成對稱論述（見 &lt;a href="../threat-model-explicitness/">#101 threat-model-explicitness&lt;/a>）&lt;/li>
&lt;li>&lt;strong>Mitigation 對位驗證&lt;/strong>：防護措施跟 threat 的對應鏈要可驗證、不能只是「業界常用」（見 &lt;a href="../mitigation-threat-alignment/">#102 mitigation-threat-alignment&lt;/a>）&lt;/li>
&lt;li>&lt;strong>Context-dependence 顯式化&lt;/strong>：mitigation 在不同 deployment 的有效性差異要寫出來、不假設讀者知道（見 &lt;a href="../mitigation-context-dependence/">#103 mitigation-context-dependence&lt;/a>）&lt;/li>
&lt;/ol>
&lt;h3 id="寫作流程的差異">寫作流程的差異&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &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>寫得通、有 reasoning&lt;/td>
 &lt;td>+ 列 threat model 範圍 + 列「不在範圍內的 threat」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Review&lt;/td>
 &lt;td>多 pass review（&lt;a href="../writing-multi-pass-review/">#83&lt;/a>）&lt;/td>
 &lt;td>+ audit pass（reviewer 視角找 false sense of security）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>引用&lt;/td>
 &lt;td>引用即可&lt;/td>
 &lt;td>+ 標版本 + 驗證引用句意沒被扭曲 + 確認當前版本仍是 best practice&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;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="false-confidence-在生產系統累積">False confidence 在生產系統累積&lt;/h3>
&lt;p>讀者讀完含糊論述、心理上覺得「學到防護方法了」、實作時用最直覺的詮釋。當實作有 gap 時、讀者&lt;strong>不會警覺&lt;/strong>——因為「我學過了 / 我做了」。等同 &lt;a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉&lt;/a> 在資安領域的具體展現：教學含糊 = hook 規則太粗、看似有保護、實際抓不到行為層的破口。&lt;/p>
&lt;p>最危險的是：含糊的資安教學比沒讀過更糟。沒讀過的人會去查標準、會問人；讀過含糊版的人會跳過驗證、直接 implement。&lt;/p>
&lt;h3 id="破口的可利用窗口跟教學擴散同步放大">破口的可利用窗口跟教學擴散同步放大&lt;/h3>
&lt;p>含糊的資安內容若被多個團隊 / 文章引用、所有下游 implementer 都繼承同一個 silent gap。攻擊者只要找到原始教學的 misinterpretation pattern、就可以批量利用所有 implementation。一般教學的錯誤是 &lt;strong>個別讀者的學習成本&lt;/strong>、資安教學的錯誤是 &lt;strong>系統性風險面擴大&lt;/strong>。&lt;/p>
&lt;h3 id="後續修補無法-trace-到原文">後續修補無法 trace 到原文&lt;/h3>
&lt;p>當下游事故發生、回溯到「讀者照某教學實作」時、含糊的原文難以判定是「教學錯」還是「讀者誤解」——因為含糊本身就是 ambiguity 來源。理想的資安教學應該讓「實作 vs 教學」可以被 1:1 對照、出問題時找得到 root cause。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&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;a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉&lt;/a>&lt;/td>
 &lt;td>&lt;strong>本卡是 #82 在資安寫作的領域具體化&lt;/strong> — false confidence 透過含糊教學在實作端展現、是 #82 ceiling pattern 的高風險版本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../visual-tool-error-layer-alignment/">#92 視覺手段對齊錯誤層次&lt;/a>&lt;/td>
 &lt;td>&lt;strong>層次錯位 sibling&lt;/strong> — #92 是「呈現工具 vs 內容層次」、本卡是「審查標準 vs 內容風險」、同骨不同維度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a>&lt;/td>
 &lt;td>資安寫作最便利（通用敘述 / 省略邊界 / 不標版本）跟意圖對齊（精確 threat / boundary / 標準）反向、本卡是 #67 在資安領域的具體展現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../incremental-shipping-criteria/">#76 分批 ship&lt;/a>&lt;/td>
 &lt;td>&lt;strong>反面對照&lt;/strong> — #76 的三軸切分（可見性 / 風險 / 驗證）適合可逆內容；資安錯誤是不可逆 / 系統層、分批 ship 邏輯不適用、要在 ship 前就把 audit 跑完&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../yes-no-binary-collapse/">#80 Yes/No 二選 collapse&lt;/a>&lt;/td>
 &lt;td>「教 X 防護方法」單軸描述是把 threat model 多維度 collapse 成 1 維、跟 #80 同骨——資安教學預設要保留多維度（防什麼 / 不防什麼 / 在哪些 deployment 條件）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../layered-strategy-signal-consistency/">#90 L1+L2 訊號一致性&lt;/a>&lt;/td>
 &lt;td>Silent fallback 即 false confidence、本卡是同類議題在「教學跟實作」之間的一致性問題、訊號要對齊讀者實作端&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>徵兆&lt;/th>
 &lt;th>該做的事&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>章節用「能擋」「能防」「可以避免」、後面沒接 threat model 範圍&lt;/td>
 &lt;td>補對稱論述：寫「擋 X」也寫「不擋 Y」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>引用 OWASP / RFC / NIST、沒標版本 / 年份&lt;/td>
 &lt;td>補版本標記 + 確認該版本仍是 current best practice&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mitigation 寫得通用、沒拆 deployment 條件&lt;/td>
 &lt;td>補 context-dependence、列 deployment 變數對 mitigation 有效性的影響&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>章節結束讀者會說「我學會 X 防護了」、但你不知道他實作會不會出錯&lt;/td>
 &lt;td>Audit bar 還停在 readability、要升級到 verifiability&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「之後讀者實作有疑問再說」&lt;/td>
 &lt;td>是 &lt;a href="../external-trigger-for-high-roi-work/">#72&lt;/a> 結構性跳過、補 audit trigger&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>標題 / index hook 用通用詞（「資安最佳實踐」「防護方法」）、正文寫得精準&lt;/td>
 &lt;td>Metadata surface 漏判（&lt;a href="../metadata-surface-in-writing-review/">#97&lt;/a>）、入口層的含糊會讓正文精準度被誤導&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="適用範圍與邊界">適用範圍與邊界&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>適用&lt;/strong>：資安內容（auth / crypto / 傳輸 / 機敏資料 / 標準引用 / mitigation 設計）、以及任何「讀者照做後錯誤是不可逆 / 系統層」的高風險領域（例：concurrency correctness、distributed consistency claims、financial / medical 計算）&lt;/li>
&lt;li>&lt;strong>不適用&lt;/strong>：純概念說明文章（沒有讀者會直接照做的 step）、實驗性 / playful 內容（讀者預期自行驗證）&lt;/li>
&lt;li>&lt;strong>邊界&lt;/strong>：「verifiability-first」≠「百科全書化」——不是把所有邊界都寫滿、是讓 audit 標準對應風險量級、必要時引用更深的標準文件而不重述&lt;/li>
&lt;li>&lt;strong>過度應用反例&lt;/strong>：把每個資安句子都加滿 boundary / threat / context 補述、文章變密度爆炸、讀者讀不下去——audit bar 對應風險量級、低風險段落（背景介紹 / 概念 anchor）保持簡潔、把 verifiability 投資集中在 mitigation / 標準引用 / 實作 step 段落&lt;/li>
&lt;/ul>
&lt;p>本卡是後續資安 audit 系列卡片（#100-105）的 anchor、確立「為什麼資安寫作需要學術級審查標準」的論證基底。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資安教學內容的 audit 標準不該由「讀者讀不讀得懂」決定、該由「讀者照做後系統會不會出破口」決定。</strong> 讀懂是學習端的成本、破口是生產端的代價、兩者級數不同。</p>
<table>
  <thead>
      <tr>
          <th>教學類型</th>
          <th>寫不清楚的代價</th>
          <th>代價發生位置</th>
          <th>可逆性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一般工程教學（layout / refactor / debug）</td>
          <td>讀者學不會、要重學</td>
          <td>學習端</td>
          <td>可逆（再學一次）</td>
      </tr>
      <tr>
          <td>資安教學（auth / crypto / 防護 / 標準引用）</td>
          <td>讀者<strong>以為</strong>學會、實作時留破口</td>
          <td>生產端</td>
          <td><strong>不可逆</strong>（破口被利用）</td>
      </tr>
  </tbody>
</table>
<p>級數不對稱的後果：一般教學的 audit bar 是「讀者能不能拿到 reasoning」、資安教學的 audit bar 必須升級為「讀者照做後的實作可不可被驗證為無破口」。預設讀者會 implement、不只 read。</p>
<hr>
<h2 id="情境">情境</h2>
<p>資安章節（<code>backend/07-security-data-protection/</code>）的內容形態不是純概念說明、是「問題節點 + 判讀訊號 + 風險後果 + 前置控制面 + 交接路由」。讀者拿到的不只是知識、是<strong>會拿去做防護設計的依據</strong>。</p>
<p>寫作便利的選擇（在一般教學沒問題、在資安教學會出事）：</p>
<ul>
<li>用「能擋」「能防」「可以避免」這類動詞、沒寫適用 threat model</li>
<li>給防護方法、沒寫「這方法擋不到什麼」</li>
<li>引用 OWASP / RFC / NIST、沒寫版本 / 沒驗證引用句意</li>
<li>描述判讀訊號、沒給訊號失效的 deployment 條件</li>
<li>把 mitigation 寫得通用、沒拆 context-dependence（同 mitigation 在 SaaS / on-prem / 多租戶條件失效不同）</li>
</ul>
<p>這些選擇在一般教學是「簡潔風格」、在資安教學是 <strong>silent 破口</strong>——讀者照字面理解去實作、產生 false sense of security（見 <a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security 是資安寫作的主要失敗模式</a>）。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>把資安內容的審查標準從 <strong>readability-first</strong> 升級到 <strong>verifiability-first</strong>：每個論述要回答「讀者照做後、實作的正確性能不能被反向驗證」。</p>
<h3 id="三條-audit-bar">三條 audit bar</h3>
<ol>
<li><strong>Threat model 對稱性</strong>：講「防 X」必須寫「不防 Y」、形成對稱論述（見 <a href="../threat-model-explicitness/">#101 threat-model-explicitness</a>）</li>
<li><strong>Mitigation 對位驗證</strong>：防護措施跟 threat 的對應鏈要可驗證、不能只是「業界常用」（見 <a href="../mitigation-threat-alignment/">#102 mitigation-threat-alignment</a>）</li>
<li><strong>Context-dependence 顯式化</strong>：mitigation 在不同 deployment 的有效性差異要寫出來、不假設讀者知道（見 <a href="../mitigation-context-dependence/">#103 mitigation-context-dependence</a>）</li>
</ol>
<h3 id="寫作流程的差異">寫作流程的差異</h3>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>一般教學</th>
          <th>資安教學</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>草稿</td>
          <td>寫得通、有 reasoning</td>
          <td>+ 列 threat model 範圍 + 列「不在範圍內的 threat」</td>
      </tr>
      <tr>
          <td>Review</td>
          <td>多 pass review（<a href="../writing-multi-pass-review/">#83</a>）</td>
          <td>+ audit pass（reviewer 視角找 false sense of security）</td>
      </tr>
      <tr>
          <td>引用</td>
          <td>引用即可</td>
          <td>+ 標版本 + 驗證引用句意沒被扭曲 + 確認當前版本仍是 best practice</td>
      </tr>
      <tr>
          <td>完稿</td>
          <td>讀者讀完能套用</td>
          <td>+ 讀者實作後的正確性可被反向驗證</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="false-confidence-在生產系統累積">False confidence 在生產系統累積</h3>
<p>讀者讀完含糊論述、心理上覺得「學到防護方法了」、實作時用最直覺的詮釋。當實作有 gap 時、讀者<strong>不會警覺</strong>——因為「我學過了 / 我做了」。等同 <a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a> 在資安領域的具體展現：教學含糊 = hook 規則太粗、看似有保護、實際抓不到行為層的破口。</p>
<p>最危險的是：含糊的資安教學比沒讀過更糟。沒讀過的人會去查標準、會問人；讀過含糊版的人會跳過驗證、直接 implement。</p>
<h3 id="破口的可利用窗口跟教學擴散同步放大">破口的可利用窗口跟教學擴散同步放大</h3>
<p>含糊的資安內容若被多個團隊 / 文章引用、所有下游 implementer 都繼承同一個 silent gap。攻擊者只要找到原始教學的 misinterpretation pattern、就可以批量利用所有 implementation。一般教學的錯誤是 <strong>個別讀者的學習成本</strong>、資安教學的錯誤是 <strong>系統性風險面擴大</strong>。</p>
<h3 id="後續修補無法-trace-到原文">後續修補無法 trace 到原文</h3>
<p>當下游事故發生、回溯到「讀者照某教學實作」時、含糊的原文難以判定是「教學錯」還是「讀者誤解」——因為含糊本身就是 ambiguity 來源。理想的資安教學應該讓「實作 vs 教學」可以被 1:1 對照、出問題時找得到 root cause。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td><strong>本卡是 #82 在資安寫作的領域具體化</strong> — false confidence 透過含糊教學在實作端展現、是 #82 ceiling pattern 的高風險版本</td>
      </tr>
      <tr>
          <td><a href="../visual-tool-error-layer-alignment/">#92 視覺手段對齊錯誤層次</a></td>
          <td><strong>層次錯位 sibling</strong> — #92 是「呈現工具 vs 內容層次」、本卡是「審查標準 vs 內容風險」、同骨不同維度</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>資安寫作最便利（通用敘述 / 省略邊界 / 不標版本）跟意圖對齊（精確 threat / boundary / 標準）反向、本卡是 #67 在資安領域的具體展現</td>
      </tr>
      <tr>
          <td><a href="../incremental-shipping-criteria/">#76 分批 ship</a></td>
          <td><strong>反面對照</strong> — #76 的三軸切分（可見性 / 風險 / 驗證）適合可逆內容；資安錯誤是不可逆 / 系統層、分批 ship 邏輯不適用、要在 ship 前就把 audit 跑完</td>
      </tr>
      <tr>
          <td><a href="../yes-no-binary-collapse/">#80 Yes/No 二選 collapse</a></td>
          <td>「教 X 防護方法」單軸描述是把 threat model 多維度 collapse 成 1 維、跟 #80 同骨——資安教學預設要保留多維度（防什麼 / 不防什麼 / 在哪些 deployment 條件）</td>
      </tr>
      <tr>
          <td><a href="../layered-strategy-signal-consistency/">#90 L1+L2 訊號一致性</a></td>
          <td>Silent fallback 即 false confidence、本卡是同類議題在「教學跟實作」之間的一致性問題、訊號要對齊讀者實作端</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>章節用「能擋」「能防」「可以避免」、後面沒接 threat model 範圍</td>
          <td>補對稱論述：寫「擋 X」也寫「不擋 Y」</td>
      </tr>
      <tr>
          <td>引用 OWASP / RFC / NIST、沒標版本 / 年份</td>
          <td>補版本標記 + 確認該版本仍是 current best practice</td>
      </tr>
      <tr>
          <td>Mitigation 寫得通用、沒拆 deployment 條件</td>
          <td>補 context-dependence、列 deployment 變數對 mitigation 有效性的影響</td>
      </tr>
      <tr>
          <td>章節結束讀者會說「我學會 X 防護了」、但你不知道他實作會不會出錯</td>
          <td>Audit bar 還停在 readability、要升級到 verifiability</td>
      </tr>
      <tr>
          <td>「之後讀者實作有疑問再說」</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72</a> 結構性跳過、補 audit trigger</td>
      </tr>
      <tr>
          <td>標題 / index hook 用通用詞（「資安最佳實踐」「防護方法」）、正文寫得精準</td>
          <td>Metadata surface 漏判（<a href="../metadata-surface-in-writing-review/">#97</a>）、入口層的含糊會讓正文精準度被誤導</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：資安內容（auth / crypto / 傳輸 / 機敏資料 / 標準引用 / mitigation 設計）、以及任何「讀者照做後錯誤是不可逆 / 系統層」的高風險領域（例：concurrency correctness、distributed consistency claims、financial / medical 計算）</li>
<li><strong>不適用</strong>：純概念說明文章（沒有讀者會直接照做的 step）、實驗性 / playful 內容（讀者預期自行驗證）</li>
<li><strong>邊界</strong>：「verifiability-first」≠「百科全書化」——不是把所有邊界都寫滿、是讓 audit 標準對應風險量級、必要時引用更深的標準文件而不重述</li>
<li><strong>過度應用反例</strong>：把每個資安句子都加滿 boundary / threat / context 補述、文章變密度爆炸、讀者讀不下去——audit bar 對應風險量級、低風險段落（背景介紹 / 概念 anchor）保持簡潔、把 verifiability 投資集中在 mitigation / 標準引用 / 實作 step 段落</li>
</ul>
<p>本卡是後續資安 audit 系列卡片（#100-105）的 anchor、確立「為什麼資安寫作需要學術級審查標準」的論證基底。</p>
]]></content:encoded></item><item><title>False sense of security 是資安寫作的主要失敗模式</title><link>https://tarrragon.github.io/blog/report/false-sense-of-security-as-primary-failure/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/false-sense-of-security-as-primary-failure/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資安教學的主要失敗模式不是「讀者學不到」、是「讀者以為學到了」。&lt;/strong> 學不到是 active failure（讀者知道自己沒會、會去查）、以為學到是 silent failure（讀者跳過驗證、直接 implement、破口在生產系統累積）。&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;strong>以為學會&lt;/strong>&lt;/td>
 &lt;td>不知道自己沒會&lt;/td>
 &lt;td>跳過驗證、直接 implement&lt;/td>
 &lt;td>&lt;strong>生產破口、事件偵測前無人警覺&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀懂並會驗證&lt;/td>
 &lt;td>知道邊界、知道何時失效&lt;/td>
 &lt;td>實作 + 持續驗證&lt;/td>
 &lt;td>安全 baseline 達成&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>中間那行（false sense of security）是資安寫作要消滅的目標。&lt;strong>比沒讀過更糟&lt;/strong>——沒讀過會去查，讀過含糊版會跳過。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>讀者讀完資安章節、會自然形成一個結論：「我學到了 X 防護方法」。這個結論的安全性依賴它能不能被分解成可驗證的子句：&lt;/p>
&lt;ul>
&lt;li>對什麼 threat 安全？&lt;/li>
&lt;li>在什麼 deployment 條件下成立？&lt;/li>
&lt;li>什麼前提失效時這個防護失效？&lt;/li>
&lt;li>跟既有實作疊加會不會 silent 干擾？&lt;/li>
&lt;/ul>
&lt;p>如果讀者讀完無法回答這四題、結論就是空殼——表面上「學到了」、實質上是 false sense of security。資安章節（&lt;code>backend/07-security-data-protection/&lt;/code>）的「問題節點」表格容易長出這個結構：&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">判讀訊號：登入驗證節奏失衡
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">風險後果：身分擴散速度提升
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">前置控制面：authentication / incident-severity&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>讀者讀完知道「節奏失衡很危險」、但&lt;strong>不知道&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>「節奏失衡」具體閾值是什麼？（threat model 沒寫）&lt;/li>
&lt;li>「authentication」是哪一層的 control？適用什麼 deployment？（context-dependence 沒寫）&lt;/li>
&lt;li>用了 control 之後、什麼情況下還是會擴散？（mitigation 邊界沒寫）&lt;/li>
&lt;/ul>
&lt;p>讀者照字面實作 → 心裡覺得「節奏控管做了、authentication 用了」→ silent gap。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>把「讀者讀完會說什麼」當成 audit 主軸。對每段論述跑這個反向驗證：&lt;/p>
&lt;h3 id="反向驗證模板">反向驗證模板&lt;/h3>
&lt;p>寫完一段、自問：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>讀者讀完會在心裡形成什麼結論？&lt;/strong>（例：「我做了 session invalidation 就安全」）&lt;/li>
&lt;li>&lt;strong>這個結論能不能拆成可驗證子句？&lt;/strong>（對什麼攻擊安全 / 什麼條件下 / 什麼前提失效）&lt;/li>
&lt;li>&lt;strong>如果不能、補哪一塊讓它能？&lt;/strong>（threat model / context / boundary / 前提條件）&lt;/li>
&lt;/ol>
&lt;p>走完三步、原文若仍是「讀完會 false confidence」、必須改寫——加 contrast、加 boundary、加前提、或拆成更小的可驗證單元。&lt;/p>
&lt;h3 id="識別-false-sense-句子的訊號詞">識別 false-sense 句子的訊號詞&lt;/h3>
&lt;p>下列詞彙在資安內容是 high-risk、預設要被 audit：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號詞&lt;/th>
 &lt;th>為什麼是 risk&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「能擋」「能防」「可避免」&lt;/td>
 &lt;td>沒指定擋什麼、預設讀者會自行補完整 threat space、實際只擋作者腦中的 subset&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「最佳實踐」「業界標準」&lt;/td>
 &lt;td>隱含 universal validity、跳過 context-dependence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「使用 X 即可」&lt;/td>
 &lt;td>把 mitigation 當成銀彈、跳過邊界跟疊加&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「業界常用」「常見做法」&lt;/td>
 &lt;td>Appeal to convention、不是 mitigation 對位驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「應該足夠」「通常足夠」&lt;/td>
 &lt;td>沒給「足夠」的定義、讀者會用最寬鬆詮釋&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「有效」「有用」&lt;/td>
 &lt;td>對什麼 threat 有效？讀者預設「對所有」、實際只對 subset&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每出現一個訊號詞、檢查段落有沒有對應的 boundary 補述；沒有 → 補完或改寫。&lt;/p>
&lt;h3 id="methodology-layer-訊號詞多-layer-false-sense">Methodology-layer 訊號詞（多 layer false sense）&lt;/h3>
&lt;p>False sense of security 不只發生在 mitigation layer——還會在 &lt;strong>methodology / framework / process layer&lt;/strong> 出現。Reader 讀完「我們有方法論 / 有路由系統 / 有 maturity stage / 有 release gate / 有 tripwire」、形成「&lt;strong>有 system / framework = 安全&lt;/strong>」結論、跳過驗證下游 control 是否真擋 threat。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Layer&lt;/th>
 &lt;th>失敗模式&lt;/th>
 &lt;th>Reader 形成的 false 結論&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Mitigation layer&lt;/td>
 &lt;td>上一張表訊號詞&lt;/td>
 &lt;td>「我做了 X mitigation 就安全」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Methodology layer&lt;/td>
 &lt;td>把 framework / routing 當成已治理 risk&lt;/td>
 &lt;td>「我們有 routing system / framework 了 = 風險可控」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Process layer&lt;/td>
 &lt;td>把 gate / checklist 當成 risk reduce&lt;/td>
 &lt;td>「跑了 release gate / 例外有 tripwire = 安全」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Maturity layer&lt;/td>
 &lt;td>把 stage 等級當成 mitigation 強度&lt;/td>
 &lt;td>「我們在可稽核閉環 stage = 風險低」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Methodology-layer 訊號詞清單：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資安教學的主要失敗模式不是「讀者學不到」、是「讀者以為學到了」。</strong> 學不到是 active failure（讀者知道自己沒會、會去查）、以為學到是 silent failure（讀者跳過驗證、直接 implement、破口在生產系統累積）。</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><strong>以為學會</strong></td>
          <td>不知道自己沒會</td>
          <td>跳過驗證、直接 implement</td>
          <td><strong>生產破口、事件偵測前無人警覺</strong></td>
      </tr>
      <tr>
          <td>讀懂並會驗證</td>
          <td>知道邊界、知道何時失效</td>
          <td>實作 + 持續驗證</td>
          <td>安全 baseline 達成</td>
      </tr>
  </tbody>
</table>
<p>中間那行（false sense of security）是資安寫作要消滅的目標。<strong>比沒讀過更糟</strong>——沒讀過會去查，讀過含糊版會跳過。</p>
<hr>
<h2 id="情境">情境</h2>
<p>讀者讀完資安章節、會自然形成一個結論：「我學到了 X 防護方法」。這個結論的安全性依賴它能不能被分解成可驗證的子句：</p>
<ul>
<li>對什麼 threat 安全？</li>
<li>在什麼 deployment 條件下成立？</li>
<li>什麼前提失效時這個防護失效？</li>
<li>跟既有實作疊加會不會 silent 干擾？</li>
</ul>
<p>如果讀者讀完無法回答這四題、結論就是空殼——表面上「學到了」、實質上是 false sense of security。資安章節（<code>backend/07-security-data-protection/</code>）的「問題節點」表格容易長出這個結構：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">判讀訊號：登入驗證節奏失衡
</span></span><span class="line"><span class="ln">2</span><span class="cl">風險後果：身分擴散速度提升
</span></span><span class="line"><span class="ln">3</span><span class="cl">前置控制面：authentication / incident-severity</span></span></code></pre></div><p>讀者讀完知道「節奏失衡很危險」、但<strong>不知道</strong>：</p>
<ul>
<li>「節奏失衡」具體閾值是什麼？（threat model 沒寫）</li>
<li>「authentication」是哪一層的 control？適用什麼 deployment？（context-dependence 沒寫）</li>
<li>用了 control 之後、什麼情況下還是會擴散？（mitigation 邊界沒寫）</li>
</ul>
<p>讀者照字面實作 → 心裡覺得「節奏控管做了、authentication 用了」→ silent gap。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>把「讀者讀完會說什麼」當成 audit 主軸。對每段論述跑這個反向驗證：</p>
<h3 id="反向驗證模板">反向驗證模板</h3>
<p>寫完一段、自問：</p>
<ol>
<li><strong>讀者讀完會在心裡形成什麼結論？</strong>（例：「我做了 session invalidation 就安全」）</li>
<li><strong>這個結論能不能拆成可驗證子句？</strong>（對什麼攻擊安全 / 什麼條件下 / 什麼前提失效）</li>
<li><strong>如果不能、補哪一塊讓它能？</strong>（threat model / context / boundary / 前提條件）</li>
</ol>
<p>走完三步、原文若仍是「讀完會 false confidence」、必須改寫——加 contrast、加 boundary、加前提、或拆成更小的可驗證單元。</p>
<h3 id="識別-false-sense-句子的訊號詞">識別 false-sense 句子的訊號詞</h3>
<p>下列詞彙在資安內容是 high-risk、預設要被 audit：</p>
<table>
  <thead>
      <tr>
          <th>訊號詞</th>
          <th>為什麼是 risk</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「能擋」「能防」「可避免」</td>
          <td>沒指定擋什麼、預設讀者會自行補完整 threat space、實際只擋作者腦中的 subset</td>
      </tr>
      <tr>
          <td>「最佳實踐」「業界標準」</td>
          <td>隱含 universal validity、跳過 context-dependence</td>
      </tr>
      <tr>
          <td>「使用 X 即可」</td>
          <td>把 mitigation 當成銀彈、跳過邊界跟疊加</td>
      </tr>
      <tr>
          <td>「業界常用」「常見做法」</td>
          <td>Appeal to convention、不是 mitigation 對位驗證</td>
      </tr>
      <tr>
          <td>「應該足夠」「通常足夠」</td>
          <td>沒給「足夠」的定義、讀者會用最寬鬆詮釋</td>
      </tr>
      <tr>
          <td>「有效」「有用」</td>
          <td>對什麼 threat 有效？讀者預設「對所有」、實際只對 subset</td>
      </tr>
  </tbody>
</table>
<p>每出現一個訊號詞、檢查段落有沒有對應的 boundary 補述；沒有 → 補完或改寫。</p>
<h3 id="methodology-layer-訊號詞多-layer-false-sense">Methodology-layer 訊號詞（多 layer false sense）</h3>
<p>False sense of security 不只發生在 mitigation layer——還會在 <strong>methodology / framework / process layer</strong> 出現。Reader 讀完「我們有方法論 / 有路由系統 / 有 maturity stage / 有 release gate / 有 tripwire」、形成「<strong>有 system / framework = 安全</strong>」結論、跳過驗證下游 control 是否真擋 threat。</p>
<table>
  <thead>
      <tr>
          <th>Layer</th>
          <th>失敗模式</th>
          <th>Reader 形成的 false 結論</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mitigation layer</td>
          <td>上一張表訊號詞</td>
          <td>「我做了 X mitigation 就安全」</td>
      </tr>
      <tr>
          <td>Methodology layer</td>
          <td>把 framework / routing 當成已治理 risk</td>
          <td>「我們有 routing system / framework 了 = 風險可控」</td>
      </tr>
      <tr>
          <td>Process layer</td>
          <td>把 gate / checklist 當成 risk reduce</td>
          <td>「跑了 release gate / 例外有 tripwire = 安全」</td>
      </tr>
      <tr>
          <td>Maturity layer</td>
          <td>把 stage 等級當成 mitigation 強度</td>
          <td>「我們在可稽核閉環 stage = 風險低」</td>
      </tr>
  </tbody>
</table>
<p>Methodology-layer 訊號詞清單：</p>
<table>
  <thead>
      <tr>
          <th>訊號詞</th>
          <th>為什麼是 risk</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「有方法論」「有 framework」</td>
          <td>方法論不擋 threat、是把 threat 路由到對應控制面、實際擋 threat 仍靠下游控制</td>
      </tr>
      <tr>
          <td>「能路由」「decision system」「分類治理」</td>
          <td>Routing 提供分類、不提供 mitigation；reader 不點下游控制可能停留在 routing layer</td>
      </tr>
      <tr>
          <td>「有 maturity stage / process」</td>
          <td>Maturity 是 process metric、不等於 risk reduction；mature process 可在某 deployment 條件下 silent 失效</td>
      </tr>
      <tr>
          <td>「跑了 gate / 過了 checklist」</td>
          <td>Gate / checklist 通過 ≠ control 真擋 threat、可能是 ceremonial false sense（<a href="../literal-interception-vs-behavioral-refinement/">#82</a> 字面層）</td>
      </tr>
      <tr>
          <td>「設了 tripwire」「有重評估機制」</td>
          <td>Tripwire 沒 quantify（threshold / cadence / owner）等同沒設、見 <a href="../escalation-trigger-quantification/">#91</a></td>
      </tr>
      <tr>
          <td>「能治理」「可控」「閉環」</td>
          <td>治理 / 閉環是流程語、reader 預設「閉環 = 風險擋住」、實際閉環只是流程 cycle、不保證 mitigation 強度</td>
      </tr>
  </tbody>
</table>
<p>驗證方式跟 mitigation layer 同：reader 讀完能否拆 falsifiable 子句？能不能列出<strong>具體下游 control + 各自 boundary + 各自驗證訊號</strong>？不能 → methodology-layer false sense 產地、補「下一步路由 / 必連控制面 / 各 control 的 verification check」。</p>
<h3 id="對抗只給結論的句法">對抗「只給結論」的句法</h3>
<p>跟 <a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據</a> 同骨：資安結論單獨成立會空降、必須跟 contrast / 邊界 / 前提同句承載。</p>
<table>
  <thead>
      <tr>
          <th>危險寫法</th>
          <th>安全寫法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「使用 HTTPS 保護傳輸」</td>
          <td>「使用 HTTPS 防中間人讀取、不防 endpoint 信任失效（CA compromise / cert pinning bypass）」</td>
      </tr>
      <tr>
          <td>「JWT 用簽章驗證身分」</td>
          <td>「簽章驗 token 沒被竄改、不驗 token 沒被竊取（XSS / 明文存儲）、需配 rotation + 短 TTL」</td>
      </tr>
      <tr>
          <td>「rate limit 擋 brute force」</td>
          <td>「per-IP rate limit 擋單來源連續嘗試、不擋分散來源（botnet / credential stuffing）」</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="silent-failure-比-noisy-failure-更貴">Silent failure 比 noisy failure 更貴</h3>
<p>Noisy failure（讀者讀不懂、實作報錯、被 reviewer 抓到）發生在開發前期、修復成本是 commit 等級。silent failure（讀者以為對了、ship 進生產）發生在生產系統、可能等到事件才被發現、修復成本跳到事件處理 + 通報 + 復盤 + 信任修復。</p>
<p>跟 <a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a> 同病——#82 的核心是「驗證工具的字面層 vs 行為層 ceiling」：CI 字面層通過不代表行為層沒問題、但 CI 通過會建立 false confidence、阻止後續行為層檢查。本卡是 #82 在資安寫作的具體展現：含糊的論述提供字面 mitigation、讀者讀完建立 false confidence、阻止實作端的行為層 verify。</p>
<h3 id="教學擴散把單篇-silent-gap-放大成系統性-risk">教學擴散把單篇 silent gap 放大成系統性 risk</h3>
<p>含糊的資安內容若被多團隊引用 / 翻譯 / 二次教材化、原始 misinterpretation pattern 會被批量繼承。攻擊者只需找一次 misinterpretation、就可以利用所有 implementation。一般教學的錯誤是個別讀者的學習成本、資安教學的錯誤是 risk surface 集體放大——跟 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a> 在資安領域的展現：寫得越輕鬆、擴散越快、silent gap 越廣。</p>
<h3 id="事故發生後的-root-cause-無法還原">事故發生後的 root cause 無法還原</h3>
<p>下游事件 root cause 分析時、若實作來源是含糊的教學內容、無法判定是「教學錯」還是「讀者誤解」——含糊本身就是 ambiguity 來源、責任邊界模糊。理想的資安內容應該能讓「實作 vs 教學」1:1 對照、出問題時 trace 得到 root cause（<a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a> 的 traceability 面）。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td><strong>本卡是 #82 的領域具體化最危險版本</strong> — false confidence 在資安寫作的展現、後果不可逆、是 #82 ceiling pattern 的高風險案例</td>
      </tr>
      <tr>
          <td><a href="../layered-strategy-signal-consistency/">#90 L1+L2 訊號一致性</a></td>
          <td><strong>同骨 sibling</strong> — silent fallback 即 false confidence、本卡是同類議題在「教學跟實作之間訊號一致」的展現</td>
      </tr>
      <tr>
          <td><a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a></td>
          <td><strong>#99 的下游主軸</strong> — #99 立論「為什麼資安要學術級 audit」、本卡定義「audit 主要要找什麼」、99 → 100 是動機 → 目標的因果鏈</td>
      </tr>
      <tr>
          <td><a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據</a></td>
          <td>同骨：刪掉 contrast 讓結論空降、本卡的「只給防護不給邊界」是同病在資安領域的展現</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>含糊敘述是寫作最便利選擇、跟「讓讀者實作正確」反向、本卡是 #67 在 silent failure 維度的展現</td>
      </tr>
      <tr>
          <td><a href="../yes-no-binary-collapse/">#80 Yes/No 二選 collapse</a></td>
          <td>「我學會 X 防護了」是把多維度（threat / context / boundary）collapse 成 1 bit、跟 #80 同骨——資安結論預設保留多維度、不能 collapse</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>段落出現「能擋」「能防」「最佳實踐」「即可」</td>
          <td>預設高風險、檢查有沒有對應 boundary 補述</td>
      </tr>
      <tr>
          <td>讀者讀完會說「我做了 X 就安全」</td>
          <td>結論無法拆可驗證子句、補 threat / context / boundary / 前提</td>
      </tr>
      <tr>
          <td>Mitigation 寫得乾淨、沒有 contrast</td>
          <td>跟 <a href="../positive-rewrite-preserves-contrast/">#94</a> 同病、補對照論據</td>
      </tr>
      <tr>
          <td>引用標準（OWASP / RFC / NIST）但不寫版本</td>
          <td>假設標準 universal、補版本 + 適用條件</td>
      </tr>
      <tr>
          <td>「業界常用 / 常見做法」當論證</td>
          <td>Appeal to convention、補 mitigation 對位驗證</td>
      </tr>
      <tr>
          <td>章節結束讀者覺得「都涵蓋了」、但你列不出涵蓋邊界</td>
          <td>入口層 false confidence、補 metadata surface（<a href="../metadata-surface-in-writing-review/">#97</a>）</td>
      </tr>
      <tr>
          <td>「之後實作時應該會發現問題」</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72</a> 結構性跳過、補 audit trigger</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：資安內容（auth / crypto / 防護 / 標準引用 / mitigation 設計）的 audit；任何「讀者照做後錯誤是不可逆 / 系統層」的高風險領域（concurrency 正確性、distributed consistency claims、financial / medical 計算）</li>
<li><strong>不適用</strong>：純概念說明 / 歷史背景內容（讀者不會直接照做）、研究探討文章（讀者預期自行驗證）</li>
<li><strong>邊界</strong>：「消滅 false sense of security」≠「把所有邊界寫到極致」——是讓讀者讀完能列出邊界、不是讓讀者讀完什麼都不敢做。Audit bar 是 verifiability、不是完備性</li>
<li><strong>過度警覺反例</strong>：對所有句子都打防呆 disclaimer、把資安內容寫成 legal-style 「在 X 條件下、若無 Y 前提、且不考慮 Z 攻擊路徑、可能可以」——讀者讀不到任何 actionable 結論、退化成「什麼都不要做」式 paranoia、跟 silent failure 一樣有害；判別準則：讀者讀完應該能列出<strong>可實作的 mitigation 集合 + 各自 boundary</strong>、不是「不知道該不該做任何事」</li>
</ul>
<p>本卡是後續資安 audit 維度卡片（#101-104）的主軸——每個維度都在回答「false sense of security 在哪裡產生」。</p>
]]></content:encoded></item><item><title>Threat model 明確性：「防什麼」與「不防什麼」必須對稱</title><link>https://tarrragon.github.io/blog/report/threat-model-explicitness/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/threat-model-explicitness/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>寫資安 mitigation 必須對稱：每段「防什麼」要配「不防什麼」、不能單邊寫。&lt;/strong> 讀者沒拿到「不防 Y」、會用 universal validity 詮釋 mitigation——預設「防 X」涵蓋整個 threat space、實際只是 X 的 subset。Threat model 的 boundary 是 mitigation 論述的一部分、不是補充說明、不能省。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>寫法&lt;/th>
 &lt;th>讀者會形成的結論&lt;/th>
 &lt;th>結論的 scope&lt;/th>
 &lt;th>實作覆蓋率&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「使用 HTTPS 保護傳輸」&lt;/td>
 &lt;td>HTTPS 防傳輸風險&lt;/td>
 &lt;td>全部傳輸風險（universal）&lt;/td>
 &lt;td>subset（中間人 read）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「使用 HTTPS 防中間人讀取、不防 endpoint 信任失效」&lt;/td>
 &lt;td>HTTPS 防 X、不防 Y&lt;/td>
 &lt;td>顯式 scope&lt;/td>
 &lt;td>對應 X、reader 知道補 Y&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>差別在於讀者實作時的覆蓋判斷——前者讀完跳過 endpoint 驗證、後者讀完知道要補 Y。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>資安寫作有兩個誘因會讓 threat model boundary 被省略：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>正向陳述優先&lt;/strong>規範（AGENTS.md 原則二）會誤把「不防 Y」歸類為負面句、批量改寫時刪掉、跟 &lt;a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據&lt;/a> 同病&lt;/li>
&lt;li>&lt;strong>章節篇幅控制&lt;/strong>會把 threat boundary 當成「進階補充」往後丟、留主章節「乾淨主旨」&lt;/li>
&lt;/ol>
&lt;p>兩者都會產出 universal-flavored 的 mitigation 句子。讀者讀「使用 X 即可保護 Y」時、Y 會被腦補成「所有 Y 相關攻擊」、X 跟 Y 之間的 scope 配對被 silent 地放大成 universal。&lt;/p>
&lt;p>實際資安章節（&lt;code>backend/07-security-data-protection/&lt;/code>）會出現的形態：&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">判讀訊號：登入驗證節奏失衡
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">前置控制面：authentication / incident-severity&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個寫法把 threat 抽象成「節奏失衡」、把 mitigation 抽象成「authentication」——對熟手 OK、對學習者讀完會以為「用 authentication 就擋節奏失衡」、實作時不會去問 authentication 的局部 scope（防 credential 弱、不防 session 重放、不防 supply chain 信任傳導）。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>每個 mitigation 句子強制走「對稱論述」結構：&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">[Mitigation X] 防 [in-scope threat A]、
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">不防 [out-of-scope threat B]（[B 的補強路由 / 外部引用]）。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三個欄位都要填：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>In-scope threat&lt;/strong>：X 真正擋的攻擊類型（具體、不抽象）&lt;/li>
&lt;li>&lt;strong>Out-of-scope threat&lt;/strong>：讀者最容易誤以為 X 也擋的攻擊（讀者直覺會 extrapolate 的方向）&lt;/li>
&lt;li>&lt;strong>補強路由&lt;/strong>：Y 該由什麼補（其他章節 / 外部標準 / 已知條件）、不能只丟「自己想辦法」&lt;/li>
&lt;/ul>
&lt;p>例（HTTPS 章節）：&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">HTTPS 防中間人「讀取」傳輸內容（passive eavesdrop）、
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">不防 endpoint 「身分驗證」失效（compromised CA / cert pinning bypass）、
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">endpoint 信任靠 cert pinning + CT log monitoring 補（見 7.5）。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>例（per-IP rate limit）：&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">per-IP rate limit 擋「單來源」連續嘗試（brute force from single host）、
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">不擋「分散來源」嘗試（botnet / credential stuffing）、
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">分散攻擊靠 reputation-based filtering + adaptive challenge 補（見 7.3）。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="對稱不是補負面是scope-顯式化">對稱不是「補負面」、是「scope 顯式化」&lt;/h3>
&lt;p>跟 &lt;a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據&lt;/a> 同骨：「不防 Y」不是負向陳述、是 mitigation 的 scope qualifier。把 contrast 寫進句子、不是違反「正向陳述優先」、是讓主句的 X claim 站得住。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>寫資安 mitigation 必須對稱：每段「防什麼」要配「不防什麼」、不能單邊寫。</strong> 讀者沒拿到「不防 Y」、會用 universal validity 詮釋 mitigation——預設「防 X」涵蓋整個 threat space、實際只是 X 的 subset。Threat model 的 boundary 是 mitigation 論述的一部分、不是補充說明、不能省。</p>
<table>
  <thead>
      <tr>
          <th>寫法</th>
          <th>讀者會形成的結論</th>
          <th>結論的 scope</th>
          <th>實作覆蓋率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「使用 HTTPS 保護傳輸」</td>
          <td>HTTPS 防傳輸風險</td>
          <td>全部傳輸風險（universal）</td>
          <td>subset（中間人 read）</td>
      </tr>
      <tr>
          <td>「使用 HTTPS 防中間人讀取、不防 endpoint 信任失效」</td>
          <td>HTTPS 防 X、不防 Y</td>
          <td>顯式 scope</td>
          <td>對應 X、reader 知道補 Y</td>
      </tr>
  </tbody>
</table>
<p>差別在於讀者實作時的覆蓋判斷——前者讀完跳過 endpoint 驗證、後者讀完知道要補 Y。</p>
<hr>
<h2 id="情境">情境</h2>
<p>資安寫作有兩個誘因會讓 threat model boundary 被省略：</p>
<ol>
<li><strong>正向陳述優先</strong>規範（AGENTS.md 原則二）會誤把「不防 Y」歸類為負面句、批量改寫時刪掉、跟 <a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據</a> 同病</li>
<li><strong>章節篇幅控制</strong>會把 threat boundary 當成「進階補充」往後丟、留主章節「乾淨主旨」</li>
</ol>
<p>兩者都會產出 universal-flavored 的 mitigation 句子。讀者讀「使用 X 即可保護 Y」時、Y 會被腦補成「所有 Y 相關攻擊」、X 跟 Y 之間的 scope 配對被 silent 地放大成 universal。</p>
<p>實際資安章節（<code>backend/07-security-data-protection/</code>）會出現的形態：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">判讀訊號：登入驗證節奏失衡
</span></span><span class="line"><span class="ln">2</span><span class="cl">前置控制面：authentication / incident-severity</span></span></code></pre></div><p>這個寫法把 threat 抽象成「節奏失衡」、把 mitigation 抽象成「authentication」——對熟手 OK、對學習者讀完會以為「用 authentication 就擋節奏失衡」、實作時不會去問 authentication 的局部 scope（防 credential 弱、不防 session 重放、不防 supply chain 信任傳導）。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>每個 mitigation 句子強制走「對稱論述」結構：</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">[Mitigation X] 防 [in-scope threat A]、
</span></span><span class="line"><span class="ln">2</span><span class="cl">不防 [out-of-scope threat B]（[B 的補強路由 / 外部引用]）。</span></span></code></pre></div><p>三個欄位都要填：</p>
<ul>
<li><strong>In-scope threat</strong>：X 真正擋的攻擊類型（具體、不抽象）</li>
<li><strong>Out-of-scope threat</strong>：讀者最容易誤以為 X 也擋的攻擊（讀者直覺會 extrapolate 的方向）</li>
<li><strong>補強路由</strong>：Y 該由什麼補（其他章節 / 外部標準 / 已知條件）、不能只丟「自己想辦法」</li>
</ul>
<p>例（HTTPS 章節）：</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">HTTPS 防中間人「讀取」傳輸內容（passive eavesdrop）、
</span></span><span class="line"><span class="ln">2</span><span class="cl">不防 endpoint 「身分驗證」失效（compromised CA / cert pinning bypass）、
</span></span><span class="line"><span class="ln">3</span><span class="cl">endpoint 信任靠 cert pinning + CT log monitoring 補（見 7.5）。</span></span></code></pre></div><p>例（per-IP rate limit）：</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">per-IP rate limit 擋「單來源」連續嘗試（brute force from single host）、
</span></span><span class="line"><span class="ln">2</span><span class="cl">不擋「分散來源」嘗試（botnet / credential stuffing）、
</span></span><span class="line"><span class="ln">3</span><span class="cl">分散攻擊靠 reputation-based filtering + adaptive challenge 補（見 7.3）。</span></span></code></pre></div><h3 id="對稱不是補負面是scope-顯式化">對稱不是「補負面」、是「scope 顯式化」</h3>
<p>跟 <a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據</a> 同骨：「不防 Y」不是負向陳述、是 mitigation 的 scope qualifier。把 contrast 寫進句子、不是違反「正向陳述優先」、是讓主句的 X claim 站得住。</p>
<table>
  <thead>
      <tr>
          <th>違反「正向陳述優先」</th>
          <th>符合「正向陳述優先」 + 對稱 boundary</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「不要忘了 X 不防 Y」（命令）</td>
          <td>「X 防 A、不防 B（B 由 C 補）」（陳述）</td>
      </tr>
      <tr>
          <td>「Y 是 X 的限制」（負面 framing）</td>
          <td>「X 的 scope 是 A」（正面 framing）</td>
      </tr>
  </tbody>
</table>
<p>主句仍然承載 X 的 mitigation claim（正向）、不防 Y 是 scope qualifier、不是論述主體——結構符合「正向陳述優先」、語意保留 boundary。</p>
<h3 id="threat-model-的層級對應">Threat model 的層級對應</h3>
<p>對稱論述要在三個層級保持一致：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>對稱 threat model 的形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>章節級</td>
          <td>章節 lead 段列出整體 threat scope + 不在 scope 的 threat 路由</td>
      </tr>
      <tr>
          <td>段落級</td>
          <td>每個 mitigation 段配對應 threat 跟 boundary</td>
      </tr>
      <tr>
          <td>句子級</td>
          <td>「X 防 A、不防 B」單句承載</td>
      </tr>
  </tbody>
</table>
<p>三個層級任一缺、reader 都可能 silent universal 詮釋。實作 audit 時三層分別檢查、不是只看句子。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="reader-用-universal-詮釋實作覆蓋永遠是-worst-case">Reader 用 universal 詮釋、實作覆蓋永遠是 worst case</h3>
<p>人類讀句子時、預設的 scope 是 universal、不是 minimal——這是語言學跟認知偏差的結合。「使用 X 防 Y」讀者預設 X 防整個 Y space。要讓讀者預設 minimal、必須<strong>顯式給 boundary</strong>——這跟物件的 type narrowing 同骨：沒寫 narrowing predicate、預設 widest type。</p>
<p>跟 <a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security</a> 主軸對應——universal 詮釋是 false sense 的主要產地。</p>
<h3 id="reviewer-跟原作者對-mitigation-的-scope-認知會-silent-drift">Reviewer 跟原作者對 mitigation 的 scope 認知會 silent drift</h3>
<p>含糊 threat model 的 mitigation 段、不同 reviewer 讀會 reconstruct 出不同的 in-scope 集合。原作者腦中是 [A, B]、reviewer 讀成 [A, B, C, D]、實作者實作為 [A, B, C, D, E]——三個人對同一段話的覆蓋認知都不同、且都覺得自己對。對稱寫法讓 scope 變成 fact、不是 reconstruction。</p>
<h3 id="多-mitigation-疊加時的-gap-永遠看不到">多 mitigation 疊加時的 gap 永遠看不到</h3>
<p>多個 mitigation 各自寫 in-scope、不寫 out-of-scope、疊加時的 gap（哪個 threat 沒人擋）就看不到。對稱寫法讓每個 mitigation 都有顯式 boundary、疊加 audit 時可以做集合運算（聯集 in-scope 應涵蓋 threat space、否則有 gap）。沒對稱寫法、audit 工具只能憑感覺、無法量化覆蓋。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../false-sense-of-security-as-primary-failure/">#100 False sense of security 主要失敗模式</a></td>
          <td><strong>本卡是消滅 #100 的具體 dimension 1</strong> — universal 詮釋是 false sense 的主要產地、對稱論述是直接的 mitigation</td>
      </tr>
      <tr>
          <td><a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據</a></td>
          <td><strong>同骨 sibling</strong> — #94 在寫作規範執行層、本卡在資安寫作層、都在說「contrast 是論述完整性的一部分、不能為了正向化而刪」</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍是 sanity 防線</a></td>
          <td><strong>scope explicitness 同骨</strong> — #43 在 JS 邊界 / selector / observer scope、本卡在 mitigation 的 threat scope、都在說「scope 不顯式 = 失控的 widening」</td>
      </tr>
      <tr>
          <td><a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a></td>
          <td>上游動機 — #99 立論「為什麼要 verifiability-first」、本卡是 verifiability 的具體實現之一</td>
      </tr>
      <tr>
          <td><a href="../yes-no-binary-collapse/">#80 Yes/No 二選 collapse</a></td>
          <td>「X 防 Y 嗎」的 yes/no 詮釋是 collapse、對稱論述是把多維度（A 防 / B 不防 / 由 C 補）展開回多軸</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mitigation 段只寫 in-scope、沒寫 out-of-scope</td>
          <td>補對稱論述、加 out-of-scope + 補強路由</td>
      </tr>
      <tr>
          <td>「使用 X 防 Y」單句、Y 是抽象詞（傳輸風險 / 身分風險）</td>
          <td>Y 太寬、specific 化 Y 的 in-scope subset、列出 out-of-scope 補 boundary</td>
      </tr>
      <tr>
          <td>章節 lead 段沒列整體 threat scope</td>
          <td>補章節級 threat model、確立 scope qualifier 的 anchor</td>
      </tr>
      <tr>
          <td>多個 mitigation 段並列、各自寫 mitigation、沒寫疊加 gap</td>
          <td>補疊加 audit、聯集 in-scope vs 整體 threat space、找 gap</td>
      </tr>
      <tr>
          <td>把「不防 Y」寫成獨立警告段、跟 mitigation 分開</td>
          <td>對稱論述應該同句承載、分開寫會被讀者跳過或當成「進階補充」</td>
      </tr>
      <tr>
          <td>Reviewer 讀完問「那 Z 攻擊呢？」</td>
          <td>Z 在 reader 直覺 in-scope、原文沒對稱寫、補 Z 為 out-of-scope 並標路由</td>
      </tr>
      <tr>
          <td>「之後讀者實作時會自己想到 boundary」</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72</a> 結構性跳過、補 audit trigger</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：所有資安 mitigation 論述（auth / crypto / 傳輸 / 防護 / 標準引用）；高風險領域的「方法論述」（concurrency primitive 的 ordering 保證、distributed consensus 的 failure mode）</li>
<li><strong>不適用</strong>：純歷史 / 概念介紹文章（不教 mitigation、不需 threat model）、研究探討（讀者預期自行 explore boundary）</li>
<li><strong>邊界</strong>：「對稱論述」≠「列出所有不防的 threat」——只列讀者直覺會 extrapolate 的方向、不是 enumerate 整個 threat space。判別準則：「讀者讀完 X 防 A 之後、心裡最可能誤以為 X 也防的 B 是什麼？」——B 就是該寫的 out-of-scope</li>
<li><strong>過度對稱反例</strong>：每個 mitigation 列十個 out-of-scope threat、文體變 audit-driven（為了 audit checklist 而寫）、不是 reader-driven（為讓讀者建立可驗證 mental model 而寫）；單一 mitigation 的 out-of-scope 通常 1-2 個直覺 extrapolation 方向就夠、列 10 個 = 把 audit 模板當成寫作目標、退化成 #67 寫作便利度反向</li>
</ul>
<p>本卡是資安 audit 第一個維度（threat model）、配 #102 mitigation 對位、#103 context-dependence、#104 citation 形成完整的 audit dimension 集合。</p>
]]></content:encoded></item><item><title>Mitigation 對位：防護對應到具體 threat 的驗證</title><link>https://tarrragon.github.io/blog/report/mitigation-threat-alignment/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/mitigation-threat-alignment/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資安 mitigation 對讀者有意義的不是「mitigation 存在」、是「mitigation 跟 threat 的對應鏈成立」。&lt;/strong> 對應鏈拆三段——&lt;code>設計上 mitigation X 攔 threat Y&lt;/code> + &lt;code>攔的 mechanism 是 Z&lt;/code> + &lt;code>Z 失效時的訊號是 W&lt;/code>——任一段空、mitigation 在實作端就會跟 threat 錯位、變成「看似在防、實際只擋表面 artifact」的 defense theater。&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>攔什麼 threat（設計 in-scope）&lt;/td>
 &lt;td>mitigation 變裝飾、讀者實作時不知道測試該擋什麼&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>攔的 mechanism&lt;/td>
 &lt;td>mitigation 對位到 threat 表面 artifact、不是攻擊 mechanism、變體攻擊立刻繞過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失效訊號&lt;/td>
 &lt;td>mitigation 失效時讀者不知道、靠 silent assumption 撐著&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三段都齊、reader 才能反向驗證實作有沒有達到設計強度。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>資安章節常見的論述形態：給 mitigation 名稱（rate limit、CSRF token、prepared statement）+ 對應 threat 名稱（brute force、CSRF、SQLi）。表面對位、底層 mechanism 沒交代。讀者讀「prepared statement 防 SQLi」、實作時用 string concat + escape function、心裡覺得「我擋 SQLi 了」——因為原文只給 mitigation/threat 對應、沒給 mechanism（parameterization 跟 escape 是兩種不同 mechanism、抗的攻擊面不同）。&lt;/p>
&lt;p>實際 case 的失效模式有三類：&lt;/p>
&lt;h3 id="失效模式-1mitigation-攔表面-artifact不是攻擊-mechanism">失效模式 1：Mitigation 攔表面 artifact、不是攻擊 mechanism&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">論述：rate limit 擋 brute force
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">讀者實作：per-IP rate limit
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">攻擊 mechanism：分散來源（botnet）每個 IP 低頻率、整體高頻率
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">結果：mitigation 攔到的是「單 IP 高頻」表面、不是「身分嘗試」mechanism&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>對位該寫的是「rate limit 攔『單來源高頻嘗試』、不攔『身分嘗試』本身」——mechanism level 的對位、不是名稱對位。&lt;/p>
&lt;h3 id="失效模式-2mitigation-跟-threat-在不同抽象層">失效模式 2：Mitigation 跟 threat 在不同抽象層&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">論述：CSP 擋 XSS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">讀者實作：CSP header 設 default-src &amp;#39;self&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">Threat 抽象層：XSS 是 injection class、有 reflected / stored / DOM 三類
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">Mitigation 抽象層：CSP 是 browser-side execution policy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">結果：CSP 擋「未授權 script 執行」、不擋 stored XSS 在 DB 已 persist 的階段&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>對位該寫 mitigation 在抽象層的位置——CSP 在 browser 執行層、不在 input 處理層。讀者光看「CSP 擋 XSS」會以為 input sanitization 不必做。&lt;/p>
&lt;h3 id="失效模式-3mitigation-假設上層-threat-已擋">失效模式 3：Mitigation 假設上層 threat 已擋&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">論述：bcrypt 防 password DB 外洩後 brute force 還原
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">讀者實作：bcrypt 存 password
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">被忽略的 threat：DB 外洩前 - phishing / credential stuffing / weak password
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">結果：bcrypt 是「外洩後」的 last line、不是 password security 的 first line&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>對位該寫 mitigation 在 defense-in-depth 的層次跟前提——bcrypt 在「&lt;strong>假設&lt;/strong> DB 外洩」的條件下成立、不擋外洩前的 threat。讀者沒拿到前提、會以為 bcrypt 是 password security 的 sufficient solution。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資安 mitigation 對讀者有意義的不是「mitigation 存在」、是「mitigation 跟 threat 的對應鏈成立」。</strong> 對應鏈拆三段——<code>設計上 mitigation X 攔 threat Y</code> + <code>攔的 mechanism 是 Z</code> + <code>Z 失效時的訊號是 W</code>——任一段空、mitigation 在實作端就會跟 threat 錯位、變成「看似在防、實際只擋表面 artifact」的 defense theater。</p>
<table>
  <thead>
      <tr>
          <th>對應鏈段落</th>
          <th>缺失時的後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>攔什麼 threat（設計 in-scope）</td>
          <td>mitigation 變裝飾、讀者實作時不知道測試該擋什麼</td>
      </tr>
      <tr>
          <td>攔的 mechanism</td>
          <td>mitigation 對位到 threat 表面 artifact、不是攻擊 mechanism、變體攻擊立刻繞過</td>
      </tr>
      <tr>
          <td>失效訊號</td>
          <td>mitigation 失效時讀者不知道、靠 silent assumption 撐著</td>
      </tr>
  </tbody>
</table>
<p>三段都齊、reader 才能反向驗證實作有沒有達到設計強度。</p>
<hr>
<h2 id="情境">情境</h2>
<p>資安章節常見的論述形態：給 mitigation 名稱（rate limit、CSRF token、prepared statement）+ 對應 threat 名稱（brute force、CSRF、SQLi）。表面對位、底層 mechanism 沒交代。讀者讀「prepared statement 防 SQLi」、實作時用 string concat + escape function、心裡覺得「我擋 SQLi 了」——因為原文只給 mitigation/threat 對應、沒給 mechanism（parameterization 跟 escape 是兩種不同 mechanism、抗的攻擊面不同）。</p>
<p>實際 case 的失效模式有三類：</p>
<h3 id="失效模式-1mitigation-攔表面-artifact不是攻擊-mechanism">失效模式 1：Mitigation 攔表面 artifact、不是攻擊 mechanism</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">論述：rate limit 擋 brute force
</span></span><span class="line"><span class="ln">2</span><span class="cl">讀者實作：per-IP rate limit
</span></span><span class="line"><span class="ln">3</span><span class="cl">攻擊 mechanism：分散來源（botnet）每個 IP 低頻率、整體高頻率
</span></span><span class="line"><span class="ln">4</span><span class="cl">結果：mitigation 攔到的是「單 IP 高頻」表面、不是「身分嘗試」mechanism</span></span></code></pre></div><p>對位該寫的是「rate limit 攔『單來源高頻嘗試』、不攔『身分嘗試』本身」——mechanism level 的對位、不是名稱對位。</p>
<h3 id="失效模式-2mitigation-跟-threat-在不同抽象層">失效模式 2：Mitigation 跟 threat 在不同抽象層</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">論述：CSP 擋 XSS
</span></span><span class="line"><span class="ln">2</span><span class="cl">讀者實作：CSP header 設 default-src &#39;self&#39;
</span></span><span class="line"><span class="ln">3</span><span class="cl">Threat 抽象層：XSS 是 injection class、有 reflected / stored / DOM 三類
</span></span><span class="line"><span class="ln">4</span><span class="cl">Mitigation 抽象層：CSP 是 browser-side execution policy
</span></span><span class="line"><span class="ln">5</span><span class="cl">結果：CSP 擋「未授權 script 執行」、不擋 stored XSS 在 DB 已 persist 的階段</span></span></code></pre></div><p>對位該寫 mitigation 在抽象層的位置——CSP 在 browser 執行層、不在 input 處理層。讀者光看「CSP 擋 XSS」會以為 input sanitization 不必做。</p>
<h3 id="失效模式-3mitigation-假設上層-threat-已擋">失效模式 3：Mitigation 假設上層 threat 已擋</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">論述：bcrypt 防 password DB 外洩後 brute force 還原
</span></span><span class="line"><span class="ln">2</span><span class="cl">讀者實作：bcrypt 存 password
</span></span><span class="line"><span class="ln">3</span><span class="cl">被忽略的 threat：DB 外洩前 - phishing / credential stuffing / weak password
</span></span><span class="line"><span class="ln">4</span><span class="cl">結果：bcrypt 是「外洩後」的 last line、不是 password security 的 first line</span></span></code></pre></div><p>對位該寫 mitigation 在 defense-in-depth 的層次跟前提——bcrypt 在「<strong>假設</strong> DB 外洩」的條件下成立、不擋外洩前的 threat。讀者沒拿到前提、會以為 bcrypt 是 password security 的 sufficient solution。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>每個 mitigation 段落補三欄對位：</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">[Mitigation X]
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 攔的 threat：[具體攻擊行為、不是攻擊類別名稱]
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 攔的 mechanism：[X 在什麼層擋 / 擋的是 mechanism 的哪一步]
</span></span><span class="line"><span class="ln">4</span><span class="cl">- 失效訊號：[reader 能觀察到 mitigation 有沒有發揮的具體現象]</span></span></code></pre></div><p>例（per-IP rate limit）：</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">per-IP rate limit
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 攔的 threat：單來源連續嘗試（同 IP 短時間多次 login）
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 攔的 mechanism：在 single-source 維度限制 attempt rate、攻擊者必須切 IP 才能繞
</span></span><span class="line"><span class="ln">4</span><span class="cl">- 失效訊號：分散來源（多 IP 各自低頻）的 aggregate 嘗試率、per-IP rate limit metric 不會 trigger</span></span></code></pre></div><p>例（bcrypt password hashing）：</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">bcrypt
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 攔的 threat：DB 外洩後 password 被離線 brute force 還原
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 攔的 mechanism：work factor 控制 hash 計算成本、攻擊者每次嘗試的成本不可優化
</span></span><span class="line"><span class="ln">4</span><span class="cl">- 失效訊號：weak password / 已知 password 在 dictionary 中、攻擊者不需 brute force 全 space
</span></span><span class="line"><span class="ln">5</span><span class="cl">- 前提：上層擋住 phishing / credential stuffing、bcrypt 是 last line、不是 first line</span></span></code></pre></div><h3 id="對位的層次規則">對位的層次規則</h3>
<p>對位驗證要在三個層次都對齊：</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>對位的形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>名稱層</td>
          <td>mitigation 名稱 → threat 名稱（最弱、容易裝飾）</td>
      </tr>
      <tr>
          <td>Mechanism 層</td>
          <td>mitigation 擋的攻擊 mechanism → threat 的具體 mechanism</td>
      </tr>
      <tr>
          <td>前提層</td>
          <td>mitigation 成立的前提 → 前提失效時的 fallback / upstream control</td>
      </tr>
  </tbody>
</table>
<p>只到名稱層 = defense theater；到 mechanism 層 = 可實作驗證；到前提層 = 可疊加 defense-in-depth audit。</p>
<h3 id="對位-audit-的工具方法">對位 audit 的工具方法</h3>
<p>對 mitigation 群組做集合運算：</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">1. 列章節所有 mitigation 跟對應 threat
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 對每對 (mitigation, threat) 補 mechanism + 前提
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 集合化：聯集所有 mitigation 攔的 mechanism、聯集所有 threat 的 mechanism
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 找 gap：threat 集合裡沒被 mitigation 集合涵蓋的 mechanism
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. Gap 處理：補 mitigation / 標 out-of-scope（[#101](../threat-model-explicitness/)）/ 升級到 defense-in-depth 上層</span></span></code></pre></div><p>集合運算讓對位錯誤跟覆蓋 gap 從「靠感覺」升級到「可量化」。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="defense-theater-在-audit-跟-implementation-都通過生產系統有破口">Defense theater 在 audit 跟 implementation 都通過、生產系統有破口</h3>
<p>只到名稱層對位的 mitigation、audit 工具看到「rate limit 已部署」會 pass、implementation 看到「CSRF token 已加」會 pass、threat 還在——攻擊者用 mechanism 變體（分散來源 / DOM XSS / stored injection）繞過、mitigation 集體 silent 失效。<strong>對位錯誤的 mitigation 跟沒 mitigation 在攻擊者眼中等價、但對 audit / 對讀者不等價</strong>——這個 gap 是 defense theater 的本質。</p>
<p>跟 <a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a> 同骨：mitigation 名稱對位是字面層、mechanism 對位是行為層、前提對位是 contextual 行為層。stop at 字面層 = false confidence。</p>
<h3 id="mitigation-變體跟-threat-變體無法-trace">Mitigation 變體跟 threat 變體無法 trace</h3>
<p>新 threat 出現（如 credential stuffing 之於傳統 brute force）、reader 必須重新評估既有 mitigation 是否還對位。對位鏈寫到 mechanism + 前提的 mitigation 可被 trace（per-IP rate limit 的 mechanism 是 single-source 限制、credential stuffing 是分散來源、不對位、需新 mitigation）；只到名稱層的 mitigation 不可 trace（rate limit vs credential stuffing：名稱看起來「應該擋」、實際不擋）。寫作時的 mechanism / 前提投資、是給未來 threat evolution 留的 review 入口。</p>
<h3 id="mitigation-疊加時的責任邊界含糊">Mitigation 疊加時的責任邊界含糊</h3>
<p>多個 mitigation 共防一個 threat、若各自不寫 mechanism + 前提、疊加時無法判斷「誰負責什麼層」。修補某個 mitigation 時不知道會不會影響其他 mitigation 的前提、變更冒險成本上升。明示 mechanism + 前提 = 明示 mitigation 之間的 dependency、修補成本可控。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td><strong>本卡是 #82 在 mitigation 設計層的具體化</strong> — mitigation 名稱對位 = 字面層、mechanism 對位 = 行為層、前提對位 = contextual 行為層；stop at 名稱層 = false confidence</td>
      </tr>
      <tr>
          <td><a href="../capability-gap-three-layer-escalation/">#86 Capability gap 三層對策階梯</a></td>
          <td><strong>同骨對位邏輯</strong> — #86 是 capability gap 的 L1/L2/L3 對應；本卡是 mitigation 在「名稱 / mechanism / 前提」三層對應；都在說「層次選對才有效」</td>
      </tr>
      <tr>
          <td><a href="../main-strategy-plus-supplementary/">#75 主策略 + 補強策略</a></td>
          <td><strong>疊加 mitigation 的對位</strong> — #75 是多策略疊加判準（解不同層 / 沒副作用衝突 / 增量成本可接受），本卡補「疊加時各 mitigation 的 mechanism 跟前提要明示」、不然 #75 的判準沒法跑</td>
      </tr>
      <tr>
          <td><a href="../false-sense-of-security-as-primary-failure/">#100 False sense of security 主要失敗模式</a></td>
          <td><strong>#100 的 dimension 2</strong> — 對位失效是 false sense 的第二大產地（dimension 1 是 threat model 不對稱、見 <a href="../threat-model-explicitness/">#101</a>）</td>
      </tr>
      <tr>
          <td><a href="../threat-model-explicitness/">#101 Threat model 明確性</a></td>
          <td><strong>本卡的上游前提</strong> — #101 確立 threat space 的 scope、本卡確立 mitigation 在 scope 內的 mechanism 對位；threat model 不清的話 mitigation 對位無從談起</td>
      </tr>
      <tr>
          <td><a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a></td>
          <td>上游動機 — verifiability-first 的具體實現之二（#101 是 dimension 1、本卡是 dimension 2）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mitigation 段只寫「X 防 Y」、沒寫 mechanism</td>
          <td>補 mechanism 層：X 在什麼抽象層擋、擋的是 Y 的哪一步</td>
      </tr>
      <tr>
          <td>Mitigation 用 threat 類別名稱（brute force / SQLi / XSS）對位</td>
          <td>類別名稱太寬、specific 化到具體攻擊行為（單 IP 高頻 / payload boundary / stored vs reflected）</td>
      </tr>
      <tr>
          <td>Mitigation 段沒寫前提、讀者不知道何時失效</td>
          <td>補前提層：mitigation 在什麼條件下成立、條件失效時的 upstream control</td>
      </tr>
      <tr>
          <td>多個 mitigation 並列、各自寫對應、沒寫疊加 dependency</td>
          <td>補集合運算 audit、聯集 mechanism 集合 vs 整體 threat space</td>
      </tr>
      <tr>
          <td>Reviewer 讀完問「這跟 [新 threat 變體] 對到嗎？」</td>
          <td>對位鏈停在名稱層、補 mechanism 讓變體可被 trace</td>
      </tr>
      <tr>
          <td>「業界常用 X 防 Y」當論證</td>
          <td>Appeal to convention、補 mechanism 對位驗證、不能用「常用」代替</td>
      </tr>
      <tr>
          <td>章節開頭列 threat、結尾列 mitigation、中間沒對位鏈</td>
          <td>補對位段、把兩個列表 link 成 (threat, mitigation, mechanism, 前提) 表</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：資安 mitigation 設計（auth / crypto / 防護 / 標準 control）的論述；任何「方法 → 問題」對應的高 stakes 領域（concurrency primitive 對 race 類別 / consensus 演算法對 failure mode / financial control 對 risk 類別）</li>
<li><strong>不適用</strong>：純概念 / 歷史介紹（不教 mitigation）、研究探討（讀者預期自行 explore mechanism）</li>
<li><strong>邊界</strong>：「對位驗證」≠「列出每個攻擊變體」——mechanism 層列到攻擊行為的根 mechanism 即可、不必列所有 surface 變體；判別準則是「reader 能不能用這個 mechanism 描述去判斷新變體攻擊是否在 mitigation 覆蓋內」</li>
<li><strong>過度對位反例</strong>：每個 mitigation 寫 mechanism + 前提 + 三層 scope qualifier + 五種失效訊號、文章變 audit checklist、不是教學；mitigation 對位的投資量級對應 mitigation 在系統的責任比重——核心 mitigation（auth / crypto primitive）值得三層完整對位、輔助 mitigation（log redaction / banner notice）只到 mechanism 層即可</li>
</ul>
<p>本卡是資安 audit 第二個維度（mitigation 對位）、配 <a href="../threat-model-explicitness/">#101</a> threat model 明確性、後續 #103 context-dependence、#104 citation 形成完整 audit dimension 集合。</p>
]]></content:encoded></item><item><title>Mitigation 的 context-dependence：deployment 條件改變有效性</title><link>https://tarrragon.github.io/blog/report/mitigation-context-dependence/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/mitigation-context-dependence/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資安 mitigation 的有效性不是 mitigation 本身決定的、是 mitigation × deployment 條件決定的。&lt;/strong> 同一個 mitigation 在不同 deployment / config / scale / runtime 條件下、強度光譜從「完整擋」到「等同沒部署」都可能。寫作時忽略 deployment 變數、讀者實作時用最直覺條件詮釋、實際部署條件不對 mitigation silent 失效。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>描述形態&lt;/th>
 &lt;th>讀者實作判斷&lt;/th>
 &lt;th>部署條件不對的後果&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「使用 X 保護 Y」（universal-flavored）&lt;/td>
 &lt;td>在「正常」條件下 X 防 Y&lt;/td>
 &lt;td>條件不對、X silent 失效、無人警覺&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「使用 X 保護 Y、條件 Z」&lt;/td>
 &lt;td>條件 Z 成立才用 X、否則補 W&lt;/td>
 &lt;td>條件不對時 reader 知道補 W&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>差別在於：reader 在實作 review 階段有沒有 context 變數可檢查。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>資安 mitigation 在文獻 / 標準 / 教學裡常被描述成「方法 → 防什麼 threat」對應、跳過 deployment 條件這個變數。讀者讀完套到自己 deployment 上、條件可能不一致。常見的 context dimension 有四類：&lt;/p>
&lt;h3 id="context-維度-1config-完整性">Context 維度 1：Config 完整性&lt;/h3>
&lt;p>Mitigation 通常需要多個 config 同時成立才有效、單一 config 不夠：&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">HTTPS 防中間人：成立條件 = TLS + HSTS + cert pinning（針對重要 endpoint）+ CT log monitoring
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> 失效條件 = 只有 TLS、沒 HSTS → 第一次連線可被 downgrade
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> 沒 cert pinning → 受信任 CA 簽出假 cert 可繞過
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">JWT 驗身分： 成立條件 = 簽章驗證 + 短 TTL + rotation + 安全儲存（HttpOnly cookie 或 secure storage）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> 失效條件 = 簽章對但 TTL 太長 → token 被竊後長期可用
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> XSS 可讀取 → 簽章保護被繞過
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> 沒 rotation → 一次外洩永久暴露&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>寫「使用 HTTPS」「使用 JWT」是把 mitigation 縮成單一 control name、reader 預設 default config、實際要 5-7 個 config 同時對才完整。&lt;/p>
&lt;h3 id="context-維度-2scale--多實例">Context 維度 2：Scale / 多實例&lt;/h3>
&lt;p>某些 mitigation 在單機 OK、多實例失效：&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">Rate limit： 單實例 = local counter、per-IP rate 控管準確
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> 多實例 = 每實例各自 count、攻擊者打不同實例可繞過 N 倍上限
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> 修法 = 用 distributed counter（Redis / 共享 store）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">Session 失效：單實例 = local session store、invalidate 即時
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> 多實例 = invalidate 訊號需 broadcast、舊 token 在其他實例還可用
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> 修法 = 用 stateless token + revocation list 或 共享 session store&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Reader 看到「rate limit 防 brute force」、實作時若不知道 deployment scale、單實例 OK / 多實例 silent 失效。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資安 mitigation 的有效性不是 mitigation 本身決定的、是 mitigation × deployment 條件決定的。</strong> 同一個 mitigation 在不同 deployment / config / scale / runtime 條件下、強度光譜從「完整擋」到「等同沒部署」都可能。寫作時忽略 deployment 變數、讀者實作時用最直覺條件詮釋、實際部署條件不對 mitigation silent 失效。</p>
<table>
  <thead>
      <tr>
          <th>描述形態</th>
          <th>讀者實作判斷</th>
          <th>部署條件不對的後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「使用 X 保護 Y」（universal-flavored）</td>
          <td>在「正常」條件下 X 防 Y</td>
          <td>條件不對、X silent 失效、無人警覺</td>
      </tr>
      <tr>
          <td>「使用 X 保護 Y、條件 Z」</td>
          <td>條件 Z 成立才用 X、否則補 W</td>
          <td>條件不對時 reader 知道補 W</td>
      </tr>
  </tbody>
</table>
<p>差別在於：reader 在實作 review 階段有沒有 context 變數可檢查。</p>
<hr>
<h2 id="情境">情境</h2>
<p>資安 mitigation 在文獻 / 標準 / 教學裡常被描述成「方法 → 防什麼 threat」對應、跳過 deployment 條件這個變數。讀者讀完套到自己 deployment 上、條件可能不一致。常見的 context dimension 有四類：</p>
<h3 id="context-維度-1config-完整性">Context 維度 1：Config 完整性</h3>
<p>Mitigation 通常需要多個 config 同時成立才有效、單一 config 不夠：</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">HTTPS 防中間人：成立條件 = TLS + HSTS + cert pinning（針對重要 endpoint）+ CT log monitoring
</span></span><span class="line"><span class="ln">2</span><span class="cl">                失效條件 = 只有 TLS、沒 HSTS → 第一次連線可被 downgrade
</span></span><span class="line"><span class="ln">3</span><span class="cl">                          沒 cert pinning → 受信任 CA 簽出假 cert 可繞過
</span></span><span class="line"><span class="ln">4</span><span class="cl">JWT 驗身分：    成立條件 = 簽章驗證 + 短 TTL + rotation + 安全儲存（HttpOnly cookie 或 secure storage）
</span></span><span class="line"><span class="ln">5</span><span class="cl">                失效條件 = 簽章對但 TTL 太長 → token 被竊後長期可用
</span></span><span class="line"><span class="ln">6</span><span class="cl">                          XSS 可讀取 → 簽章保護被繞過
</span></span><span class="line"><span class="ln">7</span><span class="cl">                          沒 rotation → 一次外洩永久暴露</span></span></code></pre></div><p>寫「使用 HTTPS」「使用 JWT」是把 mitigation 縮成單一 control name、reader 預設 default config、實際要 5-7 個 config 同時對才完整。</p>
<h3 id="context-維度-2scale--多實例">Context 維度 2：Scale / 多實例</h3>
<p>某些 mitigation 在單機 OK、多實例失效：</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">Rate limit： 單實例 = local counter、per-IP rate 控管準確
</span></span><span class="line"><span class="ln">2</span><span class="cl">            多實例 = 每實例各自 count、攻擊者打不同實例可繞過 N 倍上限
</span></span><span class="line"><span class="ln">3</span><span class="cl">            修法 = 用 distributed counter（Redis / 共享 store）
</span></span><span class="line"><span class="ln">4</span><span class="cl">Session 失效：單實例 = local session store、invalidate 即時
</span></span><span class="line"><span class="ln">5</span><span class="cl">            多實例 = invalidate 訊號需 broadcast、舊 token 在其他實例還可用
</span></span><span class="line"><span class="ln">6</span><span class="cl">            修法 = 用 stateless token + revocation list 或 共享 session store</span></span></code></pre></div><p>Reader 看到「rate limit 防 brute force」、實作時若不知道 deployment scale、單實例 OK / 多實例 silent 失效。</p>
<h3 id="context-維度-3runtime-環境">Context 維度 3：Runtime 環境</h3>
<p>執行環境差異改變 mitigation 適用性：</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">Cookie SameSite=Strict 防 CSRF：
</span></span><span class="line"><span class="ln">2</span><span class="cl">  瀏覽器環境 = 有效（瀏覽器強制執行）
</span></span><span class="line"><span class="ln">3</span><span class="cl">  Native app webview = 部分有效（依 webview 實作）
</span></span><span class="line"><span class="ln">4</span><span class="cl">  Mobile in-app browser = 不一定有效（看實作）
</span></span><span class="line"><span class="ln">5</span><span class="cl">  Server-to-server = 不適用（無 cookie / 無 SameSite 概念）
</span></span><span class="line"><span class="ln">6</span><span class="cl">CSP 防 XSS：
</span></span><span class="line"><span class="ln">7</span><span class="cl">  Modern browser = 有效
</span></span><span class="line"><span class="ln">8</span><span class="cl">  舊瀏覽器（IE / 非 evergreen）= partial 或無效
</span></span><span class="line"><span class="ln">9</span><span class="cl">  非 browser execution（Electron / native webview）= 看 implementation</span></span></code></pre></div><h3 id="context-維度-4threat-actor-能力">Context 維度 4：Threat actor 能力</h3>
<p>Mitigation 的 work factor 跟 threat actor 計算能力對應：</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">bcrypt（work factor = 10）：
</span></span><span class="line"><span class="ln">2</span><span class="cl">  個人攻擊者 = 強保護
</span></span><span class="line"><span class="ln">3</span><span class="cl">  Nation-state（GPU farm / FPGA）= 弱保護、需提高 work factor 或換 argon2
</span></span><span class="line"><span class="ln">4</span><span class="cl">PBKDF2（100k iterations）：
</span></span><span class="line"><span class="ln">5</span><span class="cl">  2010 年 = 強
</span></span><span class="line"><span class="ln">6</span><span class="cl">  2026 年 = 弱（建議升級到 600k+ 或 argon2）</span></span></code></pre></div><p>Threat actor 能力是 deployment 隨時間變化的變數、寫作時固定描述很快過時。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>每個 mitigation 段落明示三類條件：</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">[Mitigation X]
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 成立條件：[X 發揮設計強度需要的 config / scale / runtime / 其他 control 配套]
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 失效條件：[條件不對時 X 變成 etc 等同沒部署的具體情境]
</span></span><span class="line"><span class="ln">4</span><span class="cl">- Deployment 變數：[實作時要檢查的 dimension list]</span></span></code></pre></div><p>例（rate limit 防 brute force）：</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">per-IP rate limit
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 成立條件：單實例部署 OR 多實例 + distributed counter（Redis / 共享 store）
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 失效條件：多實例 + local counter、攻擊者輪流打不同實例繞過上限
</span></span><span class="line"><span class="ln">4</span><span class="cl">- Deployment 變數：實例數量、counter 部署位置（local / shared）、IP 來源真實性（NAT / proxy 後是否還能 distinguish）</span></span></code></pre></div><p>例（HTTPS 防中間人）：</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">HTTPS
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 成立條件：TLS + HSTS（避免首連線 downgrade）+ 受信 CA chain + 在重要 endpoint 配 cert pinning
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 失效條件：沒 HSTS → 首次連線 downgrade；CA 被攻陷 → 假 cert 可繞；no cert pinning + state-level CA 攻陷 → silent MITM
</span></span><span class="line"><span class="ln">4</span><span class="cl">- Deployment 變數：HSTS preload / max-age 設定、cert pinning 範圍（哪些 endpoint）、CA list 是否最小化、CT log monitoring 是否到位</span></span></code></pre></div><h3 id="context-描述的層次規則">Context 描述的層次規則</h3>
<p>每個 mitigation 描述至少要有 deployment baseline 跟 stretch case：</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Baseline 條件</td>
          <td>最常見 deployment（單機 / 標準 config / mainstream browser）下的有效性</td>
      </tr>
      <tr>
          <td>Stretch 條件</td>
          <td>scale / 異常 runtime / 高能力 actor 下的衰減</td>
      </tr>
      <tr>
          <td>Trigger condition</td>
          <td>何時 baseline 不夠、要升級到 stretch 的訊號</td>
      </tr>
  </tbody>
</table>
<p>baseline 給 reader 入門條件、stretch 給 reader 升級判準、trigger 讓升級成 actionable signal。</p>
<h3 id="跟規模改變可行性的同骨">跟「規模改變可行性」的同骨</h3>
<p>跟 <a href="../dataset-scale-changes-feasibility/">#89 Dataset 規模改變什麼可行</a> 同骨——#89 在 dataset / index / cache 維度、本卡在 mitigation / config / scale 維度：</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">#89:    &lt; 1MB 無腦處理 → 1-10MB O(N) 可行 → &gt; 100MB 強制 index
</span></span><span class="line"><span class="ln">2</span><span class="cl">本卡：   單實例 local rate limit OK → 多實例需 distributed counter → 高 scale 需 token bucket + adaptive</span></span></code></pre></div><p>「在 X 規模 / 條件下 Y 方法 OK」這個結構在資料處理跟資安都成立、是 deployment 變數驅動的工程光譜。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="正常條件下有效silent-變成生產破口">「正常條件下有效」silent 變成生產破口</h3>
<p>讀者讀「使用 X 防 Y」、用自己 deployment 的 default config 實作、跑開發測試 OK、ship 進生產。生產可能是多實例 / 高 scale / 異常 runtime、X 在那條件下不成立、threat 進入。<strong>Mitigation 在開發環境 silent 失效、生產環境 silent 失效——兩階段都沒訊號、直到事件</strong>。</p>
<p>跟 <a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security</a> 同病：context 沒寫、reader 用最直覺條件詮釋、condition mismatch 不會被 catch。</p>
<h3 id="mitigation-升級的時機不可-trace">Mitigation 升級的時機不可 trace</h3>
<p>威脅環境變化（actor 計算能力 / 攻擊變體 / scale 增長）需要 mitigation 跟著升級。Context 寫清楚的 mitigation 可 trace（bcrypt work factor 跟 actor 能力對應、定期 review）；context 含糊的 mitigation 不可 trace（「使用 bcrypt」變成 frozen「最佳實踐」、實際強度跟著時間 decay）。</p>
<h3 id="跨環境-deployment-的-mitigation-假設衝突">跨環境 deployment 的 mitigation 假設衝突</h3>
<p>同一份教學 / spec 套到不同 deployment（dev / staging / prod / 多區域 / 不同租戶）、若 context 沒寫、各 deployment 的 mitigation 強度差異被 silent。Audit 跨 deployment 時無法判定哪個強度最弱、整個系統的 baseline 取決於最弱 deployment、但沒人知道哪個是最弱。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../dataset-scale-changes-feasibility/">#89 Dataset 規模改變什麼可行</a></td>
          <td><strong>同骨 sibling</strong> — #89 是「資料規模 → 處理方法可行性」、本卡是「deployment 條件 → mitigation 有效性」、都是「條件變數驅動的方法光譜」</td>
      </tr>
      <tr>
          <td><a href="../build-time-vs-runtime-computation-spectrum/">#87 Build-time vs Runtime 計算光譜</a></td>
          <td><strong>同骨 spectrum</strong> — #87 是計算位置光譜（build / runtime / hybrid）+ 四軸判準、本卡是 mitigation 條件光譜（baseline / stretch / trigger）+ 四 context 維度</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍是 sanity 防線</a></td>
          <td><strong>scope condition 同骨</strong> — #43 把「scope」變成顯式 fact、本卡把「deployment 條件」變成顯式 fact；都在說「不顯式 = 失控的 default 詮釋」</td>
      </tr>
      <tr>
          <td><a href="../false-sense-of-security-as-primary-failure/">#100 False sense of security 主要失敗模式</a></td>
          <td><strong>#100 的 dimension 3</strong> — context 不寫是 false sense 的第三大產地（dimension 1 = threat model 不對稱 / dimension 2 = mitigation 對位失效 / dimension 3 = context 沒寫）</td>
      </tr>
      <tr>
          <td><a href="../threat-model-explicitness/">#101 Threat model 明確性</a> + <a href="../mitigation-threat-alignment/">#102 Mitigation 對位</a></td>
          <td><strong>本卡是 #101/#102 的 condition 維度</strong> — #101 確立 in-scope threat、#102 確立 mitigation→threat 對位、本卡確立對位在 deployment 條件下的有效性；三者完整定義 mitigation 強度</td>
      </tr>
      <tr>
          <td><a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a></td>
          <td>上游動機 — verifiability-first 的 dimension 3</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「使用 X」單行 mitigation、沒寫 config / scale / runtime 條件</td>
          <td>補三類條件：成立 / 失效 / deployment 變數</td>
      </tr>
      <tr>
          <td>標準引用（OWASP / RFC）抄整段、沒寫適用 deployment</td>
          <td>標準是 universal-flavored、本地化 deployment context</td>
      </tr>
      <tr>
          <td>Mitigation 描述沒提 work factor / iteration count / 強度參數</td>
          <td>補強度參數 + 對應 actor 能力的 trigger condition</td>
      </tr>
      <tr>
          <td>多實例 / 多區域部署、rate limit / session 描述沒提 distributed</td>
          <td>補多實例 context、明示 local vs distributed 的差異</td>
      </tr>
      <tr>
          <td>「在 modern browser」「在 standard config」沒展開的修飾詞</td>
          <td>列舉 modern / standard 涵蓋什麼、不涵蓋什麼</td>
      </tr>
      <tr>
          <td>Threat actor 能力 / 計算成本沒列</td>
          <td>補 actor model、區分個人 / 組織 / nation-state 的 mitigation 強度</td>
      </tr>
      <tr>
          <td>「之後 deployment 不一樣再說」</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72</a> 結構性跳過、補 trigger</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：資安 mitigation 的所有論述（auth / crypto / 傳輸 / 防護 / scale-sensitive control）；任何「方法有效性受部署條件影響」的領域（concurrency primitive 在不同 memory model / DB transaction 在不同 isolation level / consensus 演算法在不同 network partition 假設）</li>
<li><strong>不適用</strong>：純歷史 / 概念介紹（不教 mitigation deployment）、研究探討（讀者預期自行 explore condition）</li>
<li><strong>邊界</strong>：「Context-dependence 顯式」≠「窮舉所有 deployment 排列組合」——只列 reader 直覺會誤判的 dimension（最常見 deployment 跟最常見變體）、不必涵蓋整個 deployment space；判別準則：「reader 用 default 條件詮釋會不會 silent 失效」——會 → 補 context、不會 → 不必補</li>
<li><strong>過度條件化反例</strong>：每個 mitigation 列 deployment matrix（10 個 dimension × 5 個值 = 50 個 case）、文章變 deployment guide、不是教學；條件描述的投資量級對應 mitigation 在系統的責任比重——核心 control（auth / crypto）值得三類條件完整、輔助 control 只列 baseline + 一個 stretch case 即可</li>
</ul>
<p>本卡是資安 audit 第三個維度（context-dependence）、配 <a href="../threat-model-explicitness/">#101</a> threat model + <a href="../mitigation-threat-alignment/">#102</a> 對位、後續 #104 citation 形成完整 audit dimension 集合。</p>
]]></content:encoded></item><item><title>Security 標準引用的時效性與精確度</title><link>https://tarrragon.github.io/blog/report/security-citation-currency-and-precision/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/security-citation-currency-and-precision/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資安標準引用不是「這條 control 寫在 X 文件」、是「這條 control 在 X 版本 X 年份 X 語境下對 X actor 模型成立」。&lt;/strong> 五個變數任一變、引用就過時或扭曲。資安 best practice 衰退快、universal-flavored 引用（「OWASP 建議 X」「RFC 規定 Y」）會 silent 把過時或語境外的內容傳給 reader、產生 &lt;a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security&lt;/a> 的 citation 維度產地。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>引用形態&lt;/th>
 &lt;th>reader 套用時的判斷&lt;/th>
 &lt;th>過時 / 扭曲時的後果&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「OWASP 建議 X」&lt;/td>
 &lt;td>X 是 universal best practice&lt;/td>
 &lt;td>套用時 OWASP 已改版、reader 不知道&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「OWASP Top 10 (2021) 建議 X、原文：『&amp;hellip;』」&lt;/td>
 &lt;td>X 在 2021 OWASP Top 10 語境下成立&lt;/td>
 &lt;td>過時時 reader 知道要 check 新版&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>差別在 reader 在實作 review 階段有沒有版本變數可檢查、有沒有原文語境可驗證。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>資安標準群（OWASP / RFC / NIST SP 800 系列 / CIS Benchmark / PCI DSS / ISO 27001）有三個跟一般技術文獻不同的特性：&lt;/p>
&lt;h3 id="特性-1best-practice-衰退速度快">特性 1：Best practice 衰退速度快&lt;/h3>
&lt;p>加密 / hashing 領域是最典型例子：&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">1995-2005：MD5 是 password hashing 常見選擇
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">2005-2010：MD5 deprecated、改 SHA-1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">2010-2015：SHA-1 弱、改 bcrypt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">2015-2020：bcrypt 仍 OK、PBKDF2 100k iter 仍 OK
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">2020-2026：建議升 argon2id、PBKDF2 推 600k+、bcrypt work factor 推 12+&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>任何 2020 年寫的「使用 bcrypt 即可」教學在 2026 年仍部分成立、但 work factor 推薦值已 outdated。沒標年份的引用 reader 沒有 review trigger。&lt;/p>
&lt;h3 id="特性-2原文常被引用扭曲">特性 2：原文常被引用扭曲&lt;/h3>
&lt;p>引用 chain 中常見的 drift：&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">原文（OWASP Cheat Sheet）：In contexts where session fixation is a concern, consider regenerating the session ID upon login.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">中介轉述：OWASP says regenerate session ID upon login.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">進一步引用：OWASP requires session regeneration on every login.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">最終讀者：「OWASP 強制要求每次 login 都 regenerate session」&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>語意從「conditional 建議」滑成「universal 強制」、原文的 conditional context（「session fixation is a concern」）被丟。Reader 套用時把 conditional 當 unconditional、可能在不需要的地方加複雜度、或在需要的地方因為「我已經做了」跳過 threat model 重新評估。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資安標準引用不是「這條 control 寫在 X 文件」、是「這條 control 在 X 版本 X 年份 X 語境下對 X actor 模型成立」。</strong> 五個變數任一變、引用就過時或扭曲。資安 best practice 衰退快、universal-flavored 引用（「OWASP 建議 X」「RFC 規定 Y」）會 silent 把過時或語境外的內容傳給 reader、產生 <a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security</a> 的 citation 維度產地。</p>
<table>
  <thead>
      <tr>
          <th>引用形態</th>
          <th>reader 套用時的判斷</th>
          <th>過時 / 扭曲時的後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「OWASP 建議 X」</td>
          <td>X 是 universal best practice</td>
          <td>套用時 OWASP 已改版、reader 不知道</td>
      </tr>
      <tr>
          <td>「OWASP Top 10 (2021) 建議 X、原文：『&hellip;』」</td>
          <td>X 在 2021 OWASP Top 10 語境下成立</td>
          <td>過時時 reader 知道要 check 新版</td>
      </tr>
  </tbody>
</table>
<p>差別在 reader 在實作 review 階段有沒有版本變數可檢查、有沒有原文語境可驗證。</p>
<hr>
<h2 id="情境">情境</h2>
<p>資安標準群（OWASP / RFC / NIST SP 800 系列 / CIS Benchmark / PCI DSS / ISO 27001）有三個跟一般技術文獻不同的特性：</p>
<h3 id="特性-1best-practice-衰退速度快">特性 1：Best practice 衰退速度快</h3>
<p>加密 / hashing 領域是最典型例子：</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">1995-2005：MD5 是 password hashing 常見選擇
</span></span><span class="line"><span class="ln">2</span><span class="cl">2005-2010：MD5 deprecated、改 SHA-1
</span></span><span class="line"><span class="ln">3</span><span class="cl">2010-2015：SHA-1 弱、改 bcrypt
</span></span><span class="line"><span class="ln">4</span><span class="cl">2015-2020：bcrypt 仍 OK、PBKDF2 100k iter 仍 OK
</span></span><span class="line"><span class="ln">5</span><span class="cl">2020-2026：建議升 argon2id、PBKDF2 推 600k+、bcrypt work factor 推 12+</span></span></code></pre></div><p>任何 2020 年寫的「使用 bcrypt 即可」教學在 2026 年仍部分成立、但 work factor 推薦值已 outdated。沒標年份的引用 reader 沒有 review trigger。</p>
<h3 id="特性-2原文常被引用扭曲">特性 2：原文常被引用扭曲</h3>
<p>引用 chain 中常見的 drift：</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">原文（OWASP Cheat Sheet）：In contexts where session fixation is a concern, consider regenerating the session ID upon login.
</span></span><span class="line"><span class="ln">2</span><span class="cl">中介轉述：OWASP says regenerate session ID upon login.
</span></span><span class="line"><span class="ln">3</span><span class="cl">進一步引用：OWASP requires session regeneration on every login.
</span></span><span class="line"><span class="ln">4</span><span class="cl">最終讀者：「OWASP 強制要求每次 login 都 regenerate session」</span></span></code></pre></div><p>語意從「conditional 建議」滑成「universal 強制」、原文的 conditional context（「session fixation is a concern」）被丟。Reader 套用時把 conditional 當 unconditional、可能在不需要的地方加複雜度、或在需要的地方因為「我已經做了」跳過 threat model 重新評估。</p>
<h3 id="特性-3版本之間語意可能反轉">特性 3：版本之間語意可能反轉</h3>
<p>OWASP Top 10 / NIST SP 800 系列、版本之間的 control 重點會大幅調整：</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">OWASP Top 10:
</span></span><span class="line"><span class="ln">2</span><span class="cl">  2017 → A1 Injection / A7 XSS
</span></span><span class="line"><span class="ln">3</span><span class="cl">  2021 → A03 Injection（含 XSS、合併）/ A08 Software and Data Integrity Failures（新類別）
</span></span><span class="line"><span class="ln">4</span><span class="cl">NIST SP 800-63B:
</span></span><span class="line"><span class="ln">5</span><span class="cl">  2014 版：強制 password 定期更換
</span></span><span class="line"><span class="ln">6</span><span class="cl">  2017 版：明示**不要**強制定期更換、除非有外洩證據</span></span></code></pre></div><p>引用「NIST 建議定期更換 password」在 2014 對、2017 後是反向違反 NIST。版本不標 = reader 可能引用到反向版本。</p>
<h3 id="特性-4internal-citation-也是-citation">特性 4：Internal citation 也是 citation</h3>
<p>問題節點 / problem-node 框架的章節常用內部連結（<code>[authentication]</code> <code>[session-invalidation]</code> 等 knowledge-cards）作為「control-of-record」，把實作細節下放到子頁。這些內部連結<strong>等同 citation</strong>——指向「這個 control 由那一頁定義」、章節讀者在這層形成判斷、再決定是否點進去。</p>
<p>Internal citation 同樣有四個失效模式：</p>
<table>
  <thead>
      <tr>
          <th>失效模式</th>
          <th>外部 citation（OWASP / RFC）</th>
          <th>Internal citation（knowledge-cards / 跨章引用）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>時效衰退</td>
          <td>OWASP 改版、引用過時版本</td>
          <td>knowledge-cards 內容更新、章節引用沒同步</td>
      </tr>
      <tr>
          <td>句意 drift</td>
          <td>conditional → unconditional 轉述</td>
          <td>章節用 control-name 暗示能力、子頁定義跟暗示不一致</td>
      </tr>
      <tr>
          <td>版本反轉</td>
          <td>NIST 2014 vs 2017 password 政策反向</td>
          <td>knowledge-card rewrite、原本 in-scope 變 out-of-scope</td>
      </tr>
      <tr>
          <td>Broken / dead link</td>
          <td>URL 變更、文件下架</td>
          <td>knowledge-card 改 slug / 移檔、章節連結 silent broken</td>
      </tr>
  </tbody>
</table>
<p>外部 citation 至少有版本號當 anchor、internal citation 連版本概念都沒有——更易 silent drift。所以 audit 跟 review trigger 對 internal 反而更嚴格。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>每個 security citation 加四個欄位：</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">[Citation X]
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 標準 / 文件：[全名 + 版本 + 年份]
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 原文 / quote：[原文一句、不轉述]
</span></span><span class="line"><span class="ln">4</span><span class="cl">- 引用 scope：[原文適用的 context / actor model / 前提條件]
</span></span><span class="line"><span class="ln">5</span><span class="cl">- Review trigger：[何時要 re-check 標準是否有新版]</span></span></code></pre></div><p>例（password hashing）：</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">- 標準：OWASP Password Storage Cheat Sheet（2024 update）
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 原文：「Use Argon2id with a minimum configuration of 19 MiB of memory, an iteration count of 2, and 1 degree of parallelism」
</span></span><span class="line"><span class="ln">3</span><span class="cl">- Scope：Web 應用 password hashing、針對個人 / 組織 actor、不適用 nation-state actor 或 high-throughput verification
</span></span><span class="line"><span class="ln">4</span><span class="cl">- Review trigger：每 12 月 re-check OWASP cheat sheet 是否有新建議；GPU 算力翻倍時提前 re-check</span></span></code></pre></div><p>例（session 管理）：</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">- 標準：OWASP Session Management Cheat Sheet（2024 update）
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 原文：「Session ID should be regenerated after any privilege level change (e.g., after a successful authentication or after a session token has elevated privileges)」
</span></span><span class="line"><span class="ln">3</span><span class="cl">- Scope：Web session ID rotation、conditional 在 privilege level change 時、不是「每次 request」也不是「每次 login」（對 already-authenticated session）
</span></span><span class="line"><span class="ln">4</span><span class="cl">- Review trigger：當 application 加入新的 privilege boundary（如 admin elevation）時 re-check</span></span></code></pre></div><h3 id="引用扭曲的-audit-流程">引用扭曲的 audit 流程</h3>
<p>對章節既有引用跑驗證 pass：</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">1. 列出所有 citation（外部：標準 / RFC / CVE；內部：knowledge-cards 連結 / 跨章引用）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 對每條 citation 找一手來源、記錄 URL + 版本 + 年份（外部）/ 最後修改 + slug（內部）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 對比文中轉述跟原文 / 子頁定義、check 三類 drift：
</span></span><span class="line"><span class="ln">4</span><span class="cl">   - Conditional → unconditional drift（原文有條件、文中沒條件）
</span></span><span class="line"><span class="ln">5</span><span class="cl">   - Specific → general drift（原文限特定 context、文中講通用）
</span></span><span class="line"><span class="ln">6</span><span class="cl">   - Recommendation → mandate drift（原文是 consider / recommend、文中是 must / required）
</span></span><span class="line"><span class="ln">7</span><span class="cl">4. drift 找到、補回原文 conditional / scope / language strength
</span></span><span class="line"><span class="ln">8</span><span class="cl">5. 標版本跟 review trigger（外部）/ 標 last-checked + sync owner（內部）
</span></span><span class="line"><span class="ln">9</span><span class="cl">6. 內部專屬 check：連結是否 broken（slug 改了 / 檔案移了）、子頁是否仍存在 / 仍 in scope</span></span></code></pre></div><p>集合運算讓引用扭曲從「靠記憶」升級到「可驗證」。Internal citation 多兩個專屬步驟（broken link + slug drift）、跟 <a href="../url-slug-must-be-explicit-fact/">#93 URL slug 必須顯式定義為 fact</a> 同骨——identifier 跨工具 / 跨檔案沒 fact 化、就會 silent broken。</p>
<h3 id="review-trigger-的-cadence-設計">Review trigger 的 cadence 設計</h3>
<p>不同類型 citation 的 review cadence 不同：</p>
<table>
  <thead>
      <tr>
          <th>Citation 類型</th>
          <th>建議 review cadence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Crypto primitive 強度參數</td>
          <td>每 6-12 月（actor 算力會變）</td>
      </tr>
      <tr>
          <td>OWASP Top 10 / Cheat Sheet</td>
          <td>每 12-24 月（major 改版頻率）</td>
      </tr>
      <tr>
          <td>RFC（已 finalized）</td>
          <td>每 24-36 月（除非有新 RFC supersede）</td>
      </tr>
      <tr>
          <td>CVE / 特定漏洞</td>
          <td>即時（一次性事件、不需 cadence、引用後標記「fixed in vX.Y」）</td>
      </tr>
      <tr>
          <td><strong>Internal knowledge-cards</strong></td>
          <td><strong>每 6 月（內部演化快、無版本號當 anchor）</strong></td>
      </tr>
      <tr>
          <td><strong>跨章 / 跨模組引用</strong></td>
          <td><strong>每次大改子頁時 broadcast；無 broadcast 時每 6 月 sweep</strong></td>
      </tr>
      <tr>
          <td>NIST SP 800 系列</td>
          <td>每 24 月（NIST 改版頻率）</td>
      </tr>
      <tr>
          <td>PCI DSS / ISO 27001</td>
          <td>每 24-36 月（合規標準改版頻率）</td>
      </tr>
  </tbody>
</table>
<p>跟 <a href="../escalation-trigger-quantification/">#91 升級 trigger 的量化設計</a> 同骨——「之後再 review」不是 trigger、有 cadence + owner + threshold 才是 trigger。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="過時-citation-silent-變成過時實作">過時 citation silent 變成過時實作</h3>
<p>reader 信任引用、用 citation 內容實作、citation 過時後實作不知道、新 best practice 沒被採用。Crypto 領域最常見：MD5 / SHA-1 / 弱 PBKDF2 iteration / 過時 cipher suite 在生產系統留存幾十年的案例不少、原因常常是「教學 / spec 沒更新、實作跟著沒更新」。</p>
<p>跟 <a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security</a> 同病、citation 維度的具體展現：reader 以為「我用了標準推薦」就安全、實際標準早改、自己用的是 deprecated 版本。</p>
<h3 id="扭曲-citation-把-conditional-變強制--把-specific-變通用">扭曲 citation 把 conditional 變強制 / 把 specific 變通用</h3>
<p>引用扭曲的後果有兩面：</p>
<ul>
<li><strong>Conditional → unconditional</strong>：reader 在不需要的地方加複雜度、團隊成本上升、卻不解決真 threat</li>
<li><strong>Specific → general</strong>：reader 把特定 context 的 control 套到不同 context、可能 silent 失效</li>
</ul>
<p>兩面都讓 mitigation 跟 threat 對位錯誤（<a href="../mitigation-threat-alignment/">#102 mitigation-threat-alignment</a>）。</p>
<h3 id="引用-chain-越長扭曲累積越嚴重">引用 chain 越長、扭曲累積越嚴重</h3>
<p>教學 → 教學 → 教學 的 chain 中、每一層轉述都可能 drift。citation 沒回到一手原文、整條 chain 共享同一個扭曲、攻擊者繞過扭曲版的 mitigation 一次、所有採用該 chain 的 implementation 都中。<strong>citation 的時效跟精確不是個別文章問題、是 ecosystem 問題</strong>——一手原文 + 版本 + 原文 quote 是 minimum cost 的修法。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review</a></td>
          <td><strong>citation 是 metadata surface 的延伸</strong> — citation 是讀者的「外部 source」入口、跟 title / heading / link label 並列為 metadata；本卡是 #97 在引用維度的展開</td>
      </tr>
      <tr>
          <td><a href="../url-slug-must-be-explicit-fact/">#93 URL slug 必須顯式定義為 fact</a></td>
          <td><strong>identifier 同骨 + internal citation 強相關</strong> — slug 是內部 identifier、外部 citation / 內部 citation 都需要 explicit fact（版本 / 年份 / 原文 / slug + last-checked）；internal citation 沒版本號當 anchor、跟 #93 SSoT 違反同類風險、broken-link / drift 是 internal citation 專屬失效</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td><strong>同骨 ceiling</strong> — 引用標準名稱 = 字面、引用句意對到原文 context = 行為；stop at 字面 = false confidence</td>
      </tr>
      <tr>
          <td><a href="../escalation-trigger-quantification/">#91 升級 trigger 的量化設計</a></td>
          <td><strong>review trigger 同骨</strong> — #91 在 capability 升級的 trigger 設計、本卡在 citation review 的 cadence 設計；都是「沒 trigger = 結構性跳過」</td>
      </tr>
      <tr>
          <td><a href="../false-sense-of-security-as-primary-failure/">#100 False sense of security 主要失敗模式</a></td>
          <td><strong>#100 的 dimension 4</strong> — citation 過時 / 扭曲是 false sense 的第四大產地（dimension 1-3 = threat / mitigation / context、本卡 = 引用 source）</td>
      </tr>
      <tr>
          <td><a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a></td>
          <td>上游動機 — verifiability-first 的 dimension 4</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>引用「OWASP / NIST / RFC / CIS」沒標年份 / 版本</td>
          <td>補版本 + 年份、確認當前是否仍是 current</td>
      </tr>
      <tr>
          <td>引用是轉述、沒原文 quote</td>
          <td>找一手來源、補原文 quote、check 是否被 drift</td>
      </tr>
      <tr>
          <td>「OWASP <strong>建議</strong> X」「RFC <strong>規定</strong> Y」當 universal</td>
          <td>補 scope（在什麼 context / actor model 下成立）</td>
      </tr>
      <tr>
          <td>Crypto / hashing 強度參數是固定值（10 / 100k / 32 char）</td>
          <td>補 review trigger（每 6-12 月 re-check actor 算力跟標準）</td>
      </tr>
      <tr>
          <td>Citation 是「最佳實踐」「業界標準」當 anchor、沒列具體文件</td>
          <td>補具體標準名稱 + 版本、不能用 vague reference</td>
      </tr>
      <tr>
          <td>章節寫於 N 年前、沒提 last reviewed 日期</td>
          <td>補 last reviewed 標記、設下次 review trigger</td>
      </tr>
      <tr>
          <td>Conditional 原文被引成 unconditional（「強制」「必須」「總是」）</td>
          <td>找原文 conditional context、補回 scope qualifier</td>
      </tr>
      <tr>
          <td>「之後標準改了再更新」</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72</a> 結構性跳過、補 review cadence + owner</td>
      </tr>
      <tr>
          <td>章節用 internal link（knowledge-cards / 跨章引用）作為 control-of-record、沒 last-checked / sync owner</td>
          <td>等同未驗證的 citation；補 last-checked + sync owner、子頁大改時 broadcast 到引用方</td>
      </tr>
      <tr>
          <td>Internal link 連結還在但目標頁 slug / 內容已改、章節原本暗示的 control 跟現在不對應</td>
          <td>Silent broken / drift；定期跑連結 sweep + 文意對比、跟外部 citation 同流程處理</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：資安內容引用標準（auth / crypto / 傳輸 / 防護 / 合規）；<strong>內部 citation</strong>（knowledge-cards 連結、跨章 / 跨模組引用作為 control-of-record）；任何 best practice 衰退快、版本之間語意會反轉的領域（cloud security 配置、container 安全、特定 framework 的安全 idiom）</li>
<li><strong>不適用</strong>：純歷史 / 概念介紹（不依賴 current best practice）、學術 retrospective（討論 historical 標準時版本本身是內容）</li>
<li><strong>邊界</strong>：「citation 時效跟精確」≠「窮舉所有版本變更」——只列當前文章涵蓋 scope 的 citation、追到一手 + 版本 + scope qualifier 即可；判別準則：「如果這條 citation 過時或語境變、reader 會做錯什麼？」——會做錯 → 補完整四欄位；不會做錯（純歷史 reference）→ 標年份即可</li>
<li><strong>過度引用反例</strong>：每段話都附 citation 鏈 + 原文 quote + 三條 review trigger、文章變 footnote-driven、reader 讀不下去；citation 投資量級對應該段對 reader 實作的影響——核心 mitigation 段值得四欄位完整、background 段標版本 + URL 即可</li>
</ul>
<p>本卡是資安 audit 第四個維度（citation）、配 <a href="../threat-model-explicitness/">#101</a> / <a href="../mitigation-threat-alignment/">#102</a> / <a href="../mitigation-context-dependence/">#103</a> 形成完整 audit dimension 集合（threat / mitigation / context / citation）。後續 <a href="../security-audit-recommendation-tiers/">#105 audit recommendation 層級</a> 把四維度的 weakness 統合成 recommendation 決策。</p>
]]></content:encoded></item><item><title>Audit recommendation 層級：accept / minor / major / 教錯不可保留</title><link>https://tarrragon.github.io/blog/report/security-audit-recommendation-tiers/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/security-audit-recommendation-tiers/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資安 audit 的 recommendation 是 ship 決策、不是評語。&lt;/strong> 把每個 weakness trace 到具體 tier、輸出可被 build process / publish gate 引用——不該停在「這裡可改善」的軟性建議。四個 tier 是 monotonic decision shape：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Tier&lt;/th>
 &lt;th>意涵&lt;/th>
 &lt;th>Ship 決策&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Accept&lt;/td>
 &lt;td>無 weakness 或全在容忍範圍&lt;/td>
 &lt;td>直接 ship&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Minor revise&lt;/td>
 &lt;td>邊界 / contrast / 版本標記類小改&lt;/td>
 &lt;td>補完即可 ship、不阻擋 timeline&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Major revise&lt;/td>
 &lt;td>結構性 false sense / 對位失效&lt;/td>
 &lt;td>重寫對應段、ship 前必須修復&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Withdraw&lt;/strong>&lt;/td>
 &lt;td>內容主動誤導、ship = 增加 risk&lt;/td>
 &lt;td>&lt;strong>必須移除或全換、不存在 ship&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第四層是資安 audit 跟一般學術 peer review 的關鍵差異——學術 reject 會給投稿者改寫機會、本 audit 的 withdraw 是「&lt;strong>保留 = 增加生產系統 risk&lt;/strong>」的硬決策。跟 &lt;a href="../incremental-shipping-criteria/">#76 incremental shipping criteria&lt;/a> 反向：可逆內容可分批 ship 改善、不可逆 risk 內容不能。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>audit 報告若只給「找到 N 個問題」的 flat list、團隊收到後無法決策、最後常變成「慢慢改」、article ship 跟 audit 改善的 timeline 完全脫鉤。Tier 化的 recommendation 把 weakness 轉成決策訊號：&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">Flat list（沒層級）：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">- 第 3 段沒寫 threat model boundary
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">- 第 5 段 mitigation 沒寫 mechanism
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">- 第 7 段引用 OWASP 沒標版本
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">- 第 9 段 bcrypt work factor = 10、針對 nation-state 弱
&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;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">Tiered（分層）：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">- Withdraw: 第 9 段 bcrypt work factor 描述會直接讓 reader 用 weak setting、必須改寫或移除
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">- Major revise: 第 5 段 defense theater、整段重寫 mechanism + 前提
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">- Minor revise: 第 3 段補 threat model 對稱、第 7 段補 OWASP 版本
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">決策結果：第 9 段必須現在改、第 5 段下個 sprint 改、第 3/7 段順手補&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>層級給的是「&lt;strong>先做什麼 / 什麼擋 ship / 什麼可緩&lt;/strong>」的明確排序、不是改善優先序的軟建議。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="四-tier-判準">四 tier 判準&lt;/h3>
&lt;p>每個 weakness 套這個決策樹：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資安 audit 的 recommendation 是 ship 決策、不是評語。</strong> 把每個 weakness trace 到具體 tier、輸出可被 build process / publish gate 引用——不該停在「這裡可改善」的軟性建議。四個 tier 是 monotonic decision shape：</p>
<table>
  <thead>
      <tr>
          <th>Tier</th>
          <th>意涵</th>
          <th>Ship 決策</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Accept</td>
          <td>無 weakness 或全在容忍範圍</td>
          <td>直接 ship</td>
      </tr>
      <tr>
          <td>Minor revise</td>
          <td>邊界 / contrast / 版本標記類小改</td>
          <td>補完即可 ship、不阻擋 timeline</td>
      </tr>
      <tr>
          <td>Major revise</td>
          <td>結構性 false sense / 對位失效</td>
          <td>重寫對應段、ship 前必須修復</td>
      </tr>
      <tr>
          <td><strong>Withdraw</strong></td>
          <td>內容主動誤導、ship = 增加 risk</td>
          <td><strong>必須移除或全換、不存在 ship</strong></td>
      </tr>
  </tbody>
</table>
<p>第四層是資安 audit 跟一般學術 peer review 的關鍵差異——學術 reject 會給投稿者改寫機會、本 audit 的 withdraw 是「<strong>保留 = 增加生產系統 risk</strong>」的硬決策。跟 <a href="../incremental-shipping-criteria/">#76 incremental shipping criteria</a> 反向：可逆內容可分批 ship 改善、不可逆 risk 內容不能。</p>
<hr>
<h2 id="情境">情境</h2>
<p>audit 報告若只給「找到 N 個問題」的 flat list、團隊收到後無法決策、最後常變成「慢慢改」、article ship 跟 audit 改善的 timeline 完全脫鉤。Tier 化的 recommendation 把 weakness 轉成決策訊號：</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">Flat list（沒層級）：
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">- 第 3 段沒寫 threat model boundary
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">- 第 5 段 mitigation 沒寫 mechanism
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">- 第 7 段引用 OWASP 沒標版本
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">- 第 9 段 bcrypt work factor = 10、針對 nation-state 弱
</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><span class="line"><span class="ln"> 9</span><span class="cl">Tiered（分層）：
</span></span><span class="line"><span class="ln">10</span><span class="cl">- Withdraw: 第 9 段 bcrypt work factor 描述會直接讓 reader 用 weak setting、必須改寫或移除
</span></span><span class="line"><span class="ln">11</span><span class="cl">- Major revise: 第 5 段 defense theater、整段重寫 mechanism + 前提
</span></span><span class="line"><span class="ln">12</span><span class="cl">- Minor revise: 第 3 段補 threat model 對稱、第 7 段補 OWASP 版本
</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">決策結果：第 9 段必須現在改、第 5 段下個 sprint 改、第 3/7 段順手補</span></span></code></pre></div><p>層級給的是「<strong>先做什麼 / 什麼擋 ship / 什麼可緩</strong>」的明確排序、不是改善優先序的軟建議。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="四-tier-判準">四 tier 判準</h3>
<p>每個 weakness 套這個決策樹：</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">Q1：reader 照這段實作會不會主動產生破口？
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  是 → Withdraw（不可保留）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  否 → Q2
</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">Q2：weakness 是結構性（多 dimension 同時失效）還是局部（單一 dimension 缺）？
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  結構性 → Major revise
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  局部 → Q3
</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">Q3：補完 weakness 的 cost 是「補一句 / 一表」還是「重寫一段」？
</span></span><span class="line"><span class="ln">10</span><span class="cl">  一句 / 一表 → Minor revise
</span></span><span class="line"><span class="ln">11</span><span class="cl">  重寫一段 → Major revise
</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">Q4：weakness 在容忍範圍（背景段 / 低 stakes 段、reader 不會直接照做）？
</span></span><span class="line"><span class="ln">14</span><span class="cl">  在 → Accept（可選 minor 但不要求）
</span></span><span class="line"><span class="ln">15</span><span class="cl">  不在 → 走 Q3</span></span></code></pre></div><h3 id="各-tier-的-fix-模式">各 tier 的 fix 模式</h3>
<table>
  <thead>
      <tr>
          <th>Tier</th>
          <th>Fix 模式</th>
          <th>Ship gate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Accept</td>
          <td>無 fix 或自願性 minor</td>
          <td>不阻擋</td>
      </tr>
      <tr>
          <td>Minor revise</td>
          <td>補 boundary / 加 contrast / 標版本 / 補連結</td>
          <td>不阻擋（可 follow-up）</td>
      </tr>
      <tr>
          <td>Major revise</td>
          <td>重寫段落 + 補 mechanism / 前提 / context</td>
          <td>阻擋直到 fix 完成</td>
      </tr>
      <tr>
          <td>Withdraw</td>
          <td>移除整段 / 加 deprecation banner + redirect / 全換現代版</td>
          <td>阻擋直到處理</td>
      </tr>
  </tbody>
</table>
<h3 id="withdraw-的具體訊號">Withdraw 的具體訊號</h3>
<p>什麼狀態算 withdraw？四個訊號：</p>
<ol>
<li><strong>過時 crypto / hashing primitive 沒 deprecation 標記</strong>：教 MD5 / SHA-1 / 弱 PBKDF2 但沒明示「這是過時、不要用」</li>
<li><strong>扭曲 citation 改變原文語意</strong>：把 OWASP conditional 引成 unconditional、或反向違反現行標準（NIST 的 password 定期更換 case）</li>
<li><strong>違反 current best practice 的步驟說明</strong>：教讀者主動關閉 mitigation（disable HSTS / CSP / SameSite）作為 workaround、沒明示「workaround 引入的新 risk」</li>
<li><strong>Defense theater 例子當示範</strong>：用名稱層 mitigation 對位（rate limit「擋」brute force）作為步驟、reader 照做不擋實際 mechanism</li>
</ol>
<p>四訊號的共通：<strong>reader 照做後實作會主動 worse than not having read</strong>。Withdraw 不是嚴格、是 risk-asymmetric（<a href="../security-teaching-rigor-asymmetry/">#99</a>）下的必要決策。</p>
<h3 id="audit-report-輸出格式">Audit report 輸出格式</h3>
<p>學術 peer review 的格式對應到本 audit：</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"># Audit Report: &lt;章節 / 文章 title&gt;
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">## Summary
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">&lt;1-2 句：主要 audit 結論 + 整體 tier&gt;
</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">## Strengths
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">- &lt;段 / dimension 跟其優點&gt;
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">## Weaknesses by dimension
</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">### Threat model（[#101](../threat-model-explicitness/)）
</span></span><span class="line"><span class="ln">12</span><span class="cl">- [Tier]: 段 N、[具體 weakness 描述]、[fix 建議]
</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">### Mitigation 對位（[#102](../mitigation-threat-alignment/)）
</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></span><span class="line"><span class="ln">17</span><span class="cl">### Context-dependence（[#103](../mitigation-context-dependence/)）
</span></span><span class="line"><span class="ln">18</span><span class="cl">- ...
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">### Citation（[#104](../security-citation-currency-and-precision/)）
</span></span><span class="line"><span class="ln">21</span><span class="cl">- ...
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">## Blocking conditions
</span></span><span class="line"><span class="ln">24</span><span class="cl">&lt;必須 fix 才能 ship 的 weakness 清單、按 tier 排序&gt;
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl">## Recommendation
</span></span><span class="line"><span class="ln">27</span><span class="cl">&lt;Accept / Minor revise / Major revise / Withdraw + 整體決策說明&gt;</span></span></code></pre></div><p>格式跟學術 peer review 同骨、欄位對應 audit dimension（#101-104）、輸出可直接餵 ship gate 工具。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="audit-變評語改善-timeline-跟-ship-完全脫鉤">Audit 變評語、改善 timeline 跟 ship 完全脫鉤</h3>
<p>flat list 的 audit 給「找到問題」、team 把問題列入 backlog、backlog 永遠排不到上面（<a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發會被結構性跳過</a>）。tier 化讓 audit 從「評語」變「ship 決策 input」、跟 timeline 強耦合。</p>
<h3 id="withdraw-level-內容繼續-ship生產系統-risk-持續累積">Withdraw-level 內容繼續 ship、生產系統 risk 持續累積</h3>
<p>最危險的 case 是 audit 找到 withdraw-level weakness（過時 crypto、扭曲 citation）但用 minor / major 處置——讓內容繼續存在並擴散。教學擴散 = silent gap 集體放大（<a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security</a>），withdraw 是 cut-off 訊號、不是嚴格、是必要。</p>
<h3 id="各-tier-之間的決策邏輯模糊reviewer-之間判準不一致">各 tier 之間的決策邏輯模糊、reviewer 之間判準不一致</h3>
<p>沒明確 tier 判準、不同 reviewer 對同一個 weakness 給不同建議——有人覺得「補一行就好」（minor）、有人覺得「整段重寫」（major）、有人覺得「移除」（withdraw）。決策不一致 = audit 失去結構性 value、退化成個人意見集合。tier 判準（決策樹四問題）讓判準可重現、跨 reviewer 收斂。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../decision-presentation-options-recommendation/">#74 決策呈現：選項 + 推薦 + 開放修改</a></td>
          <td><strong>同骨決策呈現</strong> — #74 是給 user 決策的 options + recommendation 模板、本卡是給 ship gate 的 tier + recommendation 模板；都把整理成本攤開、不丟「你想怎麼做」開放問</td>
      </tr>
      <tr>
          <td><a href="../incremental-shipping-criteria/">#76 分批 ship：低風險可見價值先行</a></td>
          <td><strong>反面對照</strong> — #76 適用可逆內容、本卡的 withdraw 適用不可逆 risk 內容、分批 ship 邏輯不適用；本卡是 #76 在 risk-asymmetric 領域的硬邊界</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五個維度</a></td>
          <td><strong>本卡的決策維度</strong> — #79 是 meta、本卡是其中「呈現 + 策略疊加 + 批次」三維在 audit 報告的具體實現</td>
      </tr>
      <tr>
          <td><a href="../escalation-trigger-quantification/">#91 升級 trigger 的量化設計</a></td>
          <td><strong>withdraw 是 blocking trigger</strong> — #91 在 capability 升級的 trigger 設計、本卡的 withdraw 是 ship 阻擋的 trigger；都是「沒明確 trigger = 不會 fire」</td>
      </tr>
      <tr>
          <td><a href="../false-sense-of-security-as-primary-failure/">#100 False sense of security 主要失敗模式</a></td>
          <td><strong>本卡是消滅 #100 的 ship 決策面</strong> — #101-104 是發現 false sense 的維度、本卡是發現後的處置決策</td>
      </tr>
      <tr>
          <td><a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a></td>
          <td>上游動機 — risk-asymmetric 直接驅動 withdraw tier 的存在；一般 audit（一般教學）只需要 accept / minor / major、資安 audit 必須加 withdraw</td>
      </tr>
      <tr>
          <td><a href="../yes-no-binary-collapse/">#80 Yes/No 二選 collapse</a></td>
          <td><strong>避免 collapse</strong> — 「audit 通過嗎」是 yes/no collapse、tier 化是把 1 bit 展開成 4 個 monotonic 層級、保留決策維度</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Audit 結論是「找到 N 個問題」flat list</td>
          <td>把每個 weakness 跑 tier 決策樹、輸出 tier-grouped report</td>
      </tr>
      <tr>
          <td>找到過時 crypto / 扭曲 citation 但給 minor revise</td>
          <td>升級到 withdraw、ship gate 必須阻擋</td>
      </tr>
      <tr>
          <td>「之後改善」「下個版本補」當 weakness 處置</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72</a> 結構性跳過、補 ship gate 強制 trigger</td>
      </tr>
      <tr>
          <td>不同 reviewer 對同 weakness 給不同 tier</td>
          <td>補決策樹、跑判準收斂</td>
      </tr>
      <tr>
          <td>Audit pass 但實作後事故、回溯到 audit 沒 catch 的 weakness</td>
          <td>補 weakness 到對應 dimension（#101-104）、檢查 tier 判準是否需調整</td>
      </tr>
      <tr>
          <td>沒「strengths」段</td>
          <td>補 strengths、reviewer 視角不只 weakness、strengths 是 audit completeness 的訊號</td>
      </tr>
      <tr>
          <td>Recommendation 沒明確 ship gate 對應</td>
          <td>補 blocking conditions 段、明示哪些 tier 阻擋 ship</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：資安內容 audit 的產出格式（章節 audit / 文章 audit / 跨章節 review）；任何「reader 照做後錯誤不可逆」的高 stakes 領域 audit（concurrency 正確性、distributed consistency、financial / medical 計算）</li>
<li><strong>不適用</strong>：一般技術內容 audit（不需要 withdraw tier、accept / minor / major 三層即可）、研究探討文章的 review（學術 reject 跟 withdraw 語意不同）</li>
<li><strong>邊界</strong>：「Withdraw」≠「全文重寫」——可以是「移除有問題的段 + 加 deprecation 標 + redirect 到 current best practice 段」、不必整篇重做；判別準則：「reader 看到這個處置版本後、會不會用過時 / 扭曲版本實作？」——不會 → withdraw 處置 OK、會 → 需要更深的處置（移除整段 / 整篇）</li>
<li><strong>過度 tier 化反例</strong>：把每個段都評 tier、文章變評分表、reviewer 投資爆炸；tier 投資量級對應內容對 reader 實作的影響——核心 mitigation 段需 tier、background 段直接 accept 即可</li>
</ul>
<p>本卡是資安 audit 系列（#99-105）的決策面收尾、把 #101-104 四個 dimension 的 weakness 統合成 ship 決策。後續對應的 skill reference（<code>auditing-articles.md</code>）會以本卡的 tier + report 格式為輸出模板。</p>
]]></content:encoded></item><item><title>Cross-Reviewer Convergence：多 Reviewer 收斂的 finding 比單 Reviewer flag 信號強</title><link>https://tarrragon.github.io/blog/report/cross-reviewer-convergence-priority-weighting/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/cross-reviewer-convergence-priority-weighting/</guid><description>&lt;h2 id="核心跨-reviewer-收斂的-finding-信號強">核心：跨 reviewer 收斂的 finding 信號強&lt;/h2>
&lt;p>當跑 multi-reviewer parallel audit（4-reviewer / N-reviewer）、最 high-priority 不是 &lt;em>單一 reviewer flag 的 most severe finding&lt;/em>、是 &lt;em>多個 reviewer 從不同軸獨立 flag 的同一 finding&lt;/em>。&lt;/p>
&lt;p>直覺：&lt;/p>
&lt;ul>
&lt;li>單 reviewer flag P0 finding 是 &lt;em>該軸的判斷&lt;/em>&lt;/li>
&lt;li>跨 reviewer convergence flag 是 &lt;em>多軸共同 hit 同一點&lt;/em>、信號收斂&lt;/li>
&lt;/ul>
&lt;p>機制：N 個獨立 axis 隨機 hit 同一 finding 的機率隨 N 指數下降 — 兩個 axis 偶然 hit 同點機率低、三個 axis hit 同點機率更低。所以 convergence 排除 &lt;em>單 reviewer 主觀 / 偏好 bias&lt;/em>、留 &lt;em>系統性 issue&lt;/em>。&lt;/p>
&lt;h2 id="casemysql-4-reviewer-audit">Case：MySQL 4-reviewer audit&lt;/h2>
&lt;p>跑 4-reviewer audit（A 寫作規範 / B 跨檔一致性 / C 技術準確性 / D 結構性質疑）對 MySQL 17 篇：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Finding&lt;/th>
 &lt;th>Flagged by&lt;/th>
 &lt;th>Convergence&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>4 篇 migration playbook 缺 weight + banner&lt;/td>
 &lt;td>Reviewer A + Reviewer B&lt;/td>
 &lt;td>&lt;strong>2 軸&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Frame uniformity（5 個踩雷 100% 重複）&lt;/td>
 &lt;td>Reviewer A + Reviewer D&lt;/td>
 &lt;td>&lt;strong>2 軸&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PlanetScale FK 過時 claim&lt;/td>
 &lt;td>Reviewer C 單獨&lt;/td>
 &lt;td>1 軸&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PG CTE 版本錯（6.4 vs 8.4）&lt;/td>
 &lt;td>Reviewer C 單獨&lt;/td>
 &lt;td>1 軸&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Connection memory 衝突（3MB vs 8-10MB）&lt;/td>
 &lt;td>Reviewer B 單獨&lt;/td>
 &lt;td>1 軸&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework bias（Type A/C/E 集中）&lt;/td>
 &lt;td>Reviewer D 單獨&lt;/td>
 &lt;td>1 軸&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>2 軸 convergence 的 finding（缺 weight + frame uniformity）信號特別強 — 兩個 reviewer 從不同 audit 維度（寫作規範軸 vs 跨檔一致性軸）獨立判斷出同一 issue。&lt;/p>
&lt;p>對比：PlanetScale FK 是 &lt;em>單 reviewer 找到的 highest-severity finding&lt;/em>（invalidates 整段 Phase 1 audit premise）、但是 &lt;em>單軸 flag&lt;/em>。&lt;/p>
&lt;p>兩種都 P0、但 &lt;em>priority weighting&lt;/em> 應該不同：&lt;/p>
&lt;ul>
&lt;li>2 軸 convergence finding：&lt;em>structurally important&lt;/em>、是 batch level pattern&lt;/li>
&lt;li>單軸 high-severity finding：&lt;em>technically critical&lt;/em>、specific issue&lt;/li>
&lt;/ul>
&lt;h2 id="機制為什麼-convergence-比-severity-重要">機制：為什麼 convergence 比 severity 重要&lt;/h2>
&lt;h3 id="1-單-reviewer-flag-有-axis-specific-bias">1. 單 reviewer flag 有 axis-specific bias&lt;/h3>
&lt;p>每個 reviewer 用特定 audit 軸（寫作規範 / 一致性 / 技術 / 結構）。單軸 flag 帶該軸的 &lt;em>judgment preference&lt;/em>：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心跨-reviewer-收斂的-finding-信號強">核心：跨 reviewer 收斂的 finding 信號強</h2>
<p>當跑 multi-reviewer parallel audit（4-reviewer / N-reviewer）、最 high-priority 不是 <em>單一 reviewer flag 的 most severe finding</em>、是 <em>多個 reviewer 從不同軸獨立 flag 的同一 finding</em>。</p>
<p>直覺：</p>
<ul>
<li>單 reviewer flag P0 finding 是 <em>該軸的判斷</em></li>
<li>跨 reviewer convergence flag 是 <em>多軸共同 hit 同一點</em>、信號收斂</li>
</ul>
<p>機制：N 個獨立 axis 隨機 hit 同一 finding 的機率隨 N 指數下降 — 兩個 axis 偶然 hit 同點機率低、三個 axis hit 同點機率更低。所以 convergence 排除 <em>單 reviewer 主觀 / 偏好 bias</em>、留 <em>系統性 issue</em>。</p>
<h2 id="casemysql-4-reviewer-audit">Case：MySQL 4-reviewer audit</h2>
<p>跑 4-reviewer audit（A 寫作規範 / B 跨檔一致性 / C 技術準確性 / D 結構性質疑）對 MySQL 17 篇：</p>
<table>
  <thead>
      <tr>
          <th>Finding</th>
          <th>Flagged by</th>
          <th>Convergence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>4 篇 migration playbook 缺 weight + banner</td>
          <td>Reviewer A + Reviewer B</td>
          <td><strong>2 軸</strong></td>
      </tr>
      <tr>
          <td>Frame uniformity（5 個踩雷 100% 重複）</td>
          <td>Reviewer A + Reviewer D</td>
          <td><strong>2 軸</strong></td>
      </tr>
      <tr>
          <td>PlanetScale FK 過時 claim</td>
          <td>Reviewer C 單獨</td>
          <td>1 軸</td>
      </tr>
      <tr>
          <td>PG CTE 版本錯（6.4 vs 8.4）</td>
          <td>Reviewer C 單獨</td>
          <td>1 軸</td>
      </tr>
      <tr>
          <td>Connection memory 衝突（3MB vs 8-10MB）</td>
          <td>Reviewer B 單獨</td>
          <td>1 軸</td>
      </tr>
      <tr>
          <td>Framework bias（Type A/C/E 集中）</td>
          <td>Reviewer D 單獨</td>
          <td>1 軸</td>
      </tr>
  </tbody>
</table>
<p>2 軸 convergence 的 finding（缺 weight + frame uniformity）信號特別強 — 兩個 reviewer 從不同 audit 維度（寫作規範軸 vs 跨檔一致性軸）獨立判斷出同一 issue。</p>
<p>對比：PlanetScale FK 是 <em>單 reviewer 找到的 highest-severity finding</em>（invalidates 整段 Phase 1 audit premise）、但是 <em>單軸 flag</em>。</p>
<p>兩種都 P0、但 <em>priority weighting</em> 應該不同：</p>
<ul>
<li>2 軸 convergence finding：<em>structurally important</em>、是 batch level pattern</li>
<li>單軸 high-severity finding：<em>technically critical</em>、specific issue</li>
</ul>
<h2 id="機制為什麼-convergence-比-severity-重要">機制：為什麼 convergence 比 severity 重要</h2>
<h3 id="1-單-reviewer-flag-有-axis-specific-bias">1. 單 reviewer flag 有 axis-specific bias</h3>
<p>每個 reviewer 用特定 audit 軸（寫作規範 / 一致性 / 技術 / 結構）。單軸 flag 帶該軸的 <em>judgment preference</em>：</p>
<ul>
<li>Reviewer A 偏好 <em>寫作風格規範</em>、可能 flag 過嚴</li>
<li>Reviewer C 偏好 <em>technical correctness</em>、可能 flag 一些 <em>正確但 niche</em> 議題</li>
</ul>
<p>單軸 flag finding 可能是 <em>該軸 perspective 的 P0、其他軸 perspective 不重要</em>。</p>
<h3 id="2-跨-axis-convergence-排除-axis-specific-bias">2. 跨 axis convergence 排除 axis-specific bias</h3>
<p>當兩個 reviewer 從 <em>不同 axis</em> 獨立 flag 同 finding、表示這個 issue 對 <em>多種 judgment perspective</em> 都 reachable — 是 <em>系統性 pattern</em>、不是單一 perspective 的偏好。</p>
<p>舉例：「4 篇 migration playbook 缺 weight」</p>
<ul>
<li>Reviewer A 從 <em>寫作規範</em> 角度 flag：missing frontmatter required field</li>
<li>Reviewer B 從 <em>跨檔一致性</em> 角度 flag：13 篇 deep article 有 weight、4 篇 migration 沒有、不對齊</li>
</ul>
<p>兩個獨立 reasoning path 到同一 finding、信號收斂、是 <em>結構性問題</em>。</p>
<h3 id="3-convergence-finding-修一次解決多-reviewer-flag">3. Convergence finding 修一次解決多 reviewer flag</h3>
<p>實作層：</p>
<ul>
<li>單軸 P0：修 → 解決 1 個 reviewer 的 flag</li>
<li>雙軸 convergence：修 → 解決 2 個 reviewer 的 flag</li>
</ul>
<p>ROI 上 convergence finding 修法效率 2x。</p>
<h3 id="4-convergence-揭露-audit-framework-blindspot-的補集">4. Convergence 揭露 audit framework blindspot 的補集</h3>
<p>如果某 finding <em>所有 reviewer 都沒 flag</em>、可能：</p>
<ul>
<li>沒問題（true negative）</li>
<li>所有 axis 都看不到（structural blindspot）</li>
</ul>
<p>如果某 finding <em>只一 reviewer flag</em>、可能：</p>
<ul>
<li>Niche but real（axis-specific catch）</li>
<li>Axis-specific bias</li>
</ul>
<p>如果某 finding <em>多 reviewer flag</em>、強：</p>
<ul>
<li>多 axis 收斂 → 高度 likely true positive</li>
<li>排除 axis-specific bias</li>
</ul>
<h2 id="修法cross-reviewer-convergence-matrix">修法：Cross-reviewer convergence matrix</h2>
<h3 id="1-multi-reviewer-audit-後做-convergence-matrix">1. Multi-reviewer audit 後做 convergence matrix</h3>
<p>收齊 N 個 reviewer report 後、不是 merge findings list、是建 matrix：</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">Finding          | Reviewer A | Reviewer B | Reviewer C | Reviewer D | Convergence
</span></span><span class="line"><span class="ln">2</span><span class="cl">─────────────────┼────────────┼────────────┼────────────┼────────────┼────────────
</span></span><span class="line"><span class="ln">3</span><span class="cl">Missing weight   |     P0     |     P0     |            |            |    **2**
</span></span><span class="line"><span class="ln">4</span><span class="cl">Frame uniformity |     P1     |            |            |     -      |    **2**
</span></span><span class="line"><span class="ln">5</span><span class="cl">FK claim 過時    |            |            |     P0     |            |    1
</span></span><span class="line"><span class="ln">6</span><span class="cl">CTE version 錯   |            |            |     P0     |            |    1
</span></span><span class="line"><span class="ln">7</span><span class="cl">Conn memory 衝突 |            |     P0     |            |            |    1</span></span></code></pre></div><p>Convergence column 自動標 priority bump — 2+ 列為 <em>首要 fix</em>、1 列為 <em>依 severity 處理</em>。</p>
<h3 id="2-priority-list-按-convergence-排序不是純按-severity">2. Priority list 按 convergence 排序、不是純按 severity</h3>
<p>修法 priority：</p>
<ol>
<li><strong>2+ convergence finding</strong>（系統性 pattern）— 必修、高 ROI</li>
<li><strong>單軸 + 高 severity finding</strong>（如 FK claim 過時 invalidates premise）— 必修、specific</li>
<li><strong>單軸 + 中 severity finding</strong>（如 CTE version 錯）— 修、ROI 中等</li>
<li><strong>單軸 + 低 severity finding</strong> — 可選</li>
</ol>
<h3 id="3-convergence-揭露的-pattern-寫進-retro">3. Convergence 揭露的 <em>pattern</em> 寫進 retro</h3>
<p>2+ convergence finding 通常是 <em>寫作流程 / 模板</em> 級議題、修了該 case 還要回頭看 <em>為什麼會系統性發生</em>：</p>
<ul>
<li>Missing weight：寫 migration playbook 模板沒有 weight、是 <em>template gap</em></li>
<li>Frame uniformity：「5 個踩雷」frame 在所有 article 重複、是 <em>frame template too rigid</em></li>
</ul>
<p>把這些 pattern 寫進 retro / report card、未來不再踩。</p>
<h2 id="跟既有原則的關係">跟既有原則的關係</h2>
<ul>
<li><a href="../sibling-coverage-asymmetry-blindspot-in-priority/">Sibling Coverage Asymmetry Blindspot in Priority</a>：本卡是 <em>audit finding 的 priority weighting</em>、那卡是 <em>batch coverage 的 priority weighting</em>、不同 layer</li>
<li><a href="../multi-pass-review-frame-granularity-blindspot/">Multi-Pass Review Frame Granularity Blindspot</a>：multi-pass 是 <em>同 reviewer 多輪</em>、本卡是 <em>多 reviewer 平行</em>、不同模式</li>
</ul>
<h2 id="反向驗證">反向驗證</h2>
<p>不該誤用：</p>
<ul>
<li><em>Convergence &gt; severity</em> 不是絕對 — 單軸高 severity finding（如 invalidates premise）仍是必修、不該因為「只一軸 flag」延後</li>
<li>N=1 reviewer audit 不適用本卡 — 至少 2 個 reviewer 才有 convergence 概念</li>
<li>2 個 reviewer 用 <em>同樣 axis</em> 都 flag 不算 convergence — 必須 <em>不同 axis</em> 才是真正收斂</li>
<li>Reviewer 之間 <em>互相看過彼此 report</em> 後再 flag 不算 convergence — 必須 <em>獨立 parallel</em> 跑</li>
</ul>
<h2 id="觸發再評估">觸發再評估</h2>
<ul>
<li>N-reviewer audit 跑超過 5 輪後、check convergence finding 的 follow-up rate 是否真比單軸 finding 高</li>
<li>出現 <em>3 軸以上 convergence</em> 的 finding 時、是否 trigger framework-level review（不只是 content fix）</li>
<li>累積足夠 reviewer convergence case 後、考慮抽出 <em>axis design 原則</em>：哪些 axis 組合的 convergence 最 informative</li>
</ul>
]]></content:encoded></item><item><title>MySQL Audit Log + SIEM</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/audit-log-siem/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/audit-log-siem/</guid><description>&lt;p>MySQL audit log + SIEM 的核心責任是把資料庫操作事件轉成可查詢、可保留、可告警的安全證據。Audit log 是可調查的行為紀錄；它要回答誰在何時、從哪裡、對哪個資料物件做了什麼，以及是否符合授權流程。&lt;/p>
&lt;p>本文的判讀錨點是：audit logging 要服務於 investigation 與 compliance。Slow query log、general log、binary log、error log、managed service audit log、plugin audit log 各自承擔不同證據，不應混成同一種 log。&lt;/p>
&lt;h2 id="event-taxonomy">Event Taxonomy&lt;/h2>
&lt;p>Event taxonomy 的核心責任是定義要蒐集哪些資料庫事件。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Event 類型&lt;/th>
 &lt;th>目的&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Login / logout&lt;/td>
 &lt;td>身份與來源追蹤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Failed access&lt;/td>
 &lt;td>brute force、credential misuse&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DDL&lt;/td>
 &lt;td>schema 變更與 migration evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DCL&lt;/td>
 &lt;td>grant / revoke / role 變更&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sensitive read&lt;/td>
 &lt;td>PII / payment / high-risk table&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data modification&lt;/td>
 &lt;td>bulk update / delete、admin action&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replication / backup&lt;/td>
 &lt;td>binlog、backup、restore access&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>事件分類要對應 alert。DDL 可以進 release audit；failed login 可以進 security alert；sensitive read 要連到 support ticket 或 break-glass 流程。&lt;/p>
&lt;h2 id="log-sources">Log Sources&lt;/h2>
&lt;p>Log sources 的核心責任是選出合適來源。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Source&lt;/th>
 &lt;th>適合用途&lt;/th>
 &lt;th>風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Error log&lt;/td>
 &lt;td>startup、crash、replication error&lt;/td>
 &lt;td>缺少完整 query context&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Slow log&lt;/td>
 &lt;td>performance investigation&lt;/td>
 &lt;td>安全事件覆蓋不足&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>General log&lt;/td>
 &lt;td>debug / short-term tracing&lt;/td>
 &lt;td>volume 大、PII 風險高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Binary log&lt;/td>
 &lt;td>data change recovery / CDC&lt;/td>
 &lt;td>需要解析、並非 user audit 完整替代&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Audit plugin / managed audit&lt;/td>
 &lt;td>security evidence&lt;/td>
 &lt;td>provider / edition / config 限制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>General log 在 production 要謹慎使用。它能提供完整 SQL，但 volume、PII 與成本都高；通常只用短時間 incident window 或測試環境。&lt;/p>
&lt;h2 id="siem-pipeline">SIEM Pipeline&lt;/h2>
&lt;p>SIEM pipeline 的核心責任是把 database event 轉成集中查詢與告警。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Pipeline step&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Collect&lt;/td>
 &lt;td>log file、managed log export、agent&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Normalize&lt;/td>
 &lt;td>actor、source IP、database、object、action&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mask&lt;/td>
 &lt;td>移除 SQL literal / PII&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retain&lt;/td>
 &lt;td>retention、legal hold、storage class&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Alert&lt;/td>
 &lt;td>rule、severity、owner、runbook&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Review&lt;/td>
 &lt;td>periodic access review&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Normalization 要避免把完整 SQL 直接送進 SIEM。對敏感系統，可保留 query fingerprint、table、operation、row count、actor 與 ticket id，而非 literal value。&lt;/p></description><content:encoded><![CDATA[<p>MySQL audit log + SIEM 的核心責任是把資料庫操作事件轉成可查詢、可保留、可告警的安全證據。Audit log 是可調查的行為紀錄；它要回答誰在何時、從哪裡、對哪個資料物件做了什麼，以及是否符合授權流程。</p>
<p>本文的判讀錨點是：audit logging 要服務於 investigation 與 compliance。Slow query log、general log、binary log、error log、managed service audit log、plugin audit log 各自承擔不同證據，不應混成同一種 log。</p>
<h2 id="event-taxonomy">Event Taxonomy</h2>
<p>Event taxonomy 的核心責任是定義要蒐集哪些資料庫事件。</p>
<table>
  <thead>
      <tr>
          <th>Event 類型</th>
          <th>目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Login / logout</td>
          <td>身份與來源追蹤</td>
      </tr>
      <tr>
          <td>Failed access</td>
          <td>brute force、credential misuse</td>
      </tr>
      <tr>
          <td>DDL</td>
          <td>schema 變更與 migration evidence</td>
      </tr>
      <tr>
          <td>DCL</td>
          <td>grant / revoke / role 變更</td>
      </tr>
      <tr>
          <td>Sensitive read</td>
          <td>PII / payment / high-risk table</td>
      </tr>
      <tr>
          <td>Data modification</td>
          <td>bulk update / delete、admin action</td>
      </tr>
      <tr>
          <td>Replication / backup</td>
          <td>binlog、backup、restore access</td>
      </tr>
  </tbody>
</table>
<p>事件分類要對應 alert。DDL 可以進 release audit；failed login 可以進 security alert；sensitive read 要連到 support ticket 或 break-glass 流程。</p>
<h2 id="log-sources">Log Sources</h2>
<p>Log sources 的核心責任是選出合適來源。</p>
<table>
  <thead>
      <tr>
          <th>Source</th>
          <th>適合用途</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Error log</td>
          <td>startup、crash、replication error</td>
          <td>缺少完整 query context</td>
      </tr>
      <tr>
          <td>Slow log</td>
          <td>performance investigation</td>
          <td>安全事件覆蓋不足</td>
      </tr>
      <tr>
          <td>General log</td>
          <td>debug / short-term tracing</td>
          <td>volume 大、PII 風險高</td>
      </tr>
      <tr>
          <td>Binary log</td>
          <td>data change recovery / CDC</td>
          <td>需要解析、並非 user audit 完整替代</td>
      </tr>
      <tr>
          <td>Audit plugin / managed audit</td>
          <td>security evidence</td>
          <td>provider / edition / config 限制</td>
      </tr>
  </tbody>
</table>
<p>General log 在 production 要謹慎使用。它能提供完整 SQL，但 volume、PII 與成本都高；通常只用短時間 incident window 或測試環境。</p>
<h2 id="siem-pipeline">SIEM Pipeline</h2>
<p>SIEM pipeline 的核心責任是把 database event 轉成集中查詢與告警。</p>
<table>
  <thead>
      <tr>
          <th>Pipeline step</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Collect</td>
          <td>log file、managed log export、agent</td>
      </tr>
      <tr>
          <td>Normalize</td>
          <td>actor、source IP、database、object、action</td>
      </tr>
      <tr>
          <td>Mask</td>
          <td>移除 SQL literal / PII</td>
      </tr>
      <tr>
          <td>Retain</td>
          <td>retention、legal hold、storage class</td>
      </tr>
      <tr>
          <td>Alert</td>
          <td>rule、severity、owner、runbook</td>
      </tr>
      <tr>
          <td>Review</td>
          <td>periodic access review</td>
      </tr>
  </tbody>
</table>
<p>Normalization 要避免把完整 SQL 直接送進 SIEM。對敏感系統，可保留 query fingerprint、table、operation、row count、actor 與 ticket id，而非 literal value。</p>
<h2 id="alert-rules">Alert Rules</h2>
<p>Alert rules 的核心責任是把高風險事件變成可行動訊號。</p>
<table>
  <thead>
      <tr>
          <th>Rule</th>
          <th>代表風險</th>
          <th>第一反應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Admin login outside window</td>
          <td>credential misuse / emergency access</td>
          <td>確認 ticket、限制 session</td>
      </tr>
      <tr>
          <td>Grant / revoke event</td>
          <td>權限邊界變更</td>
          <td>access review</td>
      </tr>
      <tr>
          <td>Drop / truncate table</td>
          <td>destructive DDL</td>
          <td>freeze release、restore decision</td>
      </tr>
      <tr>
          <td>Bulk update / delete</td>
          <td>application bug / misuse</td>
          <td>查 transaction、binlog、backup</td>
      </tr>
      <tr>
          <td>Sensitive table read</td>
          <td>PII exposure</td>
          <td>ticket match、scope review</td>
      </tr>
  </tbody>
</table>
<p>Alert 要有 owner 與 runbook。只把 log 送進 SIEM，缺少 triage rule，incident 時仍然難以快速定位。</p>
<h2 id="retention-and-privacy">Retention and Privacy</h2>
<p>Retention and privacy 的核心責任是讓 audit log 同時可用與合規。Audit log 可能包含帳號、IP、SQL、table name、literal value 與 PII；保存時間越長，保護責任越重。</p>
<p>Retention policy 要定義：</p>
<ol>
<li>保存天數與 storage class。</li>
<li>哪些欄位可被 masked。</li>
<li>誰能查 audit log。</li>
<li>Legal hold 如何覆蓋一般 retention。</li>
<li>Export 到外部 SIEM 的資料邊界。</li>
</ol>
<p>Audit log 本身也要納入 access control。能查敏感 audit 的人，通常也能推斷敏感資料活動。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Audit log + SIEM 完成後，加密與憑證讀 <a href="../encryption-tls-key-management/">Encryption / TLS / Key Management</a>；備份事故讀 <a href="../pitr-backup/">PITR / Backup</a>；安全治理讀 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL Security / RLS / Audit Logging</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/security-rls-audit-logging/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/security-rls-audit-logging/</guid><description>&lt;p>PostgreSQL security / RLS / audit logging 的核心責任是把資料庫安全拆成存取邊界、資料列可見性與操作證據。PostgreSQL role / grant 決定誰能連線與操作 schema；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/row-level-security/" data-link-title="Row-Level Security" data-link-desc="說明資料庫如何用 policy 限制同一張表中哪些 row 對某個角色可見或可寫">Row Level Security&lt;/a> 決定同一張表中哪些 row 對某個 role 可見；audit logging 則把敏感操作轉成可查詢、可保留、可告警的證據。&lt;/p>
&lt;p>本文的判讀錨點是：資料庫安全是 application auth 的下游防線。Application 仍要負責身份、session、租戶與 workflow；PostgreSQL security layer 負責在資料邊界補上 least privilege、tenant isolation 與 forensic evidence。&lt;/p>
&lt;h2 id="role-and-grant-baseline">Role and Grant Baseline&lt;/h2>
&lt;p>Role and grant baseline 的核心責任是把人、服務、migration 與分析查詢分開。Production database 至少要區分 application role、migration role、read-only role、admin role 與 replication / CDC role。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Role 類型&lt;/th>
 &lt;th>權限責任&lt;/th>
 &lt;th>常見風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Application&lt;/td>
 &lt;td>執行產品讀寫&lt;/td>
 &lt;td>權限過大、可 DDL、可讀所有 schema&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration&lt;/td>
 &lt;td>變更 schema&lt;/td>
 &lt;td>和 app 共用 role，事故難以追蹤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Read-only&lt;/td>
 &lt;td>分析、debug、support&lt;/td>
 &lt;td>讀到 PII 或跨 tenant 資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replication / CDC&lt;/td>
 &lt;td>logical replication、slot access&lt;/td>
 &lt;td>權限與 WAL retention 風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Admin&lt;/td>
 &lt;td>emergency operation&lt;/td>
 &lt;td>日常使用 admin role&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Grant review 要以 schema ownership 開始。Tables、sequences、functions、views、extensions 都有權限面；只管 table grant 會漏掉 sequence update、function execution 與 extension 使用。&lt;/p>
&lt;h2 id="row-level-security">Row Level Security&lt;/h2>
&lt;p>Row Level Security 的核心責任是在資料庫層 enforce row visibility。PostgreSQL 官方 RLS 文件描述 policy 可限制 normal query 返回、insert、update、delete 的 row；這讓 tenant boundary 可以在 database 層多一道 guard。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>RLS 使用情境&lt;/th>
 &lt;th>適合條件&lt;/th>
 &lt;th>審查問題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Multi-tenant SaaS&lt;/td>
 &lt;td>tenant_id 明確且每個 query 都可帶入&lt;/td>
 &lt;td>policy 是否覆蓋 SELECT / INSERT / UPDATE&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Support access&lt;/td>
 &lt;td>support role 需受限查詢&lt;/td>
 &lt;td>break-glass 是否有 audit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Regional data&lt;/td>
 &lt;td>row 上有 region / residency&lt;/td>
 &lt;td>policy 是否和 GDPR / residency 對齊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sensitive subset&lt;/td>
 &lt;td>PII row 需特別隔離&lt;/td>
 &lt;td>masking / tokenization 是否仍需存在&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>RLS policy 要有 positive allow rule。每張啟用 RLS 的 table 都要有測試：同 tenant 可讀、跨 tenant 隔離、insert tenant mismatch 被擋、admin / support 例外被記錄。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL security / RLS / audit logging 的核心責任是把資料庫安全拆成存取邊界、資料列可見性與操作證據。PostgreSQL role / grant 決定誰能連線與操作 schema；<a href="/blog/backend/knowledge-cards/row-level-security/" data-link-title="Row-Level Security" data-link-desc="說明資料庫如何用 policy 限制同一張表中哪些 row 對某個角色可見或可寫">Row Level Security</a> 決定同一張表中哪些 row 對某個 role 可見；audit logging 則把敏感操作轉成可查詢、可保留、可告警的證據。</p>
<p>本文的判讀錨點是：資料庫安全是 application auth 的下游防線。Application 仍要負責身份、session、租戶與 workflow；PostgreSQL security layer 負責在資料邊界補上 least privilege、tenant isolation 與 forensic evidence。</p>
<h2 id="role-and-grant-baseline">Role and Grant Baseline</h2>
<p>Role and grant baseline 的核心責任是把人、服務、migration 與分析查詢分開。Production database 至少要區分 application role、migration role、read-only role、admin role 與 replication / CDC role。</p>
<table>
  <thead>
      <tr>
          <th>Role 類型</th>
          <th>權限責任</th>
          <th>常見風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Application</td>
          <td>執行產品讀寫</td>
          <td>權限過大、可 DDL、可讀所有 schema</td>
      </tr>
      <tr>
          <td>Migration</td>
          <td>變更 schema</td>
          <td>和 app 共用 role，事故難以追蹤</td>
      </tr>
      <tr>
          <td>Read-only</td>
          <td>分析、debug、support</td>
          <td>讀到 PII 或跨 tenant 資料</td>
      </tr>
      <tr>
          <td>Replication / CDC</td>
          <td>logical replication、slot access</td>
          <td>權限與 WAL retention 風險</td>
      </tr>
      <tr>
          <td>Admin</td>
          <td>emergency operation</td>
          <td>日常使用 admin role</td>
      </tr>
  </tbody>
</table>
<p>Grant review 要以 schema ownership 開始。Tables、sequences、functions、views、extensions 都有權限面；只管 table grant 會漏掉 sequence update、function execution 與 extension 使用。</p>
<h2 id="row-level-security">Row Level Security</h2>
<p>Row Level Security 的核心責任是在資料庫層 enforce row visibility。PostgreSQL 官方 RLS 文件描述 policy 可限制 normal query 返回、insert、update、delete 的 row；這讓 tenant boundary 可以在 database 層多一道 guard。</p>
<table>
  <thead>
      <tr>
          <th>RLS 使用情境</th>
          <th>適合條件</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multi-tenant SaaS</td>
          <td>tenant_id 明確且每個 query 都可帶入</td>
          <td>policy 是否覆蓋 SELECT / INSERT / UPDATE</td>
      </tr>
      <tr>
          <td>Support access</td>
          <td>support role 需受限查詢</td>
          <td>break-glass 是否有 audit</td>
      </tr>
      <tr>
          <td>Regional data</td>
          <td>row 上有 region / residency</td>
          <td>policy 是否和 GDPR / residency 對齊</td>
      </tr>
      <tr>
          <td>Sensitive subset</td>
          <td>PII row 需特別隔離</td>
          <td>masking / tokenization 是否仍需存在</td>
      </tr>
  </tbody>
</table>
<p>RLS policy 要有 positive allow rule。每張啟用 RLS 的 table 都要有測試：同 tenant 可讀、跨 tenant 隔離、insert tenant mismatch 被擋、admin / support 例外被記錄。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">invoices</span><span class="w"> </span><span class="n">ENABLE</span><span class="w"> </span><span class="k">ROW</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">SECURITY</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></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">POLICY</span><span class="w"> </span><span class="n">tenant_isolation</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">invoices</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">USING</span><span class="w"> </span><span class="p">(</span><span class="n">tenant_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">current_setting</span><span class="p">(</span><span class="s1">&#39;app.tenant_id&#39;</span><span class="p">)::</span><span class="n">uuid</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="k">WITH</span><span class="w"> </span><span class="k">CHECK</span><span class="w"> </span><span class="p">(</span><span class="n">tenant_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">current_setting</span><span class="p">(</span><span class="s1">&#39;app.tenant_id&#39;</span><span class="p">)::</span><span class="n">uuid</span><span class="p">);</span></span></span></code></pre></div><p>這段 policy 依賴 application 在 transaction 內設定 <code>app.tenant_id</code>。使用 connection pooler 時，設定必須跟 transaction boundary 對齊，避免 session state 漂移。</p>
<h2 id="audit-logging">Audit Logging</h2>
<p>Audit logging 的核心責任是把敏感資料操作轉成可查詢證據。PostgreSQL 原生日誌可以記錄連線、DDL、錯誤與慢查詢；pgAudit 這類 extension 則補強 session / object audit。</p>
<table>
  <thead>
      <tr>
          <th>Audit 類型</th>
          <th>目的</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DDL audit</td>
          <td>schema 變更追蹤</td>
          <td>migration id、role、statement、timestamp</td>
      </tr>
      <tr>
          <td>Sensitive read</td>
          <td>PII / payment / health data 查詢</td>
          <td>role、tenant、operation、reason</td>
      </tr>
      <tr>
          <td>Privilege change</td>
          <td>grant / revoke / role 變更</td>
          <td>actor、target role、approval</td>
      </tr>
      <tr>
          <td>Failed access</td>
          <td>權限錯誤與 RLS block</td>
          <td>error code、role、relation</td>
      </tr>
      <tr>
          <td>Break-glass</td>
          <td>emergency admin access</td>
          <td>ticket id、duration、review result</td>
      </tr>
  </tbody>
</table>
<p>Audit log 要能進入 SIEM 或集中 log。只留在 database host 上，事故後查詢成本高；正式 runbook 要定義 retention、masking、access control 與 alert。</p>
<h2 id="pii-and-data-protection-boundary">PII and Data Protection Boundary</h2>
<p>PII and data protection boundary 的核心責任是把 database 權限和資料保護策略接起來。RLS 可以限制 row visibility，但 PII 的保護還需要 masking、tokenization、encryption、retention 與 deletion evidence。</p>
<table>
  <thead>
      <tr>
          <th>資料類型</th>
          <th>Database control</th>
          <th>跨模組路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Tenant data</td>
          <td>RLS、tenant-scoped role</td>
          <td>data access review</td>
      </tr>
      <tr>
          <td>PII</td>
          <td>column grant、masking view</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a></td>
      </tr>
      <tr>
          <td>Audit log</td>
          <td>append-only storage、retention</td>
          <td>SIEM / incident evidence</td>
      </tr>
      <tr>
          <td>Deletion request</td>
          <td>tombstone、cascade review</td>
          <td>retention policy、legal hold</td>
      </tr>
  </tbody>
</table>
<p>Column-level grant 和 masking view 適合 read-only analyst。Application role 通常需要明文處理 workflow；analyst / support role 則應走 restricted view。</p>
<h2 id="operational-evidence">Operational Evidence</h2>
<p>Operational evidence 的核心責任是讓安全設定可驗證。每次 release 或權限變更後，要跑固定檢查。</p>
<ol>
<li>Role matrix：每個 role 的 schema / table / sequence / function grant。</li>
<li>RLS test：tenant A / tenant B / support / admin 的可見性測試。</li>
<li>Audit sample：DDL、sensitive read、failed access 是否進 log。</li>
<li>Pooler compatibility：<code>SET LOCAL app.tenant_id</code> 是否跟 transaction 對齊。</li>
<li><a href="/blog/backend/knowledge-cards/break-glass-access/" data-link-title="Break-Glass Access" data-link-desc="說明緊急情況下臨時授予的高權限存取，如何用工單、時限與事後審查治理">Break-glass</a> drill：emergency access 是否可申請、可回收、可審查。</li>
</ol>
<p>Evidence 要保存在 release artifact。Security 設定只有文件描述時，incident 後難以證明它真的生效。</p>
<h2 id="failure-modes">Failure Modes</h2>
<p>Failure modes 的核心責任是把 database security 常見事故提前列出。</p>
<table>
  <thead>
      <tr>
          <th>Failure mode</th>
          <th>判讀訊號</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>App role 權限過大</td>
          <td>app 可 DDL / drop / grant</td>
          <td>role split + least privilege</td>
      </tr>
      <tr>
          <td>RLS bypass</td>
          <td>owner / superuser / policy 漏洞</td>
          <td>dedicated app role + RLS test</td>
      </tr>
      <tr>
          <td>Pooler state drift</td>
          <td>tenant setting 漂到下個 request</td>
          <td><code>SET LOCAL</code> + transaction pooling review</td>
      </tr>
      <tr>
          <td>Audit gap</td>
          <td>敏感操作查不到 actor</td>
          <td>pgAudit / log schema / SIEM route</td>
      </tr>
      <tr>
          <td>Support overread</td>
          <td>support role 可讀全 tenant</td>
          <td>masking view + ticket-scoped access</td>
      </tr>
  </tbody>
</table>
<p>RLS bypass 要特別審查 table owner 與 superuser path。正式 application 連線應使用 dedicated role，並避免使用 table owner role 執行一般 request。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Security / RLS / audit logging 完成後，權限與 PII 治理讀 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a>；connection state 風險讀 <a href="../connection-pooler-comparison/">Connection Pooler Comparison</a>；實作演練可放進 <a href="../hands-on/schema-migration-evidence-lab/">Schema Migration Evidence Lab</a> 的 release gate。</p>
]]></content:encoded></item></channel></rss>