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.Federateddoesn’t match the actual OIDC provider in the account - The audience (
aud) in the trust policy condition isn’tsts.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
AssumeRoleWithWebIdentityoutside 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.
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 →