/ DEVOPS, AUTOMATION

AWS CI/CD permissions setup guide with OIDC

If your GitHub Actions workflow still authenticates to AWS with an AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY pair stored in repository secrets, you’re carrying a long-lived credential that works from anywhere, doesn’t expire on its own, and has to be rotated by hand. OpenID Connect (OIDC) removes that liability: GitHub’s runner requests a short-lived, workflow-scoped token, and AWS exchanges it for temporary credentials — no secret ever sits in your repo.

This is a hands-on setup guide, intermediate level, using the official AWS IAM and GitHub Actions documentation.


How it works

  1. Your workflow requests an OIDC token from GitHub’s token endpoint (token.actions.githubusercontent.com).
  2. That token is a signed JWT containing claims about the run — which repo, which branch or tag, which environment.
  3. aws-actions/configure-aws-credentials sends the token to AWS STS’s AssumeRoleWithWebIdentity.
  4. AWS validates the token against an IAM OIDC identity provider you registered, checks the IAM role’s trust policy against the token’s claims, and — if it matches — hands back temporary credentials scoped to that role.

No secret changes hands. The only thing worth protecting is the trust policy’s condition, since that’s what decides who can assume the role.

Step 1: Register GitHub as an OIDC identity provider in IAM

In the IAM console, go to Identity providers → Add provider → OpenID Connect, and set:

  • Provider URL: https://token.actions.githubusercontent.com
  • Audience: sts.amazonaws.com

Or via the CLI:

aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com

You do not need to manage a certificate thumbprint yourself — AWS now validates the identity provider’s TLS certificate against its own trusted CA bundle automatically, and only falls back to a stored thumbprint if that isn’t possible. Create this provider once per AWS account; every role that trusts GitHub Actions references the same provider.

Step 2: Create an IAM role with a scoped trust policy

This is the step that actually determines your security posture. The trust policy must check the sub (subject) claim so that only the repositories and branches you intend can assume the role:

{
  "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",
          "token.actions.githubusercontent.com:sub": "repo:mikamboo/tech-blog:ref:refs/heads/main"
        }
      }
    }
  ]
}

A few things worth getting right here:

  • Scope the sub claim as tightly as you can. repo:org/repo:ref:refs/heads/main limits assumption to pushes on main. If you also deploy from tags or PRs, use StringLike with a wildcard (repo:org/repo:*) only if you genuinely need it — a wide-open sub condition is the single most common misconfiguration in OIDC setups.
  • Prefer GitHub environments for production roles. Scoping the sub claim to repo:org/repo:environment:production lets you layer GitHub’s own environment protection rules (required reviewers, wait timers) on top of the AWS trust boundary.
  • New as of July 2026: GitHub now supports immutable subject claims for repositories created after July 15, 2026, in the form repo:org@<org_id>/repo@<repo_id>:ref:refs/heads/main. Because the numeric IDs can’t be reused even if the repo is renamed or transferred, this closes a narrow spoofing window that the name-based sub claim had. If you’re setting this up on a newer repository, check whether your org has this available and prefer it.

Attach a least-privilege permissions policy to the same role — scope it to the specific resources your pipeline touches (a single S3 bucket, a specific ECR repository, a specific ECS service), not *.

Step 3: Configure the GitHub Actions workflow

name: Deploy to AWS
on:
  push:
    branches: [main]

permissions:
  id-token: write   # required to request the OIDC token
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<ACCOUNT_ID>:role/github-actions-deploy
          role-session-name: tech-blog-deploy
          aws-region: eu-west-1

      - name: Deploy
        run: aws s3 sync ./dist s3://my-bucket/ --delete

Two details that are easy to miss:

  • permissions: id-token: write only grants the workflow permission to request an OIDC token — it does not itself grant any AWS access. The actual access is entirely determined by the IAM role’s trust policy and permissions policy.
  • Pin aws-actions/configure-aws-credentials to a full commit SHA rather than a floating tag if you’re following GitHub’s supply-chain hardening guidance for third-party actions — a tag can be moved, a SHA can’t.

Verifying it

Run the workflow and check the job logs for the assumed role ARN — aws sts get-caller-identity as a debug step is the fastest way to confirm you’re running as the expected role rather than silently falling back to some other credential source in the runner environment.

Why this matters over static keys

  • No secret to leak. There’s nothing in Settings → Secrets that grants standing AWS access — the token is minted per-run and expires quickly.
  • No rotation cadence to manage. Static access keys need a rotation policy and someone remembering to run it. OIDC has nothing to rotate.
  • Auditable by construction. Every AssumeRoleWithWebIdentity call in CloudTrail carries the GitHub run’s claims, so you can trace exactly which workflow run touched AWS and when.

Sources: AWS IAM — Create an OIDC identity provider, GitHub Actions — Configuring OpenID Connect in Amazon Web Services.