Modern AWS OIDC with GitHub Actions: The Zero-Secret, Drift-Free, Multi-Account Guide


A production-grade guide for DevOps and Platform Engineers on building secure, auditable, and maintenance-free CI/CD identity federation between GitHub Actions and AWS.

#CICD
Intermediate
Published: September 21, 2026 16 min read
format_list_bulleted Table of Contents 28 sections
expand_more
1. The Modern Alternative to Permanent Credentials: OpenID Connect (OIDC) Federation 2. Under the Hood: The OIDC Authentication Flow Anatomy of the GitHub Actions OIDC Token 4. The 2023 Thumbprint Gotcha (Stop Using data "tls_certificate") Why This Caused Production Outages AWS’s July 2023 Update: Automated Root CA Validation The Modern Production Pattern 8. Securing the Trust Policy: Beyond Basic Branch Pinning Pitfall 1: Wildcard Sub Claims (A Catastrophic Vulnerability) Pitfall 2: GitHub’s 2026 Shift to Immutable Repository IDs The Enterprise Architecture: GitHub Environments for Multi-Account Isolation 12. Complete Infrastructure as Code (OpenTofu / Terraform) infra/modules/oidc/variables.tf infra/modules/oidc/main.tf 15. Configuring GitHub Actions with Dynamic CloudTrail Session Logging Why role-session-name is a Game-Changer for Incident Response 17. Troubleshooting & Debugging Playbook Step #1: Query CloudTrail for the Failed Attempt Step #2: Inspect the userName Claim 20. Summary Checklist for Production Readiness 21. Frequently Asked Questions (FAQ) What is AWS OIDC for GitHub Actions and how does it work? Do I still need thumbprints for GitHub Actions OIDC in AWS IAM? Why does GitHub Actions fail with "Not authorized to perform sts:AssumeRoleWithWebIdentity"? What is the difference between branch-based and environment-based OIDC scoping? Can multiple GitHub repositories share a single AWS OIDC Identity Provider? How do I trace which GitHub Action run performed an AWS action in CloudTrail? What IAM permissions are needed to create an AWS OIDC Provider?

For years, the standard approach to connecting GitHub Actions to AWS was deceptively simple:

  1. Create an IAM User (github-actions-deployer).

  2. Attach permissions policies to it.

  3. Generate an Access Key ID and Secret Access Key (AKIA...).

  4. Paste them into GitHub Repository Secrets as AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.

While this workflow is easy to set up, it introduces severe security and operational vulnerabilities at scale:

  • Credential Sprawl & Leak Risks: Long-lived static keys never expire on their own. If a developer accidentally echoes secrets into build logs, commits them to a public repo, or an attacker exfiltrates environment variables from an untrusted third-party GitHub Action, your entire AWS account is compromised.
  • Rotation Overhead: Security compliance frameworks (SOC 2, ISO 27001, PCI-DSS) mandate key rotation every 90 days. In an organization managing dozens or hundreds of microservice repositories across multiple AWS accounts, rotating static credentials becomes an endless, error-prone chore.
  • Audit Blind Spots: In AWS CloudTrail, all actions executed by that user show the exact same static principal. There is no cryptographic link between the AWS API call and the specific GitHub workflow run, pull request, commit SHA, or engineer who triggered it.

The Modern Alternative to Permanent Credentials: OpenID Connect (OIDC) Federation#

With OpenID Connect (OIDC), your GitHub runner does not hold any static AWS credentials. Instead:

  • GitHub acts as an external Identity Provider (IdP).
  • For each job execution, GitHub issues a short-lived, cryptographically signed JSON Web Token (JWT).
  • The runner sends this JWT to AWS Security Token Service (STS) using the sts:AssumeRoleWithWebIdentity API call.
  • AWS STS validates the token’s signature, inspects the claims against an IAM Role Trust Policy, and exchanges it for temporary credentials (ASIA...) valid for up to 1 hour.

