An OIDC trust policy is a contract: the IdP picks the aud, the
relying party must accept it. If the IdP rotates the default audience
and the trust policy is not updated, every assumption is rejected.
Pin the expected audience in CI, alert on rotation, and treat the
audience string as a shared configuration value that the workflow and
the IAM trust policy must both reference.
← All break/fix scenarios in Git, CI/CD & GitOps
OIDC misconfiguration (audience mismatch)
Reported symptoms
- ●`aws sts assume-role-with-web-identity --role-arn <arn> --role-session-name <sess> --web-identity-token <gh-jwt>` returns `Error: InvalidClientTokenId: The security token included in the request is invalid`
- ●GitHub Actions workflow log shows the OIDC token issuance succeeded and the `configure-aws-credentials` step fails with `Error: Could not assume role with OIDC: InvalidClientTokenId`
- ●The IAM role trust policy uses `token.actions.githubusercontent.com:sub` as the `ForAnyValue:StringEquals` condition
- ●The GitHub Actions workflow uses `permissions: id-token: write` (correct) and the `configure-aws-credentials` action with `role-to-assume: <arn>`
- ●`cat <jwt> | jq` shows `aud: github-actions-oidc-token` and `iss: https://token.actions.githubusercontent.com`
- ●IAM CloudTrail shows `AssumeRoleWithWebIdentity` calls returning `Error: invalid client token id` for the affected role in the last 24 hours
- ●The trust policy was written for an older GitHub OIDC behaviour; GitHub has since changed the default audience
- ●Other workflows that call `assume-role-with-web-identity` directly (not through the action) fail with the same error
Evidence
- · `cat $ACTIONS_ID_TOKEN_REQUEST_TOKEN | jq` returns `aud: github-actions-oidc-token` and `iss: https://token.actions.githubusercontent.com`
- · `cat <token> | jq -r .sub` returns `repo:<org>/<repo>:ref:refs/heads/main` (correct)
- · The IAM role trust policy JSON shows `token.actions.githubusercontent.com:aud: sts.amazonaws.com` as the audience condition — this matches the old default but no token is issued with that audience
- · `aws sts assume-role-with-web-identity --role-arn <arn> --web-identity-token <token>` returns `InvalidClientTokenId`; the same call against a different IAM role whose trust policy uses `aud: github-actions-oidc-token` succeeds
- · `gh api repos/<org>/<repo>/actions/oidc/customization/issuer` returns the organisation OIDC issuer URL, which is `https://token.actions.githubusercontent.com`
- · `gh api repos/<org>/<repo>/actions/oidc/customization` returns the trust policy audit: the org has not enabled OIDC subject claim customization, but the audience is the new default
- · CloudTrail `aws:sts:assumerolewithwebidentity` event shows `requestParameters: {roleArn: <arn>, webIdentityToken: <truncated>}` and `errorMessage: Invalid client token id`
- · GitHub changelog notes that the default audience was changed to a configurable per-workflow string, breaking IAM trust policies that hard-coded `sts.amazonaws.com`
Diagnosis and resolutionclick to reveal
Root cause
OIDC trust policies are a contract between the IdP (GitHub, in this case) and the relying party (AWS IAM, the cloud provider, or the Kubernetes API server). The contract specifies the `aud` (audience) that the IdP will set in the token; if the relying party trust policy requires a different audience, the token is rejected. GitHub rotated its default audience for `id-token` from `sts.amazonaws.com` (the legacy default for AWS integration) to a configurable string defaulting to `github-actions-oidc-token`. The IAM role trust policy was written against the legacy default and was never updated. The structural failure is that the OIDC configuration was hard-coded in the trust policy with no monitoring of token claims, and no CI verifier that proves a freshly-issued token matches the trust policy expectations. The result is that every job that assumes the role fails until the trust policy is updated.
Remediation
Update the IAM role trust policy to match GitHub current audience. Replace `token.actions.githubusercontent.com:aud: sts.amazonaws.com` with the actual audience GitHub now issues. For most workloads, this is `github-actions-oidc-token`; for tighter scoping, use the per-workflow audience configured at the org level. Restrict the `sub` claim to the expected repositories and refs (`token.actions.githubusercontent.com:sub: repo:<org>/<repo>:ref:refs/heads/main`). Verify end-to-end: from the runner, `cat $ACTIONS_ID_TOKEN_REQUEST_TOKEN | jq .aud` and confirm it matches the trust policy; `aws sts assume-role-with-web-identity --role-arn <arn> --web-identity-token <token>` should return `Credentials` with `AccessKeyId`, `SecretAccessKey`, and `SessionToken`. Then add a CI verifier that runs on every workflow that uses OIDC: a `permissions: id-token: write` step that issues a token, decodes the JWT, and asserts the `aud` matches the IAM trust policy; if the assertion fails, the workflow fails before it tries to assume. Treat the audience string as a configuration value owned by both GitHub and the IAM trust policy, and version it in a single config file.
Verification
The IAM role assumption succeeds: `aws sts assume-role-with-web-identity --role-arn <arn> --web-identity-token <gh-jwt>` returns `Credentials`. The CI workflow `configure-aws-credentials` step exits 0. `cat $ACTIONS_ID_TOKEN_REQUEST_TOKEN | jq .aud` returns the same string that the trust policy expects. The CI verifier step passes on every run. A test invocation from a different repo (whose `sub` should not match the trust policy) is rejected with `AccessDenied`, confirming the `sub` restriction works. CloudTrail shows successful `AssumeRoleWithWebIdentity` events for the expected `sub` and no `InvalidClientTokenId` errors.
Prevention
OIDC audience is part of the trust contract. Make it explicit. Pin the expected audience in a single configuration value (e.g. `OIDC_AUDIENCE: github-actions-oidc-token` in the workflow and as a hardcoded string in the IAM trust policy), and add a CI step that decodes the JWT issued by GitHub and asserts `aud == OIDC_AUDIENCE`. Treat any change to the audience as a two-sided PR: the IAM trust policy and the workflow audience declaration must move together. Subscribe to GitHub OIDC changelog (or run a daily CI that fetches the OIDC config from `gh api repos/<org>/<repo>/actions/oidc/customization`) and alert on a change to the default audience. For higher assurance, scope the audience per workflow using GitHub `oidc-customization` API so the audience embeds the workflow name and the trust policy can require `aud: "https://github.com/<org>/<repo>/.github/workflows/<name>"`, narrowing the blast radius of a leaked token. The principle is that OIDC trust is a contract, and the contract terms are chosen by the IdP — the relying party must read the terms and pin its expectations in CI.