<?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>Iam on Tarragon</title><link>https://tarrragon.github.io/blog/tags/iam/</link><description>Recent content in Iam on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 30 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/iam/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>跨帳號策略 — Organizations、SCP 與帳號工廠</title><link>https://tarrragon.github.io/blog/infra/02-identity-credentials/multi-account-strategy/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/02-identity-credentials/multi-account-strategy/</guid><description>&lt;p>單一帳號走到某個規模後，帳號本身會變成隔離的瓶頸。IAM policy 能控制「誰能做什麼」，但同一個帳號裡的所有資源共用同一組 service quota、同一份 CloudTrail、同一張帳單，一個團隊的操作失誤或資源耗盡會波及整個帳號。把環境拆成獨立帳號，讓每個帳號只承載一個職責，是 IAM 之上的第二層隔離 — &lt;a href="https://tarrragon.github.io/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 的環境隔離">模組二的身分與憑證地基&lt;/a>控制的是「誰能做什麼」，帳號邊界控制的是「做錯了波及多遠」。&lt;/p>
&lt;h2 id="單帳號-vs-多帳號什麼時候該切">單帳號 vs 多帳號：什麼時候該切&lt;/h2>
&lt;p>單帳號在早期是合理的起點 — 資源少、人少、管理成本低。帳號邊界帶來的隔離收益要跟它的管理成本比較：每多一個帳號就多一份 CloudTrail、多一組 IAM 基線、多一個需要管理的 state backend。&lt;/p>
&lt;p>三個訊號出現時，單帳號的邊際風險開始超過多帳號的管理成本：&lt;/p>
&lt;p>第一，production 和 dev 的資源開始互相影響。一個 dev 環境的壓力測試把帳號的 EC2 instance quota 吃滿，production 的 auto-scaling 因為拿不到新 instance 而失敗 — 這個故障跟程式碼品質無關，純粹是兩個環境共用同一組配額。帳號分開後，dev 吃滿自己的 quota 不會碰到 production。&lt;/p>
&lt;p>第二，權限邊界用 IAM 已經管不住。一個工程師的 IAM policy 限制他只能操作 &lt;code>env=dev&lt;/code> 的資源，但他手滑用了一個沒有 tag 條件的 policy、或者某個 IAM role 的 trust policy 太寬，他就能碰到 production 資源。帳號邊界是比 IAM policy 更硬的護欄 — 即使 IAM 設定出錯，帳號邊界本身就是物理隔離。&lt;/p>
&lt;p>第三，合規或稽核要求明確區分環境。SOC 2 或金融監管可能要求 production 環境有獨立的存取紀錄和變更審計，與開發環境完全分離。同帳號裡做這件事要靠大量的 IAM 條件和 CloudTrail filter，跨帳號則天然滿足。&lt;/p>
&lt;h2 id="ou-結構帳號怎麼分群">OU 結構：帳號怎麼分群&lt;/h2>
&lt;p>AWS Organizations 用 Organizational Unit（OU）把帳號分群，OU 是 SCP 的掛載點 — 一條 SCP 掛在 OU 上，底下所有帳號都受約束。OU 的設計決定了護欄的作用範圍。&lt;/p>
&lt;p>常見的 OU 拓撲有四層：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>OU&lt;/th>
 &lt;th>底下的帳號&lt;/th>
 &lt;th>職責&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Security&lt;/td>
 &lt;td>Log Archive、Security Tooling&lt;/td>
 &lt;td>集中存放 CloudTrail / Config 日誌、安全工具帳號&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Workload / Prod&lt;/td>
 &lt;td>每個產品線或服務的 production 帳號&lt;/td>
 &lt;td>承載正式流量，SCP 最嚴格&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Workload / NonProd&lt;/td>
 &lt;td>dev、staging 帳號&lt;/td>
 &lt;td>承載開發與驗證，SCP 較寬鬆但仍有底線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sandbox&lt;/td>
 &lt;td>個人實驗帳號&lt;/td>
 &lt;td>可隨時重建，SCP 限制預算上限和禁止的服務&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>環境怎麼對應到帳號，跟&lt;a href="https://tarrragon.github.io/blog/infra/04-environment-separation/directory-module-parameterization/" data-link-title="環境分離與模組化 — 目錄結構、module 參數化與 retrofit 路徑" data-link-desc="用目錄結構在第一天就隔開 dev 與 prod 的 state，用 module 讓環境共用同一套邏輯只差參數，以及已經單環境跑起來後怎麼安全拆分">模組四的環境分離&lt;/a>是同一個問題的不同層次 — 模組四用目錄和 state 分離環境的 IaC，這裡用帳號分離環境的雲端資源。兩者可以疊加：每個帳號裡的 IaC 仍然用獨立目錄和 state 管理。&lt;/p>
