<?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>Least Privilege Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/least-privilege/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/least-privilege/</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>Least Privilege Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/least-privilege/</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>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>IAM Roles vs Policies: How Cloud Authorization Actually Works</title>
		<link>https://linuxcent.com/iam-roles-policies-permissions-explained/</link>
					<comments>https://linuxcent.com/iam-roles-policies-permissions-explained/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Tue, 14 Apr 2026 05:05:54 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[ABAC]]></category>
		<category><![CDATA[AWS IAM Policy]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[IAM Policies]]></category>
		<category><![CDATA[IAM Roles]]></category>
		<category><![CDATA[Least Privilege]]></category>
		<category><![CDATA[RBAC]]></category>
		<guid isPermaLink="false">https://linuxcent.com/iam-roles-policies-permissions-explained/</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>A clear breakdown of IAM roles, policies, and permissions — the building blocks of cloud access control in AWS, GCP, and Azure with RBAC and ABAC patterns.</p>
<p>The post <a href="https://linuxcent.com/iam-roles-policies-permissions-explained/">IAM Roles vs Policies: How Cloud Authorization 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"> 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> → <strong>IAM Roles vs Policies</strong> → <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></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Every cloud permission is atomic: one action (<code class="" data-line="">s3:GetObject</code>) on one resource class — the indivisible unit of access</li>
<li>Policies group permissions into documents with conditions; roles carry policies and are assigned to identities</li>
<li>Never attach policies directly to users — roles are the indirection layer that makes access auditable and revocable</li>
<li>AWS roles have <strong>two</strong> required configs: trust policy (who can assume) + permission policy (what they can do) — both must be right</li>
<li>GCP binds roles to resources; AWS attaches policies to identities — the mental models run in <strong>opposite directions</strong></li>
<li><code class="" data-line="">iam:PassRole</code> in AWS and <code class="" data-line="">iam.serviceAccounts.actAs</code> in GCP are privilege escalation vectors — always scope to specific ARNs, never <code class="" data-line="">*</code></li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<p>Three primitives underlie every cloud IAM system. Learn how they connect and any cloud access model becomes readable.</p>
<pre><code class="" data-line="">  THE THREE-LAYER STACK
  Build bottom-up. Assign top-down. Change one layer without touching the others.

  ┌──────────────────────────────────────────────────────────────────────┐
  │  LAYER 3 — IDENTITY                                                  │
  │  alice@company.com  ·  backend-service  ·  ci-runner@proj           │
  │  &quot;who is acting — a human, a service, or a machine&quot;                 │
  ├──────────────────────────────────────────────────────────────────────┤
  │  LAYER 2 — ROLE                                                      │
  │  BackendDeveloper  ·  DataAnalyst  ·  DeployBot  ·  S3ReadOnly      │
  │  &quot;what function does this identity serve — the job title&quot;           │
  ├──────────────────────────────────────────────────────────────────────┤
  │  LAYER 1 — POLICY                                                    │
  │  AllowS3Read  ·  AllowECRPush  ·  DenyProdDelete  ·  RequireMFA    │
  │  &quot;what is explicitly permitted or denied, under what conditions&quot;    │
  ├──────────────────────────────────────────────────────────────────────┤
  │  LAYER 0 — PERMISSION                                                │
  │  s3:GetObject  ·  ecr:PutImage  ·  s3:DeleteObject  ·  iam:PassRole│
  │  &quot;one verb on one class of resource — the atom of access control&quot;  │
  └──────────────────────────────────────────────────────────────────────┘

  When alice joins the backend team → assign her the BackendDeveloper role
  When the S3 bucket changes → update the policy once; alice gets it automatically
  When alice leaves → remove the role assignment; policy and permissions are untouched
</code></pre>
<p>If this maps better to something physical:</p>
<pre><code class="" data-line="">  PHYSICAL WORLD            →    CLOUD IAM

  A specific door rule           Permission      s3:GetObject
  Keycard access profile    →    Policy          AllowS3Read
  Job title                 →    Role            BackendDeveloper
  The employee              →    Identity        alice@company.com

  When the employee leaves: revoke the role assignment.
  The job title, the keycard profile, the door rules — all unchanged.
  Next hire gets the same role. Same access. No manual work.