No secrets stored. No keys to rotate. No credentials to leak.

Under the Hood: The OIDC Authentication Flow#

Understanding the handshake between GitHub Actions and AWS STS is critical for debugging edge cases and architecting least-privilege trust policies.

sequenceDiagram autonumber participant GHA as GitHub Actions Runner participant GHOIDC as GitHub OIDC Token Service participant STS as AWS Security Token Service (STS) participant IAM as AWS IAM Service participant AWS as AWS Target Resource (ECR/ECS) GHA->>GHOIDC: 1. Request signed OIDC JWT (audience: sts.amazonaws.com) GHOIDC-->>GHA: 2. Return cryptographically signed JWT GHA->>STS: 3. AssumeRoleWithWebIdentity(RoleArn, RoleSessionName, WebIdentityToken) STS->>IAM: 4. Verify token against IAM OIDC Provider & Trust Policy IAM-->>STS: 5. Verification successful STS-->>GHA: 6. Return temporary AWS credentials (AccessKey, SecretKey, SessionToken) GHA->>AWS: 7. Execute AWS commands (e.g. docker push, ecs update-service)

Anatomy of the GitHub Actions OIDC Token#

When GitHub mints a JWT, it includes critical claims that AWS inspects:

json
{
  "iss": "https://token.actions.githubusercontent.com",
  "aud": "sts.amazonaws.com",
  "sub": "repo:my-org/my-app:environment:production",
  "actor": "octocat",
  "repository": "my-org/my-app",
  "repository_owner": "my-org",
  "job_workflow_ref": "my-org/my-app/.github/workflows/deploy.yml@refs/heads/main",
  "ref": "refs/heads/main",
  "sha": "a1b2c3d4e5f67890123456789abcdef012345678",
  "exp": 1758509822
}

The three most important claims for AWS IAM:

  1. iss (Issuer): Must match the OIDC Identity Provider URL (https://token.actions.githubusercontent.com).

  2. aud (Audience): The intended recipient of the token. By default, aws-actions/configure-aws-credentials sets this to sts.amazonaws.com.

  3. sub (Subject): The unique identifier representing the context of the workflow run (repository, branch, tag, or environment). This is where you enforce authorization boundaries.

The 2023 Thumbprint Gotcha (Stop Using data "tls_certificate")#

If you search the web for "Terraform AWS GitHub OIDC", 90% of tutorials and blog posts recommend something like this:

hcl
# ❌ ANTI-PATTERN: DO NOT USE THIS ANYMORE
data "tls_certificate" "github" {
  url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
}

resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = [data.tls_certificate.github.certificates[0].sha1_fingerprint]
}

Why This Caused Production Outages#

Historically, AWS IAM required the SHA-1 thumbprint of the leaf/intermediate TLS certificate protecting the OIDC provider's endpoint. Using data "tls_certificate" dynamically pulled this fingerprint from GitHub's servers during terraform plan / terraform apply.

This introduced two massive headaches:

  1. Noisy Plan Diffs: Edge routing, CDN variations, and certificate renewals caused Terraform plans to show perpetual diffs even when nothing had changed.

  2. Global CI/CD Outages: In early 2023, GitHub rotated their intermediate TLS certificate. Pipelines around the globe immediately broke with OpenIDConnectProvider has an invalid thumbprint, halting deployments until platform teams manually updated their Terraform code.

AWS’s July 2023 Update: Automated Root CA Validation#

On July 6, 2023, AWS officially updated IAM OIDC validation for well-known providers like GitHub Actions (token.actions.githubusercontent.com).

IAM now validates GitHub Actions against its own trusted library of root Certificate Authorities (CAs). During runtime token verification, AWS STS no longer depends on the leaf certificate thumbprints stored in your provider configuration.

The Modern Production Pattern#

However, because the underlying AWS IAM API schema (ThumbprintList) still expects 1 to 5 valid 40-character hexadecimal strings, you must still provide a thumbprint in your IaC.

