Cloud Infrastructure · Identity and Access
Federating Workloads with OIDC: Which Claims a Trust Policy Must Pin
How a workload proves its identity with a signed token instead of a secret, why audience and subject both need pinning, and where claim matching goes wrong.
A continuous integration job runs on infrastructure nobody in the organization owns. It needs to push an image to a registry in the organization’s account. The obvious mechanism is a long-lived key stored in the CI system’s secret store, and the obvious problem is that the key is valid from anywhere, forever, for whoever can read it. The alternative is for the CI system to sign a token asserting which repository and which branch the job is running for, and for the registry’s account to accept that assertion in place of a secret.
The token is an assertion by a third party
An OIDC identity provider publishes a discovery document at a well-known path relative to its issuer URL, containing the issuer identifier, the authorization endpoint, and a JSON Web Key Set URL. The key set is a list of public keys in JWK format, each with a key identifier. A token is a JWT whose header names the key identifier used to sign it, and whose payload contains claims: iss for the issuer, sub for the subject, aud for the audience, exp and iat for validity, plus whatever the issuer chooses to assert.
Verification is a sequence with several independent checks, and skipping any one of them is a known attack. The signature is verified against the key named in the header, but only after the key is fetched from the issuer’s published key set rather than from a URL in the token. The iss claim must equal the issuer the verifier expects. The aud claim must contain the verifier’s own identifier. The exp must be in the future and the iat not implausibly in the past. RFC 8725 collects these as best practice because each has been omitted by an implementation at some point.
The cloud-side trust policy is where the claims become an authorization decision. The provider is registered in the account with its issuer URL and an audience, and roles carry a trust policy with conditions on iss and sub, and on any other claim the issuer exposes. Assumption then proceeds through the same token exchange as any other role, described in role assumption and session tokens, with the JWT standing in for the caller’s credentials.
Subject is a string with a structure the verifier did not define
The sub claim is opaque to the verifier in the protocol. The issuer defines its format, and the trust policy matches it as a string. For a source-control platform, the subject is often a structured path identifying the repository and the ref, something like a repository identifier followed by a branch or tag reference.
This is where most federation policies are too broad. A condition that matches the subject with a wildcard over the repository portion allows any branch of any repository in the organization to assume the role. A condition that matches only the repository and not the ref allows any branch, including a branch created by anyone with write access to the repository, to assume the role. Since the role can push images, the effective permission is “anyone who can open a pull request can ship to production”, which is a sentence no reviewer would approve if it were written plainly.
The corrective is to pin the subject as tightly as the issuer’s format allows, and to understand that the format is the issuer’s, not the verifier’s. A subject that includes a branch name can be pinned to the default branch. A subject that includes a tag reference can be pinned to tags matching a release pattern. A subject that includes a environment or a workflow name can be pinned to the workflow that performs deployments. None of these are expressible unless the issuer puts them in the subject, and an issuer that puts only the repository in the subject cannot be pinned more tightly than the repository.
That limitation is a property of the issuer and cannot be fixed on the verifier’s side. When it exists, the compensating control is to make the role’s permissions narrow enough that repository-level pinning is acceptable, and to require a second approval step inside the pipeline. The alternative of assuming the subject is more specific than it is produces a policy that reads as tight and matches broadly.
Audience prevents the token from being replayed elsewhere
The aud claim exists because a token signed by a widely used issuer is a bearer token for whoever accepts it. A CI platform signs tokens for many relying parties, and the token minted for one registry should not be usable at another. The audience is the relying party’s identifier, and the verifier must reject a token whose audience does not include it.
RFC 8725 is explicit that the audience check is mandatory and that a verifier must not accept a token because the signature and issuer are valid. The failure mode without it is cross-service token reuse: an attacker who can obtain a token for a low-privilege service, or who can cause one to be minted, presents it to a high-privilege one, and the high-privilege service accepts it because the issuer is trusted.
The cloud-side equivalent is the audience configured when the provider is registered. A provider registered with a placeholder audience, or with an audience that matches a generic value shared by many integrations, weakens the check to nothing. The audience should be the specific identifier the issuer mints for this relying party, and the trust policy should test it explicitly even when the provider registration already constrains it, because the two are edited by different people at different times.
Conditions that must be present, not merely possible
A trust policy for a federated provider typically contains a condition on the issuer, a condition on the audience, and a condition on the subject. The subject condition is the one that carries the authorization intent, and it is the one most often written as a wildcard.
Two structural rules follow. First, the trust policy should require the subject to be present, not merely match if present. A condition on a claim that is absent from the token does not match, so absence fails closed, which is the right direction. The problem arises with StringLike against a pattern that can match an empty string, which converts absence into a match. Patterns should start with a non-wildcard character.
Second, the policy should constrain any claim that distinguishes a production path from a test path. If the issuer emits a claim naming the environment, an event name, or a workflow, and the trust policy does not test it, then the distinction the issuer carefully encoded is discarded at the boundary. The issuer’s claims are an interface, and a verifier that ignores half the interface has a weaker policy than the issuer intended.
Key rotation and the fetch path
The signing keys rotate. The key set endpoint publishes the current keys, and a token signed with a retired key fails verification. A verifier that caches the key set indefinitely will start rejecting valid tokens at the first rotation; a verifier that fetches on every verification adds a network call to every request and makes the issuer a hard dependency in the request path.
The standard resolution is to cache the key set, keyed by the key identifier in the token header, and to refetch when a token presents a key identifier not in the cache. That refetch is rate-limited, because otherwise an attacker can force a fetch per request by presenting tokens with random identifiers. The cache lifetime and the refetch policy are the two knobs, and their settings determine how long a rotation takes to propagate and how much load a bad actor can place on the issuer.
The cloud token service performs this on the verifier’s behalf, which is the main practical advantage of using a registered provider rather than verifying tokens in application code. The service holds the key set, applies the rate limit, and performs the signature check before the trust policy conditions are evaluated. An application that verifies JWTs itself takes on all of it, and the common failures are the ones listed above: trusting a key URL from the token header, skipping the audience check, or accepting an algorithm the issuer never uses.
That last one deserves naming. A JWT header declares its algorithm, and a verifier that honours the declared algorithm can be induced to accept a token signed with a symmetric algorithm using the public key as the secret, or with none. The defence is to accept exactly the algorithms the issuer is known to use, which is a fixed list determined at registration time and never by the token.
Where federation ends
Federation removes the stored secret from the relying party’s side. It does not remove the trust in the issuer: if the issuer mints a token for a job that should not have one, the relying party cannot tell. The issuer’s own access controls become part of the relying party’s security boundary, and the trust policy’s conditions are the only place where that boundary is narrowed.
The practical consequence is that the conditions should encode the relying party’s requirements, not the issuer’s defaults. The issuer asserts a subject; the relying party decides which subjects are acceptable for this role. When the role is broad, the subject must be narrow. When the subject cannot be narrowed, the role must be narrow. The two cannot both be loose, and a policy where both are loose is not a federated trust relationship but an open door with a signature on it.
For workloads on the provider’s own compute, the same reasoning applies to the instance identity document, which is a signed assertion by the provider and is exchanged for credentials by a service that trusts the provider’s key. That path, and its dependence on network position for the non-federated case, is in the instance credential chain. The difference between the two is which party is trusted to assert identity, and how much of the assertion the relying party chooses to verify.
References
- OpenID Connect Core 1.0 — OpenID Foundation
- RFC 7517: JSON Web Key (JWK) — IETF
- RFC 8725: JSON Web Token Best Current Practices — IETF
- Creating OpenID Connect (OIDC) identity providers — AWS Identity and Access Management User Guide