<?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>GKE Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/gke/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/gke/</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>GKE Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/gke/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>GCP Secure Boot Certificate Expiration 2026: What You Must Do Before June 24</title>
		<link>https://linuxcent.com/gcp-secure-boot-certificate-expiration-2026/</link>
					<comments>https://linuxcent.com/gcp-secure-boot-certificate-expiration-2026/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Wed, 06 May 2026 04:11:08 +0000</pubDate>
				<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[Certificate Expiry]]></category>
		<category><![CDATA[GCP]]></category>
		<category><![CDATA[GKE]]></category>
		<category><![CDATA[Infrastructure Security]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[linux-security]]></category>
		<category><![CDATA[Secure Boot]]></category>
		<category><![CDATA[Shielded VM]]></category>
		<category><![CDATA[UEFI]]></category>
		<guid isPermaLink="false">https://linuxcent.com/gcp-secure-boot-certificate-expiration-2026/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span>Three Microsoft UEFI certificates expire June 2026. Find affected GCP VMs and GKE nodes — and how to fix them before boot failures hit.</p>
<p>The post <a href="https://linuxcent.com/gcp-secure-boot-certificate-expiration-2026/">GCP Secure Boot Certificate Expiration 2026: What You Must Do Before June 24</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Three Microsoft UEFI Secure Boot certificates expire between June 24 and October 19, 2026</li>
<li>Any GCP Compute Engine instance with Secure Boot enabled, created before <strong>November 7, 2025</strong>, carries the old certs and is at risk</li>
<li>When the certs expire, instances may fail to boot after OS updates that pull in bootloaders signed only by the replacement 2023 certificates</li>
<li>GKE Shielded Nodes are affected too — node pools whose nodes haven&#8217;t been recreated since November 7, 2025 carry the old UEFI database</li>
<li>vTPM-sealed secrets, BitLocker, and Linux full disk encryption break if Secure Boot fails mid-update</li>
<li>Primary fix: recreate affected instances (post-Nov 7, 2025 instances include the updated UEFI DB automatically)</li>
<li>Emergency workaround if boot fails: temporarily disable Secure Boot, apply updates, re-enable</li>
</ul>
<hr />
<h2 id="the-big-picture-the-uefi-secure-boot-trust-chain">The Big Picture: The UEFI Secure Boot Trust Chain</h2>
<pre><code class="" data-line="">  UEFI Firmware (PK — Platform Key, set by OEM/Google)
         │
         │  PK signs KEK updates
         ▼
  ┌─────────────────────────────────────────────┐
  │        KEK (Key Exchange Key Database)       │
  │  Microsoft Corporation KEK CA 2011           │ ← EXPIRING Jun 24, 2026
  │  Microsoft Corporation KEK CA 2023           │ ← Replacement (new VMs only)
  └────────────────────┬────────────────────────┘
                       │  KEK authorizes DB/DBX updates
                       ▼
  ┌─────────────────────────────────────────────┐
  │         DB (Authorized Signature Database)   │
  │  Microsoft UEFI CA 2011 ← signs Linux Shim  │ ← EXPIRING Jun 27, 2026
  │  Microsoft Windows PCA 2011 ← signs WinBoot │ ← EXPIRING Oct 19, 2026
  │  Microsoft UEFI CA 2023 ← replacement       │ ← Present on post-Nov 7 VMs
  │  Microsoft Windows PCA 2023 ← replacement   │ ← Present on post-Nov 7 VMs
  └────────┬───────────────────────┬────────────┘
           │                       │
           ▼                       ▼
   Linux Shim (shim.efi)    Windows Boot Manager
           │
           ▼
       GRUB2 / systemd-boot
           │
           ▼
       Linux Kernel