The industry-standard solution is to pass static thumbprints and delete data "tls_certificate" completely:

hcl
# ✅ RECOMMENDED PRODUCTION PATTERN: Static Thumbprints, No Dynamic Lookups
resource "aws_iam_openid_connect_provider" "github" {
  url            = "https://token.actions.githubusercontent.com"
  client_id_list = ["sts.amazonaws.com"]

  # AWS IAM validates GitHub Actions against its own internal root CA library.
  # Static thumbprints satisfy the IAM API schema without causing perpetual plan diffs
  # or breaking on intermediate TLS rotations.
  thumbprint_list = [
    "6938fd4d98bab03faadb97b34396831e3780aea1",
    "1c58a3a8518e8759bf075b76b750d4f2df264fcd"
  ]

  tags = {
    Name = "github-actions-oidc"
  }
}

Securing the Trust Policy: Beyond Basic Branch Pinning#

Creating the OIDC provider is only step one. The real security lives in the IAM Role Trust Policy (AssumeRoleWithWebIdentity).

Pitfall 1: Wildcard Sub Claims (A Catastrophic Vulnerability)#

Consider this assume role policy condition:

json
// 🚨 DANGEROUS: DO NOT DO THIS
"Condition": {
  "StringLike": {
    "token.actions.githubusercontent.com:sub": "repo:*:*"
  }
}

If you use "repo:*:*", ANY repository on GitHub owned by ANY person or organization in the world can assume your AWS deployment role simply by sending a valid GitHub OIDC token to your role ARN! Always restrict the repository explicitly.

Pitfall 2: GitHub’s 2026 Shift to Immutable Repository IDs#

In July 2026, GitHub introduced a breaking change to OIDC subject claims for new repositories to prevent repository-renaming and token-squatting attacks:

  • Classic Subject Format: repo:my-org/my-repo:ref:refs/heads/main
  • Immutable ID Format: repo:my-org@123456/my-repo@987654321:ref:refs/heads/main

If your IAM policy uses strict StringEquals expecting only the human-readable slug:

json
"StringEquals": {
  "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
}

AWS STS will reject the token with Not authorized to perform sts:AssumeRoleWithWebIdentity because GitHub appended @<user_id> and @<repo_id>.

To support both formats safely without opening wildcard access:

hcl
Condition = {
  StringEquals = {
    "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
  }
  StringLike = {
    "token.actions.githubusercontent.com:sub" = [
      "repo:my-org/my-repo:*",
      "repo:my-org*/my-repo*:*"
    ]
  }
}

The Enterprise Architecture: GitHub Environments for Multi-Account Isolation#

For multi-account AWS topologies (e.g. dev, staging, prod across distinct AWS accounts or regions), branch pinning alone is insufficient.

A developer pushing code to a feature branch could modify workflow files to point to a production role ARN. If the production role trusts branch pushes or has loose conditions, your production environment is at risk.

The Solution: Scoping Roles to GitHub Environments#

When you assign an environment: <name> to a GitHub Actions job, GitHub alters the OIDC sub claim:

text
repo:<owner>/<repo>:environment:<environment_name>

In GitHub Repository Settings:

  1. Navigate to Settings > Environments.

  2. Create environments matching your AWS accounts: dev, staging, production-us-east-1.

  3. Under Deployment branches, configure protection rules to restrict deployments strictly to main (or version tags like v*).

  4. Require mandatory approvals from senior engineers or DevOps before production jobs can run.

In AWS IAM, restrict the assume role policy condition strictly to that environment:

hcl
Condition = {
  StringEquals = {
    "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
  }
  StringLike = {
    "token.actions.githubusercontent.com:sub" = [
      "repo:my-org*/my-repo*:environment:production"
    ]
  }
}

