<?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>Service Accounts Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/service-accounts/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/service-accounts/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Sat, 09 May 2026 18:38:27 +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>Service Accounts Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/service-accounts/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>Kubernetes RBAC and AWS IAM: The Two-Layer Access Model for EKS</title>
		<link>https://linuxcent.com/kubernetes-rbac-cloud-iam/</link>
					<comments>https://linuxcent.com/kubernetes-rbac-cloud-iam/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 17:06:27 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[EKS]]></category>
		<category><![CDATA[GKE]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Kubernetes RBAC]]></category>
		<category><![CDATA[Kubernetes Security]]></category>
		<category><![CDATA[Service Accounts]]></category>
		<guid isPermaLink="false">https://linuxcent.com/kubernetes-rbac-cloud-iam/</guid>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

gcloud resource-manager folders list --organization=ORG_ID

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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