Not authorized to perform sts:AssumeRoleWithWebIdentity
Fix "Not authorized to perform sts:AssumeRoleWithWebIdentity": add id-token: write, print the OIDC claims, and match the trust policy sub and aud exactly.
Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
Incident Overview
Fix "Not authorized to perform sts:AssumeRoleWithWebIdentity": add id-token: write, print the OIDC claims, and match the trust policy sub and aud exactly.
hard fix·5 min read
By Vishvesh Patel·DevOps Engineer·Updated 21 September 2026
Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity An error occurred (InvalidIdentityToken) when calling the AssumeRoleWithWebIdentity operation: Incorrect token audience An error occurred (AccessDenied) when calling the AssumeRoleWithWebIdentity operation: Not authorized to perform sts:AssumeRoleWithWebIdentity
Do this first3 steps
Run these in order. Each one tells you what its output means before you change anything.
• Print the claims your CI actually sends
The sub claim is what the trust policy condition must match, character for character. Guessing its format is the cause of most of these failures, and the repo/branch encoding differs between pull requests, branches, tags and environments.
• Read the role's current trust policy
Compare the Condition block against the claims. A StringEquals on sub that does not match exactly fails, and a wildcard needs StringLike rather than StringEquals.
• Confirm the OIDC provider exists in the account
Without a provider for token.actions.githubusercontent.com or gitlab.com, AWS has nothing to validate the token against and rejects it before the trust policy is even evaluated.
OIDC federation lets a CI job exchange a short-lived identity token for an AWS role session, with no stored access keys. When it fails, one of three things is wrong: the provider is missing, the audience does not match, or the trust policy condition does not match the token's claims.
The third is almost always it.
echo 'decode the JWT payload in a job step: cut -d. -f2 of the token, base64 -d'
Print the claims
Stop guessing at the sub format. Decode the token in a job:
That sub is what your condition must match. The formats differ by trigger:
A trust policy written for a branch push rejects a pull request, which is usually the intent and occasionally a surprise.
GitLab's is different again:
# GitHub Actions
permissions:
id-token: write
contents: read
steps:
- name: Show OIDC claims
run: |
TOKEN=$(curl -sH "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r .value)
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .The permissions block
Without id-token: write the job gets no token at all, and the action fails before reaching AWS. This is the most common single cause in GitHub Actions, and contents: read must be listed too, because declaring the block sets everything unlisted to none.
permissions: id-token: write contents: read
The trust policy
Three things to get right:
StringLike for wildcards. StringEquals with a * in the value matches a literal asterisk and nothing else.
Always condition on sub. A policy with only the aud condition trusts every GitHub repository in the world, because every one of them presents sts.amazonaws.com as the audience. That is a serious misconfiguration and it is easy to arrive at while debugging.
Do not over-wildcard. repo:acme/* trusts every repository in your organisation. Scope to the repository, and to the branch or environment where you can.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:acme/myapp:ref:refs/heads/*"
}
}
}]
}Audience
The token's aud and the policy's aud condition must match. aws-actions/configure-aws-credentials requests sts.amazonaws.com by default. GitLab defaults to the project URL, so you set it explicitly:
Whatever you put in aud must be what the trust policy expects.
InvalidIdentityToken: Incorrect token audience
The provider must exist
One per account, not per role. Create it if absent:
AWS stopped requiring a thumbprint for well-known providers, so older guides telling you to fetch one by hand are out of date. A stale hardcoded thumbprint in Terraform is itself a cause of sudden failures when the certificate rotates.
aws iam list-open-id-connect-providers
Session duration
The role's MaxSessionDuration caps what a job may request:
Keep it short. A CI role session should last a job, not a day.
DurationSeconds exceeds the MaxSessionDuration set for this role
A checklist
Frequently Asked Questions
Why does the role assumption fail when the trust policy looks correct?
Almost always because the sub claim is not what you assumed. Its format varies by trigger: a branch push sends repo:owner/name:ref:refs/heads/main, a tag sends refs/tags/..., a pull request sends repo:owner/name:pull_request, and an environment deployment sends repo:owner/name:environment:production. A policy written for one rejects the others. Decode the token inside a job and print the payload rather than inferring the format from documentation.
What does "id-token: write" do?
It permits the job to request an OIDC identity token from GitHub's token service. Without it, no token is issued and the AWS action fails before it reaches AWS at all. The subtlety that catches people is that adding a permissions block sets every permission you do not list to none, so a block containing only id-token: write removes the job's ability to check out the repository. Include contents: read alongside it.
Is it safe to condition only on the audience?
No, and it is a serious misconfiguration. Every GitHub Actions job on the platform presents sts.amazonaws.com as its audience when configured for AWS, so a trust policy conditioning only on aud allows any repository anywhere to assume your role. It is easy to arrive at while debugging, because removing the sub condition makes the error go away. Always condition on sub, scoped to your repository and ideally to a specific branch or environment.
Do I still need an OIDC thumbprint?
No. AWS now validates well-known OIDC providers against trusted certificate authorities, so the thumbprint is no longer required when creating the provider. Older guides and Terraform modules that hardcode one are a liability rather than a safeguard: when the provider rotates its certificate, the stale thumbprint causes sudden, confusing authentication failures across every workflow. If you have one pinned, removing it is a worthwhile change.
Why use OIDC instead of access keys in CI?
Because there is no long-lived credential to store, rotate or leak. The platform issues a signed token for each job, AWS exchanges it for a session that expires when the job ends, and nothing durable exists in your secrets store. It also gives much finer control: the trust policy can restrict a role to one repository and one branch, so a workflow on a feature branch cannot assume the production deployment role even though it runs in the same repository.
Reference and practice
Learn the underlying concept
Other CI/CD errors
Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.
Get notified about new tutorials and features.
• Roadmaps
• Interview Prep
• Certifications
• Company
© 2026 DevOpsLesson. All rights reserved.
Need to practice this scenario?
Simulate this error condition and practice recovery commands safely in our in-browser terminal.