Now you have true Defense-in-Depth:

  • An unauthorized branch or pull request cannot assume the production IAM role because GitHub enforces environment protection rules before issuing the token.
  • Even if an attacker tampers with the workflow YAML, AWS STS rejects any token that doesn't match the specific environment: production claim.

Complete Infrastructure as Code (OpenTofu / Terraform)#

Here is a complete, production-ready OpenTofu/Terraform module implementing all of the best practices discussed.

infra/modules/oidc/variables.tf#

hcl
variable "project_name" {
  description = "Project name prefix"
  type        = string
}

variable "environment" {
  description = "Target deployment environment (e.g. dev, prod)"
  type        = string
}

variable "github_repo" {
  description = "GitHub repository in 'owner/repo' format"
  type        = string
}

variable "github_branch" {
  description = "Primary branch allowed to deploy"
  type        = string
  default     = "main"
}

variable "ecr_repository_arn" {
  description = "Target ECR repository ARN for deployments"
  type        = string
}

variable "ecs_execution_role_arn" {
  description = "ECS task execution role ARN for PassRole permission"
  type        = string
}

variable "ecs_task_role_arn" {
  description = "ECS task role ARN for PassRole permission"
  type        = string
}

infra/modules/oidc/main.tf#

hcl
# -----------------------------------------------------------------------------
# GitHub OIDC Identity Provider
# -----------------------------------------------------------------------------
resource "aws_iam_openid_connect_provider" "github" {
  url            = "https://token.actions.githubusercontent.com"
  client_id_list = ["sts.amazonaws.com"]

  # AWS IAM validates GitHub Actions against its own trusted root CA library.
  # Static thumbprints satisfy the IAM API schema without dynamic cert lookups.
  thumbprint_list = [
    "6938fd4d98bab03faadb97b34396831e3780aea1",
    "1c58a3a8518e8759bf075b76b750d4f2df264fcd"
  ]

  tags = {
    Name = "${var.project_name}-github-oidc"
  }
}

# -----------------------------------------------------------------------------
# IAM Role for GitHub Actions Deployments
# -----------------------------------------------------------------------------
resource "aws_iam_role" "github_actions" {
  name = "${var.project_name}-${var.environment}-github-actions-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.github.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          }
          StringLike = {
            "token.actions.githubusercontent.com:sub" = [
              # Match classic format and 2026 immutable ID format for environment
              "repo:${var.github_repo}:environment:${var.environment}",
              "repo:${var.github_repo}*:environment:${var.environment}",
              # Match branch pushes
              "repo:${var.github_repo}:ref:refs/heads/${var.github_branch}",
              "repo:${var.github_repo}*:ref:refs/heads/${var.github_branch}"
            ]
          }
        }
      }
    ]
  })

  tags = {
    Name = "${var.project_name}-${var.environment}-github-actions-role"
  }
}

