
Deploy to AWS with GitHub Actions OIDC, Not Keys
Long-lived AWS access keys in CI are difficult to rotate and remain useful if copied from a secret store. GitHub Actions can instead request an OpenID Connect token for a specific workflow run and exchange it for temporary AWS credentials through IAM. The result is a deployment pipeline with no AWS access key stored in GitHub.
This guide configures the trust boundary, creates a least-privilege role, and uses the official AWS credentials action. The examples intentionally separate who may assume the role from what that role may do.
Understand the OIDC flow
- A workflow starts in a repository that is permitted by your trigger and environment rules.
- GitHub issues a short-lived, signed OIDC token containing claims such as audience and subject.
- AWS validates that token against the GitHub identity provider and the role trust policy.
- AWS STS returns temporary credentials for the role.
- The workflow uses only the AWS permissions attached to that role.
OIDC removes stored cloud keys, but it does not make every workflow safe. A broad trust policy or administrator permissions can still turn a compromised workflow into a serious incident.
Gather the values you will bind
Use your real AWS account ID, repository owner, repository name, region, and deployment environment. This tutorial assumes a protected GitHub environment named production. Store non-secret configuration such as region in repository or environment variables.
Create GitHub's OIDC provider in AWS
Create this provider once per AWS account. The provider URL is https://token.actions.githubusercontent.com, and the audience for the standard AWS partition is sts.amazonaws.com:
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com
If the provider already exists, do not create a duplicate. You can inspect it with aws iam list-open-id-connect-providers. Organizations using AWS Organizations or infrastructure as code should manage this account-level object centrally.
Create a narrowly scoped trust policy
The role trust policy determines which GitHub identity may request temporary credentials. Replace the account ID and repository details:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012: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:acme/example-app:environment:production"
}
}
}]
}
Save it as github-oidc-trust.json, then create the role:
aws iam create-role \
--role-name github-production-deployer \
--assume-role-policy-document file://github-oidc-trust.json
Matching the exact sub claim is crucial. An environment subject binds the role to jobs that reference that GitHub environment. If you bind a branch instead, the subject has a branch form such as repo:acme/example-app:ref:refs/heads/main. Avoid a repository-wide wildcard unless you fully understand which workflows and refs it admits.
Attach only deployment permissions
The trust policy controls assumption; a permissions policy controls AWS actions afterward. The exact policy depends on the target. For an S3-hosted static site, start with only the required bucket operations:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DeployStaticSite",
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::example-production-site"
}, {
"Sid": "WriteStaticAssets",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::example-production-site/*"
}]
}
Do not attach AdministratorAccess for convenience. An ECS deployment, Lambda update, or ECR push needs a different policy; grant only the actions and resource ARNs the deployment command actually uses.
Protect the GitHub environment
Create a production environment in the repository settings. Restrict deployment branches and, where appropriate, require reviewers. Environment protection happens before a job can obtain credentials, complementing the AWS trust policy.
Configure the workflow
The job needs id-token: write to request an OIDC token and contents: read to check out code. These permissions do not grant AWS access by themselves:
name: deploy-production
on:
push:
branches: [main]
permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
- run: npm run build
- name: Configure temporary AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::123456789012:role/github-production-deployer
aws-region: us-east-1
role-session-name: github-production
allowed-account-ids: 123456789012
- name: Confirm caller
run: aws sts get-caller-identity
- name: Deploy static assets
run: aws s3 sync dist/ s3://example-production-site/ --delete</code></pre>
As of August 26, 2026, the official action's current major release is v6. For stricter supply-chain control, pin actions to a reviewed full commit SHA and update them through an automated dependency process. The allowed-account-ids check helps catch an accidental assumption into the wrong account.
Keep build and deployment trust separate
Build and test before requesting cloud credentials. This reduces the amount of third-party code that executes while AWS variables are available. Avoid running unreviewed pull-request code in a job that can enter the production environment, and review changes to workflow files as security-sensitive changes.
Test safely before the first deployment
- Temporarily replace the deployment command with
aws sts get-caller-identity.
- Run the workflow from the permitted branch and environment.
- Confirm the returned account and assumed-role ARN.
- Try an unauthorized branch or environment and confirm role assumption fails.
- Add one deployment operation at a time, tightening the permissions policy from CloudTrail evidence.
This negative test is important: a successful main-branch run proves the happy path, while a rejected unauthorized run proves that the trust boundary is doing useful work.
Troubleshooting
Not authorized to perform sts:AssumeRoleWithWebIdentity
Compare the workflow's actual repository, ref, and environment with the trust policy subject. Confirm that the job declares the environment when the policy expects an environment subject and that the audience is sts.amazonaws.com.
Error: id-token permission is missing
Add id-token: write at the workflow or job level. If a reusable workflow performs authentication, review permission propagation in both the caller and called workflow.
The role is assumed but deployment is denied
This means federation worked and the role's AWS permissions are insufficient. Read the denied action and resource, then adjust the permissions policy narrowly. Do not broaden the trust policy to solve an authorization error.
A forked pull request can reach the job
Separate pull-request validation from deployment, use protected environments, and ensure production jobs do not execute untrusted fork code. Treat workflow modifications like application code with mandatory review.
Production checklist
- No long-lived AWS access key is stored in GitHub secrets.
- The OIDC provider uses the correct URL and AWS audience.
- The trust policy validates both
aud and a narrow sub.
- The deployment role has least-privilege resource permissions.
- The workflow requests only
contents: read and id-token: write.
- Production uses environment branch restrictions and reviewers where appropriate.
- Build and tests finish before temporary credentials are requested.
- The expected AWS account is checked and CloudTrail is monitored.
- Actions are pinned or updated through a controlled dependency process.
- An unauthorized branch has been tested and correctly rejected.
Official references