RAPID INCIDENT TRIAGE & ACTION PLAN•Security
01. Error Signature
GitHub Actions OIDC AssumeRole Permission Audit
02. Root Cause Vector

GitHub Actions OIDC AssumeRole fails with AccessDenied due to misconfigured trust policy conditions or missing identity provider in the t...

03. Rapid Prescription

Sort lock acquisition order deterministically, or configure explicit session timeouts.

Zero-Trust Client IDE/ Security

GitHub Actions OIDC AssumeRole Permission Audit

GitHub Actions OIDC AssumeRole fails with AccessDenied due to misconfigured trust policy conditions or missing identity provider in the target AWS account.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(63L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
---
# WARNING: This is the legacy deployment pipeline for the PCI-secure-zone.
# TODO: Migrate to EKS before Q4. Current workaround uses OIDC federation.
# JIRA: DEPLOY-1173

name: "Legacy Prod Deployment via Cross-Account Role"

on:
  push:
    branches:
      - "release/legacy-*"
    paths:
      - "infra/legacy/**"

# OLD CONFIG - kept for rollback reference
# env:
#   AWS_REGION: us-east-1
#   ECR_REGISTRY: my-registry.internal/legacy-api

jobs:
  assume-role-and-deploy:
    runs-on: ubuntu-latest
    # TODO: This should be in the secure runner pool, but it's broken.
    env:
      # Synthetic secrets injected for service connectivity
      STRIPE_LIVE_API_KEY: "sk_live_51Np9Z2LmQv8dJk5tR7xYwA9bC0zXyV3uH4iJ6kL7oP8qW9eR2sT4"
      LEGACY_DB_URI: "postgresql://app_user:Th1sIs@Sup3rS3cr3tP@ssw0rd!@pci-db.internal:5432/legacy_pci_data?sslmode=require"
      # VULNERABILITY: The OIDC provider ARN is incorrectly scoped to a different AWS account (111122223333) than the target role (444455556666). This will cause AssumeRole to fail.
      AWS_OIDC_PROVIDER_ARN: "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
    steps:
      - name: Checkout (with submodules)
        uses: actions/checkout@v3
        with:
          submodules: recursive
          # token: ${{ secrets.LEGACY_PAT }} # Commented out, using default

      - name: Configure AWS Credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: arn:aws:iam::444455556666:role/legacy-pci-deploy-role
          aws-region: us-west-2
          # The role-session-name is dynamically set, but the provider ARN above is wrong.
          role-session-name: github-${{ github.repository }}-${{ github.run_id }}
          # audit-token: ${{ secrets.AUDIT_TOKEN }} # Red herring - not used

      - name: Decrypt and load legacy config (race condition possible)
        run: |
          # This script loads old env vars, might conflict with OIDC step.
          echo "Loading legacy secrets..."
          export LEGACY_AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
          export LEGACY_AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
          # Embedded multi-line RSA key for SSH tunnel backup
          export BASTION_SSH_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArX8eZcBqM8HwX6Qk6T9KjP7NlY6vJfLp0qNzT1GcFmR7vQ\n... (truncated for brevity) ...\n-----END RSA PRIVATE KEY-----"
          echo "Seems loaded?"
          # TODO: fix this workaround - sometimes the OIDC token doesn't propagate.

      - name: Deploy to legacy EC2 (will fail due to missing credentials)
        run: |
          # This will fail with AccessDenied on AssumeRole because the OIDC provider is in account 111122223333, not 444455556666.
          aws ec2 describe-instances --filters "Name=tag:Environment,Values=legacy-pci" --region us-west-2
          # Fallback to old method if needed (commented out)
          # ./deploy-with-keys.sh
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

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 above 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.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

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.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

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 infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

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

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does AssumeRoleWithWebIdentity work in the source account but fail in the target account?
The root cause is a fundamental AWS security boundary: the OIDC Identity Provider `token.actions.githubusercontent.com` must be registered independently in every AWS account that will accept the token. Provider registrations are not shared. The `Principal.Federated` ARN in the target account's trust policy must reference its own OIDC provider ARN, not the source account's. This is a classic cross-account IAM misconfiguration that erodes your zero-trust posture.
Q2.What is the correct value for the `aud` (audience) condition in the GitHub Actions OIDC trust policy?
Manually, it must be `sts.amazonaws.com`. This is the audience GitHub sets in the OIDC token when using `aws-actions/configure-aws-credentials` with the default configuration. However, manually overriding the `audience` input or mismatching this value is a high-risk, error-prone process that can silently break authentication and create a security vulnerability with a wide blast radius. Instead of guessing, drop your broken IAM trust policy manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix with the correct, hardened conditions.
Q3.How do I construct the correct `sub` claim value for my GitHub Actions workflow?
The `sub` claim format varies by trigger: e.g., `repo:ORG/REPO:ref:refs/heads/BRANCH` for a branch push. While you can decode your token to confirm the exact string, manually constructing and validating these across hundreds of workflows in a large mono-repo is a compliance and audit nightmare, prone to drift and misconfiguration. For scalable, secure CI/CD, upgrade to the StackEngine CI/CD Automator (Team Edition). It provides on-prem, automated PR drafting under Maker-Checker review, enforces policy-as-code, and generates auditable compliance reports for your entire pipeline estate.