Cloud

Fix: GitHub Actions cannot deploy to AWS — OIDC setup guide

GitHub Actions gets AccessDenied when calling AWS. Here's the OIDC setup that removes long-lived credentials and gets deploys working.

Your GitHub Actions workflow tries to deploy to AWS and fails:

Error: Could not load credentials from any providers

Or:

An error occurred (AccessDenied) when calling the AssumeRole operation

You have AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in secrets — technically works, but rotating them is painful and they leak in logs. OIDC (OpenID Connect) fixes both: no long-lived credentials, short-lived tokens per workflow run. Here’s the complete setup, plus the common failures.

Why OIDC over static credentials

  • No long-lived keys — nothing to rotate, nothing to leak
  • Per-workflow scoping — restrict which repos/branches can assume which roles
  • Short-lived tokens — expire in 15-60 min automatically
  • Audit trail — CloudTrail shows the exact GitHub Actions run that assumed the role

For any AWS account touched by CI, OIDC is the current best practice.

Setup — three steps

Step 1 — Register GitHub as an OIDC provider in AWS

Only needed once per AWS account:

aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com

If it errors with EntityAlreadyExists, that’s fine — the provider already exists.

Note on thumbprints: older guides pin a specific thumbprint (6938fd4d98bab03faadb97b34396831e3780aea1). Since July 2023, AWS validates GitHub’s certificate against its library of trusted root CAs and ignores any thumbprint you pass for the GitHub provider — thumbprints are now optional and effectively a no-op for this integration. Omitting the flag lets IAM auto-record whichever thumbprint it wants at create time; it makes no runtime difference.

Step 2 — Create an IAM role GitHub Actions can assume

Save as trust-policy.json:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::YOUR_ACCOUNT_ID: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:YOUR_ORG/YOUR_REPO:*"
        }
      }
    }
  ]
}

Replace YOUR_ACCOUNT_ID, YOUR_ORG, and YOUR_REPO.

Create the role:

aws iam create-role \
  --role-name github-actions-deploy \
  --assume-role-policy-document file://trust-policy.json

Attach permissions the workflow actually needs (least-privilege):

aws iam attach-role-policy \
  --role-name github-actions-deploy \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess

Never attach AdministratorAccess here — CI has full account access and one leak = full compromise.

Step 3 — Use the role in the workflow

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write            # ← critical, or OIDC token isn't issued
    steps:
      - uses: actions/checkout@v5

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v5
        with:
          role-to-assume: arn:aws:iam::YOUR_ACCOUNT_ID:role/github-actions-deploy
          aws-region: ap-south-1

      - name: Verify
        run: aws sts get-caller-identity

      - name: Deploy
        run: aws s3 sync ./dist s3://my-bucket/

That’s it. Delete the old AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from repo secrets — you don’t need them.

Common failures and their fixes

Failure 1 — Missing id-token: write permission

Error: Error: Could not load credentials from any providers

The workflow needs explicit id-token: write at either job or workflow level. Without it, GitHub doesn’t issue the OIDC token that AWS needs.

Fix:

jobs:
  deploy:
    permissions:
      id-token: write
      contents: read

Failure 2 — Trust policy sub claim doesn’t match

Error: AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity

GitHub sends a sub claim like repo:my-org/my-repo:ref:refs/heads/main. Your trust policy’s condition must match.

Diagnose — log the actual sub claim:

Add a debug step:

- name: Debug OIDC token
  run: |
    IDTOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
      "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r .value)
    jwt=$(echo $IDTOKEN | cut -d. -f2 | base64 -d 2>/dev/null || true)
    echo "$jwt" | jq

Match the sub value to your trust policy condition. Common patterns:

  • Any branch: "repo:my-org/my-repo:*"
  • Main branch only: "repo:my-org/my-repo:ref:refs/heads/main"
  • Tags only: "repo:my-org/my-repo:ref:refs/tags/*"
  • Specific environment: "repo:my-org/my-repo:environment:production"

Failure 3 — InvalidIdentityToken for reasons unrelated to thumbprints

Error: An error occurred (InvalidIdentityToken)

Since July 2023 AWS ignores the OIDC provider’s thumbprint for the GitHub identity provider — validation is done against AWS’s trusted root CA library instead. If you’re hitting InvalidIdentityToken, the cause is almost never a “wrong” thumbprint; more common causes:

  • The provider ARN in your trust policy Principal.Federated doesn’t match the actual OIDC provider in the account
  • The audience (aud) in the trust policy condition isn’t sts.amazonaws.com
  • A network path issue prevents AWS from reaching GitHub’s JWKS endpoint

Verify the provider exists and matches the ARN in your trust policy:

aws iam list-open-id-connect-providers
aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com

If you need to force a thumbprint refresh anyway (harmless, does nothing at runtime):

aws iam update-open-id-connect-provider-thumbprint \
  --open-id-connect-provider-arn arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com \
  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

Failure 4 — Role assumed but IAM policy blocks the actual API call

Error: OIDC works, but the specific API call still fails with AccessDenied.

Diagnose:

aws iam list-attached-role-policies --role-name github-actions-deploy

Add the missing policy or extend the inline policy.

Failure 5 — Region mismatch

Error: AccessDenied on S3 in a specific region.

Some IAM policies scope to Resource: arn:aws:s3:::my-bucket/* but the code calls a bucket in a different region. Verify:

aws s3api get-bucket-location --bucket my-bucket

Match the workflow’s aws-region input to the bucket’s region.

Failure 6 — Reusable workflow inheritance

Error: OIDC works in the main workflow but fails in reusable workflows.

Permissions don’t inherit into reusable workflows. Set them explicitly:

jobs:
  call-reusable:
    permissions:
      id-token: write
      contents: read
    uses: my-org/reusable-workflows/.github/workflows/deploy.yml@main

The universal OIDC debug flow

Every failed OIDC deploy, in order:

# 1. Check workflow permissions include id-token: write
grep "id-token" .github/workflows/*.yml

# 2. Debug the actual sub claim
# (add the OIDC token debug step from Failure 2 above)

# 3. Verify trust policy condition matches
aws iam get-role --role-name github-actions-deploy | jq .Role.AssumeRolePolicyDocument

# 4. Confirm role has the AWS API permissions the workflow needs
aws iam list-attached-role-policies --role-name github-actions-deploy

# 5. Test locally with the same role (via aws sts assume-role-with-web-identity)

Ninety percent of OIDC failures resolve at step 1 or 2.

Prevention

For every new repo that touches AWS:

  • Per-repo IAM roles — no shared “ci-role”; each repo gets its own with scoped permissions
  • Branch-restricted trust policies — production role only assumable from refs/heads/main
  • CloudTrail alarms on AssumeRoleWithWebIdentity outside expected patterns
  • No static AWS keys in secrets — audit and delete any old ones
  • Reusable workflows publish their required permissions in README — so callers don’t have to guess
  • Terraform module for the OIDC provider + roles — one command per new repo

Bottom line

OIDC replaces static AWS credentials with short-lived, scoped tokens. Setup is three steps: register the provider once per AWS account, create a role with a trust policy scoped to your repo, use aws-actions/configure-aws-credentials in the workflow with id-token: write permission. The most common failures are missing that permission and mismatched sub claim in the trust policy. Both surface clearly with the debug flow above. Once running, delete the static keys — you don’t need them anymore.

Skip the boilerplate

DevOps YAML Pack

40+ production-ready configs — Kubernetes, Docker, Terraform, Ansible, Helm, GitHub Actions. Every file commented. Copy, edit two lines, ship. MIT license, no watermarks.

Get the pack — ₹499 →