Secrets, PKI & CertificatesXIII · Dynamic Credentials and Workload IdentityDynamicCredentials
Workload identity: exchanging a platform-issued token for a short-lived credential
What you'll learn
- Describe the token exchange from platform issuance to short-lived credential
- Identify which claims a relying party must pin and which are insufficient alone
- Predict the consequence of pinning only the repository in a trust condition
- Recognise a subject format that will stop matching after a rename or transfer
Prerequisites
Verified against OpenSSL 3.5.x teaching target; 3.0+ minimum · OpenSSH 10.x teaching target; 8.2+ minimum for certificate workflows · OpenBao 2.6.x · Smallstep step-ca 0.30.x · Certbot / Pebble Certbot current release; Pebble 2.10.x ACME test server · Kubernetes (cross-course target) 1.36.x · PostgreSQL 17.x · 2026-08-26
Workload identity federation is the general form of the pattern the previous lesson described in the abstract. A platform issues a short-lived, signed token containing claims about the workload. The workload presents that token to a resource provider. The provider validates the signature against the issuer’s published keys, checks the claims against a condition registered in advance, and returns a short-lived credential of its own. At no point does anything store a long-lived secret, which is why the pattern has displaced access keys in pipelines almost entirely.
The exchange, in three moves
The three parties have distinct jobs, and confusing them is the source of most misconfiguration. The identity provider knows the workload and signs statements about it. The workload is a courier; it cannot forge the token and cannot alter it. The relying party knows nothing about the workload except what the token says and what its own configuration says it will accept.
flowchart LR
A["Platform\nissues signed token"] --> B["Workload\npresents the token"]
B --> C["Relying party\nfetches issuer keys"]
C --> D{"Signature valid\nand claims match?"}
D -- "yes" --> E["Short-lived credential\nreturned to the workload"]
D -- "no" --> F["Rejected\nno credential issued"]
The security of the whole arrangement rests on the last decision node, and specifically on how narrowly the condition was written. A signature check alone establishes that the platform issued the token. It says nothing about which workload received it, or which relying party was supposed to receive it. Those are separate claims, and they have to be checked separately.
Requesting the token: the workload’s half
In a pipeline the token has to be requested explicitly, because issuing one to every job by default would hand a signed identity to every third-party action that runs. In GitHub Actions the permission is declared in the workflow, and without it the request simply fails:
permissions:
id-token: write
contents: read
The id-token: write permission does not grant write access to
anything else; it grants the ability to request the token. The job then
retrieves it from the platform through the request URL and bearer token
placed in its environment, or through the helper in the Actions
toolkit, and receives a JWT whose claims describe the repository, the
workflow and the reference being built.
The equivalent in a cluster is a projected ServiceAccount token requested with the relying party’s audience, which the previous lesson showed. The cluster publishes an OpenID provider configuration and a key set so that an external system can validate those tokens without calling the API server. Two details matter operationally. Those documents are deliberately OIDC compatible rather than strictly OIDC compliant, carrying only what is needed to validate a service account token, so an integration expecting a full provider will be disappointed. And access to them is gated by role-based access control rather than public by default, so exposing the key set to an external relying party is an explicit decision an administrator has to make.
The trust condition: the relying party’s half
On the provider side, the condition is where the design succeeds or fails. The token arriving from a pipeline carries an issuer, an audience and a subject, and all three have to be checked:
{
"iss": "https://token.actions.githubusercontent.com",
"aud": "sts.amazonaws.com",
"sub": "repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main"
}
The issuer establishes which platform is being trusted at all. The audience establishes which relying party the token was minted for, and it is what prevents a token issued for one provider from being replayed at another. The subject establishes which workload inside that platform is being trusted, and it is the claim most often left wide.
On the AWS side the exchange is AssumeRoleWithWebIdentity, which is
documented as requiring no AWS credentials of its own, which is the
entire point: a distributable application can obtain temporary
credentials without shipping long-term ones. It returns an access key,
a secret key and a session token, with a default duration of 3600
seconds and a permitted range from 900 seconds to 43200 seconds, capped
by the role’s own maximum session duration. Three failure codes are
worth memorising because they discriminate cleanly between causes.
InvalidIdentityToken means the token could not be validated at all.
ExpiredToken means it was valid and is no longer. IDPRejectedClaim
returns a 403 and means the claims did not satisfy the condition, which
is the one that follows a subject format change.
What must be pinned, and what happens when it is not
The single most common mistake is to pin the repository and stop. Consider what the subject claim actually distinguishes:
repo:ORG/REPO:ref:refs/heads/BRANCH
repo:ORG/REPO:ref:refs/tags/TAG
repo:ORG/REPO:environment:NAME
repo:ORG/REPO:pull_request
A condition that matches any subject beginning with the repository accepts all four of those. That means any branch anyone can push to the repository, any tag anyone can create, and any pull request workflow that runs in the repository context can assume the role. The intended grant was “our deployment pipeline”; the actual grant is “anyone who can cause a workflow to run in this repository”. On a repository where a hundred people can open a branch, that is a hundred people with the role’s permissions.
Pin the audience and the subject together, and make the subject as specific as the workflow allows: a named environment where protection rules gate who can deploy, a specific reference where the branch is protected, or the workflow reference itself where the same role serves several callers. Each additional pinned claim removes a population of principals who could otherwise obtain the credential.
The subject format changed, and stale policies break silently
There is a live trap here for anyone copying an older example. Repositories created after 15 July 2026 use an immutable default subject format that embeds numeric identifiers alongside the names:
repo:OWNER@OWNER-ID/REPO@REPO-ID:ref:refs/heads/BRANCH
Repositories created before that date keep the previous format unless they opt in, but a rename or a transfer after that date also moves them to the immutable form. The change exists for a good reason: a subject built from names alone stops identifying the same repository once a name is reused, and an attacker who can claim an abandoned organisation name inherits its trust conditions. The operational consequence is that a trust policy written against the old format keeps working until somebody renames a repository, and then fails with a rejected-claim error that names no cause.
Production discipline
- Pin the issuer, the audience and the subject, always all three. Any condition missing one of them accepts a population you did not intend to name.
- Make the subject as narrow as the workflow permits. Prefer a protected environment or a specific reference over a repository-wide match, and review the condition when the pipeline changes shape.
- Request a distinct audience per relying party. A token that only one system accepts cannot be replayed at another, which removes the documented multi-audience impersonation path entirely.
- Record the subject format your policies assume. After a rename or transfer the format may change underneath you, so a documented assumption turns a mystery outage into a two-minute fix.
- Keep the returned credential short. The default exchange duration is an hour and the ceiling is twelve; a pipeline step that needs twelve hours of cloud credentials is a design to revisit rather than a duration to raise.
Cross-course references
- Git, CI/CD & GitOps for Infrastructure Engineers - Part XLIII (OIDC) covers the pipeline side of this exchange in depth, including how the identity token is requested inside a job.
- Kubernetes for Production Sysadmins - Part LVIII (RBAC) covers the authorisation model that gates access to the cluster’s own issuer discovery documents.
- Terraform for Production Sysadmins - Part XXII (CI-CD) covers running infrastructure changes from a pipeline, which is the workload that most often needs these credentials.
Quiz
Knowledge check · 4 questions
Q1. A relying party validates the token signature and pins the issuer, but matches any subject beginning with the repository name. What has actually been granted?
Q2. Requesting a distinct audience for each relying party removes the impersonation path in which one recipient replays a workload's token at another.
Q3. A federated exchange that worked for two years starts returning a rejected-claim error immediately after a repository was renamed. Explain the likely cause.
Q4. Review the design and state what you would change before it ships.
A platform team is replacing static cloud access keys in forty pipelines with federated identity. The proposed trust condition pins the issuer and matches any subject for the organisation. The requested credential duration is set to 43200 seconds so that long infrastructure applies never expire mid-run. Nobody has decided on an audience value.
Passing score: 75%. Answers are checked in this browser.