Workload Identity Federation Explained (2026): OIDC Instead of Cloud Keys

Oct 20, 2026 · 13 min · Ajay Kumar

Every cloud breach write-up I read this year has the same paragraph in the middle: an access key, service account JSON file or client secret created years ago, copied into CI, and never rotated. Workload identity federation is the fix that has been available for most of a decade and is still not the default in a surprising number of the AWS, Google Cloud and Azure accounts I get asked to look at. This post explains what workload identity federation actually is, why static cloud keys keep leaking, how the OIDC token exchange works on each of the three big clouds and inside Kubernetes, where SPIFFE fits, and what changed in July 2026 when GitHub started baking immutable IDs into its tokens.

Why do long-lived cloud keys keep leaking?

Because they are files, and files get copied. GitGuardian's State of Secrets Sprawl 2026 counted roughly 29 million new hardcoded secrets pushed to public GitHub in 2025, up 34 percent on the year before, and found that 32.2 percent of internal repositories contain at least one (summary). The number that should worry an infrastructure team more is the remediation one: 64 percent of secrets confirmed valid in 2022 were still valid when GitGuardian retested them four years later. Nobody rotated them. A long-lived key does not expire just because the engineer who made it left.

CI runners are where these keys concentrate, which makes them the natural target. In GitGuardian's analysis of the Shai-Hulud 2 npm worm, 59 percent of the compromised machines were CI/CD runners rather than laptops. Security firm Cycode's deep dive counted stolen credentials for AWS, Google Cloud and Azure among the haul. In March 2025 the compromised tj-actions/changed-files action, used in over 23,000 repositories, dumped CI secrets into workflow logs, which were public for public repositories. The 2024 Snowflake campaign involved human logins, not workload keys, but showed how long stolen credentials stay useful: in the 165 organisations Mandiant notified, at least 79.7 percent of the accounts used had been exposed in earlier infostealer infections, some from 2020.

Verizon's 2026 DBIR says exploited vulnerabilities (31 percent) have now overtaken stolen credentials as the top way in. That is not credentials getting safer; attackers simply have more options. A key that exists can be stolen; a key that does not exist cannot. That is the whole argument for federation. The wider non-human identity picture is in Who Is This Agent?, and detection is covered in the secrets sprawl piece.

What is workload identity federation?

Workload identity federation lets a piece of software prove who it is to a cloud provider using a short-lived token signed by a platform it already runs on, and get short-lived cloud credentials back. No static key changes hands. The platform (GitHub Actions, a Kubernetes cluster, another cloud, an on-prem identity system) acts as an OpenID Connect identity provider. It signs a JSON Web Token whose claims describe the workload: who issued it (iss), who it is for (aud), and which workload it is (sub), plus extras such as repository, branch or namespace.

The cloud side keeps a trust configuration that names the issuer and the conditions a token must meet. When the workload presents a token, the cloud's security token service fetches the issuer's public keys from its OIDC discovery document, checks the signature and expiry, evaluates the conditions, and hands back credentials that expire on their own. GitHub's issuer publishes its discovery document at a well-known URL; when I fetched it today it listed https://token.actions.githubusercontent.com as the issuer, a JWKS endpoint for the keys and RS256 as the only signing algorithm.

Workload identity federation: a signed token in, short-lived credentials out 1. Workload GitHub job, pod or VM asks its platform IdP for a signed JWT 2. Platform IdP signs iss, sub, aud token lives minutes public keys at JWKS URL 3. Cloud STS fetches JWKS, checks iss, aud and sub against the trust policy 4. Short-lived creds AWS action: 1h default K8s token: 1h default nothing to rotate AWS IAM sub condition required for GitHub a lone * or ? is rejected Google Cloud attribute condition required prefer numeric *_id claims Microsoft Entra ID exact subject match, no wildcards max 20 credentials per app What leaks instead: 29M new secrets on public GitHub in 2025; 64% of 2022 leaks still valid in 2026 GitHub repos created after 15 July 2026 get immutable subjects: repo:org@ID/repo@ID:ref:...
The generic token exchange, with each cloud's enforced guardrails underneath. Sources: AWS IAM, Google Cloud and Microsoft Entra documentation, the configure-aws-credentials README, Kubernetes docs, GitGuardian's 2026 report and GitHub's OIDC reference.

OIDC is the token format and discovery protocol; federation is the trust relationship that accepts tokens from an identity provider you do not run. Most cloud federation is OIDC, but AWS also accepts SAML, and Google Cloud documents federation with X.509 client certificates for workloads holding an mTLS certificate.

How do AWS, Google Cloud and Azure implement it?

The three clouds converge on the same exchange with different nouns. This is the table I wish I had had the first time I set up all three.

