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
- Your workflow requests an OIDC token from GitHub’s token endpoint (
token.actions.githubusercontent.com). - That token is a signed JWT containing claims about the run — which repo, which branch or tag, which environment.
aws-actions/configure-aws-credentialssends the token to AWS STS’sAssumeRoleWithWebIdentity.- 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
subclaim as tightly as you can.repo:org/repo:ref:refs/heads/mainlimits assumption to pushes onmain. If you also deploy from tags or PRs, useStringLikewith a wildcard (repo:org/repo:*) only if you genuinely need it — a wide-opensubcondition is the single most common misconfiguration in OIDC setups. - Prefer GitHub environments for production roles. Scoping the
subclaim torepo:org/repo:environment:productionlets 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-basedsubclaim 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: writeonly 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-credentialsto 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 → Secretsthat 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
AssumeRoleWithWebIdentitycall 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.