&lt;p>OU 結構的設計原則是「按信任等級分群、按職責隔離」。Prod 跟 NonProd 分開是因為信任等級不同（prod 的 SCP 更嚴格）。Security 獨立是因為它的職責是「監控其他所有帳號」— 如果 security 帳號被攻破，攻擊者能修改稽核日誌來掩蓋行蹤，所以它的存取權限要收到最小。&lt;/p>
&lt;p>一個常見的錯誤是把 OU 當成組織架構的映射（按部門分 OU）。OU 的分群依據是安全邊界和 SCP 策略，不是彙報線。兩個部門如果需要相同的 SCP，它們的帳號應該在同一個 OU 底下；一個部門如果有 prod 和 dev 環境，它們應該在不同 OU 底下。&lt;/p></description><content:encoded><![CDATA[<p>單一帳號走到某個規模後，帳號本身會變成隔離的瓶頸。IAM policy 能控制「誰能做什麼」，但同一個帳號裡的所有資源共用同一組 service quota、同一份 CloudTrail、同一張帳單，一個團隊的操作失誤或資源耗盡會波及整個帳號。把環境拆成獨立帳號，讓每個帳號只承載一個職責，是 IAM 之上的第二層隔離 — <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>控制的是「誰能做什麼」，帳號邊界控制的是「做錯了波及多遠」。</p>
<h2 id="單帳號-vs-多帳號什麼時候該切">單帳號 vs 多帳號：什麼時候該切</h2>
<p>單帳號在早期是合理的起點 — 資源少、人少、管理成本低。帳號邊界帶來的隔離收益要跟它的管理成本比較：每多一個帳號就多一份 CloudTrail、多一組 IAM 基線、多一個需要管理的 state backend。</p>
<p>三個訊號出現時，單帳號的邊際風險開始超過多帳號的管理成本：</p>
<p>第一，production 和 dev 的資源開始互相影響。一個 dev 環境的壓力測試把帳號的 EC2 instance quota 吃滿，production 的 auto-scaling 因為拿不到新 instance 而失敗 — 這個故障跟程式碼品質無關，純粹是兩個環境共用同一組配額。帳號分開後，dev 吃滿自己的 quota 不會碰到 production。</p>
<p>第二，權限邊界用 IAM 已經管不住。一個工程師的 IAM policy 限制他只能操作 <code>env=dev</code> 的資源，但他手滑用了一個沒有 tag 條件的 policy、或者某個 IAM role 的 trust policy 太寬，他就能碰到 production 資源。帳號邊界是比 IAM policy 更硬的護欄 — 即使 IAM 設定出錯，帳號邊界本身就是物理隔離。</p>
<p>第三，合規或稽核要求明確區分環境。SOC 2 或金融監管可能要求 production 環境有獨立的存取紀錄和變更審計，與開發環境完全分離。同帳號裡做這件事要靠大量的 IAM 條件和 CloudTrail filter，跨帳號則天然滿足。</p>
<h2 id="ou-結構帳號怎麼分群">OU 結構：帳號怎麼分群</h2>
<p>AWS Organizations 用 Organizational Unit（OU）把帳號分群，OU 是 SCP 的掛載點 — 一條 SCP 掛在 OU 上，底下所有帳號都受約束。OU 的設計決定了護欄的作用範圍。</p>
<p>常見的 OU 拓撲有四層：</p>
<table>
  <thead>
      <tr>
          <th>OU</th>
          <th>底下的帳號</th>
          <th>職責</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Security</td>
          <td>Log Archive、Security Tooling</td>
          <td>集中存放 CloudTrail / Config 日誌、安全工具帳號</td>
      </tr>
      <tr>
          <td>Workload / Prod</td>
          <td>每個產品線或服務的 production 帳號</td>
          <td>承載正式流量，SCP 最嚴格</td>
      </tr>
      <tr>
          <td>Workload / NonProd</td>
          <td>dev、staging 帳號</td>
          <td>承載開發與驗證，SCP 較寬鬆但仍有底線</td>
      </tr>
      <tr>
          <td>Sandbox</td>
          <td>個人實驗帳號</td>
          <td>可隨時重建，SCP 限制預算上限和禁止的服務</td>
      </tr>
  </tbody>
</table>
<p>環境怎麼對應到帳號，跟<a href="/blog/infra/04-environment-separation/directory-module-parameterization/" data-link-title="環境分離與模組化 — 目錄結構、module 參數化與 retrofit 路徑" data-link-desc="用目錄結構在第一天就隔開 dev 與 prod 的 state，用 module 讓環境共用同一套邏輯只差參數，以及已經單環境跑起來後怎麼安全拆分">模組四的環境分離</a>是同一個問題的不同層次 — 模組四用目錄和 state 分離環境的 IaC，這裡用帳號分離環境的雲端資源。兩者可以疊加：每個帳號裡的 IaC 仍然用獨立目錄和 state 管理。</p>
<p>OU 結構的設計原則是「按信任等級分群、按職責隔離」。Prod 跟 NonProd 分開是因為信任等級不同（prod 的 SCP 更嚴格）。Security 獨立是因為它的職責是「監控其他所有帳號」— 如果 security 帳號被攻破，攻擊者能修改稽核日誌來掩蓋行蹤，所以它的存取權限要收到最小。</p>
<p>一個常見的錯誤是把 OU 當成組織架構的映射（按部門分 OU）。OU 的分群依據是安全邊界和 SCP 策略，不是彙報線。兩個部門如果需要相同的 SCP，它們的帳號應該在同一個 OU 底下；一個部門如果有 prod 和 dev 環境，它們應該在不同 OU 底下。</p>
<h2 id="scp連管理員都越不過的護欄">SCP：連管理員都越不過的護欄</h2>
<p>Service Control Policy（SCP）是掛在 OU 或帳號上的權限天花板。它跟 IAM policy 的差別是層級：IAM policy 控制「這個身分能做什麼」，SCP 控制「這個帳號裡的任何身分最多能做什麼」。即使帳號內的 root user 或 AdministratorAccess role，也受 SCP 約束。</p>
<p>SCP 的設計策略以 deny-list 為主 — 預設允許所有動作，用 SCP 明確禁止少數高風險操作。相比 allow-list（預設禁止、逐一開放），deny-list 的管理成本低得多，因為 AWS 的 service 和 action 數量龐大，逐一列舉允許清單容易漏、也容易在新服務上線時擋住正常使用。</p>
<p>三條適合從第一天就掛上去的 SCP：</p>
<h3 id="禁止關閉-cloudtrail">禁止關閉 CloudTrail</h3>





<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="nt">&#34;Sid&#34;</span><span class="p">:</span> <span class="s2">&#34;DenyCloudTrailDisable&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</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"> 6</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"> 7</span><span class="cl">      <span class="s2">&#34;cloudtrail:StopLogging&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="s2">&#34;cloudtrail:DeleteTrail&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="s2">&#34;cloudtrail:UpdateTrail&#34;</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;Resource&#34;</span><span class="p">:</span> <span class="s2">&#34;*&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="p">}]</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>CloudTrail 是事後追溯「誰做了什麼」的唯一來源。攻擊者入侵帳號後的第一步往往是關掉稽核日誌來掩蓋行蹤，用 SCP 禁止這個動作，讓日誌在帳號層級不可關閉。</p>
<h3 id="禁止離開指定-region">禁止離開指定 region</h3>





<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="nt">&#34;Sid&#34;</span><span class="p">:</span> <span class="s2">&#34;DenyOutsideRegion&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</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"> 6</span><span class="cl">    <span class="nt">&#34;NotAction&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="s2">&#34;iam:*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="s2">&#34;sts:*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="s2">&#34;organizations:*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="s2">&#34;support:*&#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 class="nt">&#34;Resource&#34;</span><span class="p">:</span> <span class="s2">&#34;*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nt">&#34;Condition&#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="nt">&#34;StringNotEquals&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="nt">&#34;aws:RequestedRegion&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;ap-northeast-1&#34;</span><span class="p">,</span> <span class="s2">&#34;us-east-1&#34;</span><span class="p">]</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="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 class="p">}</span></span></span></code></pre></div><p>限制資源只能建在指定 region，避免有人在沒人注意的 region（如 <code>af-south-1</code>）開資源 — 不管是誤操作還是攻擊者利用。<code>NotAction</code> 裡排除 IAM 和 STS 等全域服務，因為它們不分 region。<code>us-east-1</code> 通常要保留，因為 CloudFront、ACM（global cert）等服務的 API 端點在 us-east-1。</p>
<h3 id="禁止刪除-vpc-flow-logs">禁止刪除 VPC Flow Logs</h3>





<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="nt">&#34;Sid&#34;</span><span class="p">:</span> <span class="s2">&#34;DenyDeleteFlowLogs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</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">6</span><span class="cl">    <span class="nt">&#34;Action&#34;</span><span class="p">:</span> <span class="s2">&#34;ec2:DeleteFlowLogs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</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">8</span><span class="cl">  <span class="p">}]</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>VPC Flow Logs 記錄網路層的流量軌跡，是安全事件排查的關鍵資料。跟 CloudTrail 的邏輯一樣 — 稽核資料不允許被帳號內的操作者刪除。</p>
<h3 id="scp-的繼承模型">SCP 的繼承模型</h3>
<p>SCP 沿著 OU 樹向下繼承：掛在 Root OU 的 SCP 對所有帳號生效，掛在子 OU 的 SCP 只對該 OU 底下的帳號生效。多層 SCP 的效果是交集 — 父 OU 禁止的動作，子 OU 無法用 SCP 重新允許。這個交集模型讓安全團隊能在頂層設「絕對底線」，各子 OU 只能在底線之內進一步收斂、不能放寬。</p>
<p>把 SCP 用 Terraform 管理：</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_organizations_policy&#34; &#34;deny_cloudtrail_disable&#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;deny-cloudtrail-disable&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  description</span> <span class="o">=</span> <span class="s2">&#34;Prevent anyone from stopping or deleting CloudTrail&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  type</span>        <span class="o">=</span> <span class="s2">&#34;SERVICE_CONTROL_POLICY&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">  content</span>     <span class="o">=</span> <span class="k">file</span><span class="p">(</span><span class="s2">&#34;policies/deny-cloudtrail-disable.json&#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><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_organizations_policy_attachment&#34; &#34;root_deny_cloudtrail&#34;</span> {
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  policy_id</span> <span class="o">=</span> <span class="k">aws_organizations_policy</span><span class="p">.</span><span class="k">deny_cloudtrail_disable</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">  target_id</span> <span class="o">=</span> <span class="k">aws_organizations_organization</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">roots</span><span class="p">[</span><span class="m">0</span><span class="p">].</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">}</span></span></code></pre></div><p>SCP 的 JSON 存在 repo 的 <code>policies/</code> 目錄，變更走 PR review，讓護欄本身也在版本控制與審查流程裡。</p>
<p>控制面 token 的治理是 SCP 護欄之外需要同步處理的議題。Cloudflare 2023 事件中，控制面 token 的生命週期與最小權限沒有對齊，機器憑證形成跨服務的高權限風險（見 <a href="/blog/backend/07-security-data-protection/cases/cloudflare-control-plane-token-2023/" data-link-title="7.C2 Cloudflare：2023 Control-plane Token 事件" data-link-desc="控制面 token 事件如何回寫 secrets 與機器憑證治理。">Cloudflare：Control-plane Token 事件</a>）。Okta 2023 事件則顯示身份治理若只覆蓋生產系統而忽略支援工具鏈，支援系統的 session 和 token 會成為跨租戶的風險放大點（見 <a href="/blog/backend/07-security-data-protection/cases/okta-support-system-incident-2023/" data-link-title="7.C5 Okta：2023 Support System 事件" data-link-desc="支援系統憑證風險如何擴散到客戶租戶的案例。">Okta：Support System 事件</a>）。兩個案例的共同教訓是：SCP 管的是 AWS API 層的動作上限，但 token / session 這類應用層的機器憑證需要獨立的 lifecycle 治理。</p>
<h2 id="帳號工廠每個新帳號自帶安全基線">帳號工廠：每個新帳號自帶安全基線</h2>
<p>跨帳號策略（帳號數量、OU 結構、SCP 規則）屬於影響全組織的架構決策，建議在實施前取得技術主管或 CTO 的對齊。SCP 一旦套用到 OU，該 OU 下所有帳號立即受影響，回退需要修改 SCP 或移動帳號到不同 OU。</p>
<p>手動建帳號的問題跟手動建資源一樣 — 每次都靠人記得「開完帳號後要開 CloudTrail、要刪預設 VPC、要設基線 IAM role」。帳號工廠（Account Factory）把這些步驟自動化成一個可重複的流程：建一個帳號、自動套用安全基線、自動加進正確的 OU。</p>
<p>AWS Control Tower 是 AWS 提供的帳號工廠實作，它包裝了 Organizations、SCP、Config Rules 和 CloudFormation StackSet，提供一個「建帳號 → 自動配置」的流水線。它的好處是一鍵啟用、內建一組 AWS 建議的護欄；代價是它對 OU 結構和 SCP 有自己的意見，跟團隊已有的設計可能衝突，而且它用 CloudFormation StackSet 做基線配置，跟 Terraform 管理的資源需要劃清邊界。</p>
<p>不用 Control Tower 時，帳號工廠可以用 Terraform + 腳本自建。核心是一個 module 接受帳號名稱和 OU 作為參數，產出：帳號建立、CloudTrail trail、預設 VPC 刪除、基線 IAM role（讓管理帳號能 assume 進來做維護）、Config recorder 啟用。</p>
<p>每個新帳號該自帶的安全基線至少包含：</p>
<ul>
<li>CloudTrail 開啟並寫到集中的 Log Archive 帳號</li>
<li>預設 VPC 刪除（預設 VPC 的 security group 全通、CIDR 固定且跨帳號重複，留著是隱患）</li>
<li>基線 IAM role 讓管理帳號能 assume 進來</li>
<li>Config recorder 啟用（記錄資源設定變更歷史）</li>
<li>掛上所屬 OU 的 SCP</li>
</ul>
<p>導入時程參考：初次設定 Organizations + OU 結構 + day-1 SCP 約需 2-3 天；之後每開一個新帳號（含基線配置）約需 2-4 小時。</p>
<h2 id="跨帳號存取role-assumption">跨帳號存取：role assumption</h2>
<p>多帳號架構裡，人或自動化需要在不同帳號之間切換操作。跨帳號存取用 IAM role 的 trust policy 實現 — 目標帳號建一個 role，trust policy 允許來源帳號的特定身分 assume 這個 role。</p>
<p>AWS Organizations 在建子帳號時會自動建一個 <code>OrganizationAccountAccessRole</code>，讓管理帳號的 admin 能 assume 進去。這個 role 的權限是 AdministratorAccess — 它的用途是初始設定和緊急存取，日常操作不該用它。日常的跨帳號存取應該建立職責專用的 role：部署用的 role 只有部署相關權限、唯讀稽核用的 role 只有 read 權限。</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;deploy_from_cicd&#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;deploy-from-cicd-account&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  assume_role_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 class="n">      Effect</span>    <span class="o">=</span> <span class="s2">&#34;Allow&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">      Principal</span> <span class="o">=</span><span class="n"> { AWS</span> <span class="o">=</span> <span class="s2">&#34;arn:aws:iam::111111111111:role/cicd-runner&#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="s2">&#34;sts:AssumeRole&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">      Condition</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">        StringEquals</span> <span class="o">=</span><span class="n"> { &#34;sts:ExternalId&#34;</span> <span class="o">=</span> <span class="s2">&#34;deploy-prod-2026&#34;</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="p">]</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  }<span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">}</span></span></code></pre></div><p><code>ExternalId</code> 是防止 confused deputy 攻擊的機制 — 如果 trust policy 只用帳號 ID 驗證，任何能在來源帳號建 role 的人都能 assume 目標 role。加上 ExternalId 讓 assumption 多一個只有雙方知道的驗證值。</p>
<p>跨帳號存取的設計與<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 的環境隔離">模組二的 OIDC 短期憑證</a>互補 — OIDC 解決「雲外到雲內」的身分聯合（CI/CD → AWS），role assumption 解決「雲內帳號之間」的身分切換。</p>
<h2 id="帳單整合">帳單整合</h2>
<p>Organizations 的附帶收益是合併帳單（Consolidated Billing）。所有子帳號的用量合併到管理帳號的帳單裡，一方面簡化付款流程（一張帳單而非多張），另一方面可以享受跨帳號的用量折扣 — 例如 S3 的定價階梯是看總用量，三個帳號各用 1TB 分開計費跟合併成 3TB 計費，後者的單位價格更低。</p>
<p>合併帳單跟成本歸屬的 tagging 互補。合併帳單讓所有費用匯到一張帳單，tagging 讓這張帳單能拆到各團隊和用途 — 這兩件事在<a href="/blog/infra/08-governance-habits/cost-visibility-rhythm/" data-link-title="成本可見性與最小可行治理節奏" data-link-desc="用 tag 驅動的成本分攤讓帳單有人負責，以及判斷什麼治理該 day-1 就立、什麼等規模逼出來再加">模組八的成本可見性</a>展開。帳號邊界本身也是一層成本隔離：每個帳號的用量可以獨立查看，讓「這個帳號這個月花了多少」變成自動可查、不需要依賴 tag。</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>：IAM role / policy / OIDC 是帳號內的身分控制，本篇是帳號間的隔離</li>
<li>→ <a href="/blog/infra/04-environment-separation/directory-module-parameterization/" data-link-title="環境分離與模組化 — 目錄結構、module 參數化與 retrofit 路徑" data-link-desc="用目錄結構在第一天就隔開 dev 與 prod 的 state，用 module 讓環境共用同一套邏輯只差參數，以及已經單環境跑起來後怎麼安全拆分">環境分離與模組化</a>：目錄與 state 分離環境的 IaC，帳號分離是雲端資源層的對應</li>
<li>→ <a href="/blog/infra/08-governance-habits/cost-visibility-rhythm/" data-link-title="成本可見性與最小可行治理節奏" data-link-desc="用 tag 驅動的成本分攤讓帳單有人負責，以及判斷什麼治理該 day-1 就立、什麼等規模逼出來再加">成本可見性</a>：合併帳單 + tagging 的成本歸屬</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>：SCP 的 JSON 存 repo、變更走 PR review</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>團隊權限分級與存取管理</title><link>https://tarrragon.github.io/blog/infra/02-identity-credentials/team-access-management/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/02-identity-credentials/team-access-management/</guid><description>&lt;p>IAM 的 role 與 policy 提供「某個身分能不能對某個資源做某件事」的技術機制（見&lt;a href="https://tarrragon.github.io/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 的環境隔離">身分與憑證地基&lt;/a>）。機制備妥後，下一個問題是組織層面的設計：團隊裡每個角色該拿到哪一級權限、臨時需要更高權限時怎麼提權、離職或合約結束時怎麼確保存取被回收。這些設計的目的是讓「誰能動什麼」在任何時間點都有可稽核的答案。&lt;/p>
&lt;h2 id="權限分級admin--operator--viewer">權限分級：admin / operator / viewer&lt;/h2>
&lt;p>團隊成員的日常操作權限用三級來劃分，每一級對應不同的操作範圍與風險。分級的依據是「這個角色的日常工作需要碰到什麼層級的資源」，不是職稱或年資。&lt;/p>
&lt;h3 id="admin">Admin&lt;/h3>
&lt;p>Admin 能修改 IAM policy、網路拓撲、帳號層級設定（Organizations、SCP、billing）。這是影響範圍最大的一級——一條 SCP 寫錯可以鎖死整個帳號的操作，一條 IAM policy 開太寬可以讓任何角色取得不該有的權限。&lt;/p>
&lt;p>持有 admin 權限的人數應該收斂到最少：通常是平台團隊的 1-2 人加上一個 break-glass 備援角色。Admin 權限不應該是某個人的「日常身分」——即使是平台工程師，日常操作也用 operator 等級，只有在需要改 IAM 或帳號設定時才 assume 到 admin 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"># Admin role 的信任政策：只允許特定 IAM user assume
&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;admin_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;AWS&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;arn:aws:iam::123456789012:user/platform-lead&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;arn:aws:iam::123456789012:user/platform-backup&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&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">condition&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"> test&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Bool&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"> variable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;aws:MultiFactorAuthPresent&amp;#34;&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"> values&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> }
&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 class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_iam_role&amp;#34; &amp;#34;admin&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&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;infra-admin&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&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">admin_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">23&lt;/span>&lt;span class="cl">&lt;span class="n"> max_session_duration&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">3600&lt;/span>&lt;span class="c1"> # 1 小時後自動失效
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>max_session_duration&lt;/code> 限制 assume 後的有效時間。Admin session 設 1 小時是讓操作者完成當次任務後權限自動回收，不需要手動登出。MFA 條件確保即使帳號密碼外洩，沒有第二因素也無法提權。&lt;/p>
&lt;h3 id="operator">Operator&lt;/h3>
&lt;p>Operator 能部署服務、修改應用層資源（ECS task、RDS parameter group、S3 lifecycle）、查看與操作日常維運所需的一切。多數工程師的日常身分落在這一級。&lt;/p>
&lt;p>Operator 的 policy 用 resource scope 限制它碰不到 IAM 和帳號層級設定——能改 ECS service 但不能改 ECS service 用的 IAM role，能改 RDS 參數但不能改 RDS 的 subnet group。這個邊界讓 operator 的操作失誤影響範圍停在服務層，不會擴散到地基層。&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">data&lt;/span> &lt;span class="s2">&amp;#34;aws_iam_policy_document&amp;#34; &amp;#34;operator&amp;#34;&lt;/span> {&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1"> # 允許操作應用層資源
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span 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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;ecs:UpdateService&amp;#34;, &amp;#34;ecs:DescribeServices&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="s2">&amp;#34;rds:ModifyDBInstance&amp;#34;, &amp;#34;rds:DescribeDBInstances&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 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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;logs:GetLogEvents&amp;#34;, &amp;#34;logs:FilterLogEvents&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="p">]&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"> resources&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> }&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1"> # 明確拒絕碰 IAM 和帳號設定
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">statement&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"> effect&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Deny&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"> actions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;iam:*&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 class="s2">&amp;#34;organizations:*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;account:*&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 class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&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;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Deny 語句確保即使未來有人不小心把過寬的 managed policy attach 到 operator role，IAM 和帳號操作仍然被擋。Deny 在 IAM 評估中優先於 Allow。&lt;/p></description><content:encoded><![CDATA[<p>IAM 的 role 與 policy 提供「某個身分能不能對某個資源做某件事」的技術機制（見<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>）。機制備妥後，下一個問題是組織層面的設計：團隊裡每個角色該拿到哪一級權限、臨時需要更高權限時怎麼提權、離職或合約結束時怎麼確保存取被回收。這些設計的目的是讓「誰能動什麼」在任何時間點都有可稽核的答案。</p>
<h2 id="權限分級admin--operator--viewer">權限分級：admin / operator / viewer</h2>
<p>團隊成員的日常操作權限用三級來劃分，每一級對應不同的操作範圍與風險。分級的依據是「這個角色的日常工作需要碰到什麼層級的資源」，不是職稱或年資。</p>
<h3 id="admin">Admin</h3>
<p>Admin 能修改 IAM policy、網路拓撲、帳號層級設定（Organizations、SCP、billing）。這是影響範圍最大的一級——一條 SCP 寫錯可以鎖死整個帳號的操作，一條 IAM policy 開太寬可以讓任何角色取得不該有的權限。</p>
<p>持有 admin 權限的人數應該收斂到最少：通常是平台團隊的 1-2 人加上一個 break-glass 備援角色。Admin 權限不應該是某個人的「日常身分」——即使是平台工程師，日常操作也用 operator 等級，只有在需要改 IAM 或帳號設定時才 assume 到 admin 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"># Admin role 的信任政策：只允許特定 IAM user assume
</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;admin_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;AWS&#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></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="s2">&#34;arn:aws:iam::123456789012:user/platform-lead&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="s2">&#34;arn:aws:iam::123456789012:user/platform-backup&#34;</span><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></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">condition</span> {
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">      test</span>     <span class="o">=</span> <span class="s2">&#34;Bool&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">      variable</span> <span class="o">=</span> <span class="s2">&#34;aws:MultiFactorAuthPresent&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">      values</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;true&#34;</span><span class="p">]</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></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 class="k">resource</span> <span class="s2">&#34;aws_iam_role&#34; &#34;admin&#34;</span> {
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="n">  name</span>               <span class="o">=</span> <span class="s2">&#34;infra-admin&#34;</span>
</span></span><span class="line"><span class="ln">22</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">admin_trust</span><span class="p">.</span><span class="k">json</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="n">  max_session_duration</span> <span class="o">=</span> <span class="m">3600</span><span class="c1">  # 1 小時後自動失效
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1"></span>}</span></span></code></pre></div><p><code>max_session_duration</code> 限制 assume 後的有效時間。Admin session 設 1 小時是讓操作者完成當次任務後權限自動回收，不需要手動登出。MFA 條件確保即使帳號密碼外洩，沒有第二因素也無法提權。</p>
<h3 id="operator">Operator</h3>
<p>Operator 能部署服務、修改應用層資源（ECS task、RDS parameter group、S3 lifecycle）、查看與操作日常維運所需的一切。多數工程師的日常身分落在這一級。</p>
<p>Operator 的 policy 用 resource scope 限制它碰不到 IAM 和帳號層級設定——能改 ECS service 但不能改 ECS service 用的 IAM role，能改 RDS 參數但不能改 RDS 的 subnet group。這個邊界讓 operator 的操作失誤影響範圍停在服務層，不會擴散到地基層。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">data</span> <span class="s2">&#34;aws_iam_policy_document&#34; &#34;operator&#34;</span> {<span class="c1">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">  # 允許操作應用層資源
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>  <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></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="s2">&#34;ecs:UpdateService&#34;, &#34;ecs:DescribeServices&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="s2">&#34;rds:ModifyDBInstance&#34;, &#34;rds:DescribeDBInstances&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="s2">&#34;s3:GetObject&#34;, &#34;s3:PutObject&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="s2">&#34;logs:GetLogEvents&#34;, &#34;logs:FilterLogEvents&#34;</span><span class="p">,</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="n">    resources</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;*&#34;</span><span class="p">]</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">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">  # 明確拒絕碰 IAM 和帳號設定
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>  <span class="k">statement</span> {
</span></span><span class="line"><span class="ln">15</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">16</span><span class="cl"><span class="n">    actions</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">      <span class="s2">&#34;iam:*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">      <span class="s2">&#34;organizations:*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">      <span class="s2">&#34;account:*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="n">    resources</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;*&#34;</span><span class="p">]</span>
</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>Deny 語句確保即使未來有人不小心把過寬的 managed policy attach 到 operator role，IAM 和帳號操作仍然被擋。Deny 在 IAM 評估中優先於 Allow。</p>
<h3 id="viewer">Viewer</h3>
<p>Viewer 能讀取 Console、查 log、看 metric dashboard，但不能修改任何資源。適合的角色包括：值班但不需要改設定的 on-call、需要查 log 排查問題的 support 團隊、需要看資源狀態的管理層。</p>
<p>Viewer 用 AWS 的 managed policy <code>ReadOnlyAccess</code> 作為基線，再根據需要排除敏感資料的讀取（例如 Secrets Manager 的 <code>GetSecretValue</code>）。</p>
<p>三級的對應關係：</p>
<table>
  <thead>
      <tr>
          <th>級別</th>
          <th>能做什麼</th>
          <th>典型角色</th>
          <th>人數控制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Admin</td>
          <td>改 IAM、網路、帳號設定</td>
          <td>平台 lead + break-glass</td>
          <td>2-3 人</td>
      </tr>
      <tr>
          <td>Operator</td>
          <td>部署、改服務設定、查 log</td>
          <td>工程師</td>
          <td>團隊規模</td>
      </tr>
      <tr>
          <td>Viewer</td>
          <td>讀 Console、查 log、看 metrics</td>
          <td>on-call、support、管理層</td>
          <td>依需求開放</td>
      </tr>
  </tbody>
</table>
<p>導入時程參考：三級權限的 IAM role 與 policy 建立約需 1-2 天，包含 trust policy 設定與初次分配。後續的權限變更走版本控制的 PR 流程，讓每次 policy 調整都有提案、審查與歷史紀錄（見<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>）。</p>
<h2 id="臨時提權break-glass">臨時提權（break-glass）</h2>
<p>Operator 在日常工作中偶爾需要 admin 層級的操作——排查一個涉及 IAM 的事故、緊急修改一條 security group 規則、回應安全事件。常態性地把 admin 權限開給所有 operator 會讓三級分級失效，但每次都等 admin 角色的人上線又太慢。Break-glass 流程處理的就是這個中間地帶。</p>
<h3 id="機制">機制</h3>
<p>Break-glass 的實作是一個平時不被 assume 的 admin role，加上一套提權紀錄。Operator 在需要時 assume 這個 role，取得一段時效有限的 admin session。這個 assume 動作會在 CloudTrail 留下紀錄（誰、什麼時候、session 多長），事後可稽核。</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;break_glass&#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-break-glass&#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">break_glass_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 class="n">  max_session_duration</span> <span class="o">=</span> <span class="m">3600</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="n">  tags</span> <span class="o">=</span><span class="n"> { Purpose</span> <span class="o">=</span> <span class="s2">&#34;emergency-escalation&#34;</span> }
</span></span><span class="line"><span class="ln">7</span><span class="cl">}</span></span></code></pre></div><p>如果團隊有 ChatOps 或 ticketing 系統，把 break-glass 的觸發綁進去可以增加一層人為確認：operator 在 Slack 或 ticket 裡申請提權、另一個人核可、系統開放 assume。這層確認的目的是在事後稽核時留下一條清楚的「誰授權了這次提權」紀錄，而非阻止操作本身。</p>
<h3 id="事後回顧">事後回顧</h3>
<p>每一次 break-glass 使用都應該進入事後回顧：為什麼需要提權？這個操作能不能改寫成 operator 層級的權限就能完成？如果某類操作反覆觸發 break-glass，代表 operator 的權限邊界需要調整——把那類操作從 admin 降到 operator，而不是讓 break-glass 變成常態。</p>
<p>回顧的輸出是權限邊界的校準，不是對操作者的檢討。</p>
<h2 id="定期-access-review">定期 access review</h2>
<p>權限分配不是一次性的設定。人會換組、離職、從 contractor 轉正職、從開發角色轉管理角色，每一次角色變動都可能讓既有的權限配置過期。定期 review 的責任是找出「權限比當前角色需要的更寬」的身分，把它們收斂回來。</p>
<h3 id="節奏與方法">節奏與方法</h3>
<p>每季做一次 access review 是多數團隊能維持的最小節奏。Review 的步驟：</p>
<ol>
<li>拉出所有 IAM user 和 role 的清單，標注每個身分目前的分級（admin / operator / viewer）</li>
<li>比對每個身分的實際角色——這個人現在還在做需要 operator 權限的工作嗎？</li>
<li>用 IAM Access Analyzer 檢查哪些權限在過去 90 天沒被使用過——沒用到的權限是收斂候選</li>
<li>特別檢查 break-glass 的使用紀錄——有沒有人的 break-glass 使用頻率高到代表他的基線權限該調整</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"># 產出 credential report，列出所有 user 的 key 建立時間與使用時間</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws iam generate-credential-report
</span></span><span class="line"><span class="ln">3</span><span class="cl">aws iam get-credential-report --output text --query Content <span class="p">|</span> base64 -d <span class="p">|</span> head -20
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 查 Access Analyzer 的 finding（哪些權限可收斂）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">aws accessanalyzer list-findings --analyzer-arn &lt;analyzer-arn&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --filter <span class="s1">&#39;{&#34;status&#34;: {&#34;eq&#34;: [&#34;ACTIVE&#34;]}}&#39;</span></span></span></code></pre></div><h3 id="管理層報告">管理層報告</h3>
<p>Access review 的結果適合用兩個數字向管理層報告：<strong>覆蓋率</strong>（已 review 的身分數 / 總身分數）與<strong>異常數</strong>（權限過寬或長期未使用的身分數）。異常數的趨勢比單次數字更有意義——持續上升代表新人 onboarding 時的權限配置流程有缺口，持續下降代表 review 在發揮作用。</p>
<p>導入時程參考：第一次 access review 約需半天到一天（盤點 + 比對 + 收斂），後續每季約需 2-4 小時。</p>
<h2 id="職務交接與離職處理">職務交接與離職處理</h2>
<p>一個人離開團隊時，他持有的所有存取路徑都需要被回收。手動建立的存取路徑越多，離職處理越容易遺漏。</p>
<h3 id="離職-checklist">離職 checklist</h3>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>操作</th>
          <th>驗證方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>IAM user / SSO 帳號</td>
          <td>停用或刪除</td>
          <td>credential report 裡不再出現</td>
      </tr>
      <tr>
          <td>長期 access key</td>
          <td>撤銷所有 key</td>
          <td><code>list-access-keys</code> 回傳空</td>
      </tr>
      <tr>
          <td>個人 MFA 裝置</td>
          <td>解除綁定</td>
          <td><code>list-mfa-devices</code> 回傳空</td>
      </tr>
      <tr>
          <td>被加進的 IAM group</td>
          <td>移除成員</td>
          <td><code>get-group</code> 裡不再出現</td>
      </tr>
      <tr>
          <td>可 assume 的 role trust policy</td>
          <td>從 principal 清單移除</td>
          <td>trust policy 裡沒有該 user ARN</td>
      </tr>
      <tr>
          <td>第三方服務的 SSO 授權</td>
          <td>撤銷（GitHub org、CI 平台、Slack workspace 等）</td>
          <td>該帳號無法登入</td>
      </tr>
      <tr>
          <td>共用密碼 / shared credential</td>
          <td>輪替（如果存在的話）</td>
          <td>Secrets Manager 版本更新</td>
      </tr>
  </tbody>
</table>
<p>權限設計越集中在 role-based（用 IAM group 或 SSO permission set），離職處理越簡單——停用 SSO 帳號就自動切斷所有透過 SSO 取得的 role。反過來，如果有大量手動 attach 的 policy 或直接寫在 trust policy 裡的 user ARN，離職時要逐一找出並移除，容易遺漏。</p>
<p>離職後的 credential rotation 有一個常被忽略的風險：輪替範圍沒有按作用域分批。一個反例是多個服務共用同一把 secret，輪替時切新憑證的服務跟還只認舊憑證的服務之間出現認證窗口不一致，導致跨系統連鎖中斷。穩定的做法是先分域隔離受影響服務、恢復雙憑證窗口、再逐批收斂（見 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">反例：憑證輪替未分 Scope</a>）。</p>
<h3 id="交接的可執行性">交接的可執行性</h3>
<p>交接的成本取決於知識有多少沉澱在程式碼裡、有多少留在個人腦中。如果環境的建立方式是一份 IaC、變更方式是 PR 歷史，新接手的人讀 code 跟 PR 描述就能重建脈絡。如果關鍵操作（某台資料庫的特殊 parameter、某條 security group 規則的理由）只存在離職者的記憶裡，交接窗口一過就永久遺失。</p>
<p>可操作的檢驗：問「如果這個人下週離職，團隊能不能只靠讀 repo 就安全地操作他負責的環境？」答案是否定的部分，就是交接的優先補強項——優先把它們寫進 IaC 或 PR 描述，而不是寫進交接文件（交接文件會過期，IaC 跟著環境一起演進）。</p>
<p>這個議題在<a href="/blog/infra/09-driving-adoption/trust-alignment-knowledge-sharing/" data-link-title="怎麼把 infra 推動起來 — 信任赤字、期望值對齊與知識共享" data-link-desc="技術正確不等於推得動 — infra 在商業優先級裡吃虧的結構性原因，以及用可回退切片、期望值對齊與知識分散來跨過組織關卡">知識共享優於個人英雄主義</a>有組織層面的展開。</p>
<h2 id="contractor-與外部-vendor-存取">Contractor 與外部 vendor 存取</h2>
<p>外部人員（contractor、顧問、SaaS vendor 的技術支援）需要存取雲端環境時，原則是給最小範圍、設明確時限、留完整紀錄。</p>
<h3 id="範圍限制">範圍限制</h3>
<p>外部人員的 role 用 Permissions Boundary 設定權限天花板，確保即使有人誤 attach 了過寬的 policy，操作範圍也不超過 boundary 允許的上限。Scope 到具體的資源 ARN（某個 S3 bucket、某台 RDS instance），而非帳號級別的 wildcard。</p>
<p>如果團隊已經有<a href="/blog/infra/02-identity-credentials/multi-account-strategy/" data-link-title="跨帳號策略 — Organizations、SCP 與帳號工廠" data-link-desc="用 AWS Organizations 把環境拆成獨立帳號、用 SCP 設定連管理員都越不過的護欄、用帳號工廠讓每個新帳號自帶安全基線">跨帳號策略</a>，把外部人員的 workload 放在獨立帳號或 sandbox OU 裡，用 SCP 限制該帳號能操作的服務類型，是比 role 級別限制更強的隔離。</p>
<h3 id="時限控制">時限控制</h3>
<p>外部存取的 IAM user 或 SSO 帳號在建立時就設定到期日。多數雲端平台支援 session duration 限制（role 的 <code>max_session_duration</code>）和帳號層級的停用排程。合約結束日應該對應到存取到期日——這個對應關係寫進 IaC（用 tag 標注到期日）或團隊的 access review checklist，避免合約結束後存取仍然開著。</p>
<h3 id="稽核紀錄">稽核紀錄</h3>
<p>外部人員的操作需要比內部人員更嚴格的稽核。CloudTrail 預設記錄所有 API 呼叫，但 review 的頻率要提高——外部人員的操作紀錄每週抽查，而非等到季度 access review 才回頭看。查的是：有沒有存取超出約定範圍的資源？有沒有在非工作時間操作？有沒有大量的 read 操作指向敏感資料？</p>
<p>這些紀錄同時也是合約管理的依據——如果外部 vendor 的技術支援存取了超出約定範圍的資源，紀錄是釐清責任的事實基礎。</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>：IAM role / policy / OIDC 的技術機制</li>
<li>→ <a href="/blog/infra/02-identity-credentials/multi-account-strategy/" data-link-title="跨帳號策略 — Organizations、SCP 與帳號工廠" data-link-desc="用 AWS Organizations 把環境拆成獨立帳號、用 SCP 設定連管理員都越不過的護欄、用帳號工廠讓每個新帳號自帶安全基線">跨帳號策略</a>：用 OU 和 SCP 在帳號層級隔離外部人員</li>
<li>→ <a href="/blog/infra/08-governance-habits/tagging-secrets/" data-link-title="Tagging 規範與 Secrets 不進 code" data-link-desc="tag 讓資源可盤點、可清理、可歸屬；密鑰存在專用服務裡而非 code 或 state，兩者都屬於 day-1 就該立的治理地基">治理好習慣</a>：tagging 標注存取到期日、secrets 不進 code</li>
<li>→ <a href="/blog/infra/09-driving-adoption/trust-alignment-knowledge-sharing/" data-link-title="怎麼把 infra 推動起來 — 信任赤字、期望值對齊與知識共享" data-link-desc="技術正確不等於推得動 — infra 在商業優先級裡吃虧的結構性原因，以及用可回退切片、期望值對齊與知識分散來跨過組織關卡">怎麼把 infra 推動起來</a>：知識共享與交接的組織面</li>
</ul>
]]></content:encoded></item><item><title>拿到雲端帳號的第一天</title><link>https://tarrragon.github.io/blog/infra/00-infra-mindset/first-day-with-cloud-account/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/00-infra-mindset/first-day-with-cloud-account/</guid><description>&lt;p>這篇寫給一種特定的讀者：你的專業可能是後端、前端、資料工程或其他領域，但因為組織需要，你被指派處理雲端基礎設施。公司（或主管）給了你一個 AWS / GCP / Azure 帳號，你登入之後看到一個很大的 Console，不確定該做什麼、也不確定動了什麼會出事。&lt;/p>
&lt;p>這是 infra 工作最常見的真實入口。比起從零自學建一套環境，「接到指派、拿到帳號、搞清楚狀況」才是多數工程師第一次碰 infra 的方式。&lt;/p>
&lt;p>這篇用 AWS 為主要範例。GCP 和 Azure 的判讀邏輯相同（安全底線 → 現況盤點 → 路線分流），但具體服務名稱、IAM 模型和 Console 操作位置不同。&lt;/p>
&lt;h2 id="第一小時安全底線">第一小時：安全底線&lt;/h2>
&lt;p>登入帳號後，在做任何其他事之前先完成這些。這些步驟的共同目的是確保帳號的存取控制處於安全狀態——雲端帳號被入侵的代價遠高於本機電腦被入侵，因為雲端資源可以在幾分鐘內被大量建立（產生帳單）或被刪除（資料遺失）。&lt;/p>
&lt;h3 id="確認-root-帳號的-mfa">確認 root 帳號的 MFA&lt;/h3>
&lt;p>Root 帳號是雲端環境的最高權限，能做任何事，包括關閉整個帳號。如果 root 帳號沒有 MFA（Multi-Factor Authentication，多因子驗證），任何拿到 root 密碼的人都能完全控制整個環境。&lt;/p>
&lt;p>確認路徑（AWS）：Console 右上角帳號名稱 → Security credentials → Multi-factor authentication (MFA)。如果顯示「No MFA device」，立刻設定一個——手機 app（Google Authenticator / Authy）或硬體 key（YubiKey）都可以。&lt;/p>
&lt;p>如果你拿到的帳號是公司用 AWS Organizations 開出來的子帳號，子帳號 root 的密碼和 MFA 是獨立的——管理帳號無法代設。子帳號 root 通常需要先用帳號 email 做密碼重置才能首次登入。確認 root MFA 後，日常操作用 IAM Identity Center 登入。&lt;/p>
&lt;h3 id="確認你的登入身分">確認你的登入身分&lt;/h3>
&lt;p>你登入用的是哪種身分？這決定了你的權限範圍和操作方式。&lt;/p>
&lt;p>&lt;strong>IAM user&lt;/strong>：Console 右上角會顯示 &lt;code>username @ account-id&lt;/code>。這是最傳統的登入方式——帳號管理員幫你建了一個使用者，給了你一組帳密。&lt;/p>
&lt;p>&lt;strong>IAM Identity Center（SSO）&lt;/strong>：你透過一個特別的登入頁面（通常是 &lt;code>https://d-xxxxxxxxxx.awsapps.com/start&lt;/code>）登入，然後選擇帳號和角色。這是較新的做法，多帳號組織常用。&lt;/p>
&lt;p>&lt;strong>Root 帳號&lt;/strong>：Console 右上角顯示帳號 email 而非 username。如果你拿到的是 root 帳號的帳密，日常操作應該換成 IAM user 或 SSO 登入——root 帳號只在需要 root-only 操作（如設定 MFA、關閉帳號）時使用。建立 IAM user 的方式見模組一的&lt;a href="https://tarrragon.github.io/blog/infra/01-minimal-iac/iac-tool-state-backend/" data-link-title="IaC 工具選型與 state 地基" data-link-desc="Terraform / OpenTofu / CDK / Pulumi 的選型判準，state 作為 IaC 工具對現實的唯一記憶，以及 remote state backend 的自管與託管路線">動手前的前提&lt;/a>段。&lt;/p>
&lt;h3 id="檢查既存的-access-key">檢查既存的 access key&lt;/h3>
&lt;p>帳號如果被前人用過，可能有暴露風險的 access key——之前的管理員建了 IAM user、生了 key，但那組 key 可能已經寫在某個 Git repo 或環境變數裡而沒有停用。&lt;/p>
&lt;p>確認路徑：Console → IAM → Users → 逐一點每個 user → Security credentials 分頁 → Access keys。檢查每組 key 的狀態（Active / Inactive）和建立時間。超過 90 天未 rotate 的 Active key 是風險——帳號接手後優先 rotate 或停用這些 key。如果帳號裡沒有任何 IAM user，這步跳過。&lt;/p>
&lt;h3 id="確認-cloudtrail-是否開啟">確認 CloudTrail 是否開啟&lt;/h3>
&lt;p>CloudTrail 記錄帳號內所有 API 操作（誰在什麼時間做了什麼）。AWS 預設會開啟 90 天的事件歷史，但長期保存需要建一個 Trail 把 log 寫到 S3。&lt;/p>
&lt;p>確認路徑：Console 搜尋 CloudTrail → Dashboard。如果有 Trail 已建立，表示操作紀錄有長期保存。如果只有預設的 Event history，90 天前的紀錄會消失——這是一個需要但不緊急的改善點，&lt;a href="https://tarrragon.github.io/blog/infra/06-observability-logging/" data-link-title="模組六：可觀測性與 log 一併寫進 code" data-link-desc="log group、metric、alarm 跟基礎設施同生命週期管理，出事時追得到查得到">模組六：可觀測性&lt;/a>會展開。&lt;/p></description><content:encoded><![CDATA[<p>這篇寫給一種特定的讀者：你的專業可能是後端、前端、資料工程或其他領域，但因為組織需要，你被指派處理雲端基礎設施。公司（或主管）給了你一個 AWS / GCP / Azure 帳號，你登入之後看到一個很大的 Console，不確定該做什麼、也不確定動了什麼會出事。</p>
<p>這是 infra 工作最常見的真實入口。比起從零自學建一套環境，「接到指派、拿到帳號、搞清楚狀況」才是多數工程師第一次碰 infra 的方式。</p>
<p>這篇用 AWS 為主要範例。GCP 和 Azure 的判讀邏輯相同（安全底線 → 現況盤點 → 路線分流），但具體服務名稱、IAM 模型和 Console 操作位置不同。</p>
<h2 id="第一小時安全底線">第一小時：安全底線</h2>
<p>登入帳號後，在做任何其他事之前先完成這些。這些步驟的共同目的是確保帳號的存取控制處於安全狀態——雲端帳號被入侵的代價遠高於本機電腦被入侵，因為雲端資源可以在幾分鐘內被大量建立（產生帳單）或被刪除（資料遺失）。</p>
<h3 id="確認-root-帳號的-mfa">確認 root 帳號的 MFA</h3>
<p>Root 帳號是雲端環境的最高權限，能做任何事，包括關閉整個帳號。如果 root 帳號沒有 MFA（Multi-Factor Authentication，多因子驗證），任何拿到 root 密碼的人都能完全控制整個環境。</p>
<p>確認路徑（AWS）：Console 右上角帳號名稱 → Security credentials → Multi-factor authentication (MFA)。如果顯示「No MFA device」，立刻設定一個——手機 app（Google Authenticator / Authy）或硬體 key（YubiKey）都可以。</p>
<p>如果你拿到的帳號是公司用 AWS Organizations 開出來的子帳號，子帳號 root 的密碼和 MFA 是獨立的——管理帳號無法代設。子帳號 root 通常需要先用帳號 email 做密碼重置才能首次登入。確認 root MFA 後，日常操作用 IAM Identity Center 登入。</p>
<h3 id="確認你的登入身分">確認你的登入身分</h3>
<p>你登入用的是哪種身分？這決定了你的權限範圍和操作方式。</p>
<p><strong>IAM user</strong>：Console 右上角會顯示 <code>username @ account-id</code>。這是最傳統的登入方式——帳號管理員幫你建了一個使用者，給了你一組帳密。</p>
<p><strong>IAM Identity Center（SSO）</strong>：你透過一個特別的登入頁面（通常是 <code>https://d-xxxxxxxxxx.awsapps.com/start</code>）登入，然後選擇帳號和角色。這是較新的做法，多帳號組織常用。</p>
<p><strong>Root 帳號</strong>：Console 右上角顯示帳號 email 而非 username。如果你拿到的是 root 帳號的帳密，日常操作應該換成 IAM user 或 SSO 登入——root 帳號只在需要 root-only 操作（如設定 MFA、關閉帳號）時使用。建立 IAM user 的方式見模組一的<a href="/blog/infra/01-minimal-iac/iac-tool-state-backend/" data-link-title="IaC 工具選型與 state 地基" data-link-desc="Terraform / OpenTofu / CDK / Pulumi 的選型判準，state 作為 IaC 工具對現實的唯一記憶，以及 remote state backend 的自管與託管路線">動手前的前提</a>段。</p>
<h3 id="檢查既存的-access-key">檢查既存的 access key</h3>
<p>帳號如果被前人用過，可能有暴露風險的 access key——之前的管理員建了 IAM user、生了 key，但那組 key 可能已經寫在某個 Git repo 或環境變數裡而沒有停用。</p>
<p>確認路徑：Console → IAM → Users → 逐一點每個 user → Security credentials 分頁 → Access keys。檢查每組 key 的狀態（Active / Inactive）和建立時間。超過 90 天未 rotate 的 Active key 是風險——帳號接手後優先 rotate 或停用這些 key。如果帳號裡沒有任何 IAM user，這步跳過。</p>
<h3 id="確認-cloudtrail-是否開啟">確認 CloudTrail 是否開啟</h3>
<p>CloudTrail 記錄帳號內所有 API 操作（誰在什麼時間做了什麼）。AWS 預設會開啟 90 天的事件歷史，但長期保存需要建一個 Trail 把 log 寫到 S3。</p>
<p>確認路徑：Console 搜尋 CloudTrail → Dashboard。如果有 Trail 已建立，表示操作紀錄有長期保存。如果只有預設的 Event history，90 天前的紀錄會消失——這是一個需要但不緊急的改善點，<a href="/blog/infra/06-observability-logging/" data-link-title="模組六：可觀測性與 log 一併寫進 code" data-link-desc="log group、metric、alarm 跟基礎設施同生命週期管理，出事時追得到查得到">模組六：可觀測性</a>會展開。</p>
<p>現階段只需要確認 CloudTrail 存在，不需要馬上改它。</p>
<h3 id="設定帳單警報">設定帳單警報</h3>
<p>雲端帳單是開放式的——資源跑著就持續產生費用，被入侵後被開出大量資源更可能在幾小時內累積數千美元帳單。設一個帳單警報，超過閾值時收到通知。</p>
<p>設定路徑（AWS）：Console 搜尋 Billing → Budgets → Create budget → Cost budget。設一個月預算（如 $50 或 $100，依你的環境規模），超過 80% 和 100% 時發 email 通知。</p>
<h2 id="帳號現況判讀空帳號還是有東西">帳號現況判讀：空帳號還是有東西？</h2>
<p>安全底線做完後，下一步是搞清楚帳號的現況。這決定了你接下來走哪條路線。</p>
<h3 id="怎麼判斷">怎麼判斷</h3>
<p>EC2 Dashboard 只顯示當前 region 的資源。Console 右上角有 region 選擇器——先切幾個主要 region（us-east-1、ap-northeast-1、ap-southeast-1）看一下，確認資源是否分散在不同 region。</p>
<p>打開 EC2 Dashboard（Console 搜尋 EC2）。如果 Running instances 是 0、沒有 volumes、沒有 security groups（除了 default）——大概率是空帳號。也檢查 Lambda（Console 搜尋 Lambda → Functions）——如果有 function 在跑但 EC2 是空的，可能是 serverless 架構，帳號不是空的。</p>
<p>再看 S3（Console 搜尋 S3）。S3 是全域服務，不分 region。如果沒有 bucket，或只有 CloudTrail 的 log bucket——大概率是空帳號。</p>
<p>如果有正在跑的 EC2 instance、有 Lambda function、有 RDS 資料庫、有 S3 bucket 存著資料——這是一個有東西的帳號，可能是前人建的、可能是其他團隊在用的。</p>
<h3 id="空帳號--從零建置">空帳號 → 從零建置</h3>
<p>帳號是空的，你要從零開始建基礎設施。這是最乾淨的起點。</p>
<p>路線：先讀<a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零</a>建立心智模型（什麼是 infra、成熟度階梯），然後照模組一到五的順序走。模組一的<a href="/blog/infra/01-minimal-iac/iac-tool-state-backend/" data-link-title="IaC 工具選型與 state 地基" data-link-desc="Terraform / OpenTofu / CDK / Pulumi 的選型判準，state 作為 IaC 工具對現實的唯一記憶，以及 remote state backend 的自管與託管路線">動手前的前提</a>段會帶你設好本機工具和認證。</p>
<h3 id="有東西的帳號--接手維運">有東西的帳號 → 接手維運</h3>
<p>帳號裡已經有資源在跑。你需要先搞清楚「有什麼」「誰建的」「哪些還在用」，再決定怎麼處理。</p>
<p>路線：讀<a href="/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運</a>模組。它按環境類型（全手動的遺留環境、部分有 IaC、多帳號結構）分篇，教你怎麼盤點、怎麼在不搞壞的前提下逐步接管。</p>
<h3 id="不確定--先盤點再說">不確定 → 先盤點再說</h3>
<p>如果帳號裡有東西但你不確定是不是還在用、能不能動，先盤點。以下指令需要 AWS CLI 並完成認證——安裝和 <code>aws configure</code> 設定見模組一的<a href="/blog/infra/01-minimal-iac/iac-tool-state-backend/" data-link-title="IaC 工具選型與 state 地基" data-link-desc="Terraform / OpenTofu / CDK / Pulumi 的選型判準，state 作為 IaC 工具對現實的唯一記憶，以及 remote state backend 的自管與託管路線">前提段</a>（macOS 快速安裝：<code>brew install awscli &amp;&amp; aws configure</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 列出所有 region 的 EC2 instance</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">for</span> region in <span class="k">$(</span>aws ec2 describe-regions --query <span class="s1">&#39;Regions[].RegionName&#39;</span> --output text<span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;=== </span><span class="nv">$region</span><span class="s2"> ===&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  aws ec2 describe-instances --region <span class="s2">&#34;</span><span class="nv">$region</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>    --query <span class="s1">&#39;Reservations[].Instances[].[InstanceId,State.Name,Tags[?Key==`Name`].Value|[0]]&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>    --output table
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">done</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="c1"># 列出所有 S3 bucket</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">aws s3 ls
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 列出所有 RDS instance</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">aws rds describe-db-instances <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;DBInstances[].[DBInstanceIdentifier,Engine,DBInstanceStatus]&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>  --output table</span></span></code></pre></div><p>這些指令只做讀取，不會改變任何東西。如果輸出很多資源，去讀<a href="/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運</a>再決定下一步。如果幾乎是空的，走「從零建置」路線。</p>
<h2 id="雲端-console-的基本導覽">雲端 Console 的基本導覽</h2>
<p>AWS Console 列出幾百個服務，日常 infra 工作常用的集中在以下幾個：</p>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>做什麼</th>
          <th>什麼時候用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>EC2</td>
          <td>虛擬機器（運算）</td>
          <td>看有什麼機器在跑、管 security group</td>
      </tr>
      <tr>
          <td>S3</td>
          <td>物件儲存</td>
          <td>放檔案、放 Terraform state、放 log</td>
      </tr>
      <tr>
          <td>IAM</td>
          <td>身分與權限</td>
          <td>管使用者、角色、權限</td>
      </tr>
      <tr>
          <td>VPC</td>
          <td>虛擬網路</td>
          <td>管網路拓撲、子網路、路由</td>
      </tr>
      <tr>
          <td>RDS</td>
          <td>託管資料庫</td>
          <td>看有沒有資料庫在跑</td>
      </tr>
      <tr>
          <td>CloudWatch</td>
          <td>監控與 log</td>
          <td>看 metric、設 alarm、查 log</td>
      </tr>
      <tr>
          <td>CloudTrail</td>
          <td>操作審計</td>
          <td>查誰做了什麼</td>
      </tr>
      <tr>
          <td>Billing</td>
          <td>帳單</td>
          <td>看花了多少錢</td>
      </tr>
  </tbody>
</table>
<p>Console 左上角的搜尋列可以直接搜服務名稱，不用從選單找。</p>
<p>每個服務在 Console 上的操作都有一個對應的 AWS CLI 指令和 API 呼叫。這個對應關係是 IaC 的基礎——<a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一</a>會教怎麼把 Console 上的操作轉成程式碼。</p>
<h2 id="你接下來該讀什麼">你接下來該讀什麼</h2>
<p>根據你的情境選一條路線：</p>
<table>
  <thead>
      <tr>
          <th>你的情境</th>
          <th>路線</th>
          <th>從哪裡開始</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>完全沒碰過雲端、想先理解概念</td>
          <td>入門認識</td>
          <td><a href="/blog/infra/00-infra-mindset/personal-project-to-infra/" data-link-title="雲端部署裡已經存在的 infra 元件" data-link-desc="VPC、security group、IAM、儲存 — 這些元件在任何雲端部署裡都已經在運作，差別在於有沒有被有意識地管理">個人專案到團隊服務</a></td>
      </tr>
      <tr>
          <td>空帳號、要從零建 infra</td>
          <td>從零建置</td>
          <td><a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一：最小可行 IaC</a></td>
      </tr>
      <tr>
          <td>帳號有東西、要接手維運</td>
          <td>接手前人專案</td>
          <td><a href="/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運</a></td>
      </tr>
      <tr>
          <td>手動環境、暫時無法導入 IaC</td>
          <td>還沒有 IaC</td>
          <td><a href="/blog/infra/before-infra/" data-link-title="模組負一：還沒有 infra 的環境怎麼盡量做好" data-link-desc="手動點起家的環境怎麼守底線、降低未來納管成本、辨識何時該開始導入 IaC — 給還沒有能力上 IaC 的真實起點">模組負一：還沒有 infra 的環境</a></td>
      </tr>
      <tr>
          <td>要跟主管解釋為什麼要做 infra</td>
          <td>說服決策者</td>
          <td><a href="/blog/infra/09-driving-adoption/infra-explained-for-non-engineers/" data-link-title="給非工程背景決策者的 infra 說明" data-link-desc="從管理視角解釋基礎設施在解決什麼營運問題、不做的代價、出事怎麼處理，讓參與資源決策的人能判斷投入的優先級">給非工程人員的 infra 說明</a></td>
      </tr>
      <tr>
          <td>拿到一台主機、要從 OS 層連入初始化</td>
          <td>機器初始化</td>
          <td><a href="/blog/linux/install/" data-link-title="Linux 安裝與機器初始化" data-link-desc="在 VM 或新機器從零裝好 Linux、判讀安裝程式選項、驗證最小系統、或要從外部連入跑 bootstrap 時回來讀">Linux 安裝與機器初始化</a></td>
      </tr>
  </tbody>
</table>
<p>如果你不確定自己屬於哪種情境，先做完本篇的「帳號現況判讀」再決定。</p>
]]></content:encoded></item><item><title>Access Key 輪替手冊</title><link>https://tarrragon.github.io/blog/infra/02-identity-credentials/access-key-rotation-playbook/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/02-identity-credentials/access-key-rotation-playbook/</guid><description>&lt;p>長期 access key 的風險隨時間單調上升——每多存在一天，被複製到新地方的機率就多一分，而輪替的難度也跟著副本數量增長。輪替不是「發現外洩才做」的緊急動作，而是定期執行的維運操作。本篇是操作手冊，從盤點開始、逐步完成輪替、最後建立自動化。&lt;/p>
&lt;h2 id="盤點帳號裡有哪些-key">盤點：帳號裡有哪些 key&lt;/h2>
&lt;p>第一步是拿到帳號內所有 IAM user 的 access key 清單。AWS 的 credential report 是這個問題的標準資料來源，它列出每個 user 的 key 狀態、建立時間與最後使用時間。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">aws iam generate-credential-report
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">aws iam get-credential-report &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s1">&amp;#39;Content&amp;#39;&lt;/span> --output text &lt;span class="p">|&lt;/span> base64 -d &amp;gt; credential-report.csv&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>產出的 CSV 包含每個 IAM user 的兩把 key（access_key_1、access_key_2）各自的狀態。關注的欄位：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>欄位&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>user&lt;/code>&lt;/td>
 &lt;td>key 的擁有者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>access_key_1_active&lt;/code>&lt;/td>
 &lt;td>key 是否啟用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>access_key_1_last_used_date&lt;/code>&lt;/td>
 &lt;td>最後使用時間——長期未使用代表可能是遺棄的 key&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>access_key_1_last_rotated&lt;/code>&lt;/td>
 &lt;td>建立或上次輪替的時間&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>用 csvkit 或試算表打開這份報告，按 &lt;code>access_key_1_last_rotated&lt;/code> 排序，最舊的 key 排最前面。超過 90 天未輪替的 key 列為第一批處理對象。&lt;/p>