</code></pre>
<hr />
<h2 id="introduction">Introduction</h2>
<p>IAM roles vs policies is a distinction that defines how cloud authorization actually works — and getting it wrong is how access sprawl starts. Every <a href="/authentication-vs-authorization-iam/">authentication vs authorization</a> failure at the authorization layer traces back to how these three primitives are — or aren&#8217;t — structured.</p>
<p>Every cloud IAM system — AWS, GCP, Azure — is built on the same three primitives: permissions, policies, and roles. Learn these well and any cloud provider becomes readable. Skip them and you spend years pattern-matching without understanding why anything is structured the way it is.</p>
<p><a href="/what-is-cloud-iam/">What Is Cloud IAM</a> established the foundation: IAM is the system that governs who can access what in cloud infrastructure, and its default answer is always deny. <a href="/authentication-vs-authorization-iam/">Authentication vs Authorization: AWS AccessDenied Explained</a> drew the line between authentication — proving identity — and authorization — proving you&#8217;re allowed to act. This episode is about the authorization layer specifically. These three building blocks are how authorization is expressed in practice.</p>
<p>Before walking through each one, here&#8217;s what access control looks like without any of this structure — because that&#8217;s the fastest way to understand why the layers exist.</p>
<p>In 2015 I inherited an AWS account from a 12-engineer team that had been building for 18 months. When I ran <code class="" data-line="">aws iam list-attached-user-policies</code> across the 23 users, 17 had policies attached directly to the user object — not to groups, not to roles.</p>
<p>One engineer had left six months earlier. His access key was still active. Three policies still attached: read access to prod S3, write to a DynamoDB table, ability to invoke Lambda functions. When I asked what the DynamoDB table was for, nobody could tell me. The Lambda functions no longer existed.</p>
<p>That account wasn&#8217;t built by negligent engineers. It was built by engineers reaching for whatever granted access fastest, under deadline, without a framework. Permissions scattered. Nothing tracked. Nothing removed.</p>
<p>Roles, policies, and permissions are the framework that prevents that. Understanding them is the difference between an IAM configuration you can audit in an afternoon and one that takes a week and still leaves you uncertain.</p>
<hr />
<h2 id="what-are-iam-permissions-the-atomic-unit-of-access-control">What Are IAM Permissions? The Atomic Unit of Access Control</h2>
<p>A <strong>permission</strong> is a single action on a class of resources. It is the most granular thing you can grant or deny — the atom of access control.</p>
<p>Cloud providers express permissions differently, but the structure is consistent: a service, a resource type, and an action verb.</p>
<pre><code class="" data-line=""># AWS: service:Action
s3:GetObject               # read an object from S3
ec2:StartInstances         # start EC2 instances
iam:PassRole               # assign a role to an AWS service — one of the most dangerous
kms:Decrypt                # use a KMS key to decrypt

# GCP: service.resource.verb
storage.objects.get
compute.instances.start
iam.serviceAccounts.actAs  # impersonate a service account — equivalent risk to iam:PassRole
cloudkms.cryptoKeyVersions.useToDecrypt