AWS Google Cloud Microsoft Azure
Trust object IAM OIDC identity provider plus a role trust policy (docs) Workload identity pool and provider (docs) Federated identity credential on an app registration or user-assigned managed identity (docs)
Exchange call sts:AssumeRoleWithWebIdentity STS token exchange, then direct resource access or service account impersonation Entra ID client credentials with a client assertion
Default GitHub audience sts.amazonaws.com The provider's full resource name (auth action) api://AzureADTokenExchange (azure/login)
How the subject is matched IAM condition keys, StringEquals or StringLike CEL attribute condition plus principalSet:// IAM bindings Exact string match on issuer plus subject; flexible expressions in preview
Guardrail you cannot skip Trust policy must check token.actions.githubusercontent.com:sub and not as a lone wildcard (docs) Google says you must add an attribute condition for GitHub because one issuer serves every organisation No wildcards in any field; 20 federated credentials per app or identity
Credential lifetime Role session 15 minutes to 12 hours, 1 hour default in the official action Access tokens up to 1 hour by default Entra access token, no client secret stored
TLS thumbprint Optional: IAM verifies the JWKS endpoint against trusted root CAs and only falls back to thumbprints Not applicable Not applicable

Two rows used to be traps. AWS once required a certificate thumbprint for the GitHub provider, and plenty of Terraform still carries one; it is now only a fallback. And Google Cloud's attribute condition is not decoration: without it, a pool that trusts GitHub's issuer trusts every GitHub repository on the planet.

The sub claim is the whole security model

Everything reduces to one question: which tokens does your trust configuration accept? GitHub's token for a job tied to an environment has a subject such as repo:octo-org/octo-repo:environment:prod; a branch push gets repo:octo-org/octo-repo:ref:refs/heads/main; a pull request without an environment gets repo:octo-org/octo-repo:pull_request (GitHub OIDC reference). A trust policy matching repo:octo-org/* lets any branch of any repository in the organisation, including an intern's experiment, assume your production role. AWS refuses a lone *, but not a pattern that is technically scoped and practically wide open.

Then there is the problem with names. The original subject used only organisation and repository names, and names get recycled: delete or rename an organisation, someone else registers the old name, and their workflows mint tokens with your subject. GitHub's April 2026 changelog introduced immutable subjects that append numeric owner and repository IDs, and since 15 July 2026 they are the default for new repositories and for any repository that is renamed or transferred. Older repositories keep the name-only format until you opt in at the organisation or repository level. Microsoft published a migration guide that says the quiet part: credentials that trust only the old subject will not match tokens from a repository using the new one.

Here is a trust policy for a production deploy role that pins the environment and uses the immutable format. The IDs are placeholders; GitHub's REST API returns your repository's actual sub_claim_prefix.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "GitHubActionsProdDeployOnly",
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          "token.actions.githubusercontent.com:sub": "repo:octo-org@123456/payments-api@456789:environment:prod"
        }
      }
    }
  ]
}

Pair that with a GitHub environment that has required reviewers and a deployment branch rule, as AWS's docs recommend, and production credentials require a reviewed run from the right branch of the right repository. Google Cloud similarly tells you to map the numeric repository_id and repository_owner_id claims rather than names. The step-by-step setup for all three clouds is in the companion guide, How to Use GitHub Actions OIDC With AWS, GCP and Azure.

Kubernetes service account tokens are the same idea

Kubernetes went through the same migration internally. Before v1.22, a pod's service account token was a long-lived Secret; since then the kubelet requests a short-lived, audience-bound token through the TokenRequest API and mounts it as a projected volume (docs). A projected token defaults to one hour and must be at least ten minutes (projected volumes), and the kubelet rotates it once it is older than 80 percent of its lifetime or older than 24 hours (configure service accounts). Automatic clean-up of unused legacy Secret tokens went stable in v1.30, and binding tokens to a specific Node object has been stable and locked on since v1.33, according to the feature gate reference.

The part that makes this federation rather than just in-cluster auth is service account issuer discovery, stable since v1.21: the API server publishes /.well-known/openid-configuration and its signing keys, so AWS, Google Cloud or Entra ID can verify a pod's token like any other OIDC token. The subject is system:serviceaccount:NAMESPACE:NAME. A pod asks for a token with the audience the cloud expects:

# Kubernetes v1.33+: projected, audience-bound service account token
apiVersion: v1
kind: Pod
metadata:
  name: billing-exporter
  namespace: billing
