GitHub Actions + OIDC: Kill the Long-Lived Keys

Stop pasting AWS access keys into GitHub secrets. Let Actions assume an IAM role via OIDC and hand out short-lived credentials per run.

The single most common AWS credential leak isn't a clever hack — it's a long-lived AKIA... access key sitting in a CI secret, a .env, or a Slack message. Modern pipelines don't store AWS keys at all. GitHub Actions is an OIDC identity provider, and AWS IAM can trust it directly.

How the handshake works

  1. You register GitHub's OIDC provider (token.actions.githubusercontent.com) as an IAM identity provider — once per account.
  2. You create an IAM role whose trust policy allows only your repo (and ideally only certain branches or environments) to assume it.
  3. At runtime, aws-actions/configure-aws-credentials exchanges the job's signed OIDC token for temporary STS credentials that expire in an hour.

Why this is the senior default

  • Nothing to rotate — there's no static secret to leak or expire.
  • Scoped by claim — the trust policy's sub condition pins access to a repo and ref, so a fork or a feature branch can't assume prod.
  • Auditable — CloudTrail shows exactly which workflow run assumed the role.

Example

Example · yaml
# .github/workflows/deploy.yml
name: deploy-prod
on:
  push:
    branches: [main]

permissions:
  id-token: write   # REQUIRED for the OIDC token
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (no static keys)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gha-myapp-deploy
          aws-region: us-east-1

      - name: Deploy
        run: |
          aws sts get-caller-identity
          aws ecs update-service --cluster myapp-prod --service web --force-new-deployment

When to use it

  • A security team rotates all AWS access keys out of GitHub Secrets in one afternoon by replacing them with OIDC role assumptions, eliminating secret sprawl.
  • A monorepo uses OIDC with separate IAM roles per service so a compromise of the checkout service workflow cannot touch the payment service's resources.
  • An audit shows zero long-lived AWS credentials in any CI system after OIDC is adopted, passing the company's SOC 2 access review.

More examples

Full OIDC GitHub Actions workflow

Complete workflow that exchanges a GitHub OIDC token for short-lived AWS credentials and deploys to ECS without any stored secret.

Example · yaml
name: Deploy
on:
  push:
    branches: [main]

permissions:
  id-token: write
  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::123456789012:role/github-deploy-prod
          aws-region: us-east-1
      - run: aws ecs update-service --cluster prod --service myapp --force-new-deployment

Scope OIDC role to specific branch

Restricts the IAM role's trust policy to only the main branch, preventing feature branches from assuming production credentials.

Example · json
{
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
      "token.actions.githubusercontent.com:sub": "repo:myorg/myapp:ref:refs/heads/main"
    }
  }
}

Register GitHub OIDC provider

One-time setup: registers GitHub Actions as a trusted OIDC provider so IAM can validate the JWT tokens it issues.

Example · bash
aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

echo 'GitHub OIDC provider registered'

Discussion

  • Be the first to comment on this lesson.