GitHub Actions OIDC to AWS: No Long-Lived Keys
Storing AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as GitHub secrets means a leaked key works from anywhere until someone notices and rotates it. OpenID Connect (OIDC) replaces them: each workflow run asks GitHub for a signed token, and AWS exchanges it for temporary credentials that expire in an hour or less. This guide shows the setup and how to restrict it properly.
How it works
- The workflow requests an ID token from GitHub. The token states which repository, branch and environment the job runs for.
- The job presents the token to AWS STS with
AssumeRoleWithWebIdentity. - AWS checks the signature against the identity provider you registered, evaluates the role's trust policy against the token's claims, and returns temporary credentials.
There is no secret in GitHub. The security comes from the trust policy conditions, so writing them carefully is the whole job.
Step 1: Create the identity provider
In IAM, add an OpenID Connect provider with:
- Provider URL:
https://token.actions.githubusercontent.com - Audience:
sts.amazonaws.com
AWS validates GitHub's token signatures using its own library of trusted root CAs, so you no longer need to manage a thumbprint for this provider. In the console it is a few clicks; with the CLI:
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com
You need one provider per account, shared by all roles.
Step 2: Create a role with a tight trust policy
The sub (subject) claim identifies the workflow context. Common formats:
- Branch:
repo:ORG/REPO:ref:refs/heads/main - Environment:
repo:ORG/REPO:environment:production - Pull request:
repo:ORG/REPO:pull_request - Tag:
repo:ORG/REPO:ref:refs/tags/v1.0.0
Trust policy for deploys from the production environment of one repository:
{
"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:my-org/my-repo:environment:production"
}
}
}
]
}
Use StringEquals with an exact sub where you can. If you need several branches, use StringLike with a narrow pattern such as repo:my-org/my-repo:ref:refs/heads/release/. Never use a trust policy that checks only aud, and never repo:my-org/:*: that lets any repository in the organization, including ones with weak review, assume the role.
Attach a permissions policy with only what the job needs, such as s3:PutObject on one bucket or an ECR push policy for one repository. Separate roles for build, deploy-staging and deploy-production make blast radius small, and environment-based subjects let you require manual approval on production.
Step 3: Use it in the workflow
The job needs permission to request the token:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-deploy-production
aws-region: us-east-1
role-session-name: gha-${{ github.run_id }}
- run: aws sts get-caller-identity
- run: aws s3 sync ./dist s3://my-bucket --delete
id-token: write does not give write access to your repository; it only allows requesting the OIDC token. Setting it also removes the default permissions you did not list, hence contents: read. Pin aws-actions/configure-aws-credentials to a SHA, as in securing GitHub Actions.
aws sts get-caller-identity is the quickest check that the role was assumed and that the account and role ARN are what you expect.
Customizing the subject claim
By default the sub has the form above. Organization and repository owners can customize which claims are included in sub through the OIDC settings API, for example to include repository_id or job_workflow_ref. Including job_workflow_ref lets you require that only a specific reusable workflow can assume the role:
"StringLike": {
"token.actions.githubusercontent.com:job_workflow_ref": "my-org/platform-workflows/.github/workflows/deploy.yml@refs/heads/main"
}
AWS supports a limited set of claim keys in IAM condition keys for this provider; check the AWS IAM documentation for the list of supported keys such as aud, sub and job_workflow_ref before relying on one.
Common errors
| Error | Cause | Fix |
|---|---|---|
Not authorized to perform sts:AssumeRoleWithWebIdentity | sub does not match the trust policy | Print the token claims or compare branch, environment and repo name exactly, including case |
Credentials could not be loaded | Missing id-token: write | Add the permission at job or workflow level |
No OpenIDConnect provider found | Provider missing in this account or wrong URL | Create it with the URL above |
Works on main, fails in a PR | PR runs have a different sub | Add a separate, read-only role for pull_request, or do not grant AWS access to PRs |
| Fails with environment | Job lacks environment: so sub has the ref form | Set environment: in the job |
| Session expired mid-job | Default duration is 1 hour | Use role-duration-seconds up to the role's maximum, or split the job |
Pull requests and forks
Fork PRs cannot request an OIDC token for your repository's identity, which is what you want. For same-repo PRs, avoid giving them write access to AWS: a PR can edit the workflow file, so any role a PR can assume must be safe for anyone with write access to the repo to use. Give PR roles read-only permissions, or use plan access only for infrastructure checks.
Beyond AWS
The same pattern works for GCP workload identity federation, Azure federated credentials, HashiCorp Vault, and package registries that support trusted publishing (PyPI, npm and RubyGems). Moving all of them to OIDC removes your long-lived secrets.
FAQ
Do I still need to rotate anything?
No access keys to rotate. Keep reviewing trust policies, since they are now your access control.
Can I use OIDC with self-hosted runners?
Yes. The token is issued by GitHub for the job, regardless of where it runs. The runner still needs network access to AWS STS. For third-party runners, confirm that OIDC is supported.
What does the token contain?
Claims including iss, aud, sub, repository, ref, environment, workflow and run_id. GitHub's docs list them all.
Is it safe to put the role ARN in the workflow?
Yes. An ARN is an identifier, not a secret. The trust policy decides who can use it.
Related
For general workflow hardening, read securing GitHub Actions. Estimate what your deploys cost with the EC2 cost calculator and the S3 cost calculator. compiler.dev runners can request GitHub OIDC tokens like any other runner, so a trust policy you write today keeps working if you change where jobs run.
Made by compiler.dev. Free tools · Pricing