Free — no signup required

OIDC Federation: Death to Long-Lived Keys

4 min read

The Old Way: Static Secrets

Historically, to let GitHub Actions deploy to AWS, you would:

  1. Create an IAM User in AWS.
  2. Generate a long-lived Access Key (AKIA... prefix).
  3. Paste that key into GitHub Secrets as AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.

This key is valid indefinitely — until someone manually rotates or deletes it. Long-lived credentials are a persistent liability:

  • If the key leaks (in a log, a commit, a Slack message), attackers have permanent access until you notice and rotate.
  • Rotation is manual, error-prone, and often skipped.
  • The key exists in two places (AWS and GitHub), doubling the attack surface.
  • There is no automatic expiry — a forgotten key from a decommissioned project remains valid.

The New Way: OpenID Connect (OIDC) Federation

OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0. It allows one system to prove its identity to another using cryptographically signed tokens, without sharing a password or secret.

OIDC Federation applies this to CI/CD: your CI provider (GitHub, GitLab, CircleCI) acts as an Identity Provider (IdP). Your cloud provider (AWS, GCP, Azure) acts as a Relying Party that trusts tokens issued by that IdP.

The result: no secrets are ever stored anywhere. The pipeline proves who it is using a short-lived, cryptographically signed token, and the cloud issues temporary credentials in exchange.

How It Works: Step by Step

GitHub Actions Runner          GitHub OIDC Provider          AWS STS
       |                              |                          |
       |-- 1. Request OIDC token ---> |                          |
       |<-- 2. JWT (signed, ~5min) -- |                          |
       |                              |                          |
       |-- 3. AssumeRoleWithWebIdentity(JWT) -----------------> |
       |        AWS validates JWT signature against GitHub's     |
       |        public keys, checks 'sub' claim matches          |
       |        the configured repository                        |
       |<-- 4. Temporary credentials (1hr TTL) ----------------- |
       |                                                         |
       |-- 5. Use credentials to deploy (S3, ECS, etc.) ------> |

Step 1-2: Token Generation

When a GitHub Actions job runs, it can request a JWT from GitHub's OIDC provider at https://token.actions.githubusercontent.com. This JWT contains claims — facts about the job:

{
  "iss": "https://token.actions.githubusercontent.com",
  "sub": "repo:octocat/hello-world:ref:refs/heads/main",
  "aud": "sts.amazonaws.com",
  "repository": "octocat/hello-world",
  "ref": "refs/heads/main",
  "job_workflow_ref": "octocat/hello-world/.github/workflows/deploy.yml@refs/heads/main",
  "exp": 1700000300,
  "iat": 1700000000
}

The sub (Subject) claim uniquely identifies the source of the token. The exp (Expiry) claim means the token is only valid for a few minutes.

Step 3-4: The Exchange

The runner calls AWS STS (Security Token Service) with the JWT. AWS:
1. Fetches GitHub's public signing keys from a well-known URL.
2. Validates the JWT signature — confirming it was genuinely issued by GitHub.
3. Checks the sub claim against the Condition in the IAM Role's Trust Policy.
4. If everything matches, issues temporary credentials valid for 1 hour.

Step 5: Temporary Credentials

The runner now has AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN — but they expire in 1 hour. When the job finishes, the credentials are useless. There is nothing to steal, rotate, or audit.

The Critical Security Detail: The sub Condition

When you configure the IAM Role Trust Policy, you must add a condition that restricts which repositories can assume the role. Without it, any GitHub repository in the world could request credentials for your AWS account. The policy names GitHub's OIDC provider as a trusted federated principal, and its Condition block checks two things: that the token's audience matches AWS's STS service, and that the token's sub claim matches an allowed pattern — for example, any branch or tag within a specific repository. For production deployments, you can tighten that sub condition further, matching only the exact ref refs/heads/main rather than a wildcard. This restricts credential issuance to jobs running on the main branch only — preventing a feature branch from deploying to production.

Interview Tip

A common interview question is: "What's the difference between an IAM User with an access key and OIDC federation for CI/CD?" The answer has three dimensions: (1) Lifetime — access keys are permanent until rotated; OIDC credentials expire in ~1 hour automatically. (2) Storage — access keys must be stored somewhere (GitHub Secrets, environment variables); OIDC requires no stored secrets. (3) Blast radius — a leaked access key gives persistent access; a leaked OIDC token is useless after expiry and is scoped to a specific repository by the Trust Policy condition.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free