spec:
  serviceAccountName: billing-exporter      # subject: system:serviceaccount:billing:billing-exporter
  automountServiceAccountToken: false       # no default API token; only the one below
  containers:
    - name: app
      image: ghcr.io/example/billing-exporter:1.4.2
      volumeMounts:
        - name: cloud-token
          mountPath: /var/run/secrets/cloud
          readOnly: true
  volumes:
    - name: cloud-token
      projected:
        sources:
          - serviceAccountToken:
              audience: api://AzureADTokenExchange   # match the cloud's expected audience
              expirationSeconds: 3600                # minimum 600
              path: token

AWS has two flavours on EKS. IAM Roles for Service Accounts (IRSA) is the plain OIDC version: one IAM OIDC provider per cluster. EKS Pod Identity drops the per-cluster OIDC provider in favour of a single pods.eks.amazonaws.com service principal, a node agent on link-local 169.254.170.23, and up to 5,000 associations per cluster. Pod Identity is simpler to operate but only works on EKS with Linux EC2 nodes; IRSA works anywhere you can publish a discovery document.

Where does SPIFFE fit?

SPIFFE solves the problem one level down: giving every workload a cryptographic identity in the first place, independent of any one cloud or orchestrator. A SPIFFE ID is a URI such as spiffe://prod.example.com/billing/exporter, delivered to the workload as an SVID, either a short-lived X.509 certificate or a JWT. SPIRE is the reference implementation that attests workloads (by Kubernetes pod metadata, AWS instance identity document and so on) and issues and rotates SVIDs. Both graduated from the CNCF in September 2022, and SPIRE is at v1.15.4, released on 15 October 2026.

SPIRE can act as an OIDC issuer, so a JWT-SVID can be exchanged with any of the three clouds like a GitHub or Kubernetes token, and Google's managed workload identities are built on SPIFFE identifiers. If you run workloads across clouds, on bare metal, or anywhere without a built-in token issuer, SPIFFE is how you get the "platform IdP" box in the diagram above. If everything runs in GitHub Actions and one managed Kubernetes service, you probably do not need it yet.

What workload identity federation does not fix

Federation removes the stored secret. It does not make a compromised workload trustworthy. Malware running inside your CI job can request its own OIDC token, or read the credentials the job just received, and use them before they expire, which is exactly where the Shai-Hulud worm sat on CI runners. Short lifetimes shrink the window from years to an hour; they do not close it. The defences are the boring ones: pin actions to commit SHAs, keep id-token: write on the deploy job only, scope the role tightly, and watch audit logs for odd usage. The GitHub Actions security piece goes deeper on runner hardening.

Nor does it decide what the workload may do. A federated role with AdministratorAccess is still an administrator, just one with better hygiene. AI agents make this old discipline matter more: an agent running tools in a pipeline or sandbox is a workload like any other and deserves its own identity, narrow role and audit trail, not a shared key in an environment variable. (Disclosure: I build PandaStack, a Firecracker microVM cloud for running agent code, so I spend a lot of time on exactly this boundary.) Federation gives each agent run a distinct, expiring identity; least privilege and logging are still your job.

FAQ

What is workload identity federation in simple terms?

It lets software log in to a cloud without a stored key. The platform it runs on vouches for it with a signed, short-lived token, and the cloud swaps that token for temporary credentials if it matches your rules. Nothing long-lived exists to leak or rotate.

Is workload identity federation the same as OIDC?

Not exactly. OIDC is the token and discovery standard most federation uses, while federation is the trust relationship that accepts tokens from an outside identity provider. AWS also federates with SAML, and Google Cloud can federate using X.509 certificates instead of OIDC tokens.

Does workload identity federation replace service account keys completely?

For CI/CD, Kubernetes and cross-cloud access, it can replace nearly all of them. A few tools and APIs still expect a key file, and some third-party SaaS integrations only accept static credentials. Treat every remaining key as an exception with an owner and an expiry date.

What is the difference between IRSA and EKS Pod Identity?

IRSA uses a per-cluster IAM OIDC provider and the pod's projected token, so it works on any cluster that publishes a discovery document. EKS Pod Identity uses a node agent and one pods.eks.amazonaws.com principal with no OIDC provider, but only runs on EKS with Linux EC2 nodes.

This quarter I would do three things in order. First, list every IAM user access key, Google service account key and Entra client secret, and flag the ones used from CI. Second, move those pipelines to OIDC with trust policies pinned to an environment, not a wildcard, and opt older repositories into GitHub's immutable subjects while I am in there. Third, for Kubernetes, switch to projected, audience-bound tokens and IRSA or Pod Identity, and set automountServiceAccountToken: false wherever the API token is not needed. None of it is new or clever. It just removes the line from the next breach report.

Related: Who Is This Agent? Non-Human Identity in 2026, From the RFCs to the Breaches, The Worms Learned to Use Your AI Agent: A Year of npm Supply-Chain Attacks and Policy as Code for AI Agents in 2026.

I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.

Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.


Related