&lt;p>以下腳本使用 gawk 的 &lt;code>systime()&lt;/code> 函式。如果系統的 awk 是 mawk（Ubuntu 預設），改用 &lt;code>gawk&lt;/code> 或用 &lt;code>date&lt;/code> 指令替代時間計算。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 快速列出所有啟用中、超過 90 天的 key&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">aws iam list-users --query &lt;span class="s1">&amp;#39;Users[].UserName&amp;#39;&lt;/span> --output text &lt;span class="p">|&lt;/span> tr &lt;span class="s1">&amp;#39;\t&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> &lt;span class="k">while&lt;/span> &lt;span class="nb">read&lt;/span> user&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> aws iam list-access-keys --user-name &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$user&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s2">&amp;#34;AccessKeyMetadata[?Status==&amp;#39;Active&amp;#39;].[UserName,AccessKeyId,CreateDate]&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --output text
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span> &lt;span class="p">|&lt;/span> awk -F&lt;span class="s1">&amp;#39;\t&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s1"> cmd = &amp;#34;date -d \&amp;#34;&amp;#34; $3 &amp;#34;\&amp;#34; +%s 2&amp;gt;/dev/null || date -jf \&amp;#34;%Y-%m-%dT%H:%M:%S+00:00\&amp;#34; \&amp;#34;&amp;#34; $3 &amp;#34;\&amp;#34; +%s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s1"> cmd | getline created; close(cmd)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s1"> age = (systime() - created) / 86400
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="s1"> if (age &amp;gt; 90) printf &amp;#34;%s\t%s\t%.0f days\n&amp;#34;, $1, $2, age
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="s1">}&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="識別每把-key-的用途">識別每把 key 的用途&lt;/h2>
&lt;p>知道 key 存在之後，下一個問題是「這把 key 用在哪裡」。credential report 只告訴你 key 最後被用來呼叫什麼 service（&lt;code>access_key_1_last_used_service&lt;/code>），但不告訴你它被存放在哪裡。&lt;/p>
&lt;p>用途識別需要交叉比對多個來源：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>可能的存放位置&lt;/th>
 &lt;th>檢查方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>CI 環境變數（GitHub Actions）&lt;/td>
 &lt;td>repo Settings → Secrets and variables → Actions&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CI 環境變數（GitLab CI）&lt;/td>
 &lt;td>repo Settings → CI/CD → Variables&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>EC2 instance 的 user data&lt;/td>
 &lt;td>&lt;code>aws ec2 describe-instance-attribute --attribute userData&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Lambda 環境變數&lt;/td>
 &lt;td>&lt;code>aws lambda get-function-configuration --function-name NAME&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SSM Parameter Store&lt;/td>
 &lt;td>&lt;code>aws ssm get-parameters-by-path --path / --recursive&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>開發者筆電&lt;/td>
 &lt;td>&lt;code>~/.aws/credentials&lt;/code> — 需要口頭確認&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式碼 repo&lt;/td>
 &lt;td>&lt;code>git log --all -p | grep AKIA&lt;/code> — AKIA 是 access key 的固定前綴&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Slack / email 歷史&lt;/td>
 &lt;td>無法自動掃描，靠團隊回報&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>對每把要輪替的 key，在以上位置逐一確認。找不到用途的 key 可以先停用觀察（而非直接刪除），停用後如果有服務壞了就知道它用在哪裡。&lt;/p></description><content:encoded><![CDATA[<p>長期 access key 的風險隨時間單調上升——每多存在一天，被複製到新地方的機率就多一分，而輪替的難度也跟著副本數量增長。輪替不是「發現外洩才做」的緊急動作，而是定期執行的維運操作。本篇是操作手冊，從盤點開始、逐步完成輪替、最後建立自動化。</p>
<h2 id="盤點帳號裡有哪些-key">盤點：帳號裡有哪些 key</h2>
<p>第一步是拿到帳號內所有 IAM user 的 access key 清單。AWS 的 credential report 是這個問題的標準資料來源，它列出每個 user 的 key 狀態、建立時間與最後使用時間。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws iam generate-credential-report
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws iam get-credential-report <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;Content&#39;</span> --output text <span class="p">|</span> base64 -d &gt; credential-report.csv</span></span></code></pre></div><p>產出的 CSV 包含每個 IAM user 的兩把 key（access_key_1、access_key_2）各自的狀態。關注的欄位：</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>user</code></td>
          <td>key 的擁有者</td>
      </tr>
      <tr>
          <td><code>access_key_1_active</code></td>
          <td>key 是否啟用</td>
      </tr>
      <tr>
          <td><code>access_key_1_last_used_date</code></td>
          <td>最後使用時間——長期未使用代表可能是遺棄的 key</td>
      </tr>
      <tr>
          <td><code>access_key_1_last_rotated</code></td>
          <td>建立或上次輪替的時間</td>
      </tr>
  </tbody>
</table>
<p>用 csvkit 或試算表打開這份報告，按 <code>access_key_1_last_rotated</code> 排序，最舊的 key 排最前面。超過 90 天未輪替的 key 列為第一批處理對象。</p>
<p>以下腳本使用 gawk 的 <code>systime()</code> 函式。如果系統的 awk 是 mawk（Ubuntu 預設），改用 <code>gawk</code> 或用 <code>date</code> 指令替代時間計算。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 快速列出所有啟用中、超過 90 天的 key</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws iam list-users --query <span class="s1">&#39;Users[].UserName&#39;</span> --output text <span class="p">|</span> tr <span class="s1">&#39;\t&#39;</span> <span class="s1">&#39;\n&#39;</span> <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> user<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  aws iam list-access-keys --user-name <span class="s2">&#34;</span><span class="nv">$user</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>    --query <span class="s2">&#34;AccessKeyMetadata[?Status==&#39;Active&#39;].[UserName,AccessKeyId,CreateDate]&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>    --output text
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">done</span> <span class="p">|</span> awk -F<span class="s1">&#39;\t&#39;</span> <span class="s1">&#39;{
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s1">  cmd = &#34;date -d \&#34;&#34; $3 &#34;\&#34; +%s 2&gt;/dev/null || date -jf \&#34;%Y-%m-%dT%H:%M:%S+00:00\&#34; \&#34;&#34; $3 &#34;\&#34; +%s&#34;
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s1">  cmd | getline created; close(cmd)
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s1">  age = (systime() - created) / 86400
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s1">  if (age &gt; 90) printf &#34;%s\t%s\t%.0f days\n&#34;, $1, $2, age
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s1">}&#39;</span></span></span></code></pre></div><h2 id="識別每把-key-的用途">識別每把 key 的用途</h2>
<p>知道 key 存在之後，下一個問題是「這把 key 用在哪裡」。credential report 只告訴你 key 最後被用來呼叫什麼 service（<code>access_key_1_last_used_service</code>），但不告訴你它被存放在哪裡。</p>
<p>用途識別需要交叉比對多個來源：</p>
<table>
  <thead>
      <tr>
          <th>可能的存放位置</th>
          <th>檢查方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CI 環境變數（GitHub Actions）</td>
          <td>repo Settings → Secrets and variables → Actions</td>
      </tr>
      <tr>
          <td>CI 環境變數（GitLab CI）</td>
          <td>repo Settings → CI/CD → Variables</td>
      </tr>
      <tr>
          <td>EC2 instance 的 user data</td>
          <td><code>aws ec2 describe-instance-attribute --attribute userData</code></td>
      </tr>
      <tr>
          <td>Lambda 環境變數</td>
          <td><code>aws lambda get-function-configuration --function-name NAME</code></td>
      </tr>
      <tr>
          <td>SSM Parameter Store</td>
          <td><code>aws ssm get-parameters-by-path --path / --recursive</code></td>
      </tr>
      <tr>
          <td>開發者筆電</td>
          <td><code>~/.aws/credentials</code> — 需要口頭確認</td>
      </tr>
      <tr>
          <td>程式碼 repo</td>
          <td><code>git log --all -p | grep AKIA</code> — AKIA 是 access key 的固定前綴</td>
      </tr>
      <tr>
          <td>Slack / email 歷史</td>
          <td>無法自動掃描，靠團隊回報</td>
      </tr>
  </tbody>
</table>
<p>對每把要輪替的 key，在以上位置逐一確認。找不到用途的 key 可以先停用觀察（而非直接刪除），停用後如果有服務壞了就知道它用在哪裡。</p>
<h2 id="輪替步驟五步流程">輪替步驟：五步流程</h2>
<p>輪替一把 key 的標準流程分五步，順序不能跳：</p>
<h3 id="第一步建立新-key">第一步：建立新 key</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws iam create-access-key --user-name deploy-bot</span></span></code></pre></div><p>輸出會包含新的 AccessKeyId 和 SecretAccessKey。SecretAccessKey 只在這一刻顯示一次，存進密碼管理器或 Secrets Manager，不要貼在 Slack 或 email 裡。</p>
<p>一個 IAM user 最多同時有兩把 key。如果已經有兩把，需要先刪除一把不用的才能建新的。</p>
<h3 id="第二步更新所有消費者">第二步：更新所有消費者</h3>
<p>把新 key 部署到上一節識別出的所有存放位置。CI 變數、Lambda 環境變數、SSM Parameter Store、開發者的 <code>~/.aws/credentials</code> 都要同步更新。</p>
<p>每更新一個消費者就做一次功能驗證——CI 跑一次 pipeline、Lambda 觸發一次、開發者跑一次 <code>aws sts get-caller-identity</code> 確認新 key 能用。</p>
<h3 id="第三步驗證新-key-生效">第三步：驗證新 key 生效</h3>
<p>所有消費者更新完後，等待一個完整的業務週期（至少 24 小時），確認沒有任何服務還在用舊 key。檢查方式是看舊 key 的 <code>LastUsedDate</code> 有沒有在更新之後還被使用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws iam get-access-key-last-used --access-key-id AKIAOLD12345</span></span></code></pre></div><p>如果 <code>LastUsedDate</code> 在你更新消費者之後仍有新的使用紀錄，代表有漏網的消費者還在用舊 key。</p>
<h3 id="第四步停用舊-key">第四步：停用舊 key</h3>
<p>確認無殘留使用後，停用（不是刪除）舊 key：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws iam update-access-key <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --user-name deploy-bot <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --access-key-id AKIAOLD12345 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --status Inactive</span></span></code></pre></div><p>停用是安全的中間狀態——用到這把 key 的服務會開始報 <code>InvalidClientTokenId</code> 錯誤，但 key 還在、可以隨時重新啟用。如果停用後有意料之外的服務壞了，重新啟用就能立刻恢復。</p>
<h3 id="第五步寬限期後刪除">第五步：寬限期後刪除</h3>
<p>停用後保持 7-14 天的寬限期。這段時間是「如果有漏掉的消費者」的安全網。寬限期內無異常，刪除：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws iam delete-access-key <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --user-name deploy-bot <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --access-key-id AKIAOLD12345</span></span></code></pre></div><p>刪除後不可回復。如果有服務還在用這把 key，只能建一把新 key 然後去更新那個服務。</p>
<h2 id="自動化輪替secrets-manager">自動化輪替：Secrets Manager</h2>
<p>手動輪替的瓶頸在「找到所有消費者」這一步。如果 key 的消費者都從 Secrets Manager 讀取（而非各自存一份副本），輪替就簡化成「在 Secrets Manager 裡更新值」——所有消費者下次讀取時自動拿到新 key。</p>
<p>Secrets Manager 支援自動輪替：設定一個 Lambda function 作為 rotation function，它負責建新 key → 更新 secret value → 停用舊 key 的全流程。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_secretsmanager_secret&#34; &#34;deploy_key&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;prod/deploy-bot/access-key&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">}
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_secretsmanager_secret_rotation&#34; &#34;deploy_key&#34;</span> {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">  secret_id</span>           <span class="o">=</span> <span class="k">aws_secretsmanager_secret</span><span class="p">.</span><span class="k">deploy_key</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">  rotation_lambda_arn</span> <span class="o">=</span> <span class="k">aws_lambda_function</span><span class="p">.</span><span class="k">key_rotator</span><span class="p">.</span><span class="k">arn</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">rotation_rules</span> {
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">    automatically_after_days</span> <span class="o">=</span> <span class="m">90</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  }
</span></span><span class="line"><span class="ln">12</span><span class="cl">}</span></span></code></pre></div><p>自動輪替的前提是所有消費者都改成從 Secrets Manager 讀 key，而非從環境變數或設定檔。這個前提本身就是一次 migration——跟手動輪替的固定成本（盤點 + 更新 + 驗證）相比，migration 的一次性成本更高，但之後的每次輪替接近零成本。</p>
<p>判斷該不該投入自動化的依據是 key 的數量和輪替頻率。3 把 key、每季輪替一次，手動流程 2-3 小時可以完成，自動化的 ROI 不高。10 把以上、或合規要求 30 天輪替，手動已經吃掉固定的工程師時間，自動化的投入才有回報。</p>
<h2 id="key-age-監控">Key age 監控</h2>
<p>輪替做完不代表可以不管——如果沒有監控，三個月後又會回到「不知道有幾把超齡的 key」的狀態。</p>
<p>最低成本的監控是一條定期跑的 check，掃描所有 key 的年齡並在超過閾值時告警：</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"># 列出所有超過 90 天的 active key（用 AWS Config 規則更可靠）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws configservice put-config-rule --config-rule <span class="s1">&#39;{
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s1">  &#34;ConfigRuleName&#34;: &#34;access-keys-rotated&#34;,
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s1">  &#34;Source&#34;: {
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s1">    &#34;Owner&#34;: &#34;AWS&#34;,
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s1">    &#34;SourceIdentifier&#34;: &#34;ACCESS_KEYS_ROTATED&#34;
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s1">  },
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="s1">  &#34;InputParameters&#34;: &#34;{\&#34;maxAccessKeyAge\&#34;:\&#34;90\&#34;}&#34;
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="s1">}&#39;</span></span></span></code></pre></div><p>AWS Config 的 <code>ACCESS_KEYS_ROTATED</code> managed rule 會持續掃描所有 IAM user 的 key age，超過設定天數的標記為 non-compliant。把 Config 的 non-compliant 事件接到 SNS → Slack 或 email，就有了持續的 key 超齡告警。</p>
<p>Prowler 也提供 key age 檢查（<code>prowler aws --checks access_key_1_rotated</code>），適合當一次性掃描工具。Config rule 適合持續監控。</p>
<p>管理層報告可以用 Config 的 compliance dashboard：compliant key 數 / 總 key 數 = key rotation 覆蓋率，這個百分比適合放進月報。</p>
<p>IAM Access Analyzer 的 unused access 功能（需啟用 analyzer）可以持續掃描帳號內未使用的 key 和 permission，跟 Config rule 互補——Config 看 key age，Access Analyzer 看 key 是否被使用。兩者搭配可以同時回答「這把 key 多久沒輪替」和「這把 key 有沒有在用」。</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>：access key 風險的系統性分析、OIDC 作為長期 key 的替代方案</li>
<li>→ <a href="/blog/infra/02-identity-credentials/team-access-management/" data-link-title="團隊權限分級與存取管理" data-link-desc="用 admin / operator / viewer 三級劃分團隊成員的雲端操作權限，設計臨時提權流程、定期 access review 節奏，以及 contractor 與外部 vendor 的存取邊界">團隊權限分級與存取管理</a>：離職時的 key 撤銷流程</li>
<li>→ <a href="/blog/infra/08-governance-habits/tagging-secrets/" data-link-title="Tagging 規範與 Secrets 不進 code" data-link-desc="tag 讓資源可盤點、可清理、可歸屬；密鑰存在專用服務裡而非 code 或 state，兩者都屬於 day-1 就該立的治理地基">治理好習慣</a>：secret 的儲存與引用紀律</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>AWS IAM</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/</guid><description>&lt;p>AWS IAM 是 AWS 的 cloud resource permission engine — 它回答的問題是「這個身份能對哪一個 AWS resource 做哪一個 API call」。它不是 workforce IdP、也不負責「這個人類是誰」的判定。所有 AWS API 流量（無論來自 console 操作、CI pipeline、Lambda、EC2、跨帳號 partner）最終都要經過 IAM 的 policy 評估、IAM 是 AWS 安全模型的根。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>AWS IAM 是 &lt;em>cloud resource permission engine&lt;/em>、人類 workforce 的 SSO 與 lifecycle 應該走 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center&lt;/a> 或外部 IdP（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak&lt;/a>）。Identity Center 把人類映射到 &lt;em>Permission Set&lt;/em>、Permission Set 在每個目標帳號裡實際上是 AWS-Reserved IAM Role — 也就是說：人類登入走 Identity Center、實際的 API 授權判斷一定回到 IAM。兩層責任分清楚、policy 才不會錯放在「誰是誰」的地方。&lt;/p>
&lt;p>AWS IAM 跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &amp;#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&amp;#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC&lt;/a> 在 policy model 上設計差異很大。AWS 的表達力最強 — identity-based policy、resource-based policy、Service Control Policy（SCP）、Permission Boundary、Session Policy 是五個獨立的層、最終結果由 &lt;em>Explicit Deny &amp;gt; Org SCP &amp;gt; Resource-based &amp;gt; Identity-based &amp;gt; Permission Boundary &amp;gt; Session Policy&lt;/em> 的評估順序決定。表達力換來的代價是 &lt;em>最容易設定錯&lt;/em>：S3 bucket policy 設錯 = public、KMS key policy 漏一個 condition = 跨帳號可以解密、Trust Policy 沒設 ExternalID = confused deputy 攻擊面。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>哪些 IAM first-class concept（User / Group / Role / Policy / STS）對應到自己的場景、哪些要避免（例如：給人類發 IAM User access key）&lt;/li>
&lt;li>跨帳號信任、CI / 第三方 SaaS 連進 AWS、service-to-service 認證該走 Role assumption / OIDC trust 還是 Roles Anywhere&lt;/li>
&lt;li>SCP、Permission Boundary、resource-based policy 三層上限的疊加方式、何時用哪一層&lt;/li>
&lt;li>CloudTrail + Access Analyzer 的稽核 baseline、出事時的最短取證路徑&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷一個 AWS 帳號的 IAM 配置是否健康、最少看四件事：&lt;/p></description><content:encoded><![CDATA[<p>AWS IAM 是 AWS 的 cloud resource permission engine — 它回答的問題是「這個身份能對哪一個 AWS resource 做哪一個 API call」。它不是 workforce IdP、也不負責「這個人類是誰」的判定。所有 AWS API 流量（無論來自 console 操作、CI pipeline、Lambda、EC2、跨帳號 partner）最終都要經過 IAM 的 policy 評估、IAM 是 AWS 安全模型的根。</p>
<h2 id="服務定位">服務定位</h2>
<p>AWS IAM 是 <em>cloud resource permission engine</em>、人類 workforce 的 SSO 與 lifecycle 應該走 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a> 或外部 IdP（<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / <a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a>）。Identity Center 把人類映射到 <em>Permission Set</em>、Permission Set 在每個目標帳號裡實際上是 AWS-Reserved IAM Role — 也就是說：人類登入走 Identity Center、實際的 API 授權判斷一定回到 IAM。兩層責任分清楚、policy 才不會錯放在「誰是誰」的地方。</p>
<p>AWS IAM 跟 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a> 在 policy model 上設計差異很大。AWS 的表達力最強 — identity-based policy、resource-based policy、Service Control Policy（SCP）、Permission Boundary、Session Policy 是五個獨立的層、最終結果由 <em>Explicit Deny &gt; Org SCP &gt; Resource-based &gt; Identity-based &gt; Permission Boundary &gt; Session Policy</em> 的評估順序決定。表達力換來的代價是 <em>最容易設定錯</em>：S3 bucket policy 設錯 = public、KMS key policy 漏一個 condition = 跨帳號可以解密、Trust Policy 沒設 ExternalID = confused deputy 攻擊面。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>哪些 IAM first-class concept（User / Group / Role / Policy / STS）對應到自己的場景、哪些要避免（例如：給人類發 IAM User access key）</li>
<li>跨帳號信任、CI / 第三方 SaaS 連進 AWS、service-to-service 認證該走 Role assumption / OIDC trust 還是 Roles Anywhere</li>
<li>SCP、Permission Boundary、resource-based policy 三層上限的疊加方式、何時用哪一層</li>
<li>CloudTrail + Access Analyzer 的稽核 baseline、出事時的最短取證路徑</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷一個 AWS 帳號的 IAM 配置是否健康、最少看四件事：</p>
<ul>
<li><strong>誰能 assume 哪個 Role</strong>：所有 Role 的 Trust Policy（誰能呼叫 <code>sts:AssumeRole</code>）、有沒有跨帳號 trust、跨帳號 trust 是否帶 ExternalID、有沒有 <code>*</code> 在 Principal 裡</li>
<li><strong>Resource-based policy 暴露面</strong>：S3 bucket policy、KMS key policy、Lambda function policy、SNS / SQS policy 是否有 <code>Principal: *</code> 或來自非預期帳號；用 <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html">IAM Access Analyzer</a> 找 <em>unintended external access</em></li>
<li><strong>Permission Boundary 與 SCP 是否生效</strong>：開發者建的 Role 是否 attach Permission Boundary（防止 admin 自己給自己升權）、Organization 是否 attach SCP 做整個 OU 的上限</li>
<li><strong>CloudTrail 是否完整、是否進 SIEM</strong>：management event 跟 data event 都開、跨 region、跨帳號、保留期符合稽核要求、特定事件（<code>AssumeRole</code> 失敗、root login、<code>CreateAccessKey</code>）接 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a></li>
</ul>
<p>四件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">Authorization</a> 與 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Role 設計（cross-account / service / OIDC trust）</strong>：所有 <em>持續性</em> 的身份都應該是 Role、不是 IAM User。Service Role（給 EC2 / Lambda / ECS task）是 AWS 內部 service-to-service；Cross-account Role 給 partner 帳號或自家其他帳號用 <code>sts:AssumeRole</code> 進來；OIDC trust 是現代 CI 必備路徑（GitHub Actions / GitLab / 自管 K8s 用短期 OIDC token 換 AWS STS 短期憑證、不在 secret store 存 long-lived access key）。</p>
<p><strong>Policy 種類分工</strong>：identity-based policy attach 在 User / Group / Role 上、回答「這個身份能做什麼」。Resource-based policy attach 在 resource 上（S3 bucket、KMS key、SNS topic、Lambda function）、回答「誰能對這個 resource 做什麼」— 同帳號內 identity-based 跟 resource-based 任一個 allow 就通過、跨帳號 <em>兩邊都要 allow</em>。SCP 是 Organization 層級的上限、不是 grant — SCP allow 不會給任何權限、SCP deny 會擋掉整個 OU 的所有 identity。Permission Boundary 是 <em>user 角度的上限</em>、給 admin 用來限制「我把 admin 權限委派給 developer 後、developer 自己建的 role 不能超過這條線」。</p>
<p><strong>STS 與臨時憑證</strong>：所有 cross-account、service-to-service、人類 console federation 都應該走 STS — <code>sts:AssumeRole</code>（跨帳號 / 跨 role）、<code>sts:AssumeRoleWithSAML</code>（SAML IdP）、<code>sts:AssumeRoleWithWebIdentity</code>（OIDC）、<code>sts:GetFederationToken</code>（外部 broker）。Session 預設 1 小時、最長可設 12 小時（依 Role 設定）。Debug 起手式：<code>aws sts get-caller-identity</code> 確認當前 caller 是誰、是 User、Role 還是 federated session。</p>
<p><strong>Access Key 治理</strong>：IAM User 的 long-lived access key 是 <em>最後手段</em>、用於 break-glass 或無法跑 IMDS / Roles Anywhere 的 legacy。所有 access key 走 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>、定期 rotation、IAM Access Analyzer 的 unused access finding 找閒置 key。</p>
<p><strong>CloudTrail / Access Analyzer baseline</strong>：CloudTrail organization trail 開到所有帳號、management event 必開、data event（S3 object level、Lambda invoke）依資料敏感度開。Access Analyzer 至少跑 <em>external access</em>（找 resource-based policy 把資源暴露給外部帳號）跟 <em>unused access</em>（找閒置 Role、user、permission）。</p>
<p><strong>Trust Policy / ExternalID</strong>：第三方 SaaS（監控、CSPM、備份服務）要進你的 AWS 帳號時、其 Trust Policy 必須要求 ExternalID — 否則攻擊者只要知道 Role ARN 就能假冒第三方 SaaS 的呼叫端、走 confused deputy 攻擊面（<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html">AWS confused deputy 官方說明</a>）。自家跨帳號 trust 不一定要 ExternalID、第三方一定要。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>AWS IAM</th>
          <th>Google Cloud IAM</th>
          <th>Azure RBAC</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>基本單位</td>
          <td>Policy（attach 到 identity 或 resource）</td>
          <td>Role Binding（principal + role + resource）</td>
          <td>Role Assignment（scope + principal + role）</td>
      </tr>
      <tr>
          <td>隔離邊界</td>
          <td>Account（root）+ Organization SCP</td>
          <td>Project / Folder / Org（階層 inherit）</td>
          <td>Subscription / Management Group（階層 inherit）</td>
      </tr>
      <tr>
          <td>Policy 表達力</td>
          <td>高 — identity / resource / SCP / boundary / session 五層</td>
          <td>中 — Conditional IAM + Organization Policy</td>
          <td>中 — RBAC + Azure Policy 兩層</td>
      </tr>
      <tr>
          <td>Resource-based</td>
          <td>多 service 支援（S3 / KMS / SNS / SQS / Lambda&hellip;）</td>
          <td>較少（GCS / Pub/Sub / KMS 等）</td>
          <td>較少、多走 RBAC 統一</td>
      </tr>
      <tr>
          <td>設定錯誤代價</td>
          <td>高 — bucket / key policy 設錯就 public</td>
          <td>中 — 較統一但精細度也較低</td>
          <td>中 — 階層 inherit 容易誤放</td>
      </tr>
  </tbody>
</table>
<p>AWS IAM 是 <em>表達力最強、最容易設定錯</em> 的雲端 IAM。Google Cloud IAM 設計較統一、policy model 易讀但精細度有限。Azure RBAC 走 inheritance + scope、靠 Management Group 結構治理。三家都不能直接互換、跨雲環境需要在每家自己的 IAM 模型裡建等價的 least-privilege baseline。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Service Control Policy（SCP）</strong>：Organization 層級的上限、用來宣告「整個 OU 永遠不能做什麼」 — 例如禁止 root user 操作、禁止關閉 CloudTrail、禁止在非允許 region 建 resource。SCP 是 <em>deny-list 防護網</em>、不是日常授權；日常授權交給 identity-based policy。SCP 過嚴會擋住合法操作、過鬆等於沒設、設計時要對齊 organization 的安全政策骨幹。</p>
<p><strong>Permission Boundary</strong>：用在 <em>委派 admin</em> 場景 — 公司想讓 platform team 自己建 IAM Role 給應用、但又不想讓他們建出 admin role。Admin 給 platform team 一個 Permission Boundary policy、platform team 建的所有 Role 都會被這個 boundary 限制 <em>上限</em>、就算 attach 了 <code>AdministratorAccess</code> 也只能在 boundary 範圍內生效。</p>
<p><strong>ABAC（attribute-based / tag-based access control）</strong>：大規模 multi-account 環境、每個 service 一個 Role 會 Role 爆炸。ABAC 用 <em>tag</em>（principal tag、resource tag、request tag）做 policy condition — 例如「Role 上有 <code>team=payments</code> tag 的人能操作 <code>team=payments</code> tag 的 resource」。設計成立的前提是 tag 來源可信、不能讓使用者自己改 principal tag。</p>
<p><strong>IAM Roles Anywhere</strong>：給 AWS 之外的 workload（地端 K8s、其他雲、邊緣設備）用 X.509 憑證換 STS 短期憑證。前提是有一個可信的 PKI（自管 CA 或公開 CA）跟 trust anchor。比起把 IAM User access key 放在地端 secret store、Roles Anywhere 是更安全的設計。</p>
<p><strong>OIDC trust（GitHub Actions / GitLab CI / 第三方 CI）</strong>：CI / CD 連 AWS 的標準做法。在 AWS 建一個 OIDC identity provider 指向 CI 的 OIDC issuer、Role 的 Trust Policy condition 限制 <code>repo:org/repo:ref:refs/heads/main</code>、CI workflow 直接 <code>aws sts assume-role-with-web-identity</code>。完全不需要在 CI secret store 存 long-lived AWS access key、token TTL 隨 job 結束自動失效。</p>
<p><strong>Resource-based policy 跨帳號設計</strong>：S3 bucket policy、KMS key policy、SNS / SQS / Lambda policy 都支援跨帳號授權。設計時兩件事必查：Principal 是否包含預期的帳號 / Role ARN、condition 是否限制來源（<code>aws:SourceAccount</code>、<code>aws:SourceArn</code>、<code>aws:PrincipalOrgID</code>）。漏了 condition、就可能讓任何拿到「假裝是某個 service」身份的人都能呼叫 — Capital One 2019 事件本質就是 SSRF 取得 EC2 IMDS 的 Role credential、再用該 Role 的權限去 S3 列舉跟讀取資料、揭示 <em>resource-based policy + identity-based policy 沒有最小化、就會在事故時最大化</em>。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong><code>AccessDenied</code> 但 policy 看起來 allow</strong>：先用 <a href="https://policysim.aws.amazon.com/">IAM Policy Simulator</a> 或 <code>aws iam simulate-principal-policy</code> 重算、確認是 SCP 擋、Permission Boundary 擋、resource-based policy 沒 allow、還是 condition key 不匹配。Explicit Deny 永遠贏。</li>
<li><strong>跨帳號 <code>sts:AssumeRole</code> 失敗</strong>：兩邊都要設 — caller 帳號的 identity-based policy 要 allow <code>sts:AssumeRole</code> 到目標 Role ARN、目標 Role 的 Trust Policy 要 allow caller 的 Principal。漏其一就失敗。</li>
<li><strong>S3 bucket 不小心 public</strong>：用 Access Analyzer 的 external access finding 找、用 <em>Block Public Access</em> 帳號級別開關擋掉（即使 bucket policy 寫了 public、Block Public Access 也會擋）。常見根因：bucket policy 寫 <code>Principal: *</code> 沒加 condition、或 ACL 殘留歷史設定。</li>
<li><strong>Role / access key 殘留</strong>：用 Access Analyzer 的 unused access finding、或 IAM credential report 找超過 90 天沒用的 user / role、配 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a> 的分域分批 rotation 流程清理</li>
<li><strong>第三方 SaaS Role 缺 ExternalID</strong>：稽核第三方 vendor 的 onboarding 文件、若沒要求 ExternalID 是 vendor 自己安全模型有破口、自己這邊也要拒絕這種 onboarding</li>
<li><strong>CloudTrail 落地不全</strong>：Organization trail 沒覆蓋新建帳號、data event 沒開、log 沒進 SIEM、保留期不足 — 這四件事都會讓事故發生時拿不到證據</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>人類員工 SSO 進 AWS</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a></td>
      </tr>
      <tr>
          <td>多雲 / SaaS app 統一 SSO</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / <a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a></td>
      </tr>
      <tr>
          <td>Customer / B2C identity</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0</a></td>
      </tr>
      <tr>
          <td>Google Cloud resource 權限</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a></td>
      </tr>
      <tr>
          <td>Azure resource 權限</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a></td>
      </tr>
      <tr>
          <td>Secret / API key 治理</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></td>
      </tr>
      <tr>
          <td>Key lifecycle / envelope encryption</td>
          <td>AWS KMS vendor 頁（S2 批次撰寫中）+ <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></td>
      </tr>
      <tr>
          <td>事件偵測（CloudTrail 以外）</td>
          <td>04 SIEM / detection 工具與 07 SIEM 章節</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>IAM policy JSON 語法完整 reference 與所有 condition key 清單</li>
<li>每個 AWS service 的細部 IAM 動作對照</li>
<li>AWS Organization、Control Tower、Landing Zone 完整建置流程</li>
<li>KMS / Secrets Manager / Certificate Manager 的內部細節（見對應 vendor 頁）</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 AWS IAM 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><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 Signing Key 2023</a></td>
          <td>雖是 Microsoft Entra / Exchange Online 事件、對 AWS <em>cross-account role assumption signing chain</em> 提供對照：ExternalID 設計、HSM-bound key、跨帳號 token 驗證一致性</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>IAM User access key、STS session、Role trust 的 rotation 必須分域分批、不能單一指令打全部</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain (red-team)</a></td>
          <td>對 IAM Roles Anywhere / OIDC trust 的 signing material 治理啟示：trust anchor、key custody、跨環境驗證</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a>、<a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>（AWS KMS vendor 頁 S2 批次撰寫中）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（CloudTrail / Access Analyzer 訊號如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/">AWS IAM User Guide</a>、<a href="https://docs.aws.amazon.com/singlesignon/latest/userguide/">AWS IAM Identity Center User Guide</a></li>
</ul>
]]></content:encoded></item><item><title>Google Cloud IAM</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/</guid><description>&lt;p>Google Cloud IAM 是 GCP 的 cloud resource permission engine、把 &lt;em>誰能對哪個 resource 做什麼&lt;/em> 統一成一個模型：Principal + Role + Resource scope 三件事拼成一個 &lt;em>role binding&lt;/em>。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> 等 IdP 是兩層責任 — Okta 回答「這個人是誰」、Google IAM 回答「這個身份能對 GCP resource 做什麼」。設計上比 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM&lt;/a> 統一、沒有 resource-based policy vs identity-based policy 雙軌、也沒有 SCP / Permission Boundary 多層覆蓋、policy 評估路徑短而可預測。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Google Cloud IAM 的核心抽象是 &lt;em>role binding on a resource scope&lt;/em>：把 role grant 給 principal、生效範圍是某個 Organization / Folder / Project / 個別 resource、沿 resource hierarchy 向下繼承。同一個 principal 在不同 scope 可以有不同 role、有效權限是所有 binding 的 union。這跟 AWS IAM 的「identity policy + resource policy + SCP + boundary 多層 intersect / union」相比、推理成本低、但也意味著 &lt;em>guardrail 必須走 Organization Policy 這另一個系統&lt;/em> — 不是 IAM grant 的一部分。&lt;/p>
&lt;p>跟 Azure RBAC 相比、兩者都是 scope-based、都靠 hierarchy 繼承。差異在 &lt;em>Service Account 是 GCP 的 first-class identity&lt;/em>：有自己的 email、可被 impersonate、可以 grant role 給它也可以 grant &lt;code>iam.serviceAccountUser&lt;/code> 讓人類 act-as 它。Azure 的對應是 Managed Identity、語義接近但 impersonation chain 的表達更隱晦。選 GCP（= 用 Google Cloud IAM）的核心訴求通常是：BigQuery / Vertex AI / GKE workload、想用 Workload Identity Federation 取代 long-lived key、團隊偏好較統一的 policy 模型。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>Google Cloud IAM 該承擔哪一段權限（resource access、service-to-service、cross-cloud federation）、哪一段該交給 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> / IdP&lt;/li>
&lt;li>Role 的選擇順序（Predefined &amp;gt; Custom &amp;gt; Basic）與 IAM Conditions 何時補上&lt;/li>
&lt;li>Service Account / Workload Identity Federation 的信任邊界、何時不該再發 service account key&lt;/li>
&lt;li>何時改走 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &amp;#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&amp;#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC&lt;/a> / Organization Policy / VPC Service Controls&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷一個 GCP project 的 IAM 配置是否健康、最少看五件事：&lt;/p></description><content:encoded><![CDATA[<p>Google Cloud IAM 是 GCP 的 cloud resource permission engine、把 <em>誰能對哪個 resource 做什麼</em> 統一成一個模型：Principal + Role + Resource scope 三件事拼成一個 <em>role binding</em>。它跟 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> 等 IdP 是兩層責任 — Okta 回答「這個人是誰」、Google IAM 回答「這個身份能對 GCP resource 做什麼」。設計上比 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> 統一、沒有 resource-based policy vs identity-based policy 雙軌、也沒有 SCP / Permission Boundary 多層覆蓋、policy 評估路徑短而可預測。</p>
<h2 id="服務定位">服務定位</h2>
<p>Google Cloud IAM 的核心抽象是 <em>role binding on a resource scope</em>：把 role grant 給 principal、生效範圍是某個 Organization / Folder / Project / 個別 resource、沿 resource hierarchy 向下繼承。同一個 principal 在不同 scope 可以有不同 role、有效權限是所有 binding 的 union。這跟 AWS IAM 的「identity policy + resource policy + SCP + boundary 多層 intersect / union」相比、推理成本低、但也意味著 <em>guardrail 必須走 Organization Policy 這另一個系統</em> — 不是 IAM grant 的一部分。</p>
<p>跟 Azure RBAC 相比、兩者都是 scope-based、都靠 hierarchy 繼承。差異在 <em>Service Account 是 GCP 的 first-class identity</em>：有自己的 email、可被 impersonate、可以 grant role 給它也可以 grant <code>iam.serviceAccountUser</code> 讓人類 act-as 它。Azure 的對應是 Managed Identity、語義接近但 impersonation chain 的表達更隱晦。選 GCP（= 用 Google Cloud IAM）的核心訴求通常是：BigQuery / Vertex AI / GKE workload、想用 Workload Identity Federation 取代 long-lived key、團隊偏好較統一的 policy 模型。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>Google Cloud IAM 該承擔哪一段權限（resource access、service-to-service、cross-cloud federation）、哪一段該交給 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / IdP</li>
<li>Role 的選擇順序（Predefined &gt; Custom &gt; Basic）與 IAM Conditions 何時補上</li>
<li>Service Account / Workload Identity Federation 的信任邊界、何時不該再發 service account key</li>
<li>何時改走 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a> / Organization Policy / VPC Service Controls</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷一個 GCP project 的 IAM 配置是否健康、最少看五件事：</p>
<ul>
<li><strong>Principal 級別</strong>：誰是 Owner / Editor / Viewer（Basic Role 應該幾乎為空）、Service Account 是否獨立列管、有沒有 user 直接 grant 沒走 group</li>
<li><strong>Role 種類</strong>：Predefined Role 是 baseline、Custom Role 收斂 least privilege、Basic Role 視為待修；user-managed Service Account key 是否存在（理想是 0）</li>
<li><strong>Impersonation chain 展平稽核</strong>：誰有 <code>iam.serviceAccountTokenCreator</code> / <code>iam.serviceAccountUser</code> 對哪個 SA、間接 chain（A → B → C）展平後 <em>誰最終能 act as 高權限 SA</em>。這是 GCP IAM 最容易漏稽核的一條 — 直接 binding 看 Role、但 lateral movement 走 impersonation chain</li>
<li><strong>IAM Conditions</strong>：高敏 resource（prod bucket、KMS key、BigQuery dataset）是否用 condition expression 補 attribute-level 限制（resource name prefix、request time、IP）</li>
<li><strong>Audit Logs</strong>：Admin Activity 預設開、Data Access logs 在 sensitive resource 是否手動開、System Log 是否同步到 SIEM 並 alert role 變更與 service account key 建立</li>
</ul>
<p>五件事任一缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">Authorization</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<p><strong>Role 選擇順序</strong>：Predefined Role 是 baseline、覆蓋 80% 場景；Custom Role 用於收斂 least privilege（例如只給 <code>bigquery.dataViewer</code> 的特定子集）；Basic Role（Owner / Editor / Viewer）幾乎不該再用 — Editor 預設帶寫權限到幾乎所有資源類型、Owner 還能改 IAM policy 本身、粒度過粗。Project 建立預設給的 Owner role 是 <em>人類自己 grant 自己</em>、不是無法避免的 baseline。</p>
<p><strong>Principal type</strong>：人類用 Google Workspace user / external user，群組走 Google Group（grant 給 group 比 grant 給 user 更穩、離職 lifecycle 由 IdP / HRIS 推 group 變更即可）。Service Account 是 <em>第一級身份</em>、跟 user 同等、有自己的 email（<code>name@project.iam.gserviceaccount.com</code>）、可被 grant role 也可被 impersonate。Workload identity（K8s SA、外部 OIDC subject）是 federation 層、不在 IAM 內直接列管、但 <em>最後仍 impersonate 一個 Service Account 來拿 GCP 權限</em>。</p>
<p><strong>IAM Conditions</strong>：在 role binding 上加 attribute-based 條件、補純 RBAC 不足。常見 expression：<code>resource.name.startsWith(&quot;projects/_/buckets/prod-&quot;)</code>、<code>request.time &lt; timestamp(&quot;2026-12-31T00:00:00Z&quot;)</code>、<code>resource.type == &quot;storage.googleapis.com/Bucket&quot;</code>。適合 <em>temporary access</em>、<em>resource name 範圍限定</em>、<em>環境隔離</em>；不適合複雜 ABAC 規則（會難以稽核、且 condition 只能用在支援的 resource type 上）。</p>
<p><strong>Service Account impersonation</strong>：人類或另一個 Service Account 透過 <code>iam.serviceAccountTokenCreator</code> role 借用目標 SA 的權限、不需要 SA key。impersonation chain 可以串（A 可 impersonate B、B 可 impersonate C）— 這條鏈是 lateral movement 風險、稽核時要展平看 <em>誰最終能 act as 高權限 SA</em>。對應 <a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a> 的教訓：rotation 沒分域時、單點 SA compromise 會跨環境擴散。</p>
<p><strong>Workload Identity Federation（WIF）</strong>：GCP 接受外部 OIDC / SAML issuer（GitHub Actions、AWS、Azure、自管 K8s OIDC、CircleCI 等）發的 token、在 Workload Identity Pool 設 attribute mapping 後、外部 token 換成 short-lived GCP credential、最後 impersonate 指定 Service Account。是 <em>取代 SA JSON key 的 modern best practice</em>、CI / 跨雲 / 邊緣 workload 都該優先用。Trust 條件要鎖 <em>issuer + audience + subject</em>（例：<code>assertion.repository == &quot;myorg/myrepo&quot;</code>）— 缺一個就可能被同 issuer 下其他 subject 借用，這是 <a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain</a> 對 external OIDC 信任的提醒：發 token 的 issuer 一旦被攻破、所有信任它的 audience 都跟著受害。</p>
<p><strong>Service Account key（避免）</strong>：user-managed JSON key 是 long-lived credential、無 TTL、無 IP 限制、外洩偵測難。應該以 Workload Identity Federation 或 Service Account Impersonation 取代；若必須用、走 Organization Policy <code>iam.disableServiceAccountKeyCreation</code> 預設禁用、例外申請走 ticket、key 進 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>、季度盤點未使用 key 刪除。</p>
<p><strong>Organization Policy（guardrail）</strong>：跟 IAM 完全不同層 — 不是 grant、是 <em>限制可以做什麼設定</em>。常用 constraint：<code>iam.disableServiceAccountKeyCreation</code>、<code>iam.allowedPolicyMemberDomains</code>（限制只能 grant 給特定 domain 的 principal）、<code>compute.vmExternalIpAccess</code>（限制 VM external IP）、<code>storage.publicAccessPrevention</code>。Org Policy 在 Organization / Folder / Project 層設定、IAM 即使想 grant 也擋得住。</p>
<p><strong>Audit / handoff</strong>：Admin Activity Log 預設開、不能關、保留 400 天免費；Data Access Log 預設關、開了會大量 log（也大量計費）— 對 sensitive resource（KMS key access、BigQuery dataset read、Secret Manager access）應該手動開；System Event Log 補基礎設施事件。三類都接 Cloud Logging sink 推到 SIEM、特別 alert 三件事 — IAM policy 變更、Service Account key 建立 / 上傳、Workload Identity Pool / Provider 變更。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<table>
  <thead>
      <tr>
          <th>取捨維度</th>
          <th>Google Cloud IAM</th>
          <th>AWS IAM</th>
          <th>Azure RBAC</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Policy 模型</td>
          <td>Role binding on resource scope、單軌</td>
          <td>Identity policy + resource policy + SCP + boundary</td>
          <td>Scope-based、Management Group 階層</td>
      </tr>
      <tr>
          <td>表達力</td>
          <td>中等、IAM Conditions 補 attribute</td>
          <td>最高、policy language 表達 ABAC / 條件 / 否決</td>
          <td>中等、Azure Policy 補 ABAC</td>
      </tr>
      <tr>
          <td>Guardrail 機制</td>
          <td>Organization Policy（獨立系統、constraint）</td>
          <td>SCP（policy 同語法、separate plane）</td>
          <td>Azure Policy（獨立系統、constraint）</td>
      </tr>
      <tr>
          <td>Machine identity</td>
          <td>Service Account first-class + WIF</td>
          <td>IAM Role + STS AssumeRole + OIDC trust</td>
          <td>Managed Identity + Workload Identity Federation</td>
      </tr>
      <tr>
          <td>Cross-cloud federation</td>
          <td>WIF 接外部 OIDC 是 modern best practice</td>
          <td>OIDC trust on IAM Role、表達力強</td>
          <td>Federated credentials、近年補齊</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>較緩、模型統一</td>
          <td>陡、policy 評估順序複雜</td>
          <td>中等、scope inheritance 直覺</td>
      </tr>
      <tr>
          <td>推理 / 稽核成本</td>
          <td>低 — binding union、Org Policy 獨立看</td>
          <td>高 — 多層 intersect / union、需 policy simulator</td>
          <td>中 — scope 繼承明確、policy 分散</td>
      </tr>
  </tbody>