</code></pre>
<p>GCP Compute Engine instances with Secure Boot enabled — created before November 7, 2025 — have a UEFI signature database that includes the 2011 certificates but not the 2023 replacements. When those 2011 certificates expire, new bootloader binaries (signed exclusively by the 2023 certs) will be rejected at boot time.</p>
<hr />
<h2 id="what-secure-boot-actually-does-and-why-certificate-expiry-breaks-booting">What Secure Boot Actually Does — and Why Certificate Expiry Breaks Booting</h2>
<p>Secure Boot is UEFI&#8217;s mechanism for ensuring that only cryptographically signed, trusted software runs during the boot sequence. The trust chain works like this:</p>
<ol>
<li><strong>Platform Key (PK):</strong> Root of trust, set by the hardware manufacturer or cloud provider. Authorizes updates to the KEK.</li>
<li><strong>Key Exchange Key (KEK):</strong> Authorizes modifications to the DB and DBX (the forbidden signatures database). Microsoft holds one KEK slot; OEMs often hold another.</li>
<li><strong>DB (Signature Database):</strong> Contains the public certificates used to verify bootloaders. If a bootloader binary is signed by a cert in DB, it&#8217;s allowed to run. If not, the firmware halts.</li>
<li><strong>DBX (Forbidden Signatures Database):</strong> Revocation list. Bootloaders explicitly listed here are blocked even if they were once trusted.</li>
</ol>
<p><strong>Where expiry matters:</strong> The DB certificates don&#8217;t &#8220;enforce&#8221; anything at runtime by checking dates themselves — UEFI doesn&#8217;t do certificate revocation in real time. The problem is different and more insidious: as Linux distributions and Microsoft ship updated bootloaders, those new binaries are signed only by the <strong>2023 replacement certificates</strong>, not the expiring 2011 ones. If your VM&#8217;s DB doesn&#8217;t contain the 2023 certs, the UEFI firmware will reject the new shim, and the system won&#8217;t boot after an OS update that upgrades the bootloader package.</p>
<p>On Debian/Ubuntu, <code class="" data-line="">shim-signed</code> upgrades. On RHEL/CentOS Stream, <code class="" data-line="">shim-x64</code> upgrades. Either way: new binary, new signature, old DB — boot failure.</p>
<hr />
<h2 id="the-three-certificates-expiring-in-2026">The Three Certificates Expiring in 2026</h2>
<h3 id="1-microsoft-corporation-kek-ca-2011-expires-june-24-2026">1. Microsoft Corporation KEK CA 2011 — expires June 24, 2026</h3>
<p><strong>Role:</strong> Authorizes updates to the DB and DBX signature databases.</p>
<p>When the KEK expires, firmware that enforces KEK validity may refuse to accept DB/DBX updates signed by this certificate. This means even if Google pushes an out-of-band UEFI DB update containing the 2023 certs, instances with an expired-only KEK slot may not be able to apply it cleanly.</p>
<p><strong>Replacement:</strong> Microsoft Corporation KEK CA 2023</p>
<hr />
<h3 id="2-microsoft-corporation-uefi-ca-2011-expires-june-27-2026">2. Microsoft Corporation UEFI CA 2011 — expires June 27, 2026</h3>
<p><strong>Role:</strong> Signs third-party bootloaders — specifically the Linux Shim (<code class="" data-line="">shim.efi</code>).</p>
<p>This is the most critical cert for Linux workloads. Every major Linux distribution uses a shim bootloader as the first-stage loader in a Secure Boot chain. The shim is signed by Microsoft&#8217;s UEFI CA because Linux vendors submit their shim builds to Microsoft for signing (to ensure broad UEFI compatibility). When new shim packages are released signed only by UEFI CA 2023, any VM with only the 2011 cert in its DB will reject them.</p>
<p><strong>Replacement:</strong> Microsoft UEFI CA 2023</p>
<hr />
<h3 id="3-microsoft-windows-production-pca-2011-expires-october-19-2026">3. Microsoft Windows Production PCA 2011 — expires October 19, 2026</h3>
<p><strong>Role:</strong> Signs Windows Boot Manager and other Windows boot components.</p>
<p>Windows instances on GCP using Secure Boot are affected by this cert. Post-expiry Windows OS updates that ship a new Boot Manager binary signed exclusively by the 2023 PCA will fail to boot on instances carrying only the 2011 cert.</p>
<p><strong>Replacement:</strong> Microsoft Windows Production PCA 2023</p>
<p><strong>Windows-specific signal:</strong> Event ID 1801 in the Windows System event log — &#8220;Secure Boot CA/keys need to be updated&#8221; — will appear by mid-2026 on affected instances, before actual boot failure. This is your warning window.</p>
<hr />
<h2 id="why-gcp-instances-are-specifically-affected">Why GCP Instances Are Specifically Affected</h2>
<p>Google&#8217;s Compute Engine Shielded VMs ship with a pre-populated UEFI variable database. The content of that database is fixed at instance creation time — it&#8217;s part of the VM&#8217;s UEFI firmware image. Instances created before <strong>November 7, 2025</strong> have a DB that contains the 2011 certs but not the 2023 replacements. Instances created on or after November 7, 2025 had the updated database backfilled.</p>
<p>This is not a Google-specific failure. Every cloud provider and on-premises hypervisor platform that uses Secure Boot with a pre-populated UEFI DB has the same problem. GCP is ahead of many platforms in actually documenting it.</p>
<hr />
<h2 id="gke-shielded-nodes-the-operational-blind-spot">GKE Shielded Nodes: The Operational Blind Spot</h2>
<p>GKE&#8217;s Shielded Nodes feature enables Secure Boot on node pool VMs. Each node is a Compute Engine instance — and all the same rules apply.</p>
<p><strong>The risk:</strong> Node pools whose nodes were last provisioned before November 7, 2025 carry the old UEFI database. When containerd, the OS image, or the kernel gets updated via node auto-upgrade or manual node pool upgrade, the new node VMs will carry updated certs. But nodes that haven&#8217;t been replaced since before the cutoff are sitting on the old DB.</p>
<p><strong>GKE auto-upgrade helps — but only if it&#8217;s actually running and has completed at least one full node replacement cycle since November 7, 2025.</strong></p>
<p>Node pools with auto-upgrade disabled, or clusters in maintenance windows that delayed upgrades, are at risk.</p>
<p><strong>The trigger scenario:</strong><br />
1. GKE runs a node OS update in-place on an old node (not a full node replacement)<br />
2. The update upgrades the shim package to a version signed only by UEFI CA 2023<br />
3. Next reboot: the node fails to boot<br />
4. The node is marked NotReady, workloads are rescheduled — but the underlying VM is stuck</p>
<hr />
<h2 id="detecting-affected-resources">Detecting Affected Resources</h2>
<h3 id="compute-engine-instances">Compute Engine Instances</h3>
<pre><code class="" data-line="">gcloud compute instances list \
  --filter=&quot;creationTimestamp &lt; &#039;2025-11-07&#039; AND shieldedInstanceConfig.enableSecureBoot=true&quot; \
  --format=&quot;table(name,zone,creationTimestamp,shieldedInstanceConfig.enableSecureBoot,status)&quot;
