BlogCloud

GitHub Actions to AWS with OIDC: no more long-lived keys

Federate your workflows into AWS with short-lived tokens scoped to repo and branch. Delete the access keys after.

Jan 20, 2026·8 min readIntermediate

A long-lived AWS_ACCESS_KEY_ID in your repo secrets is a credential that never expires, works from anywhere on earth, and leaks the first time someone echos the wrong variable. OIDC federation deletes it: GitHub mints a short-lived token per run, AWS trades it for temporary credentials, and there's nothing left in the repo to steal.

Token exchange
1
Workflow runs
requests OIDC token
2
GitHub signs
JWT with repo + ref claims
3
AWS STS
AssumeRoleWithWebIdentity
4
Short creds
1h, scoped to the role

A role that only your repo can assume

Register GitHub's OIDC provider once, then write a trust policy on the IAM role. The critical line is the sub condition — it pins which repo and which branch may assume the role. Get this wrong and any fork can assume it.

trust-policy.json
{
"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"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:myorg/myapp:ref:refs/heads/main"
}
}
}
Never wildcard the sub claim
A sub of repo:myorg/*:* lets any branch of any repo in your org — including an attacker’s PR branch — assume the role. Pin the repo and the ref. Use environment-scoped subs (repo:org/app:environment:prod) for production roles.

The workflow side

Grant the job id-token: write — without it GitHub won't issue the OIDC token — then let the official action do the STS exchange. No aws-access-key-id, no aws-secret-access-key.

.github/workflows/deploy.yml
permissions:
id-token: write # required to request the OIDC token
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: us-east-1
- run: aws s3 sync ./dist s3://my-artifacts

Inside the job the runner is now a first-class IAM principal — a temporary assumed-role session that expires in an hour:

bash — GitHub Actions runnerlive
aws sts get-caller-identity
using web identity token from GitHub OIDC ...
 
{
"Account": "123456789012",
"Arn": "arn:aws:sts::...:assumed-role/gha-deploy/GitHubActions"
}
 
session expires in 1h — nothing stored in the repo

Where this goes next

Once it works, delete the access keys — a federated role next to a live long-lived key is just the old attack surface with extra steps. The same pattern federates GitHub into GCP (Workload Identity) and Azure (federated credentials), and the sub claim can gate production behind a protected environment.

Go deeper in a courseSecure CI/CD with GitLabHarden pipelines end to end — identity, secrets and gates.View course

Related posts