Skip to main content
DevOps Insightsguides & answers

GitHub Actions · troubleshooting

Not authorized to perform sts:AssumeRoleWithWebIdentity

GitHub issued a valid OIDC token and AWS refused to exchange it for the role's temporary credentials. 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.

First published

New to this? Read what is actually happening first, then come back to the steps.

On this page · 10 sections

Before you start

  • Read access in the AWS account that owns the role: iam:GetRole for its trust policy, iam:ListOpenIDConnectProviders, and cloudtrail:LookupEvents in the region the workflow passes as aws-region.
  • Write access to the workflow file, and iam:UpdateAssumeRolePolicy on the role (or someone who has it) if the trust policy has to change.
  • The three facts that decide the token's sub: which event triggered the run, which branch or tag it ran on, and whether the failing job sets environment:.
  • Whether the repository sends the immutable subject claim: created after 15 July 2026, renamed or transferred since then, or opted in. gh api /repos/<org>/<repo>/actions/oidc/customization/sub shows the opt-in as use_immutable_subject, and a classic token needs the repo scope to call it.

Run this first

step 1 of 4
aws cloudtrail lookup-events --region <region the workflow uses> --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity --max-items 5

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

CloudTrail logs AssumeRoleWithWebIdentity requests together with what the identity provider supplied, so a failed event normally shows the sub AWS received. AWS notes that some unauthenticated STS requests are not logged, so an empty result does not prove nothing arrived.

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 missing from this accountOccasional

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 --region <region the workflow uses> --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity --max-items 5

    CloudTrail logs AssumeRoleWithWebIdentity requests together with what the identity provider supplied, so a failed event normally shows the sub AWS received. AWS notes that some unauthenticated STS requests are not logged, so an empty result does not prove nothing arrived.

    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 run on main whose job sets no environment, and nothing else: not a pull request, not a tag, not another branch, and not a job with environment: set, whose sub ends in environment:<name>. 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. Since 15 July 2026 it is the default for repositories created after that date and for any repository renamed or transferred after it; older repositories switch only if someone opts in. 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 new repository fails while older ones keep working, and a repository that worked yesterday fails the day after it is renamed, with nothing changed in AWS.

Confirm

aws cloudtrail lookup-events --region <region the workflow uses> --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity --max-items 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

  • If one role serves several repositories, list each in the form it actually sends. When a repository moves to the immutable claim, replace its name-only entry rather than keeping both, because a name-only entry also matches whoever owns that name later.
  • 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. GHES tokens come from a different issuer, https://HOSTNAME/_services/token, so they need their own identity provider and their own HOSTNAME/_services/token:sub condition, written in the name-only form.
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 never attempts the exchange: it logs "Did you mean to set the id-token permission?" and fails with "Credentials could not be loaded" rather than the error on this page. If that is the message you have, this is the cause. 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 when it is the trust policy's aud condition that differs, with identical error text. If the audience is missing from the IAM OIDC provider's client ID list, STS returns InvalidIdentityToken: Incorrect token audience instead.

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 missing from this account

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, with InvalidIdentityToken: No OpenIDConnect provider found in your account rather than Not authorized. 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.

In plain terms

Your workflow tried to trade a short-lived login token from GitHub for temporary AWS credentials, and AWS refused. AWS only agrees when the token matches the rules written on the AWS role, and the rule that usually fails is the one naming which repository, branch or environment is allowed. The fix is almost always in that rule or in the workflow, not in your AWS permissions.

What the token actually says

Documented, not reproduced: these are the first lines of the example token payload in GitHub's OpenID Connect documentation, for a job that sets an environment called prod. No command prints it, because GitHub mints the token inside the job; the documentation shows it decoded. GitHub notes that this example's sub uses the older, name-only format.

Command

# No command: GitHub mints this token inside the job. The documentation shows it decoded.

Output · as shown in GitHub Docs — OpenID Connect (example OIDC token)