</code></pre>
<p>Sample output:</p>
<pre><code class="" data-line="">NAME               ZONE           CREATION_TIMESTAMP        ENABLE_SECURE_BOOT  STATUS
prod-api-01        us-central1-a  2024-08-15T10:22:00Z      True                RUNNING   ← at risk
prod-db-02         us-central1-b  2023-11-01T08:15:00Z      True                RUNNING   ← at risk
prod-web-03        us-central1-a  2025-12-01T14:30:00Z      True                RUNNING   ← safe (post-Nov 7)
</code></pre>
<h3 id="gke-node-pools">GKE Node Pools</h3>
<pre><code class="" data-line=""># List node pools with Secure Boot enabled per cluster
gcloud container clusters list --format=&quot;value(name,location)&quot; | while read NAME LOCATION; do
  echo &quot;=== Cluster: $NAME ($LOCATION) ===&quot;
  gcloud container node-pools list \
    --cluster=&quot;$NAME&quot; \
    --location=&quot;$LOCATION&quot; \
    --filter=&quot;config.shieldedInstanceConfig.enableSecureBoot=true&quot; \
    --format=&quot;table(name,config.shieldedInstanceConfig.enableSecureBoot,management.autoUpgrade)&quot;
done
</code></pre>
<p>Then verify node creation timestamps within affected pools:</p>
<pre><code class="" data-line="">gcloud compute instances list \
  --filter=&quot;labels.goog-gke-node:* AND creationTimestamp &lt; &#039;2025-11-07&#039; AND shieldedInstanceConfig.enableSecureBoot=true&quot; \
  --format=&quot;table(name,zone,creationTimestamp,labels.goog-gke-node)&quot;