</table>
<p>選 Google Cloud IAM 的核心訴求：<em>已在 GCP 上、或想用 BigQuery / Vertex AI / GKE</em>、團隊偏好較統一的 policy 模型、跨雲場景靠 WIF 對外發 trust 而不維護多套 key。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Workload Identity Federation 的深層應用</strong>：除了 GitHub Actions、AWS、Azure 這類常見 issuer、WIF 也支援自管 K8s OIDC issuer（OSS K8s cluster 跑 GKE workload identity 等價物）、SaaS（Snowflake、Terraform Cloud）發的 OIDC token。trust 設定要鎖 issuer URL、audience、subject pattern 三件事 — 任何一個太寬都是同 issuer 下別人借用你 SA 的入口。</p>
<p><strong>Organization Policy 的 dry-run / 例外</strong>：constraint 可以先設 <code>dryRun</code> 觀察會擋掉哪些操作再 enforce；例外用 <em>exception folder</em>（特定 folder 不繼承上層 constraint）或 <em>condition</em>（特定 resource pattern 不擋）。直接全 org 一次 enforce 通常會打掉既有 workload、要分階段。</p>
<p><strong>IAM Conditions 的有限性</strong>：condition 只能用在支援的 resource type 上、不是全 GCP 通用；複雜 expression 難稽核（CEL 語法、不易讀）；condition 不能否決 — 只能限制 binding 的生效範圍、不能像 AWS policy 那樣寫 <code>Deny</code>。複雜 ABAC 場景該走 Organization Policy + 應用層授權邊界、不是把所有規則塞進 IAM Conditions。</p>
<p><strong>Service Account Impersonation chain 的稽核</strong>：列出 <em>有 <code>serviceAccountTokenCreator</code> 的 principal</em> 是基本；展平 chain（A → B → C）需要 graph walk 工具或 Policy Analyzer；高權限 SA（owner-equivalent custom role、跨 project 寫權限）的 impersonation 來源應該是 <em>寫死的少數 admin SA + break-glass</em>、不該開放給 CI / 一般 service。</p>
<p><strong>VPC Service Controls（資料邊界、跟 IAM 互補）</strong>：在 IAM 之外加 <em>資料 perimeter</em> — 即使 principal 有 IAM 權限、如果請求不是來自 perimeter 內（VPC、特定 IP、特定 service account），仍然會被擋。適合 BigQuery / GCS / Secret Manager 這類存資料的 service、防 <em>合法 credential 從外部 exfiltrate 資料</em>（<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 2021</a> 場景的下游補位：identity 控制面失守時、資料層仍有獨立 perimeter）。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Basic Role 還在用</strong>：Project Owner / Editor 散落、新人 onboard 直接 Editor — 改 group + Predefined Role、Basic Role 改成 break-glass 限定</li>
<li><strong>Service Account key 散落</strong>：CI 用 JSON key、key 進 git 或環境變數、無 rotation — 改 WIF（GitHub Actions / GitLab CI 都支援）、Org Policy 禁用 SA key 建立</li>
<li><strong>WIF trust 太寬</strong>：只鎖 issuer 沒鎖 subject、同 GitHub org 任何 repo 都能借用 SA — trust 要含 <code>assertion.repository</code>、<code>assertion.ref</code>（main branch only）等 condition</li>
<li><strong>IAM Conditions 越寫越多</strong>：condition expression 過度複雜、稽核時沒人讀得懂 — 簡化條件、把複雜規則上移到應用層或 Org Policy</li>
<li><strong>Data Access Logs 沒開</strong>：sensitive resource 出事時只有 Admin Activity、看不到 <em>誰讀了什麼</em> — KMS key、Secret Manager、BigQuery 高敏 dataset 必開 Data Access Log</li>
<li><strong>Impersonation chain 失控</strong>：太多人有 <code>serviceAccountTokenCreator</code> 到高權限 SA — 用 Policy Analyzer 展平、收斂到必要 admin + break-glass</li>
<li><strong>Org Policy 沒設</strong>：root org 沒有 baseline constraint、新建 project 預設可建 SA key / public IP / public bucket — 至少設 <code>disableServiceAccountKeyCreation</code> + <code>publicAccessPrevention</code> + <code>allowedPolicyMemberDomains</code></li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>人類身份的 SSO / MFA / lifecycle</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> / IdP</td>
      </tr>
      <tr>
          <td>AWS resource permission</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a></td>
      </tr>
      <tr>
          <td>Azure resource permission</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a></td>
      </tr>
      <tr>
          <td>跨雲 unified IAM</td>
          <td>沒有單一答案 — 各雲 IAM + Workload Identity Federation 對接、或外部 PAM（Teleport / Boundary）</td>
      </tr>
      <tr>
          <td>Secret / Service Account key 治理</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></td>
      </tr>
      <tr>
          <td>資料分類 / DLP / 匯出控制</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 資料保護與遮罩治理</a></td>
      </tr>
      <tr>
          <td>Workload runtime detection（容器、syscall）</td>
          <td>04 + Falco / Cilium Tetragon 類工具</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各 Predefined Role 的完整權限清單與細部 permission 差異</li>
