Solving the AWS OIDC Chicken-and-Egg Problem with GitHub Actions
2025-08-27
Solving the AWS OIDC Chicken-and-Egg Problem with GitHub Actions
AWS + GitHub OIDC: Escape from Long-Lived Keys
Everyone wants to get rid of IAM access keys in CI/CD. They’re risky, hard to rotate, and inevitably leak.
AWS solved this with OIDC federation: your GitHub Actions runner requests a short-lived token directly from AWS STS using a signed OIDC identity, no secrets required.
It sounds perfect. But there’s one catch: 👉 To configure OIDC in the first place, you need AWS credentials.
Welcome to the bootstrap paradox.
Environment Setup
mkdir oidc-bootstrap cd oidc-bootstrap
We’ll explore this bootstrap step from first principles and then run a full demo of GitHub Actions authenticating into AWS without static keys.
The Chicken-and-Egg Problem
First principles:
-
AWS will only trust GitHub’s OIDC provider once you explicitly register it.
-
Terraform needs state storage in S3, but you can’t even terraform init unless the bucket exists.
-
Docker pushes to ECR fail if the repo doesn’t exist.
So how do you create those resources… if the very credentials you need to do so don't exist yet?
Answer: you bootstrap once with minimal, temporary credentials.
Bootstrapping the OIDC Trust
Step 1 — Create an OIDC provider in IAM:
aws iam create-open-id-connect-provider \ --url https://token.actions.githubusercontent.com \ --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1 \ --client-id-list sts.amazonaws.com
Step 2 — Create an IAM role trusted by GitHub:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<account_id>:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:moabukar/oidc-bootstrap:*"
}
}
}
]
}
aws iam create-role \ --role-name github-oidc-role \ --assume-role-policy-document file://trust-policy.json
Attach permissions:
aws iam attach-role-policy \ --role-name github-oidc-role \ --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess
At this point, AWS trusts GitHub. Now you can safely delete the bootstrap credentials.
GitHub Actions in Steady State
No long-lived IAM keys needed — just OIDC:
name: Deploy
on: [push]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::<account_id>:role/github-oidc-role
aws-region: eu-west-2
- run: aws s3 ls
GitHub Actions fetches an OIDC token, exchanges it for AWS STS creds, and runs with no static secrets.
Example Scenarios
A. Terraform Remote State (S3)
Bootstrap credentials create S3 bucket + DynamoDB table.
Switch Terraform backend to S3.
All future terraform apply runs in GitHub Actions via OIDC.
B. ECR Repository
Bootstrap credentials create repo with aws ecr create-repository.
Pipelines authenticate via OIDC and push images.
C. Lambda Deployment
Bootstrap user creates role + trust policy.
GitHub OIDC assumes role and deploys Lambda updates seamlessly.
Test
To see the token exchange in action:
aws sts get-caller-identity
From GitHub Actions, the output shows the IAM role ARN, not a static user. That’s how you know OIDC federation is working.
Lessons Learned
-
Bootstrap once. OIDC can't configure itself.
-
Scope narrowly. Create the minimum S3/ECR/roles you need, then cut off bootstrap access.
-
Delete long-lived keys. After setup, your AWS account should run 100% keyless.
Pain Point: The Paradox in Practice
This bootstrap paradox frustrates everyone:
-
New engineers expect OIDC to “just work” and wonder why Terraform can’t even create its own state bucket.
-
Security teams get nervous when they see IAM users reappearing.
The fix is boring but necessary: one minimal bootstrap, then never again.