Skip to content
Latchkey

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

Terminal
# 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.json

Use it in the workflow

.github/workflows/deploy.yml
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-identity

Scope the trust condition properly

sub conditionWho 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/mainOnly workflows running on main
repo:MY_ORG/MY_REPO:environment:productionOnly 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.

Terminal
# 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-failed

What usually goes wrong first

  • The workflow file must exist on the default branch before scheduled or dispatch triggers appear at all.
  • GITHUB_TOKEN permissions default to read-only in many organisations. Declare a permissions: 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/checkout gives you depth 1 on a detached HEAD, so anything needing history or a branch name needs fetch-depth: 0.

Frequently asked questions

Why does configure-aws-credentials fail with OIDC?
Almost always a missing 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?
Yes, substantially. There is no long-lived secret to leak or rotate, credentials last only for the job, and the trust policy can restrict access to a specific repository, branch, or environment.
Can a fork pull request assume my role?
Not if the trust condition is scoped correctly. Fork pull requests get a read-only token and a 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?
Not necessarily, but scope the trust condition to the repositories that should use it. A role trusting repo:MY_ORG/* is assumable by every repository in the org, including ones added later by anyone with permission to create them.

Related guides

References

Run this faster and cheaper on Latchkey managed runners - self-healing included. Start free → 30-day trial · No credit card