How to Set Up OIDC Between GitHub Actions and AWS
OIDC removes stored AWS keys entirely: the job exchanges a short-lived GitHub token for temporary credentials. The setup has one dangerous step, and it is the trust policy condition.
Storing an AWS access key as a repository secret means a long-lived credential that never rotates and is readable by anything that can run a workflow. OIDC replaces it: GitHub issues a short-lived signed token, AWS trusts that issuer, and the job assumes a role for the length of the run.
The setup is short. The part to get right is the trust policy condition, because a policy scoped only to the GitHub issuer will let workflows in any GitHub repository assume your role.
Create the identity provider and role
# 1. register GitHub as an OIDC provider (once per account)
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com
# 2. trust policy, scoped to YOUR repo and ref
cat > trust.json <<JSON
{
"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" }
}
}]
}
JSON
aws iam create-role --role-name gha-deploy \
--assume-role-policy-document file://trust.jsonUse it in the workflow
permissions:
id-token: write # required to request the OIDC token
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::<ACCOUNT_ID>:role/gha-deploy
aws-region: us-east-1
- run: aws sts get-caller-identityScope the trust condition properly
sub condition | Who can assume the role |
|---|---|
repo:* | Any GitHub repository. Never use this |
repo:MY_ORG/* | Any repository in your organisation |
repo:MY_ORG/MY_REPO:* | Any branch, tag, or PR in that repository |
repo:MY_ORG/MY_REPO:ref:refs/heads/main | Only workflows running on main |
repo:MY_ORG/MY_REPO:environment:production | Only jobs using the production environment |
Verify it actually works
A workflow that runs is not a workflow that works. Confirm the behaviour on a real event rather than on a manual dispatch, because trigger conditions, permissions, and context values all differ between the two.
# 1. validate the file before pushing
docker run --rm -v "$(pwd):/repo" --workdir /repo rhysd/actionlint:latest -color
# 2. trigger the real event, not workflow_dispatch
git commit --allow-empty -m "ci: verify trigger" && git push
# 3. watch it and read the conclusion, not just the colour
gh run watch
gh run view --log-failedWhat usually goes wrong first
- The workflow file must exist on the default branch before scheduled or dispatch triggers appear at all.
GITHUB_TOKENpermissions default to read-only in many organisations. Declare apermissions:block listing every scope the job needs.- Fork pull requests get a read-only token and no access to secrets, regardless of workflow configuration.
actions/checkoutgives you depth 1 on a detached HEAD, so anything needing history or a branch name needsfetch-depth: 0.
Frequently asked questions
Why does configure-aws-credentials fail with OIDC?
id-token: write permission on the job. The action cannot request the OIDC token without it and reports a generic credentials error rather than a permissions one.Is OIDC more secure than AWS access keys?
Can a fork pull request assume my role?
sub that does not match a branch or environment condition. This is exactly why the sub condition must not be a wildcard.Do I need one role per repository?
repo:MY_ORG/* is assumable by every repository in the org, including ones added later by anyone with permission to create them.