Fixing GitHub Actions OIDC AssumeRole AccessDenied: Cross-Account Trust Policy Debugging Guide
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 15–30 mins
TL;DR
- What broke: GitHub Actions cannot call
sts:AssumeRoleWithWebIdentitybecause the IAM role's trust policy in the target account rejects the OIDC token — wrongsubcondition, missing OIDC provider registration, or incorrectaudclaim. - How to fix it: Register
token.actions.githubusercontent.comas an OIDC Identity Provider in every target account, then lock down the trust policy with exactsubandaudcondition keys matching your repo and workflow. - Shortcut: Use our Client-Side Sandbox below to auto-refactor this — paste your trust policy and workflow YAML, and get a corrected policy without sending your ARNs to a third-party server.
The Incident (What Does the Error Mean?)
Raw error from GitHub Actions runner or AWS CLI:
An error occurred (AccessDenied) when calling the AssumeRoleWithWebIdentity operation:
Not authorized to perform sts:AssumeRoleWithWebIdentity
Or from the AWS STS service itself:
ErrorCode: AccessDenied
Message: Not authorized to perform sts:AssumeRoleWithWebIdentity
RequestId: a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Immediate consequence: Your deployment pipeline is dead. Every job that depends on AWS credentials — ECR push, S3 sync, EKS deploy, Terraform apply — fails at the credential acquisition step. No fallback. No retry logic saves you here.
The Attack Vector / Blast Radius
This isn't just a broken pipeline — an incorrectly permissive trust policy is an active privilege escalation vector.
Scenario A — Too Restrictive (your current outage):
The sub condition in the trust policy doesn't match the token claim. GitHub sends repo:org/repo:ref:refs/heads/main; your policy has repo:org/repo:environment:production. STS rejects it. Pipeline down.
Scenario B — Too Permissive (the real danger you must avoid while fixing this):
If you "fix" the outage by removing the Condition block entirely or using StringLike with a wildcard like repo:org/*:*, any GitHub Actions workflow in your entire org can assume this role. An attacker who compromises any repo in your org — even a throwaway repo — can pivot into your AWS account with whatever permissions this role holds.
Cross-account blast radius: In a cross-account setup, the target account role is typically granted elevated permissions (e.g., AdministratorAccess or broad deployment permissions). A misconfigured trust policy here means a compromised GitHub token can traverse account boundaries and hit production infrastructure directly.
The specific failure points to audit in order:
- OIDC provider
token.actions.githubusercontent.comnot registered in the target account Principal.FederatedARN points to the wrong account's OIDC providerConditionStringEqualsontoken.actions.githubusercontent.com:subdoesn't match actualsubclaimConditionStringEqualsontoken.actions.githubusercontent.com:audis missing or set to something other thansts.amazonaws.com- Missing
sts:AssumeRoleWithWebIdentityin the trust policyAction
How to Fix It (The Solution)
Step 0 — Verify the actual sub claim in the token
Before touching IAM, decode the actual OIDC token GitHub is sending. Add this debug step to your workflow temporarily:
- name: Debug OIDC Token Claims
run: |
TOKEN=$(curl -sSfL -H "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 sub field output is the exact string your trust policy condition must match.
Basic Fix — Register OIDC Provider and Correct Trust Policy
Register the OIDC provider in the target account (do this once per account):
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1 \
--region us-east-1
Corrected trust policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
- "Federated": "arn:aws:iam::SOURCE_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
+ "Federated": "arn:aws:iam::TARGET_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
- "Action": "sts:AssumeRole",
+ "Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
- "token.actions.githubusercontent.com:aud": "https://github.com/your-org",
+ "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
- "token.actions.githubusercontent.com:sub": "repo:your-org/*"
+ "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
}
}
}
]
}
Enterprise Best Practice — Environment-Scoped, Least-Privilege Trust Policy
For production deployments, scope to a named GitHub Environment (not a branch ref). This forces the workflow to require environment protection rules (manual approval, required reviewers) before the token is issued with the matching sub.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
+ "Federated": "arn:aws:iam::TARGET_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
+ "Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
+ "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
+ "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:environment:production"
},
+ "StringLike": {
+ "aws:RequestedRegion": "us-east-1"
+ }
}
}
]
}
GitHub Actions workflow — correct configuration:
jobs:
deploy:
+ environment: production
permissions:
- id-token: none
+ id-token: write
+ contents: read
steps:
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
- role-to-assume: arn:aws:iam::SOURCE_ACCOUNT_ID:role/DeployRole
+ role-to-assume: arn:aws:iam::TARGET_ACCOUNT_ID:role/DeployRole
+ role-session-name: GitHubActions-${{ github.run_id }}
aws-region: us-east-1
+ audience: sts.amazonaws.com
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Checkov — scan trust policies before terraform apply:
Checkov rule CKV_AWS_49 flags overly broad OIDC trust conditions. Add to your pipeline:
- name: Checkov IAM Scan
uses: bridgecrewio/checkov-action@master
with:
directory: ./terraform
check: CKV_AWS_49,CKV_AWS_274
soft_fail: false
2. OPA/Conftest policy — block wildcard sub conditions:
package aws.iam.trust
deny[msg] {
stmt := input.Statement[_]
stmt.Action == "sts:AssumeRoleWithWebIdentity"
cond := stmt.Condition.StringLike["token.actions.githubusercontent.com:sub"]
endswith(cond, "*")
not contains(cond, ":environment:")
msg := sprintf("Trust policy sub condition '%v' is too broad. Scope to a named environment.", [cond])
}
3. Terraform — enforce OIDC provider existence as a data source (fail fast if missing):
data "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
# This will hard-fail plan if the provider isn't registered in the target account
}
resource "aws_iam_role" "deploy" {
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = data.aws_iam_openid_connect_provider.github.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
"token.actions.githubusercontent.com:sub" = "repo:${var.github_org}/${var.github_repo}:environment:${var.environment}"
}
}
}]
})
}
4. Audit existing roles quarterly:
# Find all roles with GitHub OIDC trust and no sub condition (dangerous)
aws iam list-roles --query 'Roles[*].RoleName' --output text | tr '\t' '\n' | while read role; do
policy=$(aws iam get-role --role-name "$role" --query 'Role.AssumeRolePolicyDocument' --output json 2>/dev/null)
if echo "$policy" | grep -q 'token.actions.githubusercontent.com'; then
if ! echo "$policy" | grep -q '"sub"'; then
echo "DANGEROUS - No sub condition: $role"
fi
fi
done