Skip to content
compiler.dev

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

  1. The workflow requests an ID token from GitHub. The token states which repository, branch and environment the job runs for.
  2. The job presents the token to AWS STS with AssumeRoleWithWebIdentity.
  3. 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

ErrorCauseFix
Not authorized to perform sts:AssumeRoleWithWebIdentitysub does not match the trust policyPrint the token claims or compare branch, environment and repo name exactly, including case
Credentials could not be loadedMissing id-token: writeAdd the permission at job or workflow level
No OpenIDConnect provider foundProvider missing in this account or wrong URLCreate it with the URL above
Works on main, fails in a PRPR runs have a different subAdd a separate, read-only role for pull_request, or do not grant AWS access to PRs
Fails with environmentJob lacks environment: so sub has the ref formSet environment: in the job
Session expired mid-jobDefault duration is 1 hourUse 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.

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