Replace Long-Lived AWS Credentials with GitHub Actions OIDC
Configure IAM and GitHub Actions to exchange short-lived, automatically-expiring AWS credentials on every workflow run — no stored access keys, no rotation toil.
What You'll Build
You'll configure AWS IAM and GitHub Actions to use OpenID Connect (OIDC) federation, so your workflow requests short-lived AWS credentials on every run instead of reading a stored AWS_ACCESS_KEY_ID secret. Zero long-lived keys, zero rotation toil.
Prerequisites
- AWS account with permissions to create IAM OIDC providers and roles
- AWS CLI v2 (
aws --version→aws-cli/2.x) installed and configured with credentials - A GitHub repository (org or personal) where you control workflow files and Settings
- Familiarity with IAM trust policies and GitHub Actions YAML syntax
OS note: All CLI commands run identically on macOS, Linux, and WSL2. Native Windows Command Prompt users should use AWS CloudShell or WSL2.
Step 1: Register GitHub's OIDC Provider in IAM
AWS must trust tokens issued by GitHub's OIDC endpoint. Run this once per AWS account—it's shared across all repos.
aws iam create-open-id-connect-provider \
--url "https://token.actions.githubusercontent.com" \
--client-id-list "sts.amazonaws.com" \
--thumbprint-list "6938fd4d98bab03faadb97b34396831e3780aea1"
Thumbprint note: As of late 2023, AWS validates GitHub OIDC tokens against the provider's root CA rather than this thumbprint value. The parameter is still required by the API but is not used for verification for GitHub's provider. See the AWS thumbprint docs for details.
The command returns an ARN like arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com. Save your Account ID—you'll need it next.
If you get EntityAlreadyExistsException, the provider already exists. Skip ahead.
Step 2: Create a Scoped IAM Role
2a. Write the trust policy
Save this as trust-policy.json, replacing ACCOUNT_ID, MY-ORG, and MY-REPO:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::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:MY-ORG/MY-REPO:ref:refs/heads/main"
}
}
}
]
}
Use the narrowest sub claim scope your workflow requires:
| Condition value | Who can assume |
|---|---|
repo:ORG/REPO:ref:refs/heads/main |
main branch pushes only |
repo:ORG/REPO:environment:production |
production environment jobs only |
repo:ORG/REPO:pull_request |
Pull request events only |
repo:ORG/REPO:* |
Any event in the repo (broadest—avoid in prod) |
2b. Create the role
aws iam create-role \
--role-name GitHubActions-Deploy \
--assume-role-policy-document file://trust-policy.json \
--description "Assumed by GitHub Actions via OIDC"
2c. Attach a permissions policy
Attach only what the workflow needs. This example grants read-only S3 access:
aws iam attach-role-policy \
--role-name GitHubActions-Deploy \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
For real deployments, write a least-privilege custom policy. Never attach AdministratorAccess.
Record the role ARN from the create-role output: arn:aws:iam::ACCOUNT_ID:role/GitHubActions-Deploy.
Step 3: Update Your GitHub Actions Workflow
Two required changes: grant id-token: write permission and use aws-actions/configure-aws-credentials.
name: Deploy
on:
push:
branches: [main]
permissions:
id-token: write # Required: allows the workflow to request an OIDC token
contents: read # Required: allows actions/checkout to fetch the repo
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/GitHubActions-Deploy
aws-region: us-east-1
- name: Smoke test — confirm assumed identity
run: aws sts get-caller-identity
Remove any AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY entries from GitHub Secrets and any env: blocks that reference them. They are no longer needed.
Scope tip: Place the
permissionsblock at the job level if other jobs in the same workflow file should not receive this grant.
Verify It Works
Push to main (or run the workflow manually via Actions → Run workflow). The aws sts get-caller-identity step should print:
{
"UserId": "AROA...:GitHubActions-Deploy",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:assumed-role/GitHubActions-Deploy/GitHubActions-Deploy"
}
The assumed-role in the ARN confirms the workflow runs as the IAM role, not a long-lived IAM user. Credentials expire automatically after one hour (the default STS session duration).
Troubleshooting
Not authorized to perform sts:AssumeRoleWithWebIdentity
The sub claim in the OIDC token didn't match your trust policy condition. Inspect the actual claim by adding this step temporarily (requires id-token: write and jq, which is pre-installed on ubuntu-latest). The JWT payload is base64url-encoded, so tr converts it to standard base64 before decoding:
curl -sH "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sts.amazonaws.com" \
| jq -r '.value' | cut -d. -f2 | tr -- '-_' '+/' | base64 -d 2>/dev/null | jq -r .sub
Compare the output against your StringLike condition. Branch names, case sensitivity, and wildcard placement are common mismatches.
id-token permission has not been granted
The permissions block is missing or id-token is set to read. It must be exactly id-token: write.
EntityAlreadyExistsException on provider creation
The OIDC provider already exists in this AWS account—one provider serves all repos. Skip Step 1.
Role assumes but downstream AWS calls are denied
The permissions policy is too narrow. Check CloudTrail (aws cloudtrail lookup-events) for the denied action and add it to your permissions policy. Don't widen the trust policy—the problem is permissions, not trust.
Next Steps
- Environments: Use GitHub Environments with required reviewers and map each to a separate IAM role with different permissions scopes.
- Multiple repos: Use
StringLikewith an org-level wildcard (repo:MY-ORG/*) carefully, or create per-repo roles for fine-grained attribution. - Audit trail: AWS CloudTrail logs every
AssumeRoleWithWebIdentityevent; thesourceIdentityfield captures thesubclaim so you can trace which repo triggered each call. - Infrastructure as code: The AWS Terraform provider and AWS CDK both support OIDC provider and role resources natively—manage this setup the same way you manage the rest of your infrastructure.
Emeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read.
Discussion 0
No comments yet
Be the first to weigh in.