Initializing Enclave...

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:AssumeRoleWithWebIdentity because the IAM role's trust policy in the target account rejects the OIDC token — wrong sub condition, missing OIDC provider registration, or incorrect aud claim.
  • How to fix it: Register token.actions.githubusercontent.com as an OIDC Identity Provider in every target account, then lock down the trust policy with exact sub and aud condition 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:

  1. OIDC provider token.actions.githubusercontent.com not registered in the target account
  2. Principal.Federated ARN points to the wrong account's OIDC provider
  3. Condition StringEquals on token.actions.githubusercontent.com:sub doesn't match actual sub claim
  4. Condition StringEquals on token.actions.githubusercontent.com:aud is missing or set to something other than sts.amazonaws.com
  5. Missing sts:AssumeRoleWithWebIdentity in the trust policy Action

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

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →