Skip to main content

GitHub Actions · troubleshooting

Not authorized to perform sts:AssumeRoleWithWebIdentity

GitHub issued a valid OIDC token and AWS refused to exchange it. Almost never a permissions problem — the role's trust policy is comparing the token's sub claim against a string that does not match it.

Run this first

step 1 of 4
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity --max-results 5

Look for The sub in the failed event, and whether it carries @<digits> segments

CloudTrail records the claims AWS actually received, which is the only place the real sub value exists. Everything else is a reconstruction of what you think GitHub sent.

Work out which cause you have

A few questions to narrow the list. Every answer ends in a command that confirms or rules the cause out — this cannot see your cluster, so nothing here is a certainty until you have checked.

Narrow it down

0 answered · nothing is sent anywhere

Does the workflow job that calls AWS declare permissions with id-token: write?

Without it GitHub mints no token at all, and the exchange fails before any policy is consulted.

Cause space

5 of 5 still possible

  • The trust policy's sub condition does not match the tokenCommon
  • The repository emits the immutable subject claimCommon
  • The workflow never requested a tokenCommon
  • The audience claim does not matchOccasional
  • The OIDC provider is absent or has the wrong thumbprintOccasional

Nothing ruled out yet. Answer the question above and the branches your answer eliminates will strike through here.

Check it with a tool

Or diagnose it manually

In this order. The first command usually contains the whole answer.

  1. Step 1

    aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity --max-results 5

    CloudTrail records the claims AWS actually received, which is the only place the real sub value exists. Everything else is a reconstruction of what you think GitHub sent.

    Look for The sub in the failed event, and whether it carries @<digits> segments

  2. Step 2

    aws iam get-role --role-name <role> --query 'Role.AssumeRolePolicyDocument' --output json

    Puts the condition next to the claim so the comparison can be read rather than guessed at. The two strings differ somewhere, and this is where.

    Look for StringEquals where StringLike is needed, a missing :ref: segment, or a sub written for a different branch

  3. Step 3

    grep -n -A3 'permissions:' .github/workflows/<workflow>.yml

    Rules out the case where there was never a token to reject, which no amount of IAM inspection will reveal.

    Look for id-token: write present on the job that authenticates

  4. Step 4

    aws iam list-open-id-connect-providers

    Confirms AWS trusts the issuer at all before you spend time on claim shapes.

    Look for A provider ARN for token.actions.githubusercontent.com

Every cause, and how to fix it

Ordered by how often each one turns out to be the answer.

The trust policy's sub condition does not match the token

Common

The condition compares a literal string against the token's sub claim, and they differ — usually in the ref. `repo:org/repo:ref:refs/heads/main` matches a push to main and nothing else: not a pull request, not a tag, not another branch. StringEquals makes this exact, so a workflow that changed trigger stops working without anything in AWS having changed.

Confirm

aws iam get-role --role-name <role> --query 'Role.AssumeRolePolicyDocument' --output json

The token.actions.githubusercontent.com:sub condition, and whether its value matches the ref your workflow actually runs on

Fix

  • Compare the condition against the ref this workflow runs on, character by character, rather than against the one you remember writing it for.
  • Use StringLike with a wildcard for the ref segment when the role legitimately serves several refs — for example repo:org/repo:* — and keep the repository part exact.
  • Never widen the repository part to a bare wildcard. A sub condition of repo:*:* lets any repository on GitHub assume your role.

The repository emits the immutable subject claim

Common

GitHub documents an immutable form of the sub claim, carrying numeric owner and repository IDs, for repositories created after 15 July 2026. It looks like repo:my-org@123456/my-repo@456789:ref:refs/heads/main. A trust policy written against the name-only form does not match it, so a newly created repository fails while every older one keeps working — which reads as a repository-specific fault rather than a policy one.

Confirm

aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity --max-results 5

The sub value in the failed event. An @ followed by digits in the org or repo segment means this repository is on the immutable claim

Fix

  • Make the condition a list that accepts both shapes, rather than replacing one with the other — the old form is still what existing repositories send.
  • Read the failing sub out of CloudTrail rather than constructing it by hand; it is the only copy that is definitely what AWS received.
  • Note the numeric IDs are not available on GitHub Enterprise Server, so a policy shared between github.com and GHES needs both forms anyway.
gh api /repos/<org>/<repo>/actions/oidc/customization/sub

Shows the subject claim customisation this repository uses. Requires the gh CLI, authenticated with access to the repository.

The workflow never requested a token

Common

