How to Fix GitHub Actions OIDC AssumeRoleWithWebIdentity Failures on AWS
Diagnosing the trust-policy, audience, and claim-format mismatches behind a misleading AccessDenied error

Proactive Security for the AI Era
NodeZero continuously and autonomously pentests infrastructure, identity, cloud, and now web applications, chaining weaknesses across every domain the way real attackers do. Every finding ships with replayable proof showing exploitable business impact, not theoretical risk.
The fix, in the overwhelming majority of cases, is not a permissions change. It is making the trust policy's sub and aud condition values match, character for character, the claims GitHub's OIDC token actually presents for that workflow run. "Not authorized to perform sts:AssumeRoleWithWebIdentity" is the single error AWS STS returns for every reason a federated role assumption can be rejected, so it looks identical whether the role's IAM permissions are wrong, the trust policy is wrong, the OIDC provider is misconfigured, or (in one documented case) the role name itself trips an undocumented restriction. Because the error carries no detail, teams commonly waste hours changing IAM permission policies that were never the problem. This guide walks through why the message is uninformative by design, a numbered diagnostic procedure with real commands and config, the specific root causes confirmed in AWS and GitHub Actions issue trackers, and the security cost of the shortcut most teams reach for first: loosening the trust policy until the error goes away. If your pipeline already touches Terraform or CloudFormation, the same OIDC role is often what applies infrastructure changes, so see this site's broader guide to CI/CD pipeline security best practices for how this failure mode fits into the rest of your build security posture.
The Error, When It Appears, and Why the Message Doesn't Tell You Why
The failure surfaces at the aws-actions/configure-aws-credentials step of a workflow, after several retried attempts, with output that looks like this:
Run aws-actions/configure-aws-credentials@v4
Assuming role with OIDC
Assuming role with OIDC
Assuming role with OIDC
Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
That message is the action's wrapper around a generic AWS STS AccessDenied response to an AssumeRoleWithWebIdentity call. AWS STS deliberately does not tell the caller which part of the request was rejected. It will not say "your aud claim did not match," or "your sub claim was repo:acme/api:environment:prod but the policy expected repo:acme/api:ref:refs/heads/main." Revealing that level of detail to an unauthenticated-looking caller would hand an attacker a way to iterate toward a valid claim value, so STS collapses every rejection reason into one response.
The practical effect is that the same error string covers at least six distinct root causes, only one of which is a genuine IAM permissions problem (the role's permissions boundary or an explicit deny, which is comparatively rare). The rest are trust-policy condition mismatches, and the fix for each looks different once you know which one you're facing.
Prerequisites
Before working through the diagnostic steps, confirm you actually have the two pieces this failure assumes are already in place:
Subscribe to unlock Remediation & Mitigation steps
Free subscribers unlock full IOC lists, Sigma detection rules, remediation steps, and every daily briefing.
Briefings like this, every morning before 9am.
Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.
Step-by-Step Diagnostic Procedure
Work through these steps in order. Each one narrows down whether the problem is on the GitHub side (the claim the token actually presents) or the AWS side (the condition the trust policy expects).
Step 1: Confirm the workflow requests an ID token at all.
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/gh-deploy-role
aws-region: us-east-1
permissions can be set at the workflow level or the job level; a workflow-level read-all or a restrictive org default can silently override a job-level grant, so check both. Missing id-token: write produces a different, more specific error (Error: Not able to fetch an OpenID Connect JWT ID Token), so if you're seeing the AssumeRoleWithWebIdentity error specifically, the token is being issued fine and the problem is downstream of this step.
Step 2: Decode the actual claims the token presents for this exact job.
GitHub documents the underlying request the action makes to fetch the token: a GET to ACTIONS_ID_TOKEN_REQUEST_URL with the workflow's ephemeral bearer token. Add a temporary debug step to see what sub and aud your workflow really produces (never leave this in a workflow that a wide group can trigger, since it prints identity claims to the log):
- name: Debug OIDC token claims
run: |
JWT=$(curl -sSL -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r '.value')
echo "$JWT" | cut -d '.' -f2 | base64 -d 2>/dev/null | jq '{sub, aud, repository, ref, job_workflow_ref}'
The sub claim differs depending on how the job triggered: a branch push produces repo:org/repo:ref:refs/heads/main, a job that declares an environment: produces repo:org/repo:environment:name instead (the ref-based form is dropped, not appended), and a pull request from a fork produces repo:org/repo:pull_request. This is precisely the mismatch documented in aws-actions/configure-aws-credentials issue #1137, where adding environment: production to a job broke a trust policy that only matched the branch-ref form, and removing the environment: line made the same policy work again.
Step 3: Read the role's actual trust policy from AWS, not from memory.
aws iam get-role \
--role-name gh-deploy-role \
--query 'Role.AssumeRolePolicyDocument' \
--output json
A correctly scoped trust policy looks like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333: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-corp/api-service:ref:refs/heads/main"
}
}
}
]
}
Step 4: Diff the claim from Step 2 against the condition from Step 3, byte for byte. Look specifically for: the audience string (must be exactly sts.amazonaws.com unless you customized audience: in the action's inputs), the claim shape (ref: vs environment: vs pull_request), the exact org and repo name casing, and whether the operator is StringEquals (exact match only) or StringLike (supports * wildcards). A StringEquals condition against a sub value that needs a wildcard, or a sub value written for the wrong claim shape, both produce the identical AccessDenied.
Step 5: Confirm the OIDC provider ARN referenced in the trust policy belongs to the same AWS account as the role, using aws iam list-open-id-connect-providers and comparing the account ID segment of the ARN. A role's trust policy can only reference an OIDC provider registered in that same account; a copy-pasted trust policy from a different account's Terraform module is a common source of this specific mismatch, which is one more reason to treat OIDC provider and role definitions as reviewed infrastructure rather than boilerplate, a gap covered in more depth in this site's guide to Infrastructure as Code security scanning (most IaC scanners check for public S3 buckets and open security groups long before they check whether a sub condition in a Terraform-managed trust policy actually matches your workflow's real claim shape).
Validation: Confirming the Fix Actually Worked
After changing the trust policy, validate with both a direct workflow check and an out-of-band AWS-side confirmation, rather than assuming a green checkmark means the condition logic is now correct for every branch and trigger type you use.
In the workflow, add a step immediately after configure-aws-credentials that confirms which identity was actually assumed:
- name: Verify assumed identity
run: aws sts get-caller-identity
The output's Arn field should show the assumed-role session, formatted as arn:aws:sts::111122223333:assumed-role/gh-deploy-role/GitHubActions. If this step runs, the OIDC exchange succeeded; if it still fails, you have not yet found the mismatched condition.
In AWS CloudTrail, confirm the event and its outcome independently of the workflow log:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
--max-results 10
Each event's errorCode and errorMessage fields confirm whether a given attempt succeeded or was denied, and the eventTime lets you correlate a specific workflow run to a specific AssumeRoleWithWebIdentity call. CloudTrail does not log the full OIDC token or every individual claim value for privacy and security reasons, so it confirms whether the call succeeded and roughly when, but it will not hand you the exact claim that mismatched; that still comes from decoding the token in Step 2 above. Re-run the debug decode step against every trigger type your workflow actually uses (a normal push, a pull request from a fork, a manual workflow_dispatch, a run inside a declared environment:) since each can produce a different sub shape and a trust policy that passes for one can still fail for another.
Root Causes Confirmed in AWS and GitHub Actions Issue Trackers
These are not hypothetical failure modes. Each of the following has a documented GitHub issue or official AWS/GitHub guidance behind it.
Subscribe to unlock Remediation & Mitigation steps
Free subscribers unlock full IOC lists, Sigma detection rules, remediation steps, and every daily briefing.
Troubleshooting Decision Tree: Symptom to Cause
Use this checklist to jump directly to the likely cause based on what changed or what you observe, before working through the full diagnostic procedure above.
Subscribe to unlock Remediation & Mitigation steps
Free subscribers unlock full IOC lists, Sigma detection rules, remediation steps, and every daily briefing.
Security Tradeoffs: Why the Quick Fix Is the Wrong Fix
The fastest way to make this error disappear is to loosen the sub condition to something broad, such as repo:acme-corp/* or, worse, dropping the sub condition entirely and relying only on the aud check. Both make the trust policy pass regardless of which repository, branch, or workflow in the organization is requesting the role, which is precisely the property you don't want.
An overly broad sub condition means any workflow in any repository under that GitHub organization, including a low-privilege internal tool, a public fork with Actions enabled, or a compromised dependency's postinstall script running inside CI, can assume the same role as your production deployment pipeline. The trust policy is the only thing standing between "a GitHub Actions job exists somewhere in this org" and "this job can obtain AWS credentials for this specific role." Widening the sub condition to make a debugging session end faster converts a scoped, single-repository, single-branch credential into an organization-wide one, which is exactly the kind of non-human identity sprawl that makes lateral movement from a single compromised workflow into a much larger blast radius; see this site's broader guide to non-human identity security for how these federated identities fit into the wider inventory of service accounts, API keys, and machine identities most organizations under-track relative to their human accounts.
The better fix, in every case above, is to identify the exact claim shape your workflow legitimately produces and scope the condition to match that shape precisely, using StringLike with a narrow wildcard only where genuinely necessary (for example, matching any tag under refs/tags/v* for a release workflow), rather than widening the match to make the error go away without understanding why it was failing.
The bottom line
"Not authorized to perform sts:AssumeRoleWithWebIdentity" is AWS STS collapsing at least six distinct failure modes into one uninformative message. Decode the actual OIDC claims your workflow presents, read the trust policy's Condition block directly from IAM, and diff them field by field before touching IAM permissions or widening the sub condition. The July 2026 shift to immutable subject claims and the long-documented environment-vs-branch claim mismatch account for most of the confirmed cases; a handful of role names beginning with GitHub account for a smaller but genuinely reported set. Scoping the trust policy tightly and re-verifying it against every trigger type your workflow uses is the only fix that does not trade this error for a much larger credential exposure later.
Frequently asked questions
What does "Not authorized to perform sts:AssumeRoleWithWebIdentity" actually mean in GitHub Actions?
It means AWS STS rejected the OIDC-based role assumption request from aws-actions/configure-aws-credentials, but the message does not say which specific condition failed. It could be an aud mismatch, a sub condition that does not match the token's actual claim shape, a missing OIDC provider, or (rarely) an IAM permissions problem, and the generic wording is intentional so the failure reason cannot be enumerated by an unauthenticated caller.
Why does this error appear even when the IAM role and its attached policies look completely correct?
Because the role's permission policies (what the role can do once assumed) are evaluated only after the trust policy's Condition block (who is allowed to assume it) passes. In most confirmed cases the permission policies were never reached; the trust policy's sub or aud condition rejected the request before that point, which is why changing permissions does not fix it.
How do I see the actual sub and aud claims my GitHub Actions OIDC token contains?
Add a temporary workflow step that calls the ACTIONS_ID_TOKEN_REQUEST_URL endpoint with the ACTIONS_ID_TOKEN_REQUEST_TOKEN bearer token, extracts the JWT's payload segment, base64-decodes it, and prints the sub, aud, and ref fields with jq. Remove the step once you've captured the claim shape, since it prints identity information to the workflow log.
What is the difference between StringEquals and StringLike in an OIDC trust policy condition?
StringEquals requires the claim to match the specified value exactly, character for character. StringLike allows wildcard patterns such as an asterisk, so a single condition can match multiple branches, tags, or repositories under a pattern. Using StringEquals against a claim that legitimately varies (for example, across multiple branches) causes every non-matching branch to fail with this same AccessDenied.
Do I need to update my AWS trust policy for GitHub's immutable subject claims change in 2026?
Only if your repository was created on or after July 15, 2026, or you opt an existing repository into the immutable claim format; in that case the sub claim appends permanent numeric organization and repository IDs, and a trust policy still written against the legacy name-only sub value will stop matching and start returning this error.
Is it safe to scope the sub condition to repo:org/* to make the AssumeRoleWithWebIdentity error go away?
No. That change makes the trust policy pass for every repository and branch in the organization, not just the one you intended, which means any workflow anywhere in that org, including a compromised dependency's build step, could assume the same role. Identify the exact claim shape your workflow produces and scope the condition to match it instead of widening it.
Sources & references
- GitHub Docs, Configuring OpenID Connect in Amazon Web Services
- AWS IAM User Guide, Create a role for OpenID Connect federation (console)
- aws-actions/configure-aws-credentials, Issue #953: restricted role name causes non-specific error
- aws-actions/configure-aws-credentials, Issue #1137: environment-scoped job breaks sub match
- GitGuardian, The State of Secrets Sprawl 2026
- GitHub Blog, Let's talk about GitHub Actions
Free resources
Critical CVE Reference Card 2025–2026
25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.
Ransomware Incident Response Playbook
Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.
Get threat intel before your inbox does.
50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.
Unsubscribe anytime. We never sell your data.

Founder & Cybersecurity Evangelist, Decryption Digest
Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.
