Enabling it
SetAPP_OIDC_ISSUER_URL on the control plane to a real, reachable HTTPS URL, typically the same URL the dashboard is served on:
oidc: on a job fails with a clear error rather than minting a token nothing can verify: the issuer URL is also where a provider fetches the JWKS document to check the signature, so an unreachable or wrong URL breaks federation silently otherwise.
Check whether it’s configured:
Job configuration
audience is required and becomes the token’s aud claim, the value the provider’s trust policy checks. The token arrives as the PIPELINE_OIDC_TOKEN env var, masked in logs the same way a secret is, valid for 10 minutes (APP_OIDC_TOKEN_TTL overrides this).
Multiple audiences in one job
A job whose steps need tokens for more than one provider (one step calls AWS, another GCP) lists every extra audience underaudiences:
audience (singular) still pre-mints a token at job start into PIPELINE_OIDC_TOKEN, unchanged. Every audience listed under audiences, or audience itself, can also be requested at runtime instead: the same “request a token for a given audience” pattern GitHub Actions’ own ACTIONS_ID_TOKEN_REQUEST_URL/ACTIONS_ID_TOKEN_REQUEST_TOKEN uses, rather than pre-minting every combination up front.
PIPELINE_OIDC_REQUEST_URL and PIPELINE_OIDC_REQUEST_TOKEN (masked in logs like the token itself) are injected whenever a job’s oidc config allows more than one audience. The URL is a local endpoint reachable only from inside a job’s own container on this host, never exposed externally; the bearer token scopes each request to that one job run and its own allowed audiences. GET $PIPELINE_OIDC_REQUEST_URL?audience=<aud> returns {"token": "<jwt>"} for any audience the job listed, and a 403 for one it did not.
This requires the control plane to have resolved a container-reachable address for the endpoint at startup (logged as a warning if it could not, for example no Docker bridge network); a job listing audiences fails clearly at start rather than getting an unreachable URL if that did not happen.
Claims
sub is repo:<app>:ref:<git ref>:job:<job key>. Match on sub, repo, or ref in a trust policy depending on how tightly scoped the role should be: a wildcard on ref trusts every branch, refs/heads/main exactly trusts only the main branch.
The signing key is ES256 (ECDSA P-256), generated once and persisted encrypted at rest, separate from the app secrets it never touches.
JWKS
GET /.well-known/jwks.json on the control plane serves the public key, unauthenticated (this is how every OIDC verifier discovers it) and rate-limited per IP (APP_OIDC_JWKS_RATE_LIMIT_PER_MINUTE tunes the limit). Point AWS, GCP, or Vault’s OIDC configuration at the issuer URL above; each fetches this path itself.
Wiring to a provider
- AWS IAM
- GCP
- Vault
- IAM > Identity providers > Add provider > OpenID Connect.
- Provider URL: the issuer URL (
APP_OIDC_ISSUER_URL). - Audience: the same value the job’s
oidc.audienceuses (sts.amazonaws.comis the AWS convention, but any string both sides agree on works, since this isn’t a fixed AWS credential exchange, it’s asts:AssumeRoleWithWebIdentitycall your job itself makes).
- Provider URL: the issuer URL (
- Create a role with a trust policy:
- Set
AWS_ROLE_ARNas a job env var (or a secret) to the role’s ARN, and callaws sts assume-role-with-web-identityas in the example above.
Key rotation
Rotate the signing key from the CLI:/.well-known/jwks.json alongside the new one until --retire-after elapses (default 24 hours, or set APP_OIDC_KEY_RETIRE_GRACE), so a token minted moments before rotation, and any provider still holding an unrefreshed copy of the JWKS document (AWS, GCP, and Vault all cache it), keeps verifying. Only once that window passes does the old key actually disappear from the published set; nothing removes it sooner. Rotating again before an earlier key’s window elapses keeps both old keys published until each retires on its own schedule.
What this does not cover
- No UI to browse past-issued tokens or their claims; nothing is persisted beyond the signing key set itself, by design, since a token is meant to be short-lived and never logged.
- No way to force-remove a retiring key before its
retire_afterdeadline from the CLI or UI; that override exists internally for a genuinely compromised key but is not wired to an operator action yet.
Next steps
Pipelines
The pipeline format and how runs execute.
Security
How secrets and credentials are handled.