mishraJi

Workload Identity Federation

Introduction

A GitHub Actions pipeline needs to deploy resources into an AWS account. Before AWS will accept the API calls, the pipeline has to prove who it is.

For a long time the answer was a static credential. You created an IAM user in AWS, generated an access key for it, and pasted the key ID and secret into GitHub Secrets.

A GitHub Actions workflow authenticating to AWS with a static access key stored in GitHub Secrets.

A GitHub Actions workflow authenticating to AWS with a static access key stored in GitHub Secrets.

That key was a long-lived credential. It did not expire and it did not rotate. Anyone who obtained a copy of it could make authenticated API calls to AWS as that IAM user.

This is where the problems started. Keys got committed to Git and stayed in the history after being removed from the code. They got shared over chat to unblock a deployment. They outlived the repositories they were created for, because nobody knew which pipeline was still using them. And because they never expired, a key that leaked three years ago was still a valid credential today.

GitHub and AWS are just the example. The same shape appears anywhere one system calls another’s API. A static credential is copied from the system being called into the system doing the calling, and it stays there.

The rest of this post is about how the industry moved away from this — first by shortening the life of the credential on the wire, and eventually by removing the stored secret altogether.

📖 Some background knowledge of OAuth and OIDC is recommended but not necessary for this post.

The First Fix: OAuth 2.0 Client Credentials

The first real improvement came with OAuth 2.0 in 2012. One of its grant types, called client credentials, was designed exactly for this machine-to-machine case.

The calling system is no longer given a credential that works directly against the API it wants to reach. Instead it is given a client ID and a client secret. Before making an API call, it sends those to an authorization server and asks for an access token. The authorization server verifies the client secret and returns a short-lived access token. The caller uses that access token for its API calls, and when it expires, it goes back and asks for a new one.

The authorization server is a third party in this arrangement. Its only job is to accept a client secret and hand back a short-lived token that the API will accept.

OAuth client credentials flow to get a short-lived access token for calling an API.

OAuth client credentials flow to get a short-lived access token for calling an API.

The improvement is real. The credential that travels on every API call is now short-lived. If an access token leaks from a log file or a proxy, it is useless within the hour. There is no longer a single permanent value that grants access forever.

But look at what did not change. The caller still has a client secret sitting in a config file. That secret typically does not expire. It does not rotate on its own. It gets committed to Git and shared over chat, exactly like the static access key did. Anyone who steals it can walk up to the authorization server and mint fresh access tokens for as long as the secret remains valid.

So the client credentials flow did not remove the stored secret. It moved it one layer back and shortened the life of what sits in front of it. The blast radius got smaller. The root problem stayed.

Removing the Secret Entirely

The next step asks a different question. Instead of “how do we protect the stored secret,” it asks “why does the caller need a stored secret at all?”

Go back to the GitHub Actions pipeline. Where does it actually run? It runs on GitHub. And GitHub already knows everything about it — which repository it belongs to, which branch triggered it, which workflow file is executing. GitHub does not need to authenticate that pipeline, because GitHub is the one running it.

That is the insight. The platform executing a workload is in a position to vouch for its identity, and it can do so without the workload holding any secret at all.

When the workflow starts, it asks GitHub’s OIDC provider for a token, and gets back a short-lived JWT signed with GitHub’s private key. The claims in that token describe the workload — the issuer, a subject identifying the exact repository and branch, the intended audience, and an expiry measured in minutes. It is a signed statement that says: this is a workflow running from this repository, on this branch, right now.

The pipeline then presents that token to AWS. AWS has been configured in advance to trust tokens issued by GitHub, and it fetches GitHub’s public keys to verify the signature. It checks the issuer, the audience, and the expiry, and then compares the subject claim against conditions defined on an IAM role — conditions that name a specific organization, repository, and branch. If everything matches, AWS STS returns temporary credentials for that role, valid for an hour.

The pipeline obtains an ID token from GitHub and exchanges it with AWS STS for temporary AWS credentials.

The pipeline obtains an ID token from GitHub and exchanges it with AWS STS for temporary AWS credentials.

This is workload identity federation.

Notice what is gone. There is no AWS access key in GitHub Secrets. There is no client secret in a config file. There is no value to rotate, and nothing left behind in AWS when the repository is deleted. The only credential the pipeline ever holds is a token it obtained seconds ago that expires in minutes, and a set of AWS credentials that expire in an hour.

The trust is not carried by a shared secret. It is carried by a cryptographic signature from a platform that both sides already trust.

GitHub and AWS are one pairing. The same mechanics run with GitLab, with Kubernetes clusters issuing tokens for their pods, and with GCP and Azure on the receiving end. What matters is the shape: a platform that knows what it is running, and a resource provider that trusts that platform’s signature.

What Protocol Is This, Exactly?