{
  "jti": "example-id",
  "sub": "repo:octo-org/octo-repo:environment:prod",
  "environment": "prod",
  "aud": "https://github.com/octo-org",
  "ref": "refs/heads/main",
  "sha": "example-sha",
  "repository": "octo-org/octo-repo",
  "repository_owner": "octo-org",
  "actor_id": "12",
  "repository_visibility": "private",
  "repository_id": "74",
  "repository_owner_id": "65",

How to read it

  • ref is refs/heads/main, yet sub ends in environment:prod. When a job sets an environment, the environment replaces the ref in sub, so a trust policy written for repo:octo-org/octo-repo:ref:refs/heads/main rejects this token.
  • aud here is GitHub's default, the URL of the repository owner. configure-aws-credentials requests sts.amazonaws.com instead unless you set its audience input, and that is the value your :aud condition should hold.
  • repository_owner_id and repository_id are the kind of numeric IDs the immutable format puts after @ in the repo segment of sub. A repository created after 15 July 2026 sends that form, and a condition written in the name-only form will not match it.
  • AWS compares your conditions against these exact strings. Write the sub condition for what this job sends, not for the branch you think of it as running on.

Words used on this page

OIDC token
A short-lived, signed JSON Web Token that GitHub mints for a job that asks for one. AWS reads the claims inside it to decide whether to hand over credentials.
Claim
One named field in the token, such as sub, aud, ref or repository.
sub (subject)
The claim trust policies match on. It starts repo:<org>/<repo>: and ends with environment:<name> if the job names an environment, pull_request for a pull request event, or ref:refs/heads/<branch> or ref:refs/tags/<tag> otherwise.
aud (audience)
Who the token is for. aws-actions/configure-aws-credentials asks for sts.amazonaws.com by default, and both the IAM identity provider and the trust policy must accept that value.
Trust policy
The policy on a role that says who may assume it. For GitHub it names the token.actions.githubusercontent.com provider and sets conditions on token.actions.githubusercontent.com:sub and :aud.
IAM OIDC identity provider
The record in your AWS account that tells AWS to trust tokens from https://token.actions.githubusercontent.com. It must be in the same account as the role, and each provider URL can exist only once per account.
Immutable subject claim
The newer sub format that adds numeric owner and repository IDs, such as repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main. A recycled repository name cannot reproduce it.
id-token: write
The workflow permission that lets a job request an OIDC token. On its own it does not let the job change any resources.
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 plus one of three things: the environment if the job sets one, pull_request for a pull request event, or the ref otherwise. 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 keeps working for existing repositories until one is renamed, transferred or opted in, and rejects every repository created after 15 July 2026, 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 logs "Authenticated as assumedRoleId" followed by the role session.
  • 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 new conditions in the immutable subject form, and keep a name-only entry only for a repository that still sends it. A name-only entry kept just in case matches whoever owns that name next.
  • 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.

Security considerations

  • Keep the repository part of sub exact. IAM refuses a GitHub trust policy whose sub condition is missing or is only * or ?, but it accepts repo:*, which lets any repository on GitHub assume the role.
  • Write new conditions in the immutable form, and drop a repository's name-only entry once it sends the new one. GitHub's reference explains the risk: if a namespace is recycled, a different owner can produce the same name-only subject.
  • For a role that deploys, match sub on an environment such as repo:<org>/<repo>:environment:prod, and give that environment protection rules and deployment branches. AWS strongly recommends protection rules whenever environments appear in OIDC policies.
  • Do not use ForAllValues: operators in the Allow statement. The action's README warns they return true when a claim is absent or misspelled; use StringEquals or StringLike.
  • Grant id-token: write only on the job that calls AWS. The permission cannot change resources by itself, but every step in a job that has it can request a token your role accepts.

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.

Terms used here (1)

Short definitions for the vocabulary on this page, in case any of it is new. Each links to the full entry.

CI/CD
Continuous Integration and Continuous Delivery/Deployment — the practice of automating the build, test, and release process. CI merges code changes frequently and runs automated tests. Continuous delivery keeps every change that passes in a releasable state, so it can go to production at any time; continuous deployment goes one step further and releases every passing change to production automatically.

All 59 terms in the glossary

Did this get you to an answer?

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

Something wrong or out of date? Report it