# -----------------------------------------------------------------------------
# IAM Policy: Scoped Least-Privilege Deployment Permissions
# -----------------------------------------------------------------------------
resource "aws_iam_policy" "github_actions_deploy" {
  name        = "${var.project_name}-${var.environment}-github-deploy-policy"
  description = "Allows GitHub Actions to push images to ECR and deploy to ECS"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      # ECR Auth Token (requires * resource per AWS specification)
      {
        Effect   = "Allow"
        Action   = ["ecr:GetAuthorizationToken"]
        Resource = "*"
      },
      # ECR Image Push/Pull scoped strictly to the target repository
      {
        Effect = "Allow"
        Action = [
          "ecr:BatchCheckLayerAvailability",
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage",
          "ecr:PutImage",
          "ecr:InitiateLayerUpload",
          "ecr:UploadLayerPart",
          "ecr:CompleteLayerUpload",
          "ecr:DescribeRepositories",
          "ecr:ListImages"
        ]
        Resource = var.ecr_repository_arn
      },
      # ECS Service Deployments
      {
        Effect = "Allow"
        Action = [
          "ecs:DescribeServices",
          "ecs:DescribeTaskDefinition",
          "ecs:DescribeTasks",
          "ecs:ListTasks",
          "ecs:RegisterTaskDefinition",
          "ecs:UpdateService"
        ]
        Resource = "*"
      },
      # IAM PassRole scoped strictly to the ECS task execution and task roles
      {
        Effect = "Allow"
        Action = ["iam:PassRole"]
        Resource = [
          var.ecs_execution_role_arn,
          var.ecs_task_role_arn
        ]
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "github_actions" {
  role       = aws_iam_role.github_actions.name
  policy_arn = aws_iam_policy.github_actions_deploy.arn
}

Configuring GitHub Actions with Dynamic CloudTrail Session Logging#

In your deployment workflow (.github/workflows/deploy-app.yml), configure authentication with two critical properties:

  1. environment: dev (or staging, production) to activate GitHub Environment governance.

  2. role-session-name: gha-${{ github.job }}-${{ github.run_id }} to gain full traceability in AWS CloudTrail.

yaml
name: Deploy Application to ECS

on:
  push:
    branches: [ main ]
    paths:
      - "app/**"
      - ".github/workflows/deploy-app.yml"
  workflow_dispatch:

permissions:
  id-token: write   # Required for requesting the OIDC JWT
  contents: read    # Required to checkout code

jobs:
  test-and-build-deploy:
    name: Build, Push & Deploy to ECS Fargate
    runs-on: ubuntu-latest
    environment: dev   # Enables environment protection rules & claims

    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      # -----------------------------------------------------------------------
      # Authenticate to AWS via OIDC
      # -----------------------------------------------------------------------
      - name: Configure AWS Credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/ecs-flask-dev-github-actions-role
          aws-region: us-east-1
          audience: sts.amazonaws.com
          role-session-name: gha-${{ github.job }}-${{ github.run_id }}

      - name: Log in to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build, Tag, and Push Docker Image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ecs-flask-dev
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -t $ECR_REGISTRY/$ECR_REPOSITORY:latest ./app
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

Why role-session-name is a Game-Changer for Incident Response#

By default, if role-session-name is omitted, the aws-actions/configure-aws-credentials action passes a generic static string: GitHubActions.

In AWS CloudTrail, every single API event executed by any workflow across any repo will look identical:

text
arn:aws:sts::123456789012:assumed-role/ecs-flask-dev-github-actions-role/GitHubActions

If 50 deployments run in a day, tracing a rogue change or failed deployment requires manually cross-referencing timestamps between systems.

With role-session-name: gha-${{ github.job }}-${{ github.run_id }}, CloudTrail logs:

json
{
  "eventSource": "sts.amazonaws.com",
  "eventName": "AssumeRoleWithWebIdentity",
  "roleSessionName": "gha-test-and-build-deploy-12345678901",
  "userIdentity": {
    "arn": "arn:aws:sts::123456789012:assumed-role/ecs-flask-dev-github-actions-role/gha-test-and-build-deploy-12345678901"
  }
}

Now, anyone investigating an alert in CloudTrail or Amazon GuardDuty can copy the run ID (12345678901) and immediately open the exact GitHub run:

text
https://github.com/<owner>/<repo>/actions/runs/12345678901

You instantly see the triggering commit, PR author, build status, and console logs.

AWS Session Name Constraints to Keep in Mind:#

  • Length: Must be between 2 and 64 characters.
  • Allowed Characters: [\w+=,.@-] (alphanumeric, period, dash, underscore, plus).
  • Avoid: ${{ github.ref_name }} (branches like feature/login have slashes / that STS rejects) and ${{ github.workflow }} (workflow names often contain spaces).

Troubleshooting & Debugging Playbook#

When an OIDC setup fails, AWS returns a generic error to prevent information disclosure:

text
Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity

Here is how to diagnose the issue in minutes using CloudTrail:

Step #1: Query CloudTrail for the Failed Attempt#

Run the AWS CLI to inspect the rejected AssumeRoleWithWebIdentity call:

bash
aws cloudtrail lookup-events \
  --region us-east-1 \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
  --max-results 5

Step #2: Inspect the userName Claim#

Look inside the CloudTrailEvent payload for:

json
"userName": "repo:my-org@123456/my-repo@987654321:environment:dev"

Common failure scenarios and remedies:

Cause in CloudTrail Symptoms Remedy
Immutable ID mismatch userName contains @<id> (e.g. repo:org@123/repo@456:...) Switch Condition from StringEquals to StringLike with repo:org*/repo*:*.
Environment vs. Branch Workflow specifies environment: dev, but IAM trust policy only checks ref:refs/heads/main Add "repo:...:environment:<env>" to the trust policy sub array.
Case Sensitivity GitHub username or repo has uppercase characters (e.g. MyOrg/Repo) Claims in JWT are case-sensitive. Ensure IAM trust policy matches GitHub casing exactly.
Missing permissions Workflow runner fails before calling AWS Add permissions: id-token: write at the root or job level in the workflow YAML.
Audience mismatch Policy checks aud: sts.amazonaws.com, but token was requested with default GitHub URL Explicitly set audience: sts.amazonaws.com in configure-aws-credentials.

Summary Checklist for Production Readiness#

Before pushing your OIDC setup to production, review this checklist:

  • Zero Static IAM Keys: No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY in GitHub Secrets.
  • Drift-Free IaC: No dynamic data "tls_certificate" lookups in Terraform/OpenTofu; static thumbprints used to satisfy the IAM API schema.
  • No Wide Wildcards: Trust policy restricts repository ownership explicitly (no "repo:*:*").
  • Immutable ID Compatibility: Trust policy handles GitHub's modern @<user_id> and @<repo_id> subject format using StringLike.
  • Multi-Account Isolation: Deployment jobs bound to GitHub Environments (environment: <name>), paired with GitHub branch protection rules pinned to main.
  • CloudTrail Traceability: role-session-name configured with gha-${{ github.job }}-${{ github.run_id }} for single-click investigation of AWS audit logs.
  • Least Privilege Execution: Deployment IAM role grants access strictly to designated ECR repos, ECS services, and iam:PassRole restricted to ECS task execution roles.

Frequently Asked Questions (FAQ)#

What is AWS OIDC for GitHub Actions and how does it work?#

Direct Answer: AWS OIDC (OpenID Connect) for GitHub Actions is a federated identity mechanism that allows GitHub workflow runners to authenticate directly to AWS without static access keys. GitHub generates a short-lived, digitally signed JSON Web Token (JWT) for each workflow job, which AWS Security Token Service (STS) validates and exchanges for temporary AWS credentials (ASIA...) valid for up to one hour via sts:AssumeRoleWithWebIdentity.


Do I still need thumbprints for GitHub Actions OIDC in AWS IAM?#

Direct Answer: No, AWS IAM no longer uses certificate thumbprints to validate GitHub Actions at runtime. Since July 6, 2023, AWS validates token.actions.githubusercontent.com against its own internal library of trusted root Certificate Authorities (CAs).

However, because the underlying AWS IAM API schema still requires 1 to 5 40-character hex values in the ThumbprintList parameter, you should pass static known thumbprints (such as ["6938fd4d98bab03faadb97b34396831e3780aea1", "1c58a3a8518e8759bf075b76b750d4f2df264fcd"]) in Terraform or OpenTofu. Do not use data "tls_certificate" to dynamically fetch fingerprints, as it creates perpetual plan diffs and risks pipeline outages during intermediate TLS certificate rotations.


Why does GitHub Actions fail with "Not authorized to perform sts:AssumeRoleWithWebIdentity"?#

Direct Answer: This error indicates that AWS STS rejected the GitHub OIDC token because the token's claims did not satisfy the IAM Role's trust policy conditions.

The top five causes are:

  1. Subject (sub) claim mismatch: The repository name, branch, or environment in the token does not match the trust policy Condition block (claims are strictly case-sensitive).

  2. GitHub Immutable IDs: For new repositories, GitHub appends numerical IDs (e.g., repo:org@123/repo@456:...), causing strict StringEquals checks on raw names to fail. Use StringLike with repo:org*/repo*:* instead.

  3. Missing workflow permission: The workflow lacks permissions: id-token: write, preventing the runner from generating a JWT.

  4. Audience (aud) mismatch: The token was requested with a custom audience or GitHub's default URL rather than sts.amazonaws.com.

  5. Environment vs. branch mismatch: The workflow specifies environment: dev, changing the sub claim to repo:org/repo:environment:dev, while the IAM policy only permits ref:refs/heads/main.

To diagnose the exact failure reason, search AWS CloudTrail Event History for EventName = AssumeRoleWithWebIdentity and inspect the userName field in the event record.


What is the difference between branch-based and environment-based OIDC scoping?#

Direct Answer: Branch-based scoping locks AWS role assumption to git branches (e.g. repo:org/repo:ref:refs/heads/main), whereas environment-based scoping locks assumption to a declared GitHub Environment (e.g. repo:org/repo:environment:production).

Environment-based scoping is superior for production deployments because it unlocks GitHub Environment Protection Rules:

  • Manual Approvals: Deployment jobs can require mandatory review from designated platform or lead engineers before execution.
  • Branch Restrictions: GitHub allows you to enforce that only protected branches (like main) can run jobs targeting that environment.
  • Multi-Account Governance: Each AWS account (e.g., dev, staging, prod) can have an IAM role that trusts exclusively its corresponding GitHub Environment, preventing non-production feature branches from ever assuming production credentials.

Can multiple GitHub repositories share a single AWS OIDC Identity Provider?#

Direct Answer: Yes. You only need to create the IAM OpenID Connect Provider (https://token.actions.githubusercontent.com) once per AWS account. Multiple repositories, pipelines, and teams can share the same provider.

Authorization boundaries are enforced at the IAM Role level, not the provider level. Each repository assumes its own dedicated IAM Role with a trust policy that restricts access using token.actions.githubusercontent.com:sub and token.actions.githubusercontent.com:aud.


How do I trace which GitHub Action run performed an AWS action in CloudTrail?#

Direct Answer: By setting role-session-name: gha-${{ github.job }}-${{ github.run_id }} in the aws-actions/configure-aws-credentials step.

By default, all workflow executions show the generic session name GitHubActions in AWS CloudTrail. Passing github.run_id dynamically embeds the workflow execution ID directly into the CloudTrail assumed-role ARN (e.g., arn:aws:sts::123456789012:assumed-role/my-role/gha-deploy-12345678901). Security and DevOps teams can extract that run ID and immediately navigate to https://github.com/<owner>/<repo>/actions/runs/<run_id> to view the exact commit, PR, and build output.


What IAM permissions are needed to create an AWS OIDC Provider?#

Direct Answer: To provision the provider and its associated deployment role via Terraform, OpenTofu, or CloudFormation, your deployment identity requires:

  • iam:CreateOpenIDConnectProvider, iam:GetOpenIDConnectProvider, iam:DeleteOpenIDConnectProvider, iam:TagOpenIDConnectProvider
  • iam:CreateRole, iam:GetRole, iam:UpdateAssumeRolePolicy, iam:DeleteRole
  • iam:CreatePolicy, iam:AttachRolePolicy
Indika Kodagoda

Indika Kodagoda

Indika Kodagoda is a Lead DevOps Engineer, AWS certification instructor, and the creator of CloudQubes. He specializes in cloud infrastructure, automation, and modern Ruby on Rails development. When he’s not deploying code or mentoring aspiring engineers, he’s usually enjoying nature and cycling local gravel paths.


Large View