Workload identity federation is not a new protocol. It is two existing standards used together. Before seeing how they combine, it helps to be clear on what each one does on its own.

OAuth 2.0 is an authorization protocol. Its job is to get a short-lived access token to the client so it can make the API call. The token says what the holder is allowed to do — it says nothing about who they are. In the client credentials flow described earlier, the client sends its client secret to an authorization server and gets a short-lived access token back.

OpenID Connect is an identity layer built on top of OAuth 2.0. It adds the ID token, a signed JWT containing claims about who the authenticated user is — issuer, subject, audience, expiry. In a normal OIDC login, the identity provider authenticates the user and then issues both the ID token and the access token. The relying party reads the ID token to learn who the user is, and uses the access token for API calls. Both tokens come from the same party.

Now the combination.

The token GitHub issues to the workflow is an OIDC ID token. It follows the specification exactly. What is missing is the step that normally comes first — the user authentication. GitHub does not need it, because it is the one executing the workload and already knows its identity. It issues the ID token directly.

The second half diverges from OIDC. GitHub issues only the ID token. It never issues an access token, and it has no authority over anything in the AWS account. The workload carries that ID token to AWS, which validates it and issues its own credentials. One security token traded for another, at a different party — that is the OAuth 2.0 Token Exchange pattern, described in RFC 8693.

So the split is this. OIDC supplies the identity. OAuth token exchange converts that identity into usable credentials. The identity provider proves who the workload is; the resource provider decides what it can do. Those two responsibilities, which sit together in a standard OIDC login, are pulled apart and handled by two different parties.

That separation is the point. It is what lets a resource provider grant access to a workload running on infrastructure it does not control, without either side ever holding a shared secret.

What Protocol Is This, Exactly?

Workload identity federation is not a new protocol. It is two familiar flows stitched together. To see the join, it helps to look at what each one does when a human is involved.

OAuth 2.0: exchanging a code for an access token

OAuth 2.0 is about authorization. Its most common flow is the authorization code flow. A user logs in at the identity provider, approves the request, and the identity provider hands back an authorization code. That code is not a credential. It cannot be used to call an API. It is a short-lived voucher, and the application has to take it to the identity provider’s token endpoint and trade it in. What comes back is an access token, and that is what the application uses to call the API.

The part worth holding on to is the shape: a short-lived artifact is presented somewhere and exchanged for a usable credential.

OpenID Connect: proving who the user is

OpenID Connect is an identity layer on top of OAuth 2.0. It adds the ID token, a signed JWT with claims about the authenticated user — issuer, subject, audience, expiry. The identity provider authenticates the user and issues that ID token. The relying party verifies the signature and reads the claims to learn who logged in.

The part worth holding on to here: the identity provider issues a signed statement of identity, and the relying party verifies it.

Now put the two halves together

In workload identity federation, GitHub is the identity provider and AWS is the relying party.

The first half is OIDC. GitHub issues an ID token for the workflow — a signed JWT with an issuer, a subject naming the repository and branch, an audience, and an expiry. It is a normal OIDC ID token. The only thing missing is the authentication step that usually comes first, because GitHub is running the workload and already knows what it is.

The second half is OAuth. The workflow takes that ID token to AWS and trades it in. AWS verifies the signature, checks the claims against the IAM role’s trust policy, and returns temporary credentials. The ID token plays the role the authorization code played — a short-lived voucher, presented and exchanged for something usable.

So the flow is half OIDC and half OAuth. GitHub proves who the workload is. AWS decides what it can do.

The one thing that is genuinely different

In a normal OIDC login, the identity provider issues both tokens. It authenticates the user, issues the ID token, and issues the access token. Everything comes from one party.

That is not the case here. GitHub issues only the identity token. It has no authority over anything in the AWS account and cannot issue AWS credentials. AWS issues those, and it does so only after validating a token that GitHub signed.

Normal OIDC login Workload identity federation
Who is authenticated a human user a workload
Authentication step user logs in skipped — the platform already knows
Who issues the identity token identity provider identity provider (GitHub)
Who issues the access credential identity provider Resource Provider (AWS)
What is exchanged authorization code ID token

That split is the whole point. The identity provider vouches for the workload. The resource provider grants the access. Neither side has to hold a secret belonging to the other, which is what makes it possible for AWS to trust a workload running on infrastructure it does not control.

One note for precision. The exchange at AWS is not literally the authorization code grant — that grant trades a code at the identity provider’s own token endpoint. Here the token is presented to a different party entirely. The formal name for that pattern is OAuth 2.0 Token Exchange, described in RFC 8693. The authorization code flow is the right thing to picture, but the specification to cite is that one.

Conclusion

We started with an AWS access key pasted into GitHub Secrets, valid forever and dangerous the moment it leaked. OAuth’s client credentials flow shortened the life of what travelled on the wire, but the client secret stayed behind in a config file. Workload identity federation removes that last secret!

References