<li>IAM Conditions CEL 語法的完整 spec</li>
<li>Workload Identity Federation 跟特定 issuer（GitHub / AWS / Azure）的逐步設定教學</li>
<li>BigQuery / GCS / KMS 等服務的 service-specific IAM 行為細節</li>
<li>GCP 計費 / SKU 對 Audit Log 開關的影響</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Google Cloud IAM 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><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 2021</a></td>
          <td>Identity 控制面故障不直接打到 Google IAM、但設計啟示是 IAM evaluation 路徑必須 HA、且 VPC Service Controls 等資料 perimeter 是 identity 失守時的下游補位</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Service Account key、WIF provider 的 rotation 必須分域 — 跨 project / 跨環境的 SA 共用是 blast radius 放大器</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain</a></td>
          <td>對 WIF 的提醒 — 信任 external OIDC issuer 時、issuer 自己被攻破會打到所有 audience；trust condition 必須鎖 issuer + audience + subject 三件事</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a>、<a href="/blog/backend/07-security-data-protection/vendors/azure-rbac/" data-link-title="Azure RBAC &#43; Entra ID" data-link-desc="Azure 雙層身份/權限體系、Entra ID（IdP）&#43; Azure RBAC（resource permission）、Conditional Access、PIM、Managed Identity">Azure RBAC</a>、<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a>、<a href="/blog/backend/07-security-data-protection/vendors/aws-iam-identity-center/" data-link-title="AWS IAM Identity Center" data-link-desc="AWS 原生 workforce SSO、前 AWS SSO、Permission Set 跨帳號 access、可串外部 IdP federation">AWS IAM Identity Center</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>（Google Secret Manager / Google Cloud KMS 個別 vendor 頁 S2 批次撰寫中）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（GCP IAM 事件如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://cloud.google.com/iam/docs">Google Cloud IAM Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Azure RBAC + Entra ID</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-rbac/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/azure-rbac/</guid><description>&lt;p>Azure 的身份與權限體系是 &lt;em>雙層&lt;/em> — Entra ID（前 Azure AD）是 IdP，承擔人類與 workload 的身份來源、SSO、MFA 與 Conditional Access；Azure RBAC 是 cloud resource 的 permission engine，把 role 指派到 scope（Management Group / Subscription / Resource Group / Resource）上的 principal。兩層責任不同、設定介面不同、出事故時的徵兆也不同 — 把兩者寫成同一件事是 Azure 治理最常見的混淆來源。&lt;/p>