A workflow gets no OIDC token unless it asks for one. Without `permissions: id-token: write`, the action has nothing to present and the exchange fails before any policy is consulted. This is the version that fails on the very first attempt, and it is a one-line fix in the workflow rather than anything in AWS.

Confirm

grep -n -A3 'permissions:' .github/workflows/<workflow>.yml

id-token: write — absent, or set at a job that is not the one calling AWS

Fix

  • Add `permissions:` with `id-token: write` and `contents: read` to the job that authenticates, not only at workflow level if other jobs override it.
  • Remember that declaring any permissions block narrows every permission not listed, so contents: read usually has to be stated alongside it.

The audience claim does not match

Occasional

The token's aud claim identifies who the token is for. GitHub's AWS documentation expects sts.amazonaws.com. A custom audience configured on the action, or a trust policy checking a different value, fails the same way a sub mismatch does — and looks identical in the error text.

Confirm

aws iam get-role --role-name <role> --query 'Role.AssumeRolePolicyDocument' --output json

The token.actions.githubusercontent.com:aud condition, compared against any audience the workflow sets explicitly

Fix

  • Leave the audience at the default unless something specifically requires otherwise, and make the trust policy match it exactly.
  • If the workflow sets an audience input, the trust policy has to be changed at the same time — they are one setting in two files.

The OIDC provider is absent or has the wrong thumbprint

Occasional

AWS will not accept a token from an issuer it has no identity provider for. In a fresh account, or one where the provider was removed during cleanup, the exchange fails before claims are examined. Distinguished from the claim mismatches by affecting every workflow and every repository at once rather than one of them.

Confirm

aws iam list-open-id-connect-providers

An ARN containing token.actions.githubusercontent.com. No such entry means the provider does not exist in this account

Fix

  • Create the identity provider for token.actions.githubusercontent.com in the account that owns the role.
  • Create it once per account — the role can then be assumed from any repository the trust policy admits.

Understanding it properly

Skip this if you are mid-incident — the working part of the page is above. Worth reading afterwards, because understanding the mechanism is what stops the next one.

What is actually happening

Keyless CI works by exchange rather than by storing a key. GitHub mints a short-lived OIDC token describing the workflow, and AWS trades it for temporary credentials if — and only if — the token matches what a role's trust policy says it will accept. This error is that trade being refused.

The wording sends almost everyone the wrong way. "Not authorized to perform sts:AssumeRoleWithWebIdentity" reads like a missing permission, so people go and widen an IAM policy. The permission is not the thing being checked. AssumeRoleWithWebIdentity is authorised by the role's *trust* policy — its condition block — and what fails there is a string comparison against the token's claims.

Which claim, almost always, is `sub`. GitHub builds it from the repository and the ref, and the trust policy has to match that exact shape. A policy written for one branch rejects a tag; a policy written without the ref segment rejects everything.

Since 15 July 2026 there is a second shape to know about. GitHub's documentation records that repositories created after that date emit an immutable subject claim carrying numeric IDs — `repo:my-org@123456/my-repo@456789:ref:refs/heads/main` — rather than the name-only form. A trust policy matching only the old shape works for every existing repository and rejects every new one, which is why this tends to arrive as "it works in the old repo and not the new one".

How to tell this is your problem
WhereWhat you see
The workflow log"Could not assume role with OIDC" followed by "Not authorized to perform sts:AssumeRoleWithWebIdentity"
Where it failsThe aws-actions/configure-aws-credentials step, before any AWS call your workflow makes
CloudTrailAn AssumeRoleWithWebIdentity event with an AccessDenied error code

How to know it is actually fixed

  • The configure-aws-credentials step completes and prints an assumed-role ARN.
  • aws sts get-caller-identity in a later step returns the role you expected, not a leftover static key.
  • Re-run the workflow from a second trigger — a tag as well as a branch push, if both are meant to work. A policy that matches one ref and not the other passes this test only once.
  • A CloudTrail AssumeRoleWithWebIdentity event appears with no error code.

Stopping it happening again

  • Keep the repository segment of the sub condition exact and wildcard only the ref. A wildcard in the repository position is an open door for any repository on GitHub.
  • Write the condition as a list covering both the name-only and the immutable subject shapes, so a repository created after July 2026 does not need a policy change to deploy.
  • Set permissions explicitly on the authenticating job rather than relying on a workflow-level default that a later edit can narrow.
  • Prefer this over long-lived access keys regardless — the failure mode here is a workflow that cannot deploy, which is a great deal better than a key in a log.

Did this get you to an answer?

No text box on purpose — please do not paste production logs anywhere

Sources

Behaviour described here is drawn from official documentation. Where a figure could not be confirmed on an official page it is attributed in the text rather than stated as canonical.

Related