</code></pre>
<h3 id="checking-the-uefi-db-on-a-running-instance">Checking the UEFI DB on a Running Instance</h3>
<p>SSH into an affected instance and verify which certs are in the DB:</p>
<pre><code class="" data-line=""># On the instance (requires mokutil and/or efitools)
sudo mokutil --db | grep -A3 &quot;Subject:&quot;
</code></pre>
<p>Look for <code class="" data-line="">CN=Microsoft UEFI CA 2023</code> in the output. Its absence means your instance has only the 2011 certs.</p>
<p>On GKE nodes (where you have node shell access via a DaemonSet or node debug pod):</p>
<pre><code class="" data-line=""># Using kubectl debug for node access
kubectl debug node/NODE_NAME -it --image=ubuntu -- bash
# Then inside the debug pod:
chroot /host
mokutil --db 2&gt;/dev/null | grep &quot;Microsoft.*2023&quot; || echo &quot;2023 cert NOT present — node at risk&quot;
</code></pre>
<hr />
<h2 id="solutions">Solutions</h2>
<h3 id="option-1-recreate-instances-primary-recommended-by-google">Option 1: Recreate Instances (Primary — Recommended by Google)</h3>
<p>Instances created after November 7, 2025 automatically receive the updated UEFI certificate database. The simplest fix is to recreate affected instances.</p>
<p><strong>For Compute Engine:</strong></p>
<pre><code class="" data-line=""># Step 1: Create a machine image (snapshot) of the existing instance
gcloud compute machine-images create INSTANCE_NAME-backup \
  --source-instance=INSTANCE_NAME \
  --source-instance-zone=ZONE

# Step 2: Delete the old instance (after verifying backup)
gcloud compute instances delete INSTANCE_NAME --zone=ZONE

# Step 3: Create new instance from machine image
gcloud compute instances create INSTANCE_NAME \
  --source-machine-image=INSTANCE_NAME-backup \
  --zone=ZONE \
  --shielded-secure-boot \
  --shielded-vtpm \
  --shielded-integrity-monitoring
</code></pre>
<p>The new instance will have the post-November 7, 2025 UEFI DB.</p>
<p><strong>For GKE Node Pools:</strong></p>
<pre><code class="" data-line=""># Option A: Upgrade the node pool (triggers node recreation)
gcloud container clusters upgrade CLUSTER_NAME \
  --location=LOCATION \
  --node-pool=NODE_POOL_NAME

# Option B: Recreate the node pool entirely
gcloud container node-pools create NODE_POOL_NAME-new \
  --cluster=CLUSTER_NAME \
  --location=LOCATION \
  --shielded-secure-boot \
  --shielded-integrity-monitoring \
  [... your existing pool config ...]

# Then cordon and drain the old pool nodes
kubectl cordon NODE_NAME
kubectl drain NODE_NAME --ignore-daemonsets --delete-emptydir-data

# Finally delete the old node pool
gcloud container node-pools delete NODE_POOL_NAME \
  --cluster=CLUSTER_NAME \
  --location=LOCATION
</code></pre>
<hr />
<h3 id="option-2-disable-secure-boot-temporarily-emergency-workaround">Option 2: Disable Secure Boot Temporarily (Emergency Workaround)</h3>
<p>If an instance has already failed to boot after an OS update, or if you need to apply bootloader updates before recreating the instance:</p>
<pre><code class="" data-line=""># Disable Secure Boot on the stopped instance
gcloud compute instances update INSTANCE_NAME \
  --zone=ZONE \
  --no-shielded-secure-boot

# Start the instance
gcloud compute instances start INSTANCE_NAME --zone=ZONE

# SSH in, apply OS updates and any pending bootloader upgrades
# (The system will boot without Secure Boot enforcement)
sudo apt-get update &amp;&amp; sudo apt-get upgrade -y   # Debian/Ubuntu
# or
sudo dnf update -y                                # RHEL/CentOS

# Stop the instance again
gcloud compute instances stop INSTANCE_NAME --zone=ZONE

# Re-enable Secure Boot
gcloud compute instances update INSTANCE_NAME \
  --zone=ZONE \
  --shielded-secure-boot

# Start again — now boots with new bootloader binaries
gcloud compute instances start INSTANCE_NAME --zone=ZONE
</code></pre>
<blockquote>
<p><strong>Note:</strong> This workaround doesn&#8217;t add the 2023 certs to the DB. It bypasses Secure Boot enforcement temporarily. The underlying UEFI DB still only has the 2011 certs. You still need to recreate the instance to get the updated DB — this is only a bridge to keep the instance alive while you plan migration.</p>
</blockquote>
<hr />
<h3 id="option-3-restore-from-machine-image">Option 3: Restore from Machine Image</h3>
<p>If an instance is already in a boot failure state and the workaround above doesn&#8217;t apply:</p>
<pre><code class="" data-line=""># List available machine images
gcloud compute machine-images list

