Retire the AWS keys in your GitHub secrets. OIDC, in one sitting.


Every repository I inherit has the same two secrets in it: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. Nobody remembers which IAM user they belong to. The access key was created during a deploy setup in some previous year, its rotation date has been aspirational ever since, and the one thing everybody agrees on is that rotating it might break the deploy, so it does not get rotated. That is not a credential. That is a liability with a name.

GitHub Actions can drop both secrets entirely. The workflow asks GitHub for a short-lived signed token, AWS trusts GitHub as an identity provider, and the run assumes a role for an hour. Nothing long-lived exists to leak, rotate or mourn. Setup is fifteen minutes of IAM, once.

The setup

  1. Register the identity provider — once per AWS account: an OIDC provider for token.actions.githubusercontent.com with audience sts.amazonaws.com. The console does it in four clicks.
  2. Create the role the workflow will assume. Attach your least-privilege deployment policy to it — this part you were doing anyway.
  3. Write the trust policy, which is where all the security lives:
{
  "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:ref:refs/heads/main"
      }
    }
  }]
}
  1. Point the workflow at it:
permissions:
  id-token: write
  contents: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
          aws-region: eu-west-1
      - run: aws sts get-caller-identity

The permissions block is not optional — without id-token: write the workflow cannot ask for a token, and the failure message will not tell you that is why.

The sub claim is the whole security model

The sub condition is what stops every other GitHub customer from assuming your role, so it deserves respect. The format is exact and case-sensitive: repo:ORG/REPO:ref:refs/heads/main for branch pushes, repo:ORG/REPO:pull_request for pull request events, and repo:ORG/REPO:environment:production when you deploy from a GitHub environment — which is the pattern I prefer for production, because the environment protection rules (approvers, branch restrictions) then sit in front of the role.

The trap is StringLike with a wildcard. repo:my-org/* is five characters of convenience and a blast radius that includes the next repo somebody creates in the org. Keep the condition exact; add roles when you add repos.

The honest trade-offs

Where I landed

No new repository gets static keys — the template has the OIDC wiring in it. The existing repos migrate the next time their key would have needed rotating, which is now never. The account has exactly as many long-lived AWS credentials as it has humans, and none of them live in a secrets drawer.