GitHub Actions OIDC with AWS: short-lived CI credentials without static access keys
Replace static AWS keys in CI with OIDC to IAM roles: GitHub issuer registration, trust policies on jwt claims, least-privilege roles, and common integration failures.
A pipeline fails at 2 a.m. because someone rotated an IAM user’s access key—but half the repositories still referenced the old secret name in GitHub Actions. Another team discovers an unused deploy key with AdministratorAccess that predates anyone currently on staff. These incidents share a root cause: long-lived symmetric credentials embedded in CI systems outlive the humans who created them, accumulate across repos, and resist orderly revocation. OpenID Connect (OIDC) federation between GitHub and AWS fixes the credential model: each workflow run obtains short-lived credentials by proving identity and context (org, repo, branch, environment) to IAM—no permanent keys in Secrets Manager for the common case.
This article explains why OIDC beats static keys, how AWS IAM trust policies bind to GitHub’s issuer and subject claims, how to shape least-privilege roles for deploy versus build jobs, and what breaks in real integrations (wrong aud, ref patterns that never match, confusion between GitHub-hosted and self-hosted runners). The patterns come from production migrations I have helped teams execute when tightening deploy pipelines and preparing systems for predictable, auditable access—the same bar I apply when advising on scalable release workflows (About).
Why static keys in CI rot faster than your runbooks
IAM access keys are shared secrets. Their security properties are poor for automation that runs continuously:
- Duplication: every fork or template copy multiplies the secret; rotation becomes a scavenger hunt across orgs.
- Blast radius: a single key often grants broader API surface than any single pipeline needs.
- Weak binding: the key proves “someone who once had this blob”—not “this workflow on
mainin repository X.”
OIDC shifts the proof to cryptographic assertions: GitHub’s Actions service signs JWTs; AWS trusts GitHub’s OIDC issuer and exchanges those assertions for STS AssumeRoleWithWebIdentity credentials that expire in minutes. Leaks hurt less when credentials are scoped, short-lived, and non-reusable outside the intended trust chain.
OIDC federation at a glance
- GitHub registers as an OIDC identity provider in your AWS account (issuer URL, thumbprints).
- You create one or more IAM roles whose trust policy allows
sts:AssumeRoleWithWebIdentityfrom that provider only when JWT claims match constraints you define (repository, environment, ref). - The workflow uses
aws-actions/configure-aws-credentials(or equivalent) withrole-to-assume, no access key secrets. - Each job receives temporary credentials (
ASIA…) scoped to the role’s policies and duration.
The mental model: the workflow is the client, GitHub is the token issuer, and IAM is the relying party that maps (issuer, subject, audience) → role session.
AWS: identity provider and trust policy mechanics
Register the GitHub OIDC provider
You add an IAM OIDC provider pointing at GitHub’s issuer (commonly https://token.actions.githubusercontent.com). AWS documents thumbprints for the issuer’s TLS chain; treat this as infrastructure as code so every account and landing zone stays consistent.
Trust policy: sub, aud, and string conditions
GitHub’s JWT includes claims such as:
iss: stable issuer URL.sub: a string likerepo:ORG/REPO:ref:refs/heads/mainorrepo:ORG/REPO:environment:productionwhen using environments.aud: defaults to a URL-shaped audience; with AWS federation you typically set this explicitly (see pitfalls).
Your trust policy uses StringEquals / StringLike on these claims. Example shape (illustrative—adjust org/repo patterns):
{
"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"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:my-org/my-service:ref:refs/heads/main"
}
}
}
]
}
Why aud matters: AWS expects a specific audience when you configure the federation path used by configure-aws-credentials. Mismatch manifests as InvalidIdentityToken—a frequent copy-paste failure when mixing older blog posts with current Action defaults.
Separate roles by blast radius
Production hygiene usually means multiple roles, not one “CI admin” role:
- Read-only for plans, tests, and Terraform validate that should never mutate data.
- Deploy with permission to update specific CloudFormation stacks, Lambda aliases, or ECS services—not full IAM or organization APIs unless unavoidable.
- Release artifacts (ECR push) separated from runtime deploy so a compromised build step cannot silently rewrite unrelated infrastructure.
This mirrors how I structure reviews for teams shipping production-ready pipelines: separate what proves code from what mutates live systems.
GitHub Actions: permissions, environments, and subject strings
Minimal permissions
Workflows need:
permissions:
id-token: write # required for OIDC token minting
contents: read # typical for checkout
Without id-token: write, OIDC token issuance fails in opaque ways. This trips teams migrating from key-based auth where permissions blocks were never defined.
Repositories, refs, and GitHub Environments
Plain branch filters map cleanly to refs/heads/main. Pull requests from forks are a different threat model: GitHub deliberately restricts secrets and OIDC behavior for fork PRs—do not assume your deploy role trust pattern works for arbitrary external contributors.
GitHub Environments (with protection rules and optional reviewers) change the sub claim to include environment:NAME, which is excellent for production roles: you bind IAM trust to repo:ORG/REPO:environment:production instead of a branch name alone—aligning approval gates with AWS trust.
Practical example: deploy role + workflow snippet
Assume account 111111111111, role name gha-deploy-my-service, region eu-west-1. IAM policy attachments allow updating a named CloudFormation stack and publishing versions—details omitted for brevity.
Workflow fragment:
name: deploy
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111111111111:role/gha-deploy-my-service
aws-region: eu-west-1
- name: Deploy stack
run: aws cloudformation deploy --stack-name my-service-prod --template-file packaged.yaml --capabilities CAPABILITY_IAM
On the AWS side, the trust policy’s sub condition aligns with either environment:production or ref:refs/heads/main—pick one coherent strategy per role so operators can reason about access during incidents.
For Terraform or CDK-driven stacks, the same role assumption precedes terraform apply only after plan review—often split across jobs with different roles for plan (read) versus apply (write).
Common mistakes and pitfalls
- Missing
id-token: write— OIDC never reaches AWS; debugging begins at job permissions, not IAM. - Wrong
aud/ configure-aws-credentials version drift — validate against current AWS + Action docs when templates age past a year. - Over-broad
StringLikeonsub—repo:my-org/*style patterns are easy to mis-specify; typos silently deny access or, worse, you widen until “it works.” Prefer environments or explicit repo lists generated from IaC. - Using deploy roles on PR workflows from forks — security controls differ; treat fork builds as untrusted inputs.
- Ignoring session duration and chaining — keep sessions short; avoid naively nesting multiple role assumptions without mapping trust through single-hop designs teams can trace.
- Self-hosted runners — the OIDC token still comes from GitHub, but network egress and runner integrity become your operational surface; threat model accordingly.
None of these are hypothetical—they are the recurring checklist items when hardening CI for teams that need clear audit trails and bounded blast radius.
Conclusion
Replacing static AWS keys in GitHub Actions with OIDC federation is one of the highest-leverage security upgrades a backend team can make: credentials become short-lived, context-bound, and easier to reason about in IAM alone—without secret rotation theater across dozens of repos. The implementation is mostly discipline in trust policies and workflow permissions, not exotic cryptography. Invest up front in role splitting, environment-based subjects, and IaC-managed OIDC providers so every new service inherits the same bar.
If you are designing CI/CD hardening, cloud landing zones, or production deploy pipelines and want a second pair of eyes on trust boundaries, Contact is the best place to reach out.
Suscríbete al boletín
Recibe un correo cuando se publiquen artículos nuevos. Sin spam — solo entradas nuevas de este blog.
Con Resend. Puedes darte de baja en cualquier correo.