<?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>AWS Access Analyzer Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/aws-access-analyzer/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/aws-access-analyzer/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Sat, 09 May 2026 18:38:21 +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>AWS Access Analyzer Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/aws-access-analyzer/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<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>
	</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-24 07:16:03 by W3 Total Cache
-->