Deploying code to AWS (or any cloud) requires credentials. Long-lived access keys are a security liability — steal them and someone can access your cloud account. OIDC federation: GitHub proves which repository a workflow is running in, and AWS temporarily grants permissions just for that workflow, without long-lived secrets.
4 min read
Deploy code to AWS → GitHub Actions needs AWS credentials → store AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as GitHub Secrets → workflow uses those credentials.
Problems:
OpenID Connect (OIDC) is a standard for proving identity. GitHub can issue OIDC tokens that say "this workflow is running in repository alice/my-app on branch main." AWS trusts GitHub and exchanges those tokens for temporary credentials — no permanent secrets needed.
First, create an OIDC provider in your AWS account that trusts GitHub:
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
# GitHub publishes a signing certificate; include its thumbprint
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aca1"]
}Then create an IAM role that GitHub workflows can assume:
resource "aws_iam_role" "github_deploy" {
name = "github-deploy-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:alice/my-app:ref:refs/heads/main"
}
}
}]
})
}
# Attach deployment permissions to the role
resource "aws_iam_role_policy" "deploy_policy" {
role = aws_iam_role.github_deploy.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"ecr:PutImage",
"ecr:GetAuthorizationToken",
"lambda:UpdateFunctionCode"
]
Resource = [
"arn:aws:ecr:us-east-1:123456789:repository/my-app",
"arn:aws:lambda:us-east-1:123456789:function:my-function"
]
}]
})
}The Condition block is critical: the workflow can only assume the role if it's running in the alice/my-app repository on the main branch. No other repository, no other branch, no manually-created OIDC tokens can use this role.
name: Deploy
on:
push:
branches:
- main
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Assume AWS role
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-deploy-role
aws-region: us-east-1
- name: Push to ECR and deploy
run: |
aws ecr get-login-password | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com
docker build -t my-app:latest .
docker tag my-app:latest $AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/my-app:latest
docker push $AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/my-app:latestThe configure-aws-credentials action automatically:
aws CLI commandsNo secrets stored; no keys to rotate.
Even with OIDC, apply least-privilege to the role:
main branch; pull request workflows use a different, more-restricted role.# Main-branch deploy role (can update production)
resource "aws_iam_role" "github_prod_deploy" {
assume_role_policy = jsonencode({
Condition = {
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:alice/my-app:ref:refs/heads/main"
}
}
})
}
# PR workflow role (can only run tests, not deploy)
resource "aws_iam_role" "github_pr_checks" {
assume_role_policy = jsonencode({
Condition = {
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:alice/my-app:pull_request"
}
}
})
}Thumbprint changes: GitHub's OIDC signing certificate eventually rotates. AWS publishes a new thumbprint, and you must update your Terraform configuration. Most CI/CD services automate this, but Terraform's thumbprint list is static — set a calendar reminder to update it annually, or use a data source that fetches it dynamically.
Overly broad sub condition: sub: "repo:alice/my-app:*" allows any branch to assume the role. Tighten it to specific branches or environments. Even better, use ref:refs/heads/main for production deploys.
Missing permissions block: workflows need id-token: write permission to generate and sign OIDC tokens:
permissions:
id-token: write # required for OIDC
contents: read # required to check out codeWithout this, the OIDC token generation fails silently or with a cryptic error.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why is OIDC federation safer than storing AWS credentials in GitHub Secrets?
2. What does the 'sub' condition in an OIDC trust policy control, and why is it important?
3. Which GitHub Actions workflow permission is required for OIDC to work?
4. Why should production and development deployments use different IAM roles?
CI/CD & Deployment Pipelines