# Restore from a pre-failure machine image
gcloud compute instances create INSTANCE_NAME-restored \
  --source-machine-image=MACHINE_IMAGE_NAME \
  --zone=ZONE
</code></pre>
<p>Then immediately plan recreation on a post-November 7, 2025 instance.</p>
<hr />
<h2 id="vtpm-bitlocker-and-full-disk-encryption-the-hidden-risk">vTPM, BitLocker, and Full Disk Encryption: The Hidden Risk</h2>
<p>For VMs using Shielded VM features beyond just Secure Boot — specifically vTPM with sealed secrets — certificate expiry creates a more dangerous failure mode.</p>
<p><strong>How vTPM sealing works:</strong></p>
<pre><code class="" data-line="">  Boot sequence measurements → PCR registers (PCR 0–7 for UEFI, PCR 8–15 for OS)
         │
         ▼
  TPM seals secrets (FDE key, BitLocker key) to specific PCR values
         │
         ▼
  On next boot: PCR values must match for TPM to release the key
         │
         ▼
  If Secure Boot state changes (cert DB changes, Secure Boot disabled) →
  PCR values change → TPM refuses to unseal → FDE fails → disk inaccessible
</code></pre>
<p><strong>What this means in practice:</strong></p>
<ul>
<li>
<p><strong>Linux FDE (LUKS with TPM2 unsealing):</strong> If Secure Boot fails or is temporarily disabled per the workaround above, the TPM will not release the LUKS volume key. The system will drop to a recovery prompt. You need the LUKS recovery passphrase.</p>
</li>
<li>
<p><strong>Windows BitLocker:</strong> If PCR values shift (Secure Boot disabled, cert DB changed), BitLocker enters recovery mode. The VM prompts for the BitLocker recovery key on next boot. Without it, the volume is inaccessible.</p>
</li>
<li>
<p><strong>Windows Virtual Secure Mode:</strong> VSM uses vTPM to protect credentials. If Secure Boot state changes, VSM-protected secrets become inaccessible until re-enrollment.</p>
</li>
</ul>
<p><strong>Action before any changes:</strong></p>
<pre><code class="" data-line=""># For Linux: ensure you have the LUKS recovery key
sudo cryptsetup luksDump /dev/sda3 | grep &quot;Key Slot&quot;

# For Windows: export BitLocker recovery key before touching Secure Boot state
# (Do this from within the running Windows instance via PowerShell)
Get-BitLockerVolume | Select-Object -ExpandProperty KeyProtector | Where-Object {$_.KeyProtectorType -eq &quot;RecoveryPassword&quot;}
</code></pre>
<p>Store recovery keys in Secret Manager, not just locally:</p>
<pre><code class="" data-line=""># Store LUKS key in GCP Secret Manager
echo -n &quot;YOUR_RECOVERY_KEY&quot; | gcloud secrets create luks-recovery-INSTANCE_NAME \
  --data-file=- \
  --replication-policy=automatic
</code></pre>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>1. OS update automation is the trigger, not the cert expiry date itself.</strong><br />
The certs don&#8217;t enforce anything at runtime. The actual failure happens when an unattended-upgrade, <code class="" data-line="">yum-cron</code>, or GKE node OS update pulls in a new shim/Boot Manager binary signed only by the 2023 cert. Instances may fail to boot weeks or months before the official cert expiry date if distros ship updated bootloaders early.</p>
<p><strong>2. GKE surge upgrades can mask the problem — temporarily.</strong><br />
During a node pool upgrade, GKE creates new nodes (with updated certs) before draining old ones. Workloads move to new nodes. The old nodes get deleted. This looks fine — until you realize some in-place operations (node taints, label changes, manual kubelet restarts) could force old nodes to reboot without triggering node replacement.</p>
<p><strong>3. Disabling Secure Boot changes vTPM PCR values — plan FDE recovery before touching anything.</strong><br />
The temporary workaround (disable Secure Boot) will invalidate TPM-bound disk encryption. Have recovery keys ready before running <code class="" data-line="">--no-shielded-secure-boot</code>.</p>
<p><strong>4. Windows Event ID 1801 is an early warning — act on it.</strong><br />
If you see this event in your Windows Compute Engine instances before June 2026, that instance has already identified itself as carrying the old certs. Use it as your automated detection signal in Cloud Logging.</p>
<pre><code class="" data-line=""># Query Cloud Logging for Event ID 1801 across Windows instances
gcloud logging read &#039;resource.type=&quot;gce_instance&quot; AND jsonPayload.EventID=1801&#039; \
  --format=&quot;table(resource.labels.instance_id,timestamp,jsonPayload.Message)&quot; \
  --limit=50