&lt;h2 id="服務定位">服務定位&lt;/h2>
&lt;p>Entra ID 是 &lt;em>Microsoft 自有的 workforce IdP&lt;/em>、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta&lt;/a> 是直接競爭者。M365 / Azure-heavy 的組織通常直接用 Entra ID 當主 IdP；Okta-first 的組織可以把 Entra ID 當下游 SP（federation）、也可以雙 IdP 並存、但雙 IdP 的 break-glass 跟 lifecycle 路徑要重新設計。Entra ID 同時承擔 &lt;em>consumer-side 跟 partner-side 的 multi-tenant app&lt;/em> 信任、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0&lt;/a> 在 B2C 場景有交集。&lt;/p>
&lt;p>Azure RBAC 是 cloud resource permission engine、跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM&lt;/a> 同層 — 都在解「身份對 cloud resource 能做什麼」。差異在 &lt;em>scope hierarchy&lt;/em> — Azure 用 Management Group → Subscription → Resource Group → Resource 四層繼承、AWS 用 account + organization、Google 用 organization → folder → project。Azure RBAC 預期 &lt;em>role assignment 沿 scope 向下繼承&lt;/em>、這跟 AWS 在每個 account 重新指派的習慣不一樣、跨雲團隊轉過來常踩到。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本頁、讀者能判斷：&lt;/p>
&lt;ol>
&lt;li>哪一段控制屬於 Entra ID（身份）、哪一段屬於 Azure RBAC（resource permission）、不要把兩層當同一件事&lt;/li>
&lt;li>Entra ID tenant 的最低稽核需求（Global Admin、App Registration、Conditional Access、Managed Identity）&lt;/li>
&lt;li>Azure RBAC 的 scope 設計、Custom Role 跟 PIM 何時必要&lt;/li>
&lt;li>Entra ID 控制面事故的降級路徑、跟 Azure RBAC 出事的徵兆差異&lt;/li>
&lt;/ol>
&lt;h2 id="最短判讀路徑">最短判讀路徑&lt;/h2>
&lt;p>判斷 Azure 雙層體系是否健康、要分兩層各看兩件事、跟「日常操作與決策形狀」段的兩層結構對齊。&lt;/p></description><content:encoded><![CDATA[<p>Azure 的身份與權限體系是 <em>雙層</em> — Entra ID（前 Azure AD）是 IdP，承擔人類與 workload 的身份來源、SSO、MFA 與 Conditional Access；Azure RBAC 是 cloud resource 的 permission engine，把 role 指派到 scope（Management Group / Subscription / Resource Group / Resource）上的 principal。兩層責任不同、設定介面不同、出事故時的徵兆也不同 — 把兩者寫成同一件事是 Azure 治理最常見的混淆來源。</p>
<h2 id="服務定位">服務定位</h2>
<p>Entra ID 是 <em>Microsoft 自有的 workforce IdP</em>、跟 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a> 是直接競爭者。M365 / Azure-heavy 的組織通常直接用 Entra ID 當主 IdP；Okta-first 的組織可以把 Entra ID 當下游 SP（federation）、也可以雙 IdP 並存、但雙 IdP 的 break-glass 跟 lifecycle 路徑要重新設計。Entra ID 同時承擔 <em>consumer-side 跟 partner-side 的 multi-tenant app</em> 信任、跟 <a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0</a> 在 B2C 場景有交集。</p>
<p>Azure RBAC 是 cloud resource permission engine、跟 <a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a> / <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a> 同層 — 都在解「身份對 cloud resource 能做什麼」。差異在 <em>scope hierarchy</em> — Azure 用 Management Group → Subscription → Resource Group → Resource 四層繼承、AWS 用 account + organization、Google 用 organization → folder → project。Azure RBAC 預期 <em>role assignment 沿 scope 向下繼承</em>、這跟 AWS 在每個 account 重新指派的習慣不一樣、跨雲團隊轉過來常踩到。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本頁、讀者能判斷：</p>
<ol>
<li>哪一段控制屬於 Entra ID（身份）、哪一段屬於 Azure RBAC（resource permission）、不要把兩層當同一件事</li>
<li>Entra ID tenant 的最低稽核需求（Global Admin、App Registration、Conditional Access、Managed Identity）</li>
<li>Azure RBAC 的 scope 設計、Custom Role 跟 PIM 何時必要</li>
<li>Entra ID 控制面事故的降級路徑、跟 Azure RBAC 出事的徵兆差異</li>
</ol>
<h2 id="最短判讀路徑">最短判讀路徑</h2>
<p>判斷 Azure 雙層體系是否健康、要分兩層各看兩件事、跟「日常操作與決策形狀」段的兩層結構對齊。</p>
<p><strong>Entra ID 層</strong>（身份控制面）：</p>
<ul>
<li><strong>誰能做什麼</strong>：Global Admin / Privileged Role Administrator 的人數、是否走 <a href="#%e9%80%b2%e9%9a%8e%e4%b8%bb%e9%a1%8c">PIM</a> just-in-time、Conditional Access 是否強制 <a href="/blog/backend/knowledge-cards/authentication/" data-link-title="Authentication" data-link-desc="說明系統如何確認呼叫者身份">phishing-resistant 認證</a>、break-glass 帳號是否 <em>exclude</em> 自所有 CA policy 又單獨監控</li>
<li><strong>入口如何暴露</strong>：App Registration 是否限定 single-tenant、multi-tenant app 的 admin consent 流程是否經審查、Managed Identity 是否取代 service principal client secret</li>
</ul>
<p><strong>Azure RBAC 層</strong>（resource permission）：</p>
<ul>
<li><strong>誰能對 resource 做什麼</strong>：Owner / Contributor 在哪個 scope（Management Group 還是 Subscription）、production 環境是否用 Custom Role 收緊權限、有沒有 standing assignment 該改 PIM</li>
<li><strong>證據是否可回查</strong>：Entra ID Sign-in Log / Audit Log 是否同步到 SIEM、Azure Activity Log 是否設保留與 alert、admin consent / role assignment 變更是否觸發 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a></li>
</ul>
<p>兩層任一邊任一條缺失、就是 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 與 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">Authorization</a> 邊界的待補項目。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="entra-id-層">Entra ID 層</h3>
<p><strong>User / Group / lifecycle</strong>：HRIS 推 SCIM 進 Entra ID、Entra ID 同步到下游 SaaS 跟 Azure RBAC group。決策點是 <em>source of truth</em> — 多數組織把 HRIS 設為人員來源、Entra ID 當分發層、避免雙寫造成 stale account。</p>
<p><strong>Conditional Access 是 MFA <em>主要強制機制</em></strong>：MFA 不是設在 user 屬性上、是 Conditional Access policy 在登入時判斷 user / device / location / app / risk 後觸發。常見設定錯誤包含 <em>exclude legacy auth 沒做、break-glass 規則太寬、emergency access 帳號沒獨立監控</em>。Conditional Access 規則設計錯、就是高權限 bypass 的入口。</p>
<p><strong>App Registration vs Enterprise Application</strong>：開發者註冊 multi-tenant app 走 <em>App Registration</em>（app 的定義）、組織 admin 為某 app 設定 SAML SSO / admin consent 走 <em>Enterprise Application</em>（該 tenant 對 app 的信任）。兩者常被混講、但安全意義不同 — App Registration 是「我們做了一個 app」、Enterprise Application 是「我們信任這個 app 用我們的身份」。Consent phishing 攻擊就是針對後者。</p>
<p><strong>Managed Identity</strong>：Azure resource（VM、Function、AKS pod）自帶身份、不需要 service principal client secret、跟 <a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Workload Identity Federation</a> 同概念但 Azure-internal。System-assigned 跟 resource 生命週期綁定、resource 刪掉 identity 跟著刪；User-assigned 獨立、可跨 resource 共用。production 環境的服務存取 Key Vault / Storage 應走 Managed Identity、不該用 client secret。</p>
<p><strong>Workload Identity Federation</strong>：Entra ID 可以 <em>trust 外部 OIDC issuer</em>（GitHub Actions、AWS、Google）、讓外部 workload 直接拿 Entra ID token、不用儲存 client secret。CI/CD 的 OIDC 整合是這層的主用例、比把 client secret 塞進 CI variable 安全很多。</p>
<p><strong>Signing key 是 control plane 託管</strong>：Entra ID 不暴露 signing key、客戶沒有 rotate 它的能力。這層信任邊界一旦失守、客戶側 <em>直接修不了</em>、要等供應商發 patch 或公告 — Storm-0558 揭示了這條依賴的代價。客戶側能做的補強是 <em>下游檢查</em> 而非 <em>上游修復</em>：</p>
<ul>
<li>訂閱 Microsoft Security Advisory（MSRC）+ tenant-specific notification、讓事件公告第一時間進 IR pipeline、不要靠新聞才知道</li>
<li>SIEM alert <em>anomalous token issuance pattern</em>（跨租戶 token 在 Exchange / Graph API 出現異常存取序列）、不能只信 token signature valid</li>
<li>高敏 app 的 token validation 不只看 Entra ID 標準驗證、加 <em>issuer + tenant + audience + nonce</em> 多層比對、攻擊者偽造跨租戶 token 時可能漏掉某層</li>
<li>Conditional Access 配 <em>token protection</em>（token binding to device）、降低 stolen token replay 的命中率</li>
<li>IR playbook 預設 <em>signing key 事件</em> 一條 — 一旦供應商公告、強制 sign-out 高權限 user、token TTL 收短、回頭看 90 天 sign-in log 找異常</li>
</ul>
<h3 id="azure-rbac-層">Azure RBAC 層</h3>
<p><strong>Scope 設計</strong>：role assignment 沿 Management Group → Subscription → Resource Group → Resource 向下繼承。在 Management Group 給 Contributor、底下所有 subscription / RG / resource 都繼承 — 這既是優點（統一治理）也是風險（誤指派擴散範圍大）。設計原則是 <em>指派盡量低、不要對全 Management Group 給 Contributor</em>。</p>
<p><strong>Built-in role vs Custom Role</strong>：Owner（含 user access admin）/ Contributor（不含權限管理）/ Reader 是 built-in、通常太粗。production 環境需要 Custom Role 把 <code>Microsoft.Storage/storageAccounts/listKeys/action</code> 之類的高風險 action 收掉、只留 read。Custom Role 是 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">least privilege</a> 在 Azure 的落實工具、不做就是用 Contributor 當預設、權限過寬。</p>
<p><strong>Privileged Identity Management（PIM）</strong>：高權限角色（Global Admin、Subscription Owner、User Access Administrator）應走 just-in-time activation、需要 MFA 跟 approval、不該 permanent assignment。沒上 PIM 的組織通常會發現 <em>standing Global Admin 超過 10 個</em>、那是 phishing / token theft 的高價值靶。</p>
<p><strong>Service principal vs Managed Identity</strong>：service principal 是 app 在 Entra ID 的代表、可以用 client secret 或 certificate 認證；Managed Identity 是 service principal 的特殊形式、由 Azure 自動管 credential。能用 Managed Identity 就不用 service principal client secret — 後者要自己 rotate、要存 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">secret management</a>、容易 stale。</p>
<p><strong>Azure Policy 是 RBAC 的補位</strong>：RBAC 管 <em>principal 能不能對 resource 做這個 action</em>、Azure Policy 管 <em>允不允許這樣設定 resource</em>（例如 storage account 強制加密、VM 只能用認可的 image）。RBAC 給 Contributor 的人可以建 storage account、但 Azure Policy 可以拒絕未加密的 storage account 建立 — 兩層互補、缺一不可。</p>
<h2 id="核心取捨表">核心取捨表</h2>
<p>Azure 雙層體系的取捨要分開看 — 一張表回答 <em>cloud resource permission 該選哪家</em>（Azure RBAC vs AWS IAM vs Google IAM）、一張表回答 <em>workforce IdP 該選哪家</em>（Entra ID vs Okta）。兩個決策獨立、可以混搭（例如：Okta 當 workforce IdP + federate 到 Entra ID + 走 Azure RBAC 管 Azure resource）。</p>
<h3 id="azure-rbac-vs-aws-iam-vs-google-cloud-iam">Azure RBAC vs AWS IAM vs Google Cloud IAM</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Azure RBAC</th>
          <th>AWS IAM</th>
          <th>Google Cloud IAM</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Scope</td>
          <td>Management Group → Subscription → RG → Resource</td>
          <td>Account + Organization、policy attach</td>
          <td>Organization → Folder → Project</td>
      </tr>
      <tr>
          <td>繼承模型</td>
          <td>scope 向下繼承</td>
          <td>account boundary 強、跨 account 用 assume role</td>
          <td>scope 向下繼承、condition 強</td>
      </tr>
      <tr>
          <td>自訂角色</td>
          <td>Custom Role（JSON）</td>
          <td>Custom managed policy（JSON）</td>
          <td>Custom Role（YAML / API）</td>
      </tr>
      <tr>
          <td>JIT 機制</td>
          <td>Privileged Identity Management（PIM）內建</td>
          <td>無原生 JIT、要靠 IAM Identity Center / 第三方</td>
          <td>無原生 JIT、要靠 third-party / 自建</td>
      </tr>
      <tr>
          <td>Workload</td>
          <td>Managed Identity（內部）+ Workload Identity Fed</td>
          <td>IAM role + OIDC trust</td>
          <td>Workload Identity Federation</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>Azure-heavy、M365 整合</td>
          <td>AWS-heavy、account isolation 模型成熟</td>
          <td>GCP-heavy、resource hierarchy 治理</td>
      </tr>
  </tbody>
</table>
<h3 id="entra-id-vs-oktaworkforce-idp">Entra ID vs Okta（workforce IdP）</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Entra ID</th>
          <th>Okta</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主場</td>
          <td>M365 / Azure 原生、跟 RBAC 共生</td>
          <td>多雲 + SaaS、跨平台 SSO</td>
      </tr>
      <tr>
          <td>MFA 機制</td>
          <td>Conditional Access 觸發、Authenticator app / FIDO2</td>
          <td>Sign-On / Authentication Policy、多 factor 選擇</td>
      </tr>
      <tr>
          <td>Lifecycle</td>
          <td>SCIM + cross-tenant sync</td>
          <td>SCIM + Lifecycle Management、整合更廣</td>
      </tr>
      <tr>
          <td>Workload</td>
          <td>Managed Identity / Workload Identity Federation</td>
          <td>較弱、CI 通常 federate 到雲 IAM</td>
      </tr>
      <tr>
          <td>整合廣度</td>
          <td>M365 / Azure / Office app 深、外部 SaaS 比 Okta 少</td>
          <td>7000+ SaaS app 預建</td>
      </tr>
      <tr>
          <td>第三方風險</td>
          <td>Microsoft 控制面（Storm-0558、Midnight Blizzard）</td>
          <td>Okta 控制面（2022 / 2023 多起）</td>
      </tr>
  </tbody>
</table>
<p>選 Entra ID 的核心訴求：<em>M365 / Azure 重度使用、要跟 RBAC + Managed Identity 直接整合、能接受 Microsoft 控制面風險</em>；選 Okta 的核心訴求看 <a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta vendor 頁</a>。</p>
<h2 id="進階主題">進階主題</h2>
<p><strong>Conditional Access 進階規則</strong>：除了 user / device / location 基本條件、進階場景包含 <em>risk-based</em>（Identity Protection 給的 user risk / sign-in risk）、<em>token protection</em>（token binding 到 device、防止 token replay）、<em>authentication strength</em>（強制 phishing-resistant factor）。production tenant 至少要有「Global Admin 必須走 phishing-resistant + compliant device」這條規則。</p>
<p><strong>Privileged Identity Management（PIM）的設計細節</strong>：activation 要求 MFA、approval（高權限角色）、justification、時限（預設 8 小時、最長 24）。Access Review 是 PIM 的配套 — 季度檢視 standing assignment 是否還需要、不需要的撤掉。沒做 Access Review 的 PIM 等於只把問題從 standing 推到 <em>誰申請就給</em> — 不是 least privilege。</p>
<p><strong>Workload Identity Federation 跨雲</strong>：Entra ID 可以 trust GitHub Actions / GitLab / AWS / Google 的 OIDC issuer、讓 CI 直接拿 Azure token。同向也成立 — Azure workload 可以拿 Google ID token federate 進 GCP。多雲 CI 不該存任何 client secret、走 federation 比較安全。</p>
<p><strong>Custom Role 設計實務</strong>：用 <code>Microsoft.Authorization/roleDefinitions</code> API 或 portal 定義、<code>actions</code> / <code>notActions</code> / <code>dataActions</code> 各自獨立 — <code>actions</code> 是 control plane、<code>dataActions</code> 是 data plane（讀寫 blob、key vault secret 內容）。常見錯誤是只收 <code>actions</code> 沒收 <code>dataActions</code>、結果 storage account 設定改不了但 blob 內容隨便讀。</p>
<p><strong>Azure Policy 跟 Initiative</strong>：Policy 是單一規則、Initiative 是 policy 的集合（用來組 baseline、例如 CIS、ISO 27001）。Policy effect 有 audit / deny / deployIfNotExists、後者可以自動補洞（例如自動加 diagnostic setting）。RBAC + Policy 一起設計才是完整的 <a href="/blog/backend/knowledge-cards/authorization/" data-link-title="Authorization" data-link-desc="說明授權如何判斷誰能對哪些資源執行哪些操作">Authorization</a> 邊界。</p>
<h2 id="排錯與失敗快速判讀">排錯與失敗快速判讀</h2>
<ul>
<li><strong>Global Admin 過多</strong>：standing Global Admin 超過 5 個就要警惕 — 上 PIM、把日常運維改用 Privileged Role Administrator + 特定 admin role group</li>
<li><strong>Conditional Access 規則漏 legacy auth</strong>：規則只 cover modern auth、IMAP / POP / SMTP 等 legacy protocol 不走 CA — 用「Block legacy authentication」baseline policy 補</li>
<li><strong>App Registration / Enterprise Application admin consent 沒審查</strong>：使用者自己 consent 把 mail.read 給三方 app、變 consent phishing 入口 — 關閉 user consent、改 admin consent workflow</li>
<li><strong>Service principal client secret 散落</strong>：CI / 服務裡有大量 client secret、rotate 沒節奏 — 改 Managed Identity（內部）或 Workload Identity Federation（跨雲 CI）</li>
<li><strong>Subscription Owner 太多</strong>：subscription 級 Owner 是高風險、應該收到 Management Group 級 Reader + 必要時 PIM activate Owner</li>
<li><strong>Azure Activity Log 沒進 SIEM</strong>：role assignment 變更、Key Vault access policy 變更只在 Azure portal 看得到、沒 alert — 用 Diagnostic Setting 推 Event Hub / Log Analytics、再進 SIEM</li>
<li><strong>Break-glass 帳號 exclude 自所有 CA policy、但沒監控</strong>：emergency access 帳號不能被 CA 鎖、但 <em>任何登入都該 alert</em> — 配對 Sign-in Log alert + 季度驗證可用</li>
</ul>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS-only 環境</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a></td>
      </tr>
      <tr>
          <td>GCP-only 環境</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a></td>
      </tr>
      <tr>
          <td>多雲 + 大量 SaaS、IdP 中心化</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a></td>
      </tr>
      <tr>
          <td>Customer / B2C identity</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/auth0/" data-link-title="Auth0" data-link-desc="B2C / B2B Customer Identity Provider、Universal Login、Action / Rule hook、屬 Okta 旗下 Customer Identity Cloud">Auth0</a></td>
      </tr>
      <tr>
          <td>自管 IdP / 不接受 SaaS</td>
          <td><a href="/blog/backend/07-security-data-protection/vendors/keycloak/" data-link-title="Keycloak" data-link-desc="Open source self-hosted Identity Provider、Red Hat 主導、Realm-based multi-tenancy、適合資料主權與自訂 flow 需求">Keycloak</a></td>
      </tr>
      <tr>
          <td>Secret / Key 管理</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>（Azure Key Vault vendor 頁 S2 批次撰寫中）</td>
      </tr>
      <tr>
          <td>偵測訊號（不只 Entra ID 內部）</td>
          <td>07 SIEM 章節、04 observability</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Entra ID 完整 SAML / OIDC / SCIM 規格細節</li>
<li>Azure RBAC built-in role 完整清單與 action 對照</li>
<li>Conditional Access policy template 細節</li>
<li>Azure Policy 內建 initiative 完整清單</li>
<li>Microsoft 365 / Defender for Identity 等周邊產品</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 Entra ID / Azure RBAC 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><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 2021</a></td>
          <td>Entra ID 控制面故障外溢到 Teams / SharePoint / Exchange、業務必須有降級與切換策略、不能完全依賴單一 IdP 可用性</td>
      </tr>
      <tr>
          <td><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 Signing Key 2023</a></td>
          <td>signing key 治理失效會跨租戶影響 token 驗證信任、客戶側只能等供應商修復（MSRC / CSRB 公開報告補充了 crash dump / Exchange Online 等具體外洩路徑、屬 case 檔之外的歷史 reference）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/identity-access/microsoft-storm-0558-2023-signing-key-chain/" data-link-title="7.R7.1.5 Microsoft Storm-0558 2023：簽章金鑰鏈與郵件存取" data-link-desc="從簽章金鑰保護失效到雲端郵件存取，拆解身分信任鏈的關鍵控制點">Microsoft Storm-0558 Signing Key Chain (red-team)</a></td>
          <td>HSM-bound key 是 control plane 必要前提、跨租戶 token 異常要立即升級、不能等供應商先公告</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/cases/failure-credential-rotation-without-scope/" data-link-title="7.C9 反例：憑證輪替未分 Scope" data-link-desc="憑證輪替若未分域分批，容易造成跨系統連鎖中斷。">Failure: Credential Rotation Without Scope</a></td>
          <td>Entra ID app secret 跟 Managed Identity 的 rotation 分域、不該把 service principal client secret 跟 user password 混在同一個 rotation policy</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li>平行：<a href="/blog/backend/07-security-data-protection/vendors/aws-iam/" data-link-title="AWS IAM" data-link-desc="AWS cloud resource permission engine、Role / Policy / STS、跨帳號信任邊界與 OIDC federation 的核心">AWS IAM</a>、<a href="/blog/backend/07-security-data-protection/vendors/google-cloud-iam/" data-link-title="Google Cloud IAM" data-link-desc="GCP cloud resource permission engine、Role Binding / Service Account / Workload Identity Federation、resource hierarchy 為核心的權限治理">Google Cloud IAM</a>、<a href="/blog/backend/07-security-data-protection/vendors/okta/" data-link-title="Okta" data-link-desc="SaaS Identity Provider 主流選項、SSO / MFA / lifecycle 整合、第三方信任邊界的代價">Okta</a></li>
<li>下游：<a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>（Entra ID / Managed Identity 之後的 secret / key 層、Azure Key Vendor 個別 vendor 頁 S2 批次撰寫中）</li>
<li>跨模組：<a href="/blog/backend/08-incident-response/vendors/" data-link-title="事故處理 Vendor 清單" data-link-desc="規劃 on-call、incident response、status page 與 postmortem 工具的服務頁撰寫順序與判準">8 事故處理 vendor 清單</a>（Entra ID / Azure 事件如何 routing 進 IR 流程）</li>
<li>官方：<a href="https://learn.microsoft.com/entra/">Microsoft Entra Documentation</a>、<a href="https://learn.microsoft.com/azure/role-based-access-control/">Azure RBAC Documentation</a></li>
</ul>
]]></content:encoded></item><item><title>IAM（Identity and Access Management）</title><link>https://tarrragon.github.io/blog/infra/knowledge-cards/iam/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/knowledge-cards/iam/</guid><description>&lt;p>IAM（Identity and Access Management）是雲端平台用來回答「某個身分能不能對某個資源做某件事」的授權系統。它把授權拆成三個獨立的元件：identity（身分，發起動作的主體）、policy（政策，描述「允許或拒絕對哪些資源做哪些動作」的規則）、role（角色，一組可以被臨時取得的權限集合）。這三者的分工是後面所有憑證決策的前提。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>IAM 是&lt;a href="https://tarrragon.github.io/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 的環境隔離">模組二：身分與憑證地基&lt;/a>的核心機制。它決定了誰能動什麼——人、服務、CI pipeline 各拿剛好夠用的權限（最小權限），憑證有明確的生命週期。身分層失守的代價在五個 infra 責任面向中最高，因為它是其他所有資源的閘門。&lt;/p>
&lt;p>在 infra 系列中，IAM 的設計從三個維度展開：最小權限的持續收斂（不是一次設定就結束）、用 &lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/oidc/" data-link-title="OIDC 聯合" data-link-desc="讓 CI/CD 平台用短期 token 取代長期 access key 存取雲端資源的身分聯合機制">OIDC&lt;/a> 短期憑證取代長期 access key、以及跨帳號的權限邊界（SCP + Permissions Boundary）。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>IAM 需要關注的訊號：某個 role 的 policy 有 &lt;code>*:*&lt;/code> 或 &lt;code>AdministratorAccess&lt;/code>（權限過大）；credential report 顯示有長期 access key 超過 90 天未輪替（憑證散落風險）；Access Analyzer 顯示某個 role 的實際使用 action 遠少於授予的 action（權限擴散）；dev 環境的 CI role 能列出 production 的資源（環境隔離失效）。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>IAM 設計時要決定：&lt;/p>
&lt;ul>
&lt;li>身分類型區分：人用 SSO 登入（強制 MFA）、雲上服務用 instance profile / task role、雲外 CI 用 OIDC 聯合&lt;/li>
&lt;li>權限分級：admin / operator / viewer 三級，見&lt;a href="https://tarrragon.github.io/blog/infra/02-identity-credentials/team-access-management/" data-link-title="團隊權限分級與存取管理" data-link-desc="用 admin / operator / viewer 三級劃分團隊成員的雲端操作權限，設計臨時提權流程、定期 access review 節奏，以及 contractor 與外部 vendor 的存取邊界">團隊權限分級&lt;/a>&lt;/li>
&lt;li>環境隔離：每個環境的 role 不能存取其他環境的資源&lt;/li>
&lt;li>收斂節奏：定期用 Access Analyzer 觀察實際使用的 action，收掉沒用到的權限&lt;/li>
&lt;/ul>
&lt;h2 id="鄰卡">鄰卡&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/oidc/" data-link-title="OIDC 聯合" data-link-desc="讓 CI/CD 平台用短期 token 取代長期 access key 存取雲端資源的身分聯合機制">OIDC&lt;/a> — 用短期 token 取代長期 access key 的聯合機制&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> — 網路層的存取控制（IAM 是 API 層的存取控制）&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/infra/knowledge-cards/cloudtrail/" data-link-title="CloudTrail" data-link-desc="AWS 的 API 層稽核日誌服務，記錄誰在什麼時候對什麼資源做了什麼操作">CloudTrail&lt;/a> — 記錄 IAM 身分的 API 呼叫歷史&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>IAM（Identity and Access Management）是雲端平台用來回答「某個身分能不能對某個資源做某件事」的授權系統。它把授權拆成三個獨立的元件：identity（身分，發起動作的主體）、policy（政策，描述「允許或拒絕對哪些資源做哪些動作」的規則）、role（角色，一組可以被臨時取得的權限集合）。這三者的分工是後面所有憑證決策的前提。</p>
<h2 id="概念位置">概念位置</h2>
<p>IAM 是<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>的核心機制。它決定了誰能動什麼——人、服務、CI pipeline 各拿剛好夠用的權限（最小權限），憑證有明確的生命週期。身分層失守的代價在五個 infra 責任面向中最高，因為它是其他所有資源的閘門。</p>
<p>在 infra 系列中，IAM 的設計從三個維度展開：最小權限的持續收斂（不是一次設定就結束）、用 <a href="/blog/infra/knowledge-cards/oidc/" data-link-title="OIDC 聯合" data-link-desc="讓 CI/CD 平台用短期 token 取代長期 access key 存取雲端資源的身分聯合機制">OIDC</a> 短期憑證取代長期 access key、以及跨帳號的權限邊界（SCP + Permissions Boundary）。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>IAM 需要關注的訊號：某個 role 的 policy 有 <code>*:*</code> 或 <code>AdministratorAccess</code>（權限過大）；credential report 顯示有長期 access key 超過 90 天未輪替（憑證散落風險）；Access Analyzer 顯示某個 role 的實際使用 action 遠少於授予的 action（權限擴散）；dev 環境的 CI role 能列出 production 的資源（環境隔離失效）。</p>
<h2 id="設計責任">設計責任</h2>
<p>IAM 設計時要決定：</p>
<ul>
<li>身分類型區分：人用 SSO 登入（強制 MFA）、雲上服務用 instance profile / task role、雲外 CI 用 OIDC 聯合</li>
<li>權限分級：admin / operator / viewer 三級，見<a href="/blog/infra/02-identity-credentials/team-access-management/" data-link-title="團隊權限分級與存取管理" data-link-desc="用 admin / operator / viewer 三級劃分團隊成員的雲端操作權限，設計臨時提權流程、定期 access review 節奏，以及 contractor 與外部 vendor 的存取邊界">團隊權限分級</a></li>
<li>環境隔離：每個環境的 role 不能存取其他環境的資源</li>
<li>收斂節奏：定期用 Access Analyzer 觀察實際使用的 action，收掉沒用到的權限</li>
</ul>
<h2 id="鄰卡">鄰卡</h2>
<ul>
<li><a href="/blog/infra/knowledge-cards/oidc/" data-link-title="OIDC 聯合" data-link-desc="讓 CI/CD 平台用短期 token 取代長期 access key 存取雲端資源的身分聯合機制">OIDC</a> — 用短期 token 取代長期 access key 的聯合機制</li>
<li><a href="/blog/infra/knowledge-cards/security-group/" data-link-title="Security Group" data-link-desc="掛在資源網卡層級的有狀態防火牆，逐埠決定哪些來源能連進這個資源">Security Group</a> — 網路層的存取控制（IAM 是 API 層的存取控制）</li>
<li><a href="/blog/infra/knowledge-cards/cloudtrail/" data-link-title="CloudTrail" data-link-desc="AWS 的 API 層稽核日誌服務，記錄誰在什麼時候對什麼資源做了什麼操作">CloudTrail</a> — 記錄 IAM 身分的 API 呼叫歷史</li>
</ul>
]]></content:encoded></item></channel></rss>