<?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>IRSA Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/irsa/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/irsa/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Sat, 09 May 2026 18:38:15 +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>IRSA Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/irsa/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<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>
	</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:17:44 by W3 Total Cache
-->