</code></pre>
<p><strong>5. Instance templates propagate the old DB.</strong><br />
If you use instance templates or managed instance groups (MIGs) to create VMs, and those templates were created before November 7, 2025, new instances created from them may or may not inherit updated certs depending on how the template configures the UEFI DB. Verify by checking creation timestamp of the resulting instance, not the template.</p>
<p><strong>6. Custom OS images don&#8217;t fix this.</strong><br />
Importing a custom image or using a custom OS does not update the UEFI certificate database. The DB is part of the VM&#8217;s UEFI firmware state, not the OS disk image. Recreating the instance is the only reliable path.</p>
<hr />
<h2 id="quick-reference-commands">Quick Reference: Commands</h2>
<table>
<thead>
<tr>
<th>Task</th>
<th>Command</th>
</tr>
</thead>
<tbody>
<tr>
<td>List affected Compute Engine VMs</td>
<td><code class="" data-line="">gcloud compute instances list --filter=&quot;creationTimestamp &lt; &#039;2025-11-07&#039; AND shieldedInstanceConfig.enableSecureBoot=true&quot;</code></td>
</tr>
<tr>
<td>Check UEFI DB on a Linux VM</td>
<td><code class="" data-line="">sudo mokutil --db \| grep -E &quot;Subject\|Not After&quot;</code></td>
</tr>
<tr>
<td>Check for 2023 cert presence</td>
<td><code class="" data-line="">mokutil --db 2&gt;/dev/null \| grep &quot;Microsoft.*2023&quot; \|\| echo &quot;2023 cert absent&quot;</code></td>
</tr>
<tr>
<td>Disable Secure Boot (emergency)</td>
<td><code class="" data-line="">gcloud compute instances update INSTANCE --zone=ZONE --no-shielded-secure-boot</code></td>
</tr>
<tr>
<td>Re-enable Secure Boot</td>
<td><code class="" data-line="">gcloud compute instances update INSTANCE --zone=ZONE --shielded-secure-boot</code></td>
</tr>
<tr>
<td>Find affected GKE nodes</td>
<td><code class="" data-line="">gcloud compute instances list --filter=&quot;labels.goog-gke-node:* AND creationTimestamp &lt; &#039;2025-11-07&#039; AND shieldedInstanceConfig.enableSecureBoot=true&quot;</code></td>
</tr>
<tr>
<td>Trigger GKE node pool upgrade</td>
<td><code class="" data-line="">gcloud container clusters upgrade CLUSTER --location=LOCATION --node-pool=POOL</code></td>
</tr>
<tr>
<td>Store LUKS key in Secret Manager</td>
<td><code class="" data-line="">echo -n &quot;KEY&quot; \| gcloud secrets create NAME --data-file=-</code></td>
</tr>
<tr>
<td>Query Windows Event 1801 in Logging</td>
<td><code class="" data-line="">gcloud logging read &#039;resource.type=&quot;gce_instance&quot; AND jsonPayload.EventID=1801&#039;</code></td>
</tr>
<tr>
<td>Create machine image backup</td>
<td><code class="" data-line="">gcloud compute machine-images create BACKUP --source-instance=INSTANCE --source-instance-zone=ZONE</code></td>
</tr>
</tbody>
</table>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Domain</th>
<th>Relevance</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 7: Security Operations</td>
<td>Patch management, boot integrity, incident response</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 3: Security Architecture</td>
<td>Secure Boot trust chain, TPM integration, cryptographic key lifecycle</td>
</tr>
<tr>
<td>NIST CSF 2.0</td>
<td>ID.AM, PR.IP</td>
<td>Asset inventory of affected VMs; integrity protection of boot chain</td>
</tr>
<tr>
<td>CIS Benchmarks</td>
<td>CIS Google Cloud Computing Foundations</td>
<td>Shielded VM controls, vTPM configuration</td>
</tr>
<tr>
<td>OWASP Top 10</td>
<td>A05: Security Misconfiguration</td>
<td>Failure to maintain certificate currency in security-critical infrastructure</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>The expiry of three Microsoft UEFI CA certificates in 2026 creates a window where GCP VMs with Secure Boot enabled — created before November 7, 2025 — will fail to boot after pulling in new bootloader packages</li>
<li>The failure is not instantaneous on the cert expiry date. It&#8217;s triggered by the next OS update that ships a bootloader signed exclusively by the 2023 replacement certs</li>
<li>GKE Shielded Nodes are affected through the same mechanism: node VMs that haven&#8217;t been recreated since November 7, 2025 carry the old UEFI database</li>
<li>vTPM-sealed secrets (FDE, BitLocker, VSM) add a secondary failure mode if Secure Boot state is changed as part of remediation — have recovery keys before touching anything</li>
<li>Google&#8217;s recommended fix is instance recreation. The workaround (disable Secure Boot temporarily) keeps instances alive but doesn&#8217;t fix the underlying DB — treat it as a bridge, not a resolution</li>
<li>Audit now, before June 24. The command is one line. The blast radius of missing this is a production boot failure at 2 AM after a routine security patch run</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>If you&#8217;re running Shielded VMs in production, this certificate expiry is the kind of quiet deadline that fails silently — not with an alarm, but with a VM that doesn&#8217;t come back after a patch cycle. The time to audit is before your automated patching runs, not after.</p>
<p>If you found this useful, the <a href="https://linuxcent.com">linuxcent.com newsletter</a> covers infrastructure security at this depth regularly — kernel internals, cloud platform gotchas, and the operational implications that vendor docs bury in footnotes.</p>
<p>Get the next deep-dive in your inbox when it publishes → [subscribe link]</p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-secure-boot-certificate-expiration-2026%2F&amp;linkname=GCP%20Secure%20Boot%20Certificate%20Expiration%202026%3A%20What%20You%20Must%20Do%20Before%20June%2024" 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-secure-boot-certificate-expiration-2026%2F&amp;linkname=GCP%20Secure%20Boot%20Certificate%20Expiration%202026%3A%20What%20You%20Must%20Do%20Before%20June%2024" 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-secure-boot-certificate-expiration-2026%2F&amp;linkname=GCP%20Secure%20Boot%20Certificate%20Expiration%202026%3A%20What%20You%20Must%20Do%20Before%20June%2024" 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-secure-boot-certificate-expiration-2026%2F&amp;linkname=GCP%20Secure%20Boot%20Certificate%20Expiration%202026%3A%20What%20You%20Must%20Do%20Before%20June%2024" 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-secure-boot-certificate-expiration-2026%2F&amp;linkname=GCP%20Secure%20Boot%20Certificate%20Expiration%202026%3A%20What%20You%20Must%20Do%20Before%20June%2024" 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-secure-boot-certificate-expiration-2026%2F&amp;linkname=GCP%20Secure%20Boot%20Certificate%20Expiration%202026%3A%20What%20You%20Must%20Do%20Before%20June%2024" 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-secure-boot-certificate-expiration-2026%2F&amp;linkname=GCP%20Secure%20Boot%20Certificate%20Expiration%202026%3A%20What%20You%20Must%20Do%20Before%20June%2024" 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-secure-boot-certificate-expiration-2026%2F&#038;title=GCP%20Secure%20Boot%20Certificate%20Expiration%202026%3A%20What%20You%20Must%20Do%20Before%20June%2024" data-a2a-url="https://linuxcent.com/gcp-secure-boot-certificate-expiration-2026/" data-a2a-title="GCP Secure Boot Certificate Expiration 2026: What You Must Do Before June 24"></a></p><p>The post <a href="https://linuxcent.com/gcp-secure-boot-certificate-expiration-2026/">GCP Secure Boot Certificate Expiration 2026: What You Must Do Before June 24</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/gcp-secure-boot-certificate-expiration-2026/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1900</post-id>	</item>
		<item>
		<title>Kubernetes RBAC and AWS IAM: The Two-Layer Access Model for EKS</title>
		<link>https://linuxcent.com/kubernetes-rbac-cloud-iam/</link>
					<comments>https://linuxcent.com/kubernetes-rbac-cloud-iam/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 17:06:27 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[EKS]]></category>
		<category><![CDATA[GKE]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Kubernetes RBAC]]></category>
		<category><![CDATA[Kubernetes Security]]></category>
		<category><![CDATA[Service Accounts]]></category>
		<guid isPermaLink="false">https://linuxcent.com/kubernetes-rbac-cloud-iam/</guid>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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