<?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>Oidc on Tarragon</title><link>https://tarrragon.github.io/blog/tags/oidc/</link><description>Recent content in Oidc 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/oidc/index.xml" rel="self" type="application/rss+xml"/><item><title>身分與憑證地基 — IAM 模型、OIDC 短期憑證與權限邊界設計</title><link>https://tarrragon.github.io/blog/infra/02-identity-credentials/iam-oidc-privilege-boundary/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/02-identity-credentials/iam-oidc-privilege-boundary/</guid><description>&lt;p>權限一旦散落，後面每一層都建在沙上。網路收斂得再好，只要一把權限過大的長期憑證流出，攻擊者就能繞過所有邊界直接動到核心資源；環境分得再乾淨，只要 production 跟 staging 共用同一組身分，一次誤操作就跨環境炸開。身分與憑證是地基層最先該收斂的能力，因為它決定了「誰能動什麼」這個問題有沒有可信的答案。&lt;/p>
&lt;h2 id="iam-的心智模型">IAM 的心智模型&lt;/h2>
&lt;p>IAM（Identity and Access Management）是雲端平台用來回答「某個身分能不能對某個資源做某件事」的授權系統。它把授權拆成三個獨立的零件：identity（身分，發起動作的主體）、policy（政策，描述允許或拒絕的規則）、role（角色，一組可以被臨時取得的權限集合）。理解這三者的分工，是後面所有憑證決策的前提。&lt;/p>
&lt;h3 id="identity長期主體-vs-臨時假扮">identity：長期主體 vs 臨時假扮&lt;/h3>
&lt;p>identity 分兩類，這個區分在後面設計權限邊界時會反覆用到。一類是 user，代表一個長期存在的主體，通常對應到一個真人或一個固定的服務帳號，本身可以持有長期憑證（密碼或 access key）。另一類是 role，代表一組權限的暫時授予 — 沒有自己的長期密碼，而是讓某個被信任的身分「假扮（assume）」成它、換取一段有時效的臨時憑證。&lt;/p>
&lt;p>把 identity 想成「護照」和「通行證」的差別：user 是護照，長期有效、全程攜帶；role 是通行證，到了管制區域臨時換發、離開就失效。多數安全事故源自於把通行證當護照用 — 某個 role 被長期假扮且從未被撤回，或某個 user 持有永不輪替的 access key。&lt;/p>
&lt;h3 id="policy描述允許對什麼做什麼">policy：描述「允許對什麼做什麼」&lt;/h3>
&lt;p>policy 是貼在 user 或 role 上的規則文件，列出 &lt;code>Action&lt;/code>（能做什麼，如 &lt;code>s3:GetObject&lt;/code>）、&lt;code>Resource&lt;/code>（對哪個資源，如特定 bucket 的 ARN）、&lt;code>Effect&lt;/code>（Allow 或 Deny）。一條 policy 可以包含多個 statement，每條 statement 描述一組操作許可。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 最小權限範例：CI 只能讀寫特定 bucket，不給整個 S3
&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">data&lt;/span> &lt;span class="s2">&amp;#34;aws_iam_policy_document&amp;#34; &amp;#34;ci_artifacts&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">statement&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n"> effect&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Allow&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="n"> actions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;s3:GetObject&amp;#34;, &amp;#34;s3:PutObject&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="n"> resources&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;arn:aws:s3:::myapp-artifacts/*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段 policy 只允許對 &lt;code>myapp-artifacts&lt;/code> 這一個 bucket 做讀寫。如果寫成 &lt;code>resources = [&amp;quot;*&amp;quot;]&lt;/code>，同一把身分被攻破時，攻擊者就能讀寫帳號內所有 bucket — 差別不在語法，在 &lt;code>Resource&lt;/code> 欄位收到多緊。&lt;/p>
&lt;h3 id="role臨時身分的載體">role：臨時身分的載體&lt;/h3>
&lt;p>role 本身不持有長期密碼。它靠 trust policy（信任政策）定義「誰能假扮我」，靠 permissions policy 定義「假扮後能做什麼」。trust policy 和 permissions policy 是兩份獨立的文件，分別回答「誰進得來」與「進來後能做什麼」。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># trust policy：只允許 ECS 服務假扮此 role
&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">data&lt;/span> &lt;span class="s2">&amp;#34;aws_iam_policy_document&amp;#34; &amp;#34;ecs_trust&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">statement&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n"> actions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sts:AssumeRole&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">principals&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Service&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="n"> identifiers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;ecs-tasks.amazonaws.com&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&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"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_iam_role&amp;#34; &amp;#34;api_task&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;api-task-prod&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="n"> assume_role_policy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">aws_iam_policy_document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">ecs_trust&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>trust policy 裡的 &lt;code>principals&lt;/code> 決定能進門的身分。上面這段把進門權限限給 ECS 服務本身，意味著只有跑在 ECS 上的 task 才能取得這個 role 的臨時憑證 — 一個在本地筆電跑的程式呼叫 &lt;code>AssumeRole&lt;/code> 會被拒絕。&lt;/p>
&lt;h2 id="最小權限持續收斂而非一次設定">最小權限：持續收斂而非一次設定&lt;/h2>
&lt;p>最小權限（least privilege）是貫穿整套系統的設計原則：一個身分只應該拿到完成它本職工作所需的最小權限集合。多一個 action 是多一條攻擊面，多一個 resource 是多一個爆炸半徑。&lt;/p>
&lt;p>最小權限是持續收斂的過程，而非一次設定就結束的靜態狀態。服務初期常為了快速上線給寬鬆權限 — 一個新的 ECS task role 掛上 &lt;code>AmazonS3FullAccess&lt;/code> 讓它能跑起來，半年後這個 role 實際只用了 &lt;code>s3:GetObject&lt;/code> 和 &lt;code>s3:PutObject&lt;/code> 兩個 action、針對一個 bucket，但 policy 裡寫的還是全部 S3 操作對所有 bucket。&lt;/p>
&lt;p>收斂的工具是 access analyzer。AWS IAM Access Analyzer 能分析 CloudTrail 日誌，列出某個 role 在過去 N 天內實際用了哪些 action 與 resource，據此產出一份建議的最小 policy。用它的步驟是：開著寬 policy 跑一段時間 → 用 access analyzer 產出實際使用清單 → 把 policy 收斂到這份清單 → 確認服務仍正常。&lt;/p></description><content:encoded><![CDATA[<p>權限一旦散落，後面每一層都建在沙上。網路收斂得再好，只要一把權限過大的長期憑證流出，攻擊者就能繞過所有邊界直接動到核心資源；環境分得再乾淨，只要 production 跟 staging 共用同一組身分，一次誤操作就跨環境炸開。身分與憑證是地基層最先該收斂的能力，因為它決定了「誰能動什麼」這個問題有沒有可信的答案。</p>
<h2 id="iam-的心智模型">IAM 的心智模型</h2>
<p>IAM（Identity and Access Management）是雲端平台用來回答「某個身分能不能對某個資源做某件事」的授權系統。它把授權拆成三個獨立的零件：identity（身分，發起動作的主體）、policy（政策，描述允許或拒絕的規則）、role（角色，一組可以被臨時取得的權限集合）。理解這三者的分工，是後面所有憑證決策的前提。</p>
<h3 id="identity長期主體-vs-臨時假扮">identity：長期主體 vs 臨時假扮</h3>
<p>identity 分兩類，這個區分在後面設計權限邊界時會反覆用到。一類是 user，代表一個長期存在的主體，通常對應到一個真人或一個固定的服務帳號，本身可以持有長期憑證（密碼或 access key）。另一類是 role，代表一組權限的暫時授予 — 沒有自己的長期密碼，而是讓某個被信任的身分「假扮（assume）」成它、換取一段有時效的臨時憑證。</p>
<p>把 identity 想成「護照」和「通行證」的差別：user 是護照，長期有效、全程攜帶；role 是通行證，到了管制區域臨時換發、離開就失效。多數安全事故源自於把通行證當護照用 — 某個 role 被長期假扮且從未被撤回，或某個 user 持有永不輪替的 access key。</p>
<h3 id="policy描述允許對什麼做什麼">policy：描述「允許對什麼做什麼」</h3>
<p>policy 是貼在 user 或 role 上的規則文件，列出 <code>Action</code>（能做什麼，如 <code>s3:GetObject</code>）、<code>Resource</code>（對哪個資源，如特定 bucket 的 ARN）、<code>Effect</code>（Allow 或 Deny）。一條 policy 可以包含多個 statement，每條 statement 描述一組操作許可。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 最小權限範例：CI 只能讀寫特定 bucket，不給整個 S3
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">data</span> <span class="s2">&#34;aws_iam_policy_document&#34; &#34;ci_artifacts&#34;</span> {
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">statement</span> {
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">    effect</span>    <span class="o">=</span> <span class="s2">&#34;Allow&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">    actions</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;s3:GetObject&#34;, &#34;s3:PutObject&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">    resources</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;arn:aws:s3:::myapp-artifacts/*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  }
</span></span><span class="line"><span class="ln">8</span><span class="cl">}</span></span></code></pre></div><p>這段 policy 只允許對 <code>myapp-artifacts</code> 這一個 bucket 做讀寫。如果寫成 <code>resources = [&quot;*&quot;]</code>，同一把身分被攻破時，攻擊者就能讀寫帳號內所有 bucket — 差別不在語法，在 <code>Resource</code> 欄位收到多緊。</p>
<h3 id="role臨時身分的載體">role：臨時身分的載體</h3>
<p>role 本身不持有長期密碼。它靠 trust policy（信任政策）定義「誰能假扮我」，靠 permissions policy 定義「假扮後能做什麼」。trust policy 和 permissions policy 是兩份獨立的文件，分別回答「誰進得來」與「進來後能做什麼」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># trust policy：只允許 ECS 服務假扮此 role
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">data</span> <span class="s2">&#34;aws_iam_policy_document&#34; &#34;ecs_trust&#34;</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">statement</span> {
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">    actions</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;sts:AssumeRole&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">principals</span> {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">      type</span>        <span class="o">=</span> <span class="s2">&#34;Service&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">      identifiers</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;ecs-tasks.amazonaws.com&#34;</span><span class="p">]</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></span><span class="line"><span class="ln">10</span><span class="cl">}
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_iam_role&#34; &#34;api_task&#34;</span> {
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">  name</span>               <span class="o">=</span> <span class="s2">&#34;api-task-prod&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">  assume_role_policy</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_iam_policy_document</span><span class="p">.</span><span class="k">ecs_trust</span><span class="p">.</span><span class="k">json</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">}</span></span></code></pre></div><p>trust policy 裡的 <code>principals</code> 決定能進門的身分。上面這段把進門權限限給 ECS 服務本身，意味著只有跑在 ECS 上的 task 才能取得這個 role 的臨時憑證 — 一個在本地筆電跑的程式呼叫 <code>AssumeRole</code> 會被拒絕。</p>
<h2 id="最小權限持續收斂而非一次設定">最小權限：持續收斂而非一次設定</h2>
<p>最小權限（least privilege）是貫穿整套系統的設計原則：一個身分只應該拿到完成它本職工作所需的最小權限集合。多一個 action 是多一條攻擊面，多一個 resource 是多一個爆炸半徑。</p>
<p>最小權限是持續收斂的過程，而非一次設定就結束的靜態狀態。服務初期常為了快速上線給寬鬆權限 — 一個新的 ECS task role 掛上 <code>AmazonS3FullAccess</code> 讓它能跑起來，半年後這個 role 實際只用了 <code>s3:GetObject</code> 和 <code>s3:PutObject</code> 兩個 action、針對一個 bucket，但 policy 裡寫的還是全部 S3 操作對所有 bucket。</p>
<p>收斂的工具是 access analyzer。AWS IAM Access Analyzer 能分析 CloudTrail 日誌，列出某個 role 在過去 N 天內實際用了哪些 action 與 resource，據此產出一份建議的最小 policy。用它的步驟是：開著寬 policy 跑一段時間 → 用 access analyzer 產出實際使用清單 → 把 policy 收斂到這份清單 → 確認服務仍正常。</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"># 產出建議 policy：分析 api-task-prod role 過去 90 天的實際用量</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws accessanalyzer generate-policy <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --policy-generation-details <span class="s1">&#39;{
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s1">    &#34;principalArn&#34;: &#34;arn:aws:iam::123456789012:role/api-task-prod&#34;,
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s1">    &#34;cloudTrailDetails&#34;: {
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s1">      &#34;trailArn&#34;: &#34;arn:aws:cloudtrail:ap-northeast-1:123456789012:trail/main&#34;,
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s1">      &#34;startTime&#34;: &#34;2026-03-01T00:00:00Z&#34;,
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s1">      &#34;endTime&#34;: &#34;2026-06-01T00:00:00Z&#34;
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s1">    }
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s1">  }&#39;</span></span></span></code></pre></div><p>一個快速的盤點方式：列出所有掛著 <code>AdministratorAccess</code>、<code>PowerUserAccess</code>、<code>*FullAccess</code> 這類寬鬆 managed policy 的 role，每個命中都問一次「這個 role 確實需要這些權限嗎」。CI role 的 policy 裡出現 <code>*:*</code> 更是明確的收斂目標。</p>
<h2 id="長期-access-key-的風險">長期 access key 的風險</h2>
<p>長期 access key 是一組沒有到期時間的靜態憑證（access key ID + secret），任何持有它的人或程式都能以對應身分的全部權限呼叫 API，直到有人手動撤銷為止。它最大的問題是「沒有時效」這個性質本身，會在三個方向上累積風險，而且風險隨團隊規模與時間單調上升。</p>
<h3 id="散落">散落</h3>
<p>長期 key 為了被程式使用，會被複製進 <code>.env</code> 檔、CI 設定、本機 <code>~/.aws/credentials</code>、Slack 訊息、甚至誤推進 git 歷史。每多一個副本就多一個外洩點。一把 key 在半年內可能被貼到六個地方 — 部署腳本、兩個 CI 平台的環境變數、某台共用跳板機的 profile、一封交接信、一位已離職同事的筆電 — 而這六個副本沒有任何中央清單能列舉。</p>
<h3 id="權限過大">權限過大</h3>
<p>因為輪替麻煩，團隊傾向給一把 key 配足夠寬的權限「一次搞定」。建立時圖方便掛了 AdministratorAccess，打算「等穩定了再收斂」，但那天從來沒有到來。於是一把本來只該讀 artifact 的 key 同時握有刪除 production 資料庫的能力。</p>
<h3 id="難以輪替">難以輪替</h3>
<p>輪替一把長期 key 意味著找出所有副本、同步替換、確認沒有遺漏。這個成本高到讓多數團隊選擇拖延，於是 key 的有效期變成「無限」，外洩後的曝險窗口也跟著變成無限。用一個問題辨認風險：能不能在五分鐘內回答「這把 key 被用在哪些地方、上次輪替是什麼時候」？答不出來，它就已經是技術債。</p>
<p>常見的散落路徑：部署腳本使用的 admin key 留在 CI 環境變數，建立者離職後沒人知道這把 key 的存在與權限範圍。這類情境的風險在於外洩後沒有手段限制影響範圍 — key 的權限有多大，影響範圍就有多大。用 credential report 定期盤點帳號內所有 access key 的建立時間與使用時間，見<a href="/blog/infra/before-infra/" data-link-title="模組負一：還沒有 infra 的環境怎麼盡量做好" data-link-desc="手動點起家的環境怎麼守底線、降低未來納管成本、辨識何時該開始導入 IaC — 給還沒有能力上 IaC 的真實起點">模組負一：還沒有 infra 的環境</a>。</p>
<p>長期憑證風險的實際規模可以從兩個案例看到。Snowflake 2024 事件中，攻擊者利用外洩的長期憑證登入缺少 MFA 的客戶環境，執行大量資料匯出，造成跨客戶的資料竊取與勒索（見 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/snowflake-2024-credential-abuse/" data-link-title="7.R7.4.2 Snowflake 2024：憑證濫用與資料竊取" data-link-desc="外洩憑證與 MFA 缺口如何在資料平台形成高風險外送事件">Snowflake 2024：憑證濫用與資料竊取</a>）。LastPass 2022 事件則顯示備份路徑的憑證管理缺口會讓影響範圍沿信任鏈擴散——開發環境取得的資訊被用來存取雲端備份，整條路徑的金鑰隔離不足是根因（見 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/lastpass-2022-backup-chain/" data-link-title="7.R7.4.1 LastPass 2022：備份路徑與鏈式入侵" data-link-desc="開發環境資訊外流如何沿著備份路徑擴大成資料風險">LastPass 2022：備份路徑與鏈式入侵</a>）。兩個案例的共同教訓是：長期憑證的風險不止於外洩本身，而在於外洩後缺乏限制影響範圍的機制。</p>
<h2 id="oidc給-cicd-的短期憑證">OIDC：給 CI/CD 的短期憑證</h2>
<p>OIDC（OpenID Connect）聯合讓 CI/CD 平台用一段每次執行才簽發、幾分鐘後就失效的短期憑證取代長期 key，從根本上消掉「靜態密鑰散落」這個問題。它的運作方式是建立信任關係：雲端帳號信任某個外部 identity provider（如 GitHub Actions 的 OIDC issuer），當管線執行時，CI 平台簽發一個帶有可驗證 claim 的 token（描述「這是哪個 repo、哪個 branch、哪個 workflow 在跑」），雲端用這個 token 換出一段臨時憑證。沒有任何長期 secret 需要被儲存在 CI 設定裡。</p>
<h3 id="trust-policy-的收斂">trust policy 的收斂</h3>
<p>關鍵設計在 role 的 trust policy 上 — 它規定「哪個外部身分被允許假扮成這個 role」。trust policy 要用 token 的 claim 把假扮條件收到最緊。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># OIDC trust policy：只允許特定 repo 的 main branch 假扮此 role
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">data</span> <span class="s2">&#34;aws_iam_policy_document&#34; &#34;ci_trust&#34;</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">statement</span> {
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">    actions</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;sts:AssumeRoleWithWebIdentity&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">principals</span> {
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">      type</span>        <span class="o">=</span> <span class="s2">&#34;Federated&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">      identifiers</span> <span class="o">=</span> <span class="p">[</span><span class="k">aws_iam_openid_connect_provider</span><span class="p">.</span><span class="k">github</span><span class="p">.</span><span class="k">arn</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    }
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">condition</span> {
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">      test</span>     <span class="o">=</span> <span class="s2">&#34;StringEquals&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">      variable</span> <span class="o">=</span> <span class="s2">&#34;token.actions.githubusercontent.com:aud&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">      values</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;sts.amazonaws.com&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    }
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">condition</span> {
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="n">      test</span>     <span class="o">=</span> <span class="s2">&#34;StringLike&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">      variable</span> <span class="o">=</span> <span class="s2">&#34;token.actions.githubusercontent.com:sub&#34;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">      values</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;repo:my-org/my-app:ref:refs/heads/main&#34;</span><span class="p">]</span>
</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">}</span></span></code></pre></div><p>每個 condition 各守一段邊界。<code>aud</code> 的 <code>StringEquals</code> 確認 token 是發給 AWS STS 的（防止用錯 audience 的 token 闖入）。<code>sub</code> 的 <code>StringLike</code> 把假扮限定在特定 repo 的 main branch — 設成 <code>repo:my-org/*</code> 等於讓組織內任何 repo 的任何 branch 都能假扮這個 role，這是常見的設定陷阱。</p>
<p>收斂 trust policy 的判讀問法是：「如果 my-org 底下某個公開 fork 跑了一個惡意 workflow，它能不能假扮這個 role？」如果答案是能，<code>sub</code> 條件就太鬆了。</p>
<h3 id="分離-plan-與-apply-的-role">分離 plan 與 apply 的 role</h3>
<p>進一步的收斂是替 <code>plan</code> 和 <code>apply</code> 分別建立 role。plan 只需要唯讀存取（讀 state、讀雲端現況），apply 需要寫入權限。把兩者分成獨立 role，讓 PR 階段的 CI 用唯讀 role 跑 plan、合併後才用寫入 role 跑 apply。任何拿到 plan role 的 token 無法修改基礎設施。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># plan role：只需讀取 state 與雲端現況
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">resource</span> <span class="s2">&#34;aws_iam_role&#34; &#34;ci_plan&#34;</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  name</span>               <span class="o">=</span> <span class="s2">&#34;infra-ci-plan&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  assume_role_policy</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_iam_policy_document</span><span class="p">.</span><span class="k">ci_trust</span><span class="p">.</span><span class="k">json</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">}
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_iam_role_policy_attachment&#34; &#34;ci_plan_read&#34;</span> {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  role</span>       <span class="o">=</span> <span class="k">aws_iam_role</span><span class="p">.</span><span class="k">ci_plan</span><span class="p">.</span><span class="k">name</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  policy_arn</span> <span class="o">=</span> <span class="s2">&#34;arn:aws:iam::aws:policy/ReadOnlyAccess&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">}<span class="c1">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># apply role：需要寫入權限，trust policy 限定只有 main branch
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="k">resource</span> <span class="s2">&#34;aws_iam_role&#34; &#34;ci_apply&#34;</span> {
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">  name</span>               <span class="o">=</span> <span class="s2">&#34;infra-ci-apply&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">  assume_role_policy</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_iam_policy_document</span><span class="p">.</span><span class="k">ci_trust_main_only</span><span class="p">.</span><span class="k">json</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">}</span></span></code></pre></div><p>這一章把 role 與 trust policy 設計好，OIDC 的實際回報要到<a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>建管線時才兌現 — 屆時管線用這裡定義好的 role 取得短期權限執行 <code>plan</code> 與 <code>apply</code>，CI 環境裡不需要存任何 access key。</p>
<h2 id="權限邊界設計">權限邊界設計</h2>
<p>權限邊界是把不同類型的身分與不同環境之間的權限刻意隔開，讓任何一個身分被攻破時，爆炸半徑都被限制在它本職的範圍內。邊界設計有兩條軸線需要分別處理：人 vs 機器，以及環境之間。</p>
<h3 id="人-vs-機器">人 vs 機器</h3>
<p>兩者的存取模式根本不同，混在同一個身分上會同時喪失兩邊的保護。</p>
<p>人類身分需要互動式登入、應該強制 MFA、權限隨職責變動，且通常透過 SSO 集中管理。機器身分（CI runner、ECS task、Lambda function）需要的是程式化、無人值守的存取，應該用 role 假扮取得短期憑證，永遠不該配長期 key。</p>
<p>機器身分還要再依「跑在哪裡」分兩類。跑在雲上的 workload（EC2 instance、ECS task、Lambda）由平台直接把 role 綁在執行環境上 — AWS 用 instance profile 把 role 掛在 EC2、用 task role 掛在 ECS task，workload 從實例 metadata 端點自動取得輪替的短期憑證。跑在雲外的 CI/CD（GitHub Actions、GitLab CI）拿不到實例 metadata，需要前面那套 OIDC 信任關係換憑證。</p>
<p>一個常見陷阱是工程師用自己的個人 key 跑自動化腳本 — 這把人的廣泛權限直接送進了無人值守的執行環境，MFA 保護形同虛設（API 呼叫不需要 MFA challenge），權限範圍比任何 CI role 都大。</p>
<h3 id="環境之間">環境之間</h3>
<p>環境之間的邊界，目的是讓 production 的權限與 staging、dev 完全不交叉。驗證邊界的方式是用 dev 環境的 CI role 嘗試列出或刪除 production 的資源——能做到，就代表邊界沒有建立。</p>
<h4 id="帳號級護欄scp">帳號級護欄：SCP</h4>
<p>Organizations 把環境拆成獨立帳號，再用 SCP（Service Control Policy）對整個帳號或組織單位設定權限天花板，連帳號內的管理員都越不過去。SCP 是 deny-based 的頂層限制 — 它不授予任何權限，只限制「即使有人給了權限也不准做」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;Version&#34;</span><span class="p">:</span> <span class="s2">&#34;2012-10-17&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&#34;Statement&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="nt">&#34;Sid&#34;</span><span class="p">:</span> <span class="s2">&#34;DenyLeaveOrg&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="nt">&#34;Effect&#34;</span><span class="p">:</span> <span class="s2">&#34;Deny&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="nt">&#34;Action&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;organizations:LeaveOrganization&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="nt">&#34;Resource&#34;</span><span class="p">:</span> <span class="s2">&#34;*&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="nt">&#34;Sid&#34;</span><span class="p">:</span> <span class="s2">&#34;DenyDisableCloudTrail&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">      <span class="nt">&#34;Effect&#34;</span><span class="p">:</span> <span class="s2">&#34;Deny&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">      <span class="nt">&#34;Action&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="s2">&#34;cloudtrail:StopLogging&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="s2">&#34;cloudtrail:DeleteTrail&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">      <span class="p">],</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">      <span class="nt">&#34;Resource&#34;</span><span class="p">:</span> <span class="s2">&#34;*&#34;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這份 SCP 掛在整個組織底下的所有帳號上，確保任何帳號都不能關閉稽核日誌或退出組織 — 即使該帳號裡有人持有 AdministratorAccess。SCP 的定位是組織層的不可踰越底線。</p>
<h4 id="role-級護欄permissions-boundary">Role 級護欄：Permissions Boundary</h4>
<p>Permissions Boundary 是掛在單一 role 上的權限上限。它跟 SCP 的差別在粒度：SCP 管整個帳號，Permissions Boundary 管單一身分。即使有人後來給一個 role 貼了過寬的 policy，Boundary 也會擋住超出上限的部分。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Permissions Boundary：CI role 最多只能操作特定服務
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">resource</span> <span class="s2">&#34;aws_iam_policy&#34; &#34;ci_boundary&#34;</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;ci-boundary-prod&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  policy</span> <span class="o">=</span> <span class="k">jsonencode</span><span class="p">(</span>{
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">    Version</span> <span class="o">=</span> <span class="s2">&#34;2012-10-17&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">    Statement</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">        Effect</span>   <span class="o">=</span> <span class="s2">&#34;Allow&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">        Action</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;ecs:*&#34;, &#34;ecr:*&#34;, &#34;s3:*&#34;, &#34;logs:*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">        Resource</span> <span class="o">=</span> <span class="s2">&#34;*&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      }<span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">      {
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">        Effect</span>   <span class="o">=</span> <span class="s2">&#34;Deny&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">        Action</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;iam:*&#34;, &#34;organizations:*&#34;, &#34;account:*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">        Resource</span> <span class="o">=</span> <span class="s2">&#34;*&#34;</span>
</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">    <span class="p">]</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  }<span class="p">)</span>
</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">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_iam_role&#34; &#34;ci_apply&#34;</span> {
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="n">  name</span>                 <span class="o">=</span> <span class="s2">&#34;infra-ci-apply&#34;</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="n">  assume_role_policy</span>   <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_iam_policy_document</span><span class="p">.</span><span class="k">ci_trust</span><span class="p">.</span><span class="k">json</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="n">  permissions_boundary</span> <span class="o">=</span> <span class="k">aws_iam_policy</span><span class="p">.</span><span class="k">ci_boundary</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">}</span></span></code></pre></div><p>SCP 與 Permissions Boundary 疊起來的效果是：SCP 在帳號層鎖住最危險的操作（關日誌、退組織），Boundary 在 role 層限制單一身分最多能做什麼，permissions policy 在這兩層天花板之內授予實際需要的權限。三者各管一層，缺一層就少一道屏障。</p>
<p>身分控制面本身的韌性在兩個案例中被檢驗。Azure AD 2021 事件中，身分服務的控制面故障導致所有依賴身份驗證的服務同時受影響，事故處理需要在身份恢復與服務降級策略之間排優先序（見 <a href="/blog/backend/07-security-data-protection/cases/azure-ad-identity-control-plane-2021/" data-link-title="7.C3 Azure AD：2021 Identity Control-plane 事件" data-link-desc="身分控制面事件如何影響多服務信任鏈與回復優先序。">Azure AD：Identity Control-plane 事件</a>）。Microsoft Storm-0558 事件則顯示簽章金鑰一旦失守，token 驗證的信任鏈會跨租戶失效，修復不只是修補漏洞、而是重建整條 key lifecycle 與 issuer 驗證流程（見 <a href="/blog/backend/07-security-data-protection/cases/microsoft-storm-0558-signing-key-2023/" data-link-title="7.C4 Microsoft：Storm-0558 簽章金鑰事件" data-link-desc="簽章金鑰事件如何回寫 identity 信任邊界與觀測證據鏈。">Microsoft：Storm-0558 簽章金鑰事件</a>）。這兩個案例揭露的是：權限邊界只管「某個身分能做什麼」，但身分系統本身的控制面如果失效，所有建立在它之上的邊界都跟著失效。</p>
<p>環境隔離的更完整實作（帳號結構、模組化參數）會在<a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>展開。</p>
<h2 id="身分層-vs-應用層-secret-的邊界">身分層 vs 應用層 secret 的邊界</h2>
<p>這一章談的是身分與憑證 — 誰是誰、怎麼證明、能動什麼。憑證背後引用的應用層 secret（資料庫密碼、第三方 API key）怎麼安全儲存與注入，屬於<a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>的 secret management 範圍。兩者的交集是：身分層決定「誰能讀到 secret store」，secret 層決定「secret 怎麼存與輪替」。把 IAM role 的 policy 收到只能讀取該服務路徑下的 secret（如 <code>prod/payments/*</code>），是同時落實最小權限與 secret 隔離的結合點。</p>
<p>身分與憑證的地基備妥後，下一步是劃清服務之間的網路邊界——這正是<a href="/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">模組三：網路地基</a>的範圍。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/before-infra/" data-link-title="模組負一：還沒有 infra 的環境怎麼盡量做好" data-link-desc="手動點起家的環境怎麼守底線、降低未來納管成本、辨識何時該開始導入 IaC — 給還沒有能力上 IaC 的真實起點">模組負一：還沒有 infra 的環境</a>：長期 key 盤點與護欄</li>
<li>→ <a href="/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">模組三：網路地基</a>：身分備妥後，劃清服務之間的網路邊界</li>
<li>→ <a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>：環境之間的帳號結構與隔離強度</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>：CI/CD 管線用 OIDC 取得短期權限</li>
<li>→ <a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>：應用層 secret 的儲存與引用</li>
<li>→ <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>：Secret Management 與憑證管理交集</li>
<li>→ <a href="/blog/infra/02-identity-credentials/access-key-rotation-playbook/" data-link-title="Access Key 輪替手冊" data-link-desc="從 credential report 盤點散落的長期 access key，到逐把輪替、自動化輪替與 key age 監控的完整操作步驟">Access Key 輪替手冊</a>：key 盤點與輪替的操作步驟</li>
<li>→ <a href="/blog/infra/02-identity-credentials/oidc-trust-policy-setup/" data-link-title="OIDC Trust Policy 設定指南" data-link-desc="GitHub Actions 與 AWS 之間的 OIDC 聯合設定：建立 provider、設計 trust policy 的 claim 收斂、plan 與 apply role 分離、常見錯誤排查">OIDC Trust Policy 設定指南</a>：GitHub Actions OIDC 的 step-by-step 設定</li>
</ul>
]]></content:encoded></item><item><title>模組二：身分與憑證地基 — IAM 與 OIDC</title><link>https://tarrragon.github.io/blog/infra/02-identity-credentials/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/02-identity-credentials/</guid><description>&lt;p>權限一旦散落，後面每一層都建在沙上。網路收斂得再好，只要一把權限過大的長期憑證流出，攻擊者就能繞過所有邊界直接動到核心資源；環境分得再乾淨，只要 production 跟 staging 共用同一組身分，一次誤操作就跨環境炸開。身分與憑證是地基層最先該收斂的能力，因為它決定了「誰能動什麼」這個問題有沒有可信的答案。這一章把這個地基設計好，讓後面的網路、環境分離、服務上線都有一個明確的權限模型可以掛靠。&lt;/p>
&lt;h2 id="iam-的心智模型">IAM 的心智模型&lt;/h2>
&lt;p>IAM（Identity and Access Management）是雲端平台用來回答「某個身分能不能對某個資源做某件事」的授權系統。它把授權拆成三個獨立的零件：identity（身分，發起動作的主體）、policy（政策，描述「允許/拒絕對哪些資源做哪些動作」的規則）、role（角色，一組可以被臨時取得的權限集合）。理解這三者的分工，是後面所有憑證決策的前提。&lt;/p>
&lt;p>identity 分兩類，這個區分在後面設計權限邊界時會反覆用到。一類是 user，代表一個長期存在的主體，通常對應到一個真人或一個固定的服務帳號，本身可以持有長期憑證。另一類是 role，代表一組權限的暫時授予 — 沒有自己的長期密碼，而是讓某個被信任的身分「假扮（assume）」成它、換取一段有時效的臨時憑證。policy 則是貼在 user 或 role 上的規則文件，列出 &lt;code>Action&lt;/code>（能做什麼，如 &lt;code>s3:GetObject&lt;/code>）、&lt;code>Resource&lt;/code>（對哪個資源）、&lt;code>Effect&lt;/code>（允許或拒絕）。&lt;/p>
&lt;p>最小權限（least privilege）是貫穿這套系統的設計原則：一個身分只應該拿到完成它本職工作所需的最小權限集合，多一個 action、多一個 resource 都是攻擊面。最小權限是持續收斂的過程，而非一次設定就結束的靜態狀態 — 服務初期常為了快速上線給寬鬆權限，之後要靠 access analyzer 這類工具觀察「實際用到哪些 action」，再把沒用到的權限收掉。判讀訊號很直接：如果一個 CI role 的 policy 裡有 &lt;code>*:*&lt;/code> 或 &lt;code>AdministratorAccess&lt;/code>，它就是下一個 incident 的入口。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 最小權限：CI 只能讀寫特定 bucket、不給整個 S3
&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">data&lt;/span> &lt;span class="s2">&amp;#34;aws_iam_policy_document&amp;#34; &amp;#34;ci_artifacts&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">statement&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n"> actions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;s3:GetObject&amp;#34;, &amp;#34;s3:PutObject&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="n"> resources&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;arn:aws:s3:::myapp-artifacts/*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="長期-access-key-的風險">長期 access key 的風險&lt;/h2>
&lt;p>長期 access key 是一組沒有到期時間的靜態憑證（access key ID + secret），任何持有它的人或程式都能以對應身分的全部權限呼叫 API，直到有人手動撤銷為止。它最大的問題是「沒有時效」這個性質本身，會在三個方向上累積風險，而且風險隨團隊規模與時間單調上升。&lt;/p>
&lt;p>第一是散落。長期 key 為了被程式使用，會被複製進 &lt;code>.env&lt;/code> 檔、CI 設定、本機 &lt;code>~/.aws/credentials&lt;/code>、Slack 訊息、甚至誤推進 git 歷史。每多一個副本就多一個外洩點，而你很難盤點清楚一把 key 到底被貼進了多少地方。第二是權限過大。因為輪替麻煩，團隊傾向給一把 key 配足夠寬的權限「一次搞定」，於是一把本來只該讀 artifact 的 key 同時握有刪除 production 資料庫的能力。第三是難以輪替。輪替一把長期 key 意味著找出所有副本、同步替換、確認沒有遺漏，這個成本高到讓多數團隊選擇拖延，於是 key 的有效期變成「無限」，外洩後的曝險窗口也跟著變成無限。&lt;/p>
&lt;p>判讀訊號是：如果你無法在五分鐘內回答「這把 key 被用在哪些地方、上次輪替是什麼時候」，它就已經是技術債。早期新創特別容易踩這個坑 — 一個工程師為了讓部署腳本跑起來，在筆電上建了一把 admin key，半年後這把 key 還在 CI 環境變數裡，建立它的人已經離職。這類事故的代價不在於「key 外洩」這個事件本身，而在於外洩之後你沒有任何手段限制爆炸半徑。&lt;/p>
&lt;h2 id="oidc給-cicd-的短期憑證">OIDC：給 CI/CD 的短期憑證&lt;/h2>
&lt;p>OIDC（OpenID Connect）聯合讓 CI/CD 平台用一段每次執行才簽發、幾分鐘後就失效的短期憑證取代長期 key，從根本上消掉「靜態密鑰散落」這個問題。它的運作方式是建立信任關係：雲端帳號信任某個外部 identity provider（如 GitHub Actions、GitLab CI 的 OIDC issuer），當管線執行時，CI 平台簽發一個帶有可驗證 claim 的 token（描述「這是哪個 repo、哪個 branch、哪個 workflow 在跑」），雲端用這個 token 換出一段臨時憑證。沒有任何長期 secret 需要被儲存在 CI 設定裡。&lt;/p>
&lt;p>關鍵設計在 role 的 trust policy（信任政策）上 — 它規定「哪個外部身分被允許假扮成這個 role」。trust policy 要用 token 的 claim 把假扮條件收到最緊：限定 issuer、限定 audience、限定特定 repo 與 branch。收得太鬆（例如只驗 issuer、不驗 repo）等於任何掛在同一個 CI 平台的專案都能假扮你的 role，這是常見的設定陷阱。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># OIDC trust policy：只允許特定 repo 的 main branch 假扮此 role
&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">data&lt;/span> &lt;span class="s2">&amp;#34;aws_iam_policy_document&amp;#34; &amp;#34;ci_trust&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">statement&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n"> actions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sts:AssumeRoleWithWebIdentity&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">principals&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Federated&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="n"> identifiers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="k">aws_iam_openid_connect_provider&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">github&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>&lt;span class="p">]&lt;/span>
&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"> &lt;span class="k">condition&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="n"> test&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;StringEquals&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="n"> variable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;token.actions.githubusercontent.com:aud&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="n"> values&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sts.amazonaws.com&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&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"> &lt;span class="k">condition&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="n"> test&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;StringLike&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="n"> variable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;token.actions.githubusercontent.com:sub&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="n"> values&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;repo:my-org/my-app:ref:refs/heads/main&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這一章只把 role 與 trust policy 設計好，OIDC 的實際回報要到&lt;a href="https://tarrragon.github.io/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程&lt;/a>建管線時才兌現 — 屆時管線用這裡定義好的 role 取得短期權限執行 &lt;code>plan&lt;/code> 與 &lt;code>apply&lt;/code>，CI 環境裡不需要存任何 access key。下一步路由很明確：role 與最小權限的 policy 屬於這裡的地基，管線怎麼觸發、怎麼卡 review 屬於模組七。&lt;/p></description><content:encoded><![CDATA[<p>權限一旦散落，後面每一層都建在沙上。網路收斂得再好，只要一把權限過大的長期憑證流出，攻擊者就能繞過所有邊界直接動到核心資源；環境分得再乾淨，只要 production 跟 staging 共用同一組身分，一次誤操作就跨環境炸開。身分與憑證是地基層最先該收斂的能力，因為它決定了「誰能動什麼」這個問題有沒有可信的答案。這一章把這個地基設計好，讓後面的網路、環境分離、服務上線都有一個明確的權限模型可以掛靠。</p>
<h2 id="iam-的心智模型">IAM 的心智模型</h2>
<p>IAM（Identity and Access Management）是雲端平台用來回答「某個身分能不能對某個資源做某件事」的授權系統。它把授權拆成三個獨立的零件：identity（身分，發起動作的主體）、policy（政策，描述「允許/拒絕對哪些資源做哪些動作」的規則）、role（角色，一組可以被臨時取得的權限集合）。理解這三者的分工，是後面所有憑證決策的前提。</p>
<p>identity 分兩類，這個區分在後面設計權限邊界時會反覆用到。一類是 user，代表一個長期存在的主體，通常對應到一個真人或一個固定的服務帳號，本身可以持有長期憑證。另一類是 role，代表一組權限的暫時授予 — 沒有自己的長期密碼，而是讓某個被信任的身分「假扮（assume）」成它、換取一段有時效的臨時憑證。policy 則是貼在 user 或 role 上的規則文件，列出 <code>Action</code>（能做什麼，如 <code>s3:GetObject</code>）、<code>Resource</code>（對哪個資源）、<code>Effect</code>（允許或拒絕）。</p>
<p>最小權限（least privilege）是貫穿這套系統的設計原則：一個身分只應該拿到完成它本職工作所需的最小權限集合，多一個 action、多一個 resource 都是攻擊面。最小權限是持續收斂的過程，而非一次設定就結束的靜態狀態 — 服務初期常為了快速上線給寬鬆權限，之後要靠 access analyzer 這類工具觀察「實際用到哪些 action」，再把沒用到的權限收掉。判讀訊號很直接：如果一個 CI role 的 policy 裡有 <code>*:*</code> 或 <code>AdministratorAccess</code>，它就是下一個 incident 的入口。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 最小權限：CI 只能讀寫特定 bucket、不給整個 S3
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">data</span> <span class="s2">&#34;aws_iam_policy_document&#34; &#34;ci_artifacts&#34;</span> {
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">statement</span> {
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">    actions</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;s3:GetObject&#34;, &#34;s3:PutObject&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">    resources</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;arn:aws:s3:::myapp-artifacts/*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  }
</span></span><span class="line"><span class="ln">7</span><span class="cl">}</span></span></code></pre></div><h2 id="長期-access-key-的風險">長期 access key 的風險</h2>
<p>長期 access key 是一組沒有到期時間的靜態憑證（access key ID + secret），任何持有它的人或程式都能以對應身分的全部權限呼叫 API，直到有人手動撤銷為止。它最大的問題是「沒有時效」這個性質本身，會在三個方向上累積風險，而且風險隨團隊規模與時間單調上升。</p>
<p>第一是散落。長期 key 為了被程式使用，會被複製進 <code>.env</code> 檔、CI 設定、本機 <code>~/.aws/credentials</code>、Slack 訊息、甚至誤推進 git 歷史。每多一個副本就多一個外洩點，而你很難盤點清楚一把 key 到底被貼進了多少地方。第二是權限過大。因為輪替麻煩，團隊傾向給一把 key 配足夠寬的權限「一次搞定」，於是一把本來只該讀 artifact 的 key 同時握有刪除 production 資料庫的能力。第三是難以輪替。輪替一把長期 key 意味著找出所有副本、同步替換、確認沒有遺漏，這個成本高到讓多數團隊選擇拖延，於是 key 的有效期變成「無限」，外洩後的曝險窗口也跟著變成無限。</p>
<p>判讀訊號是：如果你無法在五分鐘內回答「這把 key 被用在哪些地方、上次輪替是什麼時候」，它就已經是技術債。早期新創特別容易踩這個坑 — 一個工程師為了讓部署腳本跑起來，在筆電上建了一把 admin key，半年後這把 key 還在 CI 環境變數裡，建立它的人已經離職。這類事故的代價不在於「key 外洩」這個事件本身，而在於外洩之後你沒有任何手段限制爆炸半徑。</p>
<h2 id="oidc給-cicd-的短期憑證">OIDC：給 CI/CD 的短期憑證</h2>
<p>OIDC（OpenID Connect）聯合讓 CI/CD 平台用一段每次執行才簽發、幾分鐘後就失效的短期憑證取代長期 key，從根本上消掉「靜態密鑰散落」這個問題。它的運作方式是建立信任關係：雲端帳號信任某個外部 identity provider（如 GitHub Actions、GitLab CI 的 OIDC issuer），當管線執行時，CI 平台簽發一個帶有可驗證 claim 的 token（描述「這是哪個 repo、哪個 branch、哪個 workflow 在跑」），雲端用這個 token 換出一段臨時憑證。沒有任何長期 secret 需要被儲存在 CI 設定裡。</p>
<p>關鍵設計在 role 的 trust policy（信任政策）上 — 它規定「哪個外部身分被允許假扮成這個 role」。trust policy 要用 token 的 claim 把假扮條件收到最緊：限定 issuer、限定 audience、限定特定 repo 與 branch。收得太鬆（例如只驗 issuer、不驗 repo）等於任何掛在同一個 CI 平台的專案都能假扮你的 role，這是常見的設定陷阱。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># OIDC trust policy：只允許特定 repo 的 main branch 假扮此 role
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">data</span> <span class="s2">&#34;aws_iam_policy_document&#34; &#34;ci_trust&#34;</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">statement</span> {
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">    actions</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;sts:AssumeRoleWithWebIdentity&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">principals</span> {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">      type</span>        <span class="o">=</span> <span class="s2">&#34;Federated&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">      identifiers</span> <span class="o">=</span> <span class="p">[</span><span class="k">aws_iam_openid_connect_provider</span><span class="p">.</span><span class="k">github</span><span class="p">.</span><span class="k">arn</span><span class="p">]</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="k">condition</span> {
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">      test</span>     <span class="o">=</span> <span class="s2">&#34;StringEquals&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">      variable</span> <span class="o">=</span> <span class="s2">&#34;token.actions.githubusercontent.com:aud&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">      values</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;sts.amazonaws.com&#34;</span><span class="p">]</span>
</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="k">condition</span> {
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">      test</span>     <span class="o">=</span> <span class="s2">&#34;StringLike&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">      variable</span> <span class="o">=</span> <span class="s2">&#34;token.actions.githubusercontent.com:sub&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">      values</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;repo:my-org/my-app:ref:refs/heads/main&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    }
</span></span><span class="line"><span class="ln">19</span><span class="cl">  }
</span></span><span class="line"><span class="ln">20</span><span class="cl">}</span></span></code></pre></div><p>這一章只把 role 與 trust policy 設計好，OIDC 的實際回報要到<a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>建管線時才兌現 — 屆時管線用這裡定義好的 role 取得短期權限執行 <code>plan</code> 與 <code>apply</code>，CI 環境裡不需要存任何 access key。下一步路由很明確：role 與最小權限的 policy 屬於這裡的地基，管線怎麼觸發、怎麼卡 review 屬於模組七。</p>
<h2 id="權限邊界設計">權限邊界設計</h2>
<p>權限邊界是把不同類型的身分與不同環境之間的權限刻意隔開，讓任何一個身分被攻破時，爆炸半徑都被限制在它本職的範圍內。邊界設計有兩條軸線需要分別處理：人 vs 機器，以及環境之間。</p>
<p>人 vs 機器的邊界，源自兩者的存取模式根本不同。人類身分需要互動式登入、應該強制 MFA、權限隨職責變動，且通常透過 SSO 集中管理而非各自持有 key。機器身分（CI、跑在運算資源上的服務）需要的是程式化、無人值守的存取，應該用 role 假扮取得短期憑證，永遠不該配長期 key。機器身分還要再分跑在哪裡：跑在雲上的 workload（運算實例、容器任務）由平台直接把 role 綁在執行環境上 — AWS 用 instance profile 把 role 掛在 EC2 instance、用 ECS task role 把 role 掛在容器任務，workload 從實例 metadata 自動取得輪替的短期憑證，這是早於 OIDC 就存在的標準解；只有跑在雲外的 CI/CD（如 GitHub Actions）拿不到實例 metadata，才需要前面那套 OIDC 信任關係換憑證。把這兩類混在同一個身分上，會讓你既無法對人強制 MFA，也無法對機器收斂權限。一個常見陷阱是工程師用自己的個人 key 跑自動化腳本 — 這把人的廣泛權限直接送進了無人值守的執行環境。</p>
<p>環境之間的邊界，目的是讓 production 的權限與 staging、dev 完全不交叉，避免一次誤操作或一個被攻破的低敏感環境波及到核心資產。實作上常見的做法是每個環境用獨立的帳號（account）或獨立的 role，部署到 production 的身分拿不到 staging 的資源、反之亦然。這條邊界在 AWS 上有兩層具體機制可以落地：帳號級的護欄用 Organizations 把環境拆成獨立帳號，再用 SCP（Service Control Policy）對整個帳號或組織單位設定權限天花板，連帳號內的管理員都越不過去；role 級的護欄用 Permissions Boundary 這個 IAM 字面功能，給單一 role 設一個權限上限，限制它「最多能拿到什麼」，即使有人後來給它貼了過寬的 policy 也會被天花板擋住。前者收的是帳號與組織的整體範圍，後者收的是單一身分的上限，兩者疊起來才讓「權限邊界」從概念變成擋得住誤設的具體工具。判讀訊號是：如果一個 dev 環境的 CI role 能列出或刪除 production 的資源，邊界就沒有真正建立。環境隔離的更完整實作（帳號結構、模組化參數）會在<a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>展開，這裡先確保身分層的權限不跨環境。</p>
<p>這一章談的是身分與憑證 — 誰是誰、怎麼證明、能動什麼。憑證背後引用的應用層 secret（資料庫密碼、第三方 API key）怎麼安全儲存與注入，屬於<a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>的 secret management 範圍，不在這裡處理。兩者的交集是：身分層決定「誰能讀到 secret store」，secret 層決定「secret 怎麼存與輪替」。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/infra/02-identity-credentials/iam-oidc-privilege-boundary/" data-link-title="身分與憑證地基 — IAM 模型、OIDC 短期憑證與權限邊界設計" data-link-desc="IAM 的 identity / policy / role 三元件、最小權限的持續收斂、用 OIDC 取代長期 access key，以及 SCP 與 Permissions Boundary 的環境隔離">身分與憑證地基 — IAM 模型、OIDC 短期憑證與權限邊界設計</a></td>
          <td>IAM 的 identity / policy / role 三元件、最小權限的持續收斂、用 OIDC 取代長期 access key，以及 SCP 與 Permissions Boundary 的環境隔離</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/02-identity-credentials/multi-account-strategy/" data-link-title="跨帳號策略 — Organizations、SCP 與帳號工廠" data-link-desc="用 AWS Organizations 把環境拆成獨立帳號、用 SCP 設定連管理員都越不過的護欄、用帳號工廠讓每個新帳號自帶安全基線">跨帳號策略 — Organizations、SCP 與帳號工廠</a></td>
          <td>用 Organizations 把環境拆成獨立帳號、用 SCP 設定帳號級護欄、用帳號工廠自動化新帳號的建立流程</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/02-identity-credentials/team-access-management/" data-link-title="團隊權限分級與存取管理" data-link-desc="用 admin / operator / viewer 三級劃分團隊成員的雲端操作權限，設計臨時提權流程、定期 access review 節奏，以及 contractor 與外部 vendor 的存取邊界">團隊權限分級與存取管理</a></td>
          <td>三級權限模型（admin / operator / viewer）、臨時提權、定期 access review、contractor 存取</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/02-identity-credentials/access-key-rotation-playbook/" data-link-title="Access Key 輪替手冊" data-link-desc="從 credential report 盤點散落的長期 access key，到逐把輪替、自動化輪替與 key age 監控的完整操作步驟">Access Key 輪替手冊</a></td>
          <td>access key 盤點、輪替步驟、Secrets Manager 自動化輪替、key age 監控</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/02-identity-credentials/oidc-trust-policy-setup/" data-link-title="OIDC Trust Policy 設定指南" data-link-desc="GitHub Actions 與 AWS 之間的 OIDC 聯合設定：建立 provider、設計 trust policy 的 claim 收斂、plan 與 apply role 分離、常見錯誤排查">OIDC Trust Policy 設定指南</a></td>
          <td>GitHub Actions OIDC provider 設定、trust policy claim 收斂、plan/apply role 分離、常見錯誤排查</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">模組三：網路地基</a>：身分備妥後，劃清服務之間的網路邊界</li>
<li>→ <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>：Secret Management 與這裡的憑證管理交集</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>：CI/CD 用 OIDC 取得短期權限</li>
<li>→ <a href="/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運</a>：接手時的 credential 盤點與輪替</li>
</ul>
]]></content:encoded></item><item><title>OIDC Trust Policy 設定指南</title><link>https://tarrragon.github.io/blog/infra/02-identity-credentials/oidc-trust-policy-setup/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/02-identity-credentials/oidc-trust-policy-setup/</guid><description>&lt;p>OIDC 聯合讓 CI/CD pipeline 用短期 token 取代長期 access key 存取雲端資源。設定本身不複雜，但 trust policy 的 claim 條件寫錯一個字就會變成「任何 repo 都能假扮這個 role」或「完全無法 assume」。本篇是 GitHub Actions 與 AWS 之間的 OIDC 聯合的完整設定步驟，從建立 provider 到 trust policy 設計到測試驗證。其他 CI 平台（GitLab CI、CircleCI）的原理相同，差別只在 issuer URL 和 claim 結構：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>平台&lt;/th>
 &lt;th>Issuer URL&lt;/th>
 &lt;th>sub claim 格式範例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>GitHub Actions&lt;/td>
 &lt;td>&lt;code>token.actions.githubusercontent.com&lt;/code>&lt;/td>
 &lt;td>&lt;code>repo:{org}/{repo}:ref:refs/heads/{branch}&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>GitLab CI&lt;/td>
 &lt;td>&lt;code>gitlab.com&lt;/code>&lt;/td>
 &lt;td>&lt;code>project_path:{group}/{project}:ref_type:branch:ref:main&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CircleCI&lt;/td>
 &lt;td>&lt;code>oidc.circleci.com/org/{org-id}&lt;/code>&lt;/td>
 &lt;td>&lt;code>org/{org-id}/project/{project-id}/user/{user-id}&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>本篇以 GitHub Actions 為主，其他平台替換 issuer URL 和 sub condition 即可。&lt;/p>
&lt;h2 id="建立-oidc-provider">建立 OIDC Provider&lt;/h2>
&lt;p>OIDC provider 是 AWS 帳號裡的一個資源，聲明「我信任這個外部 identity provider 簽發的 token」。GitHub Actions 的 OIDC issuer URL 是固定的，每個 AWS 帳號只需要建一個 provider。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_iam_openid_connect_provider&amp;#34; &amp;#34;github&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n"> url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://token.actions.githubusercontent.com&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="n"> client_id_list&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sts.amazonaws.com&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n"> thumbprint_list&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;ffffffffffffffffffffffffffffffffffffffff&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>client_id_list&lt;/code> 設為 &lt;code>sts.amazonaws.com&lt;/code> 是 GitHub 官方建議的 audience 值。&lt;code>thumbprint_list&lt;/code> 在 2023 年之後 AWS 不再用它驗證 GitHub 的憑證鏈（改用 AWS 自己維護的根憑證清單），但欄位仍然是必填，填 40 個 &lt;code>f&lt;/code> 作為佔位值即可。&lt;/p>
&lt;p>這個 provider 建一次就好。多個 role 可以共用同一個 provider，差別在各自的 trust policy 怎麼寫。&lt;/p>
&lt;h2 id="trust-policy-設計claim-收斂">Trust Policy 設計：claim 收斂&lt;/h2>
&lt;p>Trust policy 決定「誰能假扮這個 role」。OIDC token 裡帶有多個 claim（描述「這是哪個 repo、哪個 branch、哪個 workflow 在跑」），trust policy 用 condition 比對這些 claim，全部命中才允許 assume。&lt;/p>
&lt;h3 id="最小可行的-trust-policy">最小可行的 trust policy&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">data&lt;/span> &lt;span class="s2">&amp;#34;aws_iam_policy_document&amp;#34; &amp;#34;ci_trust&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="k">statement&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n"> actions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sts:AssumeRoleWithWebIdentity&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">principals&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Federated&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="n"> identifiers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="k">aws_iam_openid_connect_provider&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">github&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>&lt;span class="p">]&lt;/span>
&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">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">condition&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="n"> test&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;StringEquals&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="n"> variable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;token.actions.githubusercontent.com:aud&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="n"> values&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sts.amazonaws.com&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="k">condition&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="n"> test&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;StringLike&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="n"> variable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;token.actions.githubusercontent.com:sub&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="n"> values&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;repo:my-org/my-app:ref:refs/heads/main&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩個 condition 各守一個邊界。&lt;code>aud&lt;/code> 驗證 audience 對不對（防止其他用途的 token 被拿來 assume）。&lt;code>sub&lt;/code> 驗證請求來自哪個 repo 和 branch——這是最關鍵的收斂點。&lt;/p></description><content:encoded><![CDATA[<p>OIDC 聯合讓 CI/CD pipeline 用短期 token 取代長期 access key 存取雲端資源。設定本身不複雜，但 trust policy 的 claim 條件寫錯一個字就會變成「任何 repo 都能假扮這個 role」或「完全無法 assume」。本篇是 GitHub Actions 與 AWS 之間的 OIDC 聯合的完整設定步驟，從建立 provider 到 trust policy 設計到測試驗證。其他 CI 平台（GitLab CI、CircleCI）的原理相同，差別只在 issuer URL 和 claim 結構：</p>
<table>
  <thead>
      <tr>
          <th>平台</th>
          <th>Issuer URL</th>
          <th>sub claim 格式範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GitHub Actions</td>
          <td><code>token.actions.githubusercontent.com</code></td>
          <td><code>repo:{org}/{repo}:ref:refs/heads/{branch}</code></td>
      </tr>
      <tr>
          <td>GitLab CI</td>
          <td><code>gitlab.com</code></td>
          <td><code>project_path:{group}/{project}:ref_type:branch:ref:main</code></td>
      </tr>
      <tr>
          <td>CircleCI</td>
          <td><code>oidc.circleci.com/org/{org-id}</code></td>
          <td><code>org/{org-id}/project/{project-id}/user/{user-id}</code></td>
      </tr>
  </tbody>
</table>
<p>本篇以 GitHub Actions 為主，其他平台替換 issuer URL 和 sub condition 即可。</p>
<h2 id="建立-oidc-provider">建立 OIDC Provider</h2>
<p>OIDC provider 是 AWS 帳號裡的一個資源，聲明「我信任這個外部 identity provider 簽發的 token」。GitHub Actions 的 OIDC issuer URL 是固定的，每個 AWS 帳號只需要建一個 provider。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_iam_openid_connect_provider&#34; &#34;github&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  url</span>             <span class="o">=</span> <span class="s2">&#34;https://token.actions.githubusercontent.com&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  client_id_list</span>  <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;sts.amazonaws.com&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">  thumbprint_list</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;ffffffffffffffffffffffffffffffffffffffff&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">}</span></span></code></pre></div><p><code>client_id_list</code> 設為 <code>sts.amazonaws.com</code> 是 GitHub 官方建議的 audience 值。<code>thumbprint_list</code> 在 2023 年之後 AWS 不再用它驗證 GitHub 的憑證鏈（改用 AWS 自己維護的根憑證清單），但欄位仍然是必填，填 40 個 <code>f</code> 作為佔位值即可。</p>
<p>這個 provider 建一次就好。多個 role 可以共用同一個 provider，差別在各自的 trust policy 怎麼寫。</p>
<h2 id="trust-policy-設計claim-收斂">Trust Policy 設計：claim 收斂</h2>
<p>Trust policy 決定「誰能假扮這個 role」。OIDC token 裡帶有多個 claim（描述「這是哪個 repo、哪個 branch、哪個 workflow 在跑」），trust policy 用 condition 比對這些 claim，全部命中才允許 assume。</p>
<h3 id="最小可行的-trust-policy">最小可行的 trust policy</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">data</span> <span class="s2">&#34;aws_iam_policy_document&#34; &#34;ci_trust&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="k">statement</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">    actions</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;sts:AssumeRoleWithWebIdentity&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">principals</span> {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">      type</span>        <span class="o">=</span> <span class="s2">&#34;Federated&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">      identifiers</span> <span class="o">=</span> <span class="p">[</span><span class="k">aws_iam_openid_connect_provider</span><span class="p">.</span><span class="k">github</span><span class="p">.</span><span class="k">arn</span><span class="p">]</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></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">condition</span> {
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">      test</span>     <span class="o">=</span> <span class="s2">&#34;StringEquals&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">      variable</span> <span class="o">=</span> <span class="s2">&#34;token.actions.githubusercontent.com:aud&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">      values</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;sts.amazonaws.com&#34;</span><span class="p">]</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></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">condition</span> {
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">      test</span>     <span class="o">=</span> <span class="s2">&#34;StringLike&#34;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="n">      variable</span> <span class="o">=</span> <span class="s2">&#34;token.actions.githubusercontent.com:sub&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">      values</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;repo:my-org/my-app:ref:refs/heads/main&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    }
</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></code></pre></div><p>兩個 condition 各守一個邊界。<code>aud</code> 驗證 audience 對不對（防止其他用途的 token 被拿來 assume）。<code>sub</code> 驗證請求來自哪個 repo 和 branch——這是最關鍵的收斂點。</p>
<h3 id="sub-claim-的結構">sub claim 的結構</h3>
<p>GitHub Actions 的 <code>sub</code> claim 格式是 <code>repo:{owner}/{repo}:{context}</code>，其中 context 隨觸發方式不同：</p>
<table>
  <thead>
      <tr>
          <th>觸發方式</th>
          <th>sub claim 值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>push to branch</td>
          <td><code>repo:my-org/my-app:ref:refs/heads/main</code></td>
      </tr>
      <tr>
          <td>pull request</td>
          <td><code>repo:my-org/my-app:pull_request</code></td>
      </tr>
      <tr>
          <td>environment deploy</td>
          <td><code>repo:my-org/my-app:environment:production</code></td>
      </tr>
      <tr>
          <td>tag push</td>
          <td><code>repo:my-org/my-app:ref:refs/tags/v1.0.0</code></td>
      </tr>
      <tr>
          <td>manual dispatch</td>
          <td><code>repo:my-org/my-app:ref:refs/heads/main</code></td>
      </tr>
  </tbody>
</table>
<p>Trust policy 的 <code>sub</code> condition 要根據實際需要選擇收斂到哪個層級。只允許 main branch 的 push 就寫 <code>repo:my-org/my-app:ref:refs/heads/main</code>；只允許 production environment 的 deploy 就寫 <code>repo:my-org/my-app:environment:production</code>。</p>
<h3 id="environment-based-收斂推薦">environment-based 收斂（推薦）</h3>
<p>GitHub Actions 的 environment 功能讓 <code>sub</code> claim 帶上 environment 名稱。搭配 environment protection rules（required reviewers、wait timer），可以在 trust policy 層和 GitHub 層各設一道 gate：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">condition</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  test</span>     <span class="o">=</span> <span class="s2">&#34;StringEquals&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  variable</span> <span class="o">=</span> <span class="s2">&#34;token.actions.githubusercontent.com:sub&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">  values</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;repo:my-org/my-app:environment:production&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">}</span></span></code></pre></div><p>Workflow 裡對應的設定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">apply</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="l">production</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">      </span><span class="nt">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">      </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</span></span></span></code></pre></div><p>只有 workflow 宣告了 <code>environment: production</code> 且通過 environment 的 protection rules 後，runner 拿到的 token 才會帶上 <code>environment:production</code> 的 sub claim，才能 assume 這個 role。</p>
<h2 id="plan-role-與-apply-role-分離">Plan Role 與 Apply Role 分離</h2>
<p>把 plan 和 apply 拆成兩個 role，各自給最小權限。plan 只需要 read 權限（讀 state、讀雲端現況），apply 需要 write 權限（建立/修改/刪除資源）。分離的好處是 PR 階段的 plan 即使被攻破，攻擊者也只能讀不能改。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_iam_role&#34; &#34;infra_plan&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  name</span>               <span class="o">=</span> <span class="s2">&#34;infra-plan&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  assume_role_policy</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_iam_policy_document</span><span class="p">.</span><span class="k">plan_trust</span><span class="p">.</span><span class="k">json</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">}
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_iam_role&#34; &#34;infra_apply&#34;</span> {
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">  name</span>               <span class="o">=</span> <span class="s2">&#34;infra-apply&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  assume_role_policy</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_iam_policy_document</span><span class="p">.</span><span class="k">apply_trust</span><span class="p">.</span><span class="k">json</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">}
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_iam_role_policy_attachment&#34; &#34;plan_readonly&#34;</span> {
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">  role</span>       <span class="o">=</span> <span class="k">aws_iam_role</span><span class="p">.</span><span class="k">infra_plan</span><span class="p">.</span><span class="k">name</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">  policy_arn</span> <span class="o">=</span> <span class="s2">&#34;arn:aws:iam::aws:policy/ReadOnlyAccess&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">}</span></span></code></pre></div><p>Trust policy 的差異：plan role 允許任何 branch 的 PR 觸發（<code>repo:my-org/my-app:pull_request</code>）；apply role 只允許 main branch 或 production environment（<code>repo:my-org/my-app:environment:production</code>）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">plan</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">github.event_name == &#39;pull_request&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">pull-requests</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">aws-actions/configure-aws-credentials@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">          </span><span class="nt">role-to-assume</span><span class="p">:</span><span class="w"> </span><span class="l">arn:aws:iam::123456789012:role/infra-plan</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">          </span><span class="nt">aws-region</span><span class="p">:</span><span class="w"> </span><span class="l">ap-northeast-1</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">terraform plan -out=plan.tfplan</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="nt">apply</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">github.ref == &#39;refs/heads/main&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="l">production</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">      </span><span class="nt">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">      </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">aws-actions/configure-aws-credentials@v4</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">          </span><span class="nt">role-to-assume</span><span class="p">:</span><span class="w"> </span><span class="l">arn:aws:iam::123456789012:role/infra-apply</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">          </span><span class="nt">aws-region</span><span class="p">:</span><span class="w"> </span><span class="l">ap-northeast-1</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">terraform apply -auto-approve</span></span></span></code></pre></div><h2 id="常見設定錯誤">常見設定錯誤</h2>
<h3 id="audience-不匹配">audience 不匹配</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">Error: Not authorized to perform sts:AssumeRoleWithWebIdentity</span></span></code></pre></div><p>最常見的原因是 trust policy 的 <code>aud</code> condition 值跟 OIDC provider 的 <code>client_id_list</code> 不一致。兩者都要是 <code>sts.amazonaws.com</code>。如果用了舊版的 <code>configure-aws-credentials</code> action（v1），它預設用 <code>sigstore</code> 作為 audience，跟 <code>sts.amazonaws.com</code> 對不上。確認 action 版本是 v4+。</p>
<h3 id="sub-condition-太寬">sub condition 太寬</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">condition</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  test</span>     <span class="o">=</span> <span class="s2">&#34;StringLike&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  variable</span> <span class="o">=</span> <span class="s2">&#34;token.actions.githubusercontent.com:sub&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">  values</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;repo:my-org/*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">}</span></span></code></pre></div><p>這允許 <code>my-org</code> 底下任何 repo 的任何 branch assume 這個 role。如果組織裡有公開 repo 或 fork 權限寬鬆的 repo，攻擊者可以在那些 repo 裡觸發 workflow 來 assume 生產環境的 role。至少收斂到 repo 層級（<code>repo:my-org/my-app:*</code>），生產環境收斂到 branch 或 environment。</p>
<h3 id="sub-condition-太緊">sub condition 太緊</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">condition</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  test</span>     <span class="o">=</span> <span class="s2">&#34;StringEquals&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  variable</span> <span class="o">=</span> <span class="s2">&#34;token.actions.githubusercontent.com:sub&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">  values</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;repo:my-org/my-app:ref:refs/heads/main&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">}</span></span></code></pre></div><p>這只允許 push to main 觸發的 workflow。PR 觸發的 workflow 拿到的 sub 是 <code>repo:my-org/my-app:pull_request</code>，跟這個 condition 不匹配，plan 階段會失敗。如果 plan 需要在 PR 階段跑，plan role 的 trust policy 要加 PR 的 sub pattern。</p>
<h3 id="忘記設-permissions">忘記設 permissions</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">deploy</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="c"># 缺少 permissions 區塊</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">aws-actions/configure-aws-credentials@v4</span></span></span></code></pre></div><p>GitHub Actions 的 OIDC token 只有在 workflow 宣告 <code>permissions: { id-token: write }</code> 時才會簽發。缺了這一行，<code>configure-aws-credentials</code> 拿不到 token，報「OIDC token not available」。這個錯誤訊息不直觀——它說的是 token 不存在，不是權限不夠。</p>
<h3 id="多帳號時忘記指定-provider">多帳號時忘記指定 provider</h3>
<p>如果組織有多個 AWS 帳號，每個帳號都要各自建 OIDC provider。trust policy 的 <code>Federated</code> principal 要指向本帳號的 provider ARN，不能跨帳號引用。跨帳號部署時，workflow 用不同的 <code>role-to-assume</code> 切換帳號，每個帳號的 role 各自信任同一個 GitHub OIDC issuer 但是各自獨立的 provider 資源。</p>
<h2 id="測試與驗證">測試與驗證</h2>
<p>設定完成後的驗證步驟：</p>
<ol>
<li><strong>手動觸發 workflow</strong>：push 一個無害的 commit 到 main、開一個 test PR，觀察 <code>configure-aws-credentials</code> 步驟是否成功</li>
<li><strong>檢查 CloudTrail</strong>：搜尋 <code>AssumeRoleWithWebIdentity</code> 事件，確認 source identity 和 assumed role 正確</li>
<li><strong>反向驗證</strong>：從一個不在 trust policy 允許範圍的 repo 或 branch 觸發 workflow，確認 assume 被拒絕</li>
<li><strong>權限範圍驗證</strong>：在 plan job 裡嘗試一個 write 操作（如 <code>aws s3 rm</code>），確認被拒絕——驗證 plan role 的 read-only 限制確實生效</li>
</ol>





<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"># 在 CloudTrail 搜尋 OIDC assume 事件</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws cloudtrail lookup-events <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --lookup-attributes <span class="nv">AttributeKey</span><span class="o">=</span>EventName,AttributeValue<span class="o">=</span>AssumeRoleWithWebIdentity <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --max-items <span class="m">5</span></span></span></code></pre></div><p>驗證通過後，這套 OIDC 設定就取代了所有存放在 CI 環境變數裡的 access key。原有的 key 可以排程停用和刪除，排程的節奏見<a href="/blog/infra/02-identity-credentials/access-key-rotation-playbook/" data-link-title="Access Key 輪替手冊" data-link-desc="從 credential report 盤點散落的長期 access key，到逐把輪替、自動化輪替與 key age 監控的完整操作步驟">access key 輪替</a>。trust policy 的持續維護重點是：新增 repo 時 sub condition 要同步更新、組織改名時 issuer 的 repo 路徑要全面修正。</p>
<p>時程參考：OIDC provider 建立 + trust policy 設計 + workflow 驗證約需 1-2 小時。OIDC provider 與 IAM role 本身不產生額外費用。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/02-identity-credentials/iam-oidc-privilege-boundary/" data-link-title="身分與憑證地基 — IAM 模型、OIDC 短期憑證與權限邊界設計" data-link-desc="IAM 的 identity / policy / role 三元件、最小權限的持續收斂、用 OIDC 取代長期 access key，以及 SCP 與 Permissions Boundary 的環境隔離">身分與憑證地基</a>：OIDC 的概念基礎與權限邊界設計</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>：plan/apply 的 CI pipeline 怎麼用這裡設定好的 role</li>
<li>→ <a href="/blog/infra/02-identity-credentials/multi-account-strategy/" data-link-title="跨帳號策略 — Organizations、SCP 與帳號工廠" data-link-desc="用 AWS Organizations 把環境拆成獨立帳號、用 SCP 設定連管理員都越不過的護欄、用帳號工廠讓每個新帳號自帶安全基線">跨帳號策略</a>：多帳號環境下的 OIDC provider 配置</li>
</ul>
]]></content:encoded></item><item><title>OIDC 聯合</title><link>https://tarrragon.github.io/blog/infra/knowledge-cards/oidc/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/knowledge-cards/oidc/</guid><description>&lt;p>OIDC（OpenID Connect）聯合的核心職責是讓跑在雲外的 CI/CD 平台（GitHub Actions、GitLab CI）用每次執行才簽發、幾分鐘後就失效的短期憑證存取雲端資源，從根本上消除「在 CI 環境裡存放長期 access key」這個攻擊面。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>OIDC 聯合在身分與憑證地基裡的角色是「雲外機器身分的認證機制」。跑在雲上的 workload（EC2、ECS task）可以用平台原生的 instance profile 或 task role 取得短期憑證；跑在雲外的 CI/CD 沒有這個管道，OIDC 就是替代方案。&lt;/p>
&lt;p>運作方式是建立信任關係：雲端帳號信任某個外部 identity provider（如 GitHub Actions 的 OIDC issuer），CI 執行時平台簽發一個帶 claim 的 token（描述哪個 repo、哪個 branch、哪個 workflow），雲端用這個 token 換出一段臨時憑證。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>以下狀況指向 OIDC 相關問題：&lt;/p>
&lt;ul>
&lt;li>CI pipeline 裡有 &lt;code>AWS_ACCESS_KEY_ID&lt;/code> 和 &lt;code>AWS_SECRET_ACCESS_KEY&lt;/code> 環境變數 — 這是長期 key，應該替換成 OIDC&lt;/li>
&lt;li>Trust policy 只驗 issuer 不驗 repo — 任何掛在同一個 CI 平台的專案都能假扮這個 role&lt;/li>
&lt;li>Pipeline 突然無法取得權限 — 可能是 trust policy 的 condition 跟 token claim 不匹配（常見於 repo 改名或 branch 改名後）&lt;/li>
&lt;/ul>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設定 OIDC 聯合時要決定：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Trust policy 的 claim 收斂&lt;/strong>：限定 issuer + audience + 特定 repo + 特定 branch，每個條件都收到最緊&lt;/li>
&lt;li>&lt;strong>Role 的權限範圍&lt;/strong>：OIDC 換到的 role 仍然要遵循最小權限 — 只給 pipeline 需要的 action&lt;/li>
&lt;li>&lt;strong>Plan 與 apply 分開的 role&lt;/strong>：plan 只需要 read 權限、apply 需要 write 權限，用兩個 role 降低 PR 階段的風險&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> — OIDC 是 IAM 身分系統的一種外部身分來源&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/security-group/" data-link-title="Security Group" data-link-desc="掛在資源網卡層級的有狀態防火牆，逐埠決定哪些來源能連進這個資源">Security Group&lt;/a> — OIDC 解的是身分層的認證問題，跟網路層的 security group 正交&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>OIDC（OpenID Connect）聯合的核心職責是讓跑在雲外的 CI/CD 平台（GitHub Actions、GitLab CI）用每次執行才簽發、幾分鐘後就失效的短期憑證存取雲端資源，從根本上消除「在 CI 環境裡存放長期 access key」這個攻擊面。</p>
<h2 id="概念位置">概念位置</h2>
<p>OIDC 聯合在身分與憑證地基裡的角色是「雲外機器身分的認證機制」。跑在雲上的 workload（EC2、ECS task）可以用平台原生的 instance profile 或 task role 取得短期憑證；跑在雲外的 CI/CD 沒有這個管道，OIDC 就是替代方案。</p>
<p>運作方式是建立信任關係：雲端帳號信任某個外部 identity provider（如 GitHub Actions 的 OIDC issuer），CI 執行時平台簽發一個帶 claim 的 token（描述哪個 repo、哪個 branch、哪個 workflow），雲端用這個 token 換出一段臨時憑證。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>以下狀況指向 OIDC 相關問題：</p>
<ul>
<li>CI pipeline 裡有 <code>AWS_ACCESS_KEY_ID</code> 和 <code>AWS_SECRET_ACCESS_KEY</code> 環境變數 — 這是長期 key，應該替換成 OIDC</li>
<li>Trust policy 只驗 issuer 不驗 repo — 任何掛在同一個 CI 平台的專案都能假扮這個 role</li>
<li>Pipeline 突然無法取得權限 — 可能是 trust policy 的 condition 跟 token claim 不匹配（常見於 repo 改名或 branch 改名後）</li>
</ul>
<h2 id="設計責任">設計責任</h2>
<p>設定 OIDC 聯合時要決定：</p>
<ul>
<li><strong>Trust policy 的 claim 收斂</strong>：限定 issuer + audience + 特定 repo + 特定 branch，每個條件都收到最緊</li>
<li><strong>Role 的權限範圍</strong>：OIDC 換到的 role 仍然要遵循最小權限 — 只給 pipeline 需要的 action</li>
<li><strong>Plan 與 apply 分開的 role</strong>：plan 只需要 read 權限、apply 需要 write 權限，用兩個 role 降低 PR 階段的風險</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> — OIDC 是 IAM 身分系統的一種外部身分來源</li>
<li><a href="/blog/infra/knowledge-cards/security-group/" data-link-title="Security Group" data-link-desc="掛在資源網卡層級的有狀態防火牆，逐埠決定哪些來源能連進這個資源">Security Group</a> — OIDC 解的是身分層的認證問題，跟網路層的 security group 正交</li>
</ul>
]]></content:encoded></item></channel></rss>