# Azure: Provider/ResourceType/Action
Microsoft.Storage/storageAccounts/blobServices/containers/read
Microsoft.Compute/virtualMachines/start/action
Microsoft.Authorization/roleAssignments/write   # grant roles — highest risk
Microsoft.KeyVault/vaults/secrets/getSecret/action
</code></pre>
<p>You generally don&#8217;t assign individual permissions directly to identities — that&#8217;s like handing someone 47 keys with no labels and expecting the system to remain auditable. Permissions are grouped into policies.</p>
<hr />
<h2 id="what-are-iam-policies-grouping-permissions-with-conditions">What Are IAM Policies? Grouping Permissions with Conditions</h2>
<p>A <strong>policy</strong> is a document that groups permissions and defines the conditions under which they apply.</p>
<h3 id="aws-policy-structure">AWS policy structure</h3>
<p>An AWS policy document is JSON. Every field is a deliberate decision:</p>
<pre><code class="" data-line="">{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Sid&quot;: &quot;AllowReadS3Backups&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:::company-backups&quot;,
        &quot;arn:aws:s3:::company-backups/*&quot;
      ],
      &quot;Condition&quot;: {
        &quot;StringEquals&quot;: { &quot;s3:prefix&quot;: [&quot;2024/&quot;, &quot;2025/&quot;] }
      }
    },
    {
      &quot;Sid&quot;: &quot;DenyDeleteEverywhere&quot;,
      &quot;Effect&quot;: &quot;Deny&quot;,
      &quot;Action&quot;: &quot;s3:DeleteObject&quot;,
      &quot;Resource&quot;: &quot;*&quot;
    }
  ]
}
</code></pre>
<p>The <code class="" data-line="">Sid</code> is a comment — use it. <code class="" data-line="">AllowReadS3Backups</code> tells a future auditor why this statement exists. <code class="" data-line="">Statement1</code> is technical debt.</p>
<p>The <code class="" data-line="">Effect</code> is either <code class="" data-line="">Allow</code> or <code class="" data-line="">Deny</code>. A <code class="" data-line="">Deny</code> always wins — it cannot be overridden by any <code class="" data-line="">Allow</code> anywhere in any policy on the same identity. If you have a <code class="" data-line="">Deny</code> on <code class="" data-line="">s3:DeleteObject</code> with <code class="" data-line="">&quot;Resource&quot;: &quot;*&quot;</code>, nothing can grant delete access to that identity. This asymmetry is deliberate: it&#8217;s how guardrails work.</p>
<p>The <code class="" data-line="">Resource</code> field is where access most often creeps wider than intended. <code class="" data-line="">&quot;Resource&quot;: &quot;*&quot;</code> on a write action means &#8220;every resource of this type in the account.&#8221; It works. It outlives the context that made it feel reasonable.</p>
<h3 id="aws-policy-types-which-to-reach-for">AWS policy types — which to reach for</h3>
<pre><code class="" data-line="">┌──────────────────────────┬────────────────────────────┬────────────────────────────┐
│ Type                     │ Attached to                │ What it does               │
├──────────────────────────┼────────────────────────────┼────────────────────────────┤
│ Identity-based           │ User, Group, Role          │ What the identity can do   │
│ Resource-based           │ S3 bucket, KMS key, Lambda │ Who can touch this resource │
│ Permissions boundary     │ User or Role               │ Maximum possible — ceiling  │
│ Service Control Policy   │ AWS Org OU or Account      │ Org-level guardrail         │
│ Session policy           │ AssumeRole session         │ Restricts a specific session│
│ Resource Control Policy  │ AWS Org resources          │ Resource-level org guardrail│
└──────────────────────────┴────────────────────────────┴────────────────────────────┘
</code></pre>
<p>Critical: <strong>Permissions boundaries and SCPs do not grant permissions</strong>. They constrain them. A boundary that allows <code class="" data-line="">s3:*</code> doesn&#8217;t mean the identity has S3 access. It means the identity <em>can have at most</em> S3 access, if an identity-based policy actually grants it. Many engineers set a boundary and expect it to work as a grant. It doesn&#8217;t.</p>
<h3 id="gcp-policy-bindings">GCP policy bindings</h3>
<p>GCP doesn&#8217;t attach policy documents to identities. Each resource has an IAM policy — a set of <strong>bindings</strong> mapping roles to members:</p>
<pre><code class="" data-line="">{
  &quot;bindings&quot;: [
    {
      &quot;role&quot;: &quot;roles/storage.objectViewer&quot;,
      &quot;members&quot;: [
        &quot;user:alice@company.com&quot;,
        &quot;serviceAccount:app-backend@project.iam.gserviceaccount.com&quot;
      ]
    },
    {
      &quot;role&quot;: &quot;roles/storage.objectCreator&quot;,
      &quot;members&quot;: [&quot;serviceAccount:upload-service@project.iam.gserviceaccount.com&quot;],
      &quot;condition&quot;: {
        &quot;title&quot;: &quot;Business hours only&quot;,
        &quot;expression&quot;: &quot;request.time.getHours(&#039;America/New_York&#039;) &gt;= 9 &amp;&amp; request.time.getHours(&#039;America/New_York&#039;) &lt; 18&quot;
      }
    }
  ]
}
</code></pre>
<p>The mental model shift: in AWS you ask &#8220;what can this identity do?&#8221; by looking at the identity. In GCP you ask &#8220;who can access this resource?&#8221; by looking at the resource. The question runs in the opposite direction.</p>
<h3 id="azure-role-definitions">Azure role definitions</h3>
<p>Azure separates what a role grants (role definition) from who gets it where (role assignment). Define once, assign at multiple scopes.</p>
<pre><code class="" data-line="">{
  &quot;Name&quot;: &quot;Custom Storage Reader&quot;,
  &quot;IsCustom&quot;: true,
  &quot;Actions&quot;: [
    &quot;Microsoft.Storage/storageAccounts/blobServices/containers/read&quot;,
    &quot;Microsoft.Storage/storageAccounts/blobServices/generateUserDelegationKey/action&quot;
  ],
  &quot;DataActions&quot;: [
    &quot;Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read&quot;
  ],
  &quot;AssignableScopes&quot;: [&quot;/subscriptions/SUB_ID&quot;]
}
</code></pre>
<p><code class="" data-line="">Actions</code> vs <code class="" data-line="">DataActions</code> catches people. <code class="" data-line="">Actions</code> are control plane — you can see the storage account exists. <code class="" data-line="">DataActions</code> are data plane — you can read actual blob contents. A user with <code class="" data-line="">Actions</code> can list the container but cannot read a single byte without a <code class="" data-line="">DataAction</code>. Both planes must be covered for the access to be complete.</p>
<hr />
<h2 id="what-are-iam-roles-the-layer-that-scales-access-control">What Are IAM Roles? The Layer That Scales Access Control</h2>
<p>A <strong>role</strong> is a collection of policies assigned to identities. It&#8217;s the indirection layer that makes access manageable at scale.</p>
<p>Going back to the 2015 account: the problem wasn&#8217;t that engineers had access — they needed it. The problem was that access was scattered across 23 individual user objects with no shared structure. This is what <a href="/what-is-cloud-iam/">what is cloud IAM</a> establishes as the core problem IAM exists to solve. Roles are the structural answer.</p>
<p>The role model solves this:</p>
<pre><code class="" data-line="">Policy: S3ReadAccess (s3:GetObject, s3:ListBucket on s3:::app-data/*)
  ↓ attached to
Role: BackendDeveloper
  ↓ assigned to
Users: alice, bob, charlie, dave (and six more)

When the bucket changes  → update one policy
When someone joins       → assign one role
When someone leaves      → remove one role
Access model stays coherent because it&#039;s structured.
</code></pre>
<h3 id="aws-roles-the-identity-that-issues-temporary-credentials">AWS roles — the identity that issues temporary credentials</h3>
<p>AWS roles are themselves IAM identities, not just permission containers. When something assumes a role, it gets temporary credentials from STS. Two things must be configured:</p>
<p><strong>Trust policy — who can assume:</strong></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;Service&quot;: &quot;ec2.amazonaws.com&quot; },
    &quot;Action&quot;: &quot;sts:AssumeRole&quot;
  }]
}
</code></pre>
<p>Without this, nobody can use the role regardless of its permissions. The trust policy is the gatekeeper.</p>
<p><strong>Permission policy — what it can do:</strong></p>
<pre><code class="" data-line="">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
</code></pre>
<p>When debugging &#8220;why can&#8217;t this Lambda/EC2/ECS task do X?&#8221;, the first thing I check is the trust policy. Many times the permission policy is correct — the service simply isn&#8217;t in the trust policy and cannot assume the role at all.</p>
<h3 id="gcp-role-types">GCP role types</h3>
<pre><code class="" data-line="">┌──────────────────┬──────────────────────────────┬──────────────────────────────────┐
│ Type             │ Example                      │ When to use                      │
├──────────────────┼──────────────────────────────┼──────────────────────────────────┤
│ Basic/Primitive  │ roles/editor, roles/owner    │ Never in production              │
│ Predefined       │ roles/storage.objectViewer   │ Default — service-specific       │
│ Custom           │ Your org defines             │ When predefined is too broad     │
└──────────────────┴──────────────────────────────┴──────────────────────────────────┘
</code></pre>
<p><code class="" data-line="">roles/editor</code> at the project level grants write access to almost every GCP service. I&#8217;ve seen it granted &#8220;temporarily&#8221; and found it attached six months later. Always use predefined roles.</p>
<pre><code class="" data-line=""># Find the right predefined role
gcloud iam roles list --filter=&quot;name:roles/storage&quot; --format=&quot;table(name,title)&quot;

# See exactly what permissions it includes
gcloud iam roles describe roles/storage.objectViewer

# Create a custom role when predefined is still too broad
cat &gt; custom-log-reader.yaml &lt;&lt; &#039;EOF&#039;
title: &quot;Log Reader&quot;
description: &quot;Read application logs — nothing else&quot;
stage: &quot;GA&quot;
includedPermissions:
  - logging.logEntries.list
  - logging.logs.list
  - logging.logMetrics.get
EOF
gcloud iam roles create LogReader --project=my-project --file=custom-log-reader.yaml
</code></pre>
<h3 id="azure-built-in-and-custom-roles">Azure built-in and custom roles</h3>
<pre><code class="" data-line=""># List built-in roles containing &quot;Storage&quot;
az role definition list --output table | grep Storage

# View what a built-in role grants
az role definition list --name &quot;Storage Blob Data Reader&quot;

# Create a custom role
az role definition create --role-definition custom-app-storage.json

# Assign at a specific scope
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
</code></pre>
<hr />
<h2 id="rbac-vs-abac-which-access-control-model-to-use">RBAC vs ABAC: Which Access Control Model to Use</h2>
<h3 id="rbac-role-based-access-control">RBAC — Role-Based Access Control</h3>
<p>The dominant model. Access flows from role membership:</p>
<pre><code class="" data-line="">alice     ∈ BackendDeveloper  →  s3:GetObject on app-data/*
bob       ∈ DataAnalyst       →  athena:* on analytics-queries
ci-runner ∈ DeployRole        →  ecr:PutImage, ecs:UpdateService
</code></pre>
<p>RBAC degrades two ways: <strong>role explosion</strong> (200 roles, nobody can explain what they all do) and <strong>coarse roles</strong> (avoid explosion by making roles broad, now BackendDeveloper has prod access with no distinction from dev). Both look the same on a spreadsheet — lots of access, no clear principle.</p>
<h3 id="abac-attribute-based-access-control">ABAC — Attribute-Based Access Control</h3>
<p>ABAC grants access based on attributes of the principal, resource, or environment — not role membership. This one policy replaced 12 team-specific policies in one account:</p>
<pre><code class="" data-line="">{
  &quot;Effect&quot;: &quot;Allow&quot;,
  &quot;Action&quot;: &quot;ec2:*&quot;,
  &quot;Resource&quot;: &quot;*&quot;,
  &quot;Condition&quot;: {
    &quot;StringEquals&quot;: {
      &quot;aws:ResourceTag/Team&quot;: &quot;${aws:PrincipalTag/Team}&quot;
    }
  }
}
</code></pre>
<p>An engineer tagged <code class="" data-line="">Team=Platform</code> can only act on EC2 resources tagged <code class="" data-line="">Team=Platform</code>. Add a new team — tag their resources and their identity. No new policy. No new role.</p>
<p>The risk is tag drift. If someone tags a resource incorrectly, the access model breaks silently. In practice, I use ABAC for environment and team scoping, and explicit policies for sensitive services like KMS and IAM. How these primitives combine in a full AWS account is covered in the <a href="/aws-iam-deep-dive/">AWS IAM deep dive</a>.</p>
<h3 id="conditions-when-context-determines-access">Conditions — when context determines access</h3>
<pre><code class="" data-line="">// Require MFA for any IAM or Organizations action
{
  &quot;Effect&quot;: &quot;Deny&quot;,
  &quot;Action&quot;: [&quot;iam:*&quot;, &quot;organizations:*&quot;],
  &quot;Resource&quot;: &quot;*&quot;,
  &quot;Condition&quot;: { &quot;BoolIfExists&quot;: { &quot;aws:MultiFactorAuthPresent&quot;: &quot;false&quot; } }
}

// Restrict to corporate IP range
{
  &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;] }
  }
}
</code></pre>
<p>The MFA condition is in every account I manage. A compromised API key without an MFA session can&#8217;t escalate IAM privileges — the Deny blocks it at the condition level. This single statement meaningfully reduces the blast radius of a credential compromise.</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 — Policies attached directly to users                 ║
║                                                                      ║
║  Feels fast. Creates the exact problem from 2015: access scattered  ║
║  across individual user objects with no shared structure.            ║
║  When the user leaves, their policies don&#039;t follow — they stay.     ║
║                                                                      ║
║  Fix: always use roles. Attach policies to roles. Assign roles to   ║
║  users. The role outlives the person.                               ║
╚══════════════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════════════╗
║  &#x26a0;  GOTCHA 2 — Using AWS managed policies in production            ║
║                                                                      ║
║  AmazonS3FullAccess grants s3:* on *. For a Lambda that reads one  ║
║  specific bucket, that&#039;s ~30 permissions you didn&#039;t need, all live. ║
║                                                                      ║
║  Fix: create customer managed policies scoped to the specific       ║
║  actions and ARNs the workload actually uses.                       ║
╚══════════════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════════════╗
║  &#x26a0;  GOTCHA 3 — iam:PassRole with &quot;Resource&quot;: &quot;*&quot;                   ║
║                                                                      ║
║  iam:PassRole lets an identity assign a role to an AWS service.     ║
║  With Resource: *, it can pass ANY role — including ones with more  ║
║  permissions than it currently has. That is a privilege escalation. ║
║                                                                      ║
║  Fix: always scope iam:PassRole to a specific role ARN:             ║
║  &quot;Resource&quot;: &quot;arn:aws:iam::ACCOUNT:role/SpecificRoleName&quot;          ║
╚══════════════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════════════╗
║  &#x26a0;  GOTCHA 4 — Permissions boundary ≠ policy grant                 ║
║                                                                      ║
║  Setting a boundary that allows s3:* does NOT grant S3 access.     ║
║  The boundary is a ceiling — it limits maximum possible permissions. ║
║  The identity-based policy still needs to explicitly Allow the      ║
║  action. Both must be present for the access to work.               ║
╚══════════════════════════════════════════════════════════════════════╝
</code></pre>
<hr />
<h2 id="cross-cloud-rosetta-stone">Cross-Cloud Rosetta Stone</h2>
<p>Same concepts, different names and different directions. Bookmark this table.</p>
<pre><code class="" data-line="">┌─────────────────────────┬──────────────────────────┬──────────────────────────┬──────────────────────────┐
│ Concept                 │ AWS                      │ GCP                      │ Azure                    │
├─────────────────────────┼──────────────────────────┼──────────────────────────┼──────────────────────────┤
│ Atomic permission       │ s3:GetObject             │ storage.objects.get      │ .../blobs/read           │
│ Permission document     │ Policy (JSON)            │ (built into role def)    │ Role Definition          │
│ Access grant            │ Policy attachment        │ IAM Binding              │ Role Assignment          │
│ Job-function identity   │ IAM Role                 │ Predefined Role          │ Built-in Role            │
│ Non-human identity      │ IAM Role (assumed)       │ Service Account          │ Managed Identity         │
│ Org-level guardrail     │ SCP                      │ Org Policy               │ Management Group Policy  │
│ Permission ceiling      │ Permissions Boundary     │ —                        │ —                        │
│ Session restriction     │ Session Policy           │ —                        │ —                        │
│ Attribute-based grant   │ Tag conditions in policy │ IAM Conditions           │ Conditions in assignment │
└─────────────────────────┴──────────────────────────┴──────────────────────────┴──────────────────────────┘
</code></pre>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line="">┌──────────────────────────┬────────────────────────────────────────────────────────────┐
│ Term                     │ What it is                                                 │
├──────────────────────────┼────────────────────────────────────────────────────────────┤
│ Permission               │ Atomic: one action on one resource class                   │
│ Policy                   │ Document grouping permissions + conditions                 │
│ Role (AWS)               │ Assumable identity — carries policies, issues temp creds   │
│ Trust policy (AWS)       │ Who can assume this role — separate from permissions       │
│ Permissions boundary     │ Ceiling — limits max possible permissions; does not grant  │
│ SCP                      │ Org guardrail — constrains all identities in scope         │
│ IAM Binding (GCP)        │ Maps a role to a member on a specific resource             │
│ Role Assignment (Azure)  │ Grants a role definition at a specific scope               │
│ ABAC                     │ Access by tag/attribute — one policy replaces many roles   │
│ RBAC                     │ Access by role membership — clean until roles proliferate  │
│ iam:PassRole             │ Privilege escalation vector — always scope to specific ARN │
└──────────────────────────┴────────────────────────────────────────────────────────────┘

Commands to know:
┌────────────────────────────────────────────────────────────────────────────────┐
│  # AWS — list policies attached to a role                                     │
│  aws iam list-attached-role-policies --role-name MyRole                       │
│                                                                                │
│  # AWS — view what a managed policy actually grants                           │
│  aws iam get-policy-version \                                                  │
│    --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \              │
│    --version-id v1                                                             │
│                                                                                │
│  # AWS — who can assume this role?                                            │
│  aws iam get-role --role-name MyRole --query &#039;Role.AssumeRolePolicyDocument&#039;  │
│                                                                                │
│  # GCP — view the IAM policy on a project                                    │
│  gcloud projects get-iam-policy PROJECT_ID --format=json                      │
│                                                                                │
│  # GCP — list all roles and what permissions they include                    │
│  gcloud iam roles describe roles/storage.objectViewer                         │
│                                                                                │
│  # Azure — list role assignments in a subscription                           │
│  az role assignment list --all --output table                                 │
│                                                                                │
│  # Azure — view exactly what a built-in role grants                          │
│  az role definition list --name &quot;Storage Blob Data Reader&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>RBAC and ABAC are the implementation models for authorization at scale</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 1 — Security &amp; Risk Management</td>
<td>Role design implements separation of duties and least privilege</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.15 Access control</td>
<td>Access control policy — roles and policies are the mechanism</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.18 Access rights</td>
<td>Provisioning, review, and removal of access rights — roles make this auditable</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.2 Privileged access rights</td>
<td>Permissions boundaries and conditions applied to elevated access</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1</td>
<td>Logical access security — policy documents are the technical implementation</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.3</td>
<td>Access revocation — role-based model makes removal consistent and auditable</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Permissions are atomic — one action on one resource class. Policies group permissions. Roles carry policies for assignment</li>
<li>AWS roles have two required configs: trust policy (who can assume) and permission policy (what it can do) — both must be correct</li>
<li>GCP binds roles to resources; AWS attaches policies to identities — the mental model runs in opposite directions</li>
<li>Azure separates role definition (what) from role assignment (who, where) — define once, assign at multiple scopes</li>
<li>RBAC scales through role design; ABAC scales through tag/attribute conditions — use ABAC where roles would proliferate</li>
<li><code class="" data-line="">iam:PassRole</code> and <code class="" data-line="">iam.serviceAccounts.actAs</code> are privilege escalation vectors — scope them to specific ARNs, never <code class="" data-line="">*</code></li>
<li>Conditions add context (MFA, IP, tags, time) to policies — the MFA condition on IAM actions is essential in every account</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP04 goes deep on AWS IAM — the most complex of the three cloud models. Policy evaluation order, cross-account trust, permissions boundaries in practice, SCPs, and IAM Identity Center for human access. We&#8217;ll work through the patterns that make AWS IAM maintainable at production scale.</p>
<p><em>Next: <a href="/aws-iam-deep-dive/">AWS IAM Deep Dive: Users, Groups, Roles, and Policies Explained</a></em></p>
<p>Get the AWS IAM deep dive 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-roles-policies-permissions-explained%2F&amp;linkname=IAM%20Roles%20vs%20Policies%3A%20How%20Cloud%20Authorization%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%2Fiam-roles-policies-permissions-explained%2F&amp;linkname=IAM%20Roles%20vs%20Policies%3A%20How%20Cloud%20Authorization%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%2Fiam-roles-policies-permissions-explained%2F&amp;linkname=IAM%20Roles%20vs%20Policies%3A%20How%20Cloud%20Authorization%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%2Fiam-roles-policies-permissions-explained%2F&amp;linkname=IAM%20Roles%20vs%20Policies%3A%20How%20Cloud%20Authorization%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%2Fiam-roles-policies-permissions-explained%2F&amp;linkname=IAM%20Roles%20vs%20Policies%3A%20How%20Cloud%20Authorization%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%2Fiam-roles-policies-permissions-explained%2F&amp;linkname=IAM%20Roles%20vs%20Policies%3A%20How%20Cloud%20Authorization%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%2Fiam-roles-policies-permissions-explained%2F&amp;linkname=IAM%20Roles%20vs%20Policies%3A%20How%20Cloud%20Authorization%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%2Fiam-roles-policies-permissions-explained%2F&#038;title=IAM%20Roles%20vs%20Policies%3A%20How%20Cloud%20Authorization%20Actually%20Works" data-a2a-url="https://linuxcent.com/iam-roles-policies-permissions-explained/" data-a2a-title="IAM Roles vs Policies: How Cloud Authorization Actually Works"></a></p><p>The post <a href="https://linuxcent.com/iam-roles-policies-permissions-explained/">IAM Roles vs Policies: How Cloud Authorization Actually Works</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/iam-roles-policies-permissions-explained/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1458</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-21 09:53:58 by W3 Total Cache
-->