github-actions-oidc-aws
GitHub Actions OIDC Authentication for AWS
This is a reference pattern. Learn from the approach, adapt to your context — don't copy verbatim.
Status: 🔴 CRITICAL PATTERN
Category: CI/CD / Infrastructure
Applies To: Any project using GitHub Actions to deploy to AWS
Overview
Secure authentication pattern for GitHub Actions workflows to access AWS resources using OpenID Connect (OIDC) instead of long-lived IAM credentials. Eliminates the need to store AWS access keys in GitHub secrets.
Key Benefits:
- No long-lived credentials to rotate or leak
- Temporary credentials with automatic expiration
- Repository-scoped access control
- Audit trail via AWS CloudTrail
- Industry best practice (AWS + GitHub recommended)
Problem
GitHub Actions workflows need to authenticate to AWS to deploy infrastructure, trigger pipelines, or manage resources. Traditional approaches have security issues:
Anti-Pattern: Static IAM Credentials
# ❌ Security risk: long-lived credentials in secrets
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Problems:
- Credentials never expire (must be manually rotated)
- If leaked, attacker has persistent access
- No way to scope to specific repositories
- Difficult to audit which workflow used credentials
- Violates principle of least privilege
Solution
Use GitHub's OIDC provider to issue temporary credentials via AWS IAM role assumption.
Flow:
GitHub Actions → OIDC Token → AWS STS → Temporary Credentials → AWS Resources
How it works:
- GitHub Actions requests OIDC token from GitHub
- Workflow presents token to AWS STS
- AWS validates token against IAM OIDC provider
- AWS issues temporary credentials (valid 1 hour)
- Workflow uses temporary credentials to access AWS
Components
1. AWS IAM OIDC Provider
Establishes trust between AWS and GitHub's OIDC issuer.
Configuration:
- URL:
https://token.actions.githubusercontent.com - Audience:
sts.amazonaws.com - Thumbprint:
1c58a3a8518e8759bf075b76b750d4f2df264fcd(GitHub root CA)
Why root CA thumbprint?
- More stable than intermediate certificate
- GitHub can rotate intermediate certs without breaking trust
- Recommended by AWS documentation
2. IAM Role with Trust Policy
Role that GitHub Actions can assume, with trust policy restricting access.
Trust Policy Structure:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::{account}: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:{owner}/{repo}:*"
}
}
}]
}
Trust Policy Scoping Options:
# All branches and tags
"repo:owner/repo:*"
# Specific branch only
"repo:owner/repo:ref:refs/heads/main"
# Multiple branches
["repo:owner/repo:ref:refs/heads/main", "repo:owner/repo:ref:refs/heads/dev"]
# Pull requests
"repo:owner/repo:pull_request"
# Environment-specific
"repo:owner/repo:environment:production"
3. IAM Permissions Policy
Defines what the role can do in AWS (principle of least privilege).
Examples by use case:
Pipeline Trigger Only:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "codepipeline:StartPipelineExecution",
"Resource": "arn:aws:codepipeline:*:{account}:pipeline-name-*"
}]
}
Direct S3 Deployment:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::bucket-name", "arn:aws:s3:::bucket-name/*"]
},
{
"Effect": "Allow",
"Action": "cloudfront:CreateInvalidation",
"Resource": "*"
}
]
}
Infrastructure Deployment (use with caution):
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}]
}
4. GitHub Actions Workflow Configuration
Workflow must request OIDC token and assume role.
Required Permissions:
permissions:
id-token: write # Required for OIDC token
contents: read # Required to checkout code
Authentication Step:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::{account}:role/{role-name}
aws-region: {region}
Implementation
Step 1: Create OIDC Provider (One-Time Setup)
Choose your infrastructure tool:
AWS CDK (TypeScript):
import * as iam from 'aws-cdk-lib/aws-iam';
const provider = new iam.OpenIdConnectProvider(this, 'GitHubProvider', {
url: 'https://token.actions.githubusercontent.com',
clientIds: ['sts.amazonaws.com'],
thumbprints: ['1c58a3a8518e8759bf075b76b750d4f2df264fcd']
});
Terraform:
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["1c58a3a8518e8759bf075b76b750d4f2df264fcd"]
}
AWS CLI:
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 1c58a3a8518e8759bf075b76b750d4f2df264fcd
CloudFormation:
GitHubOIDCProvider:
Type: AWS::IAM::OIDCProvider
Properties:
Url: https://token.actions.githubusercontent.com
ClientIdList:
- sts.amazonaws.com
ThumbprintList:
- 1c58a3a8518e8759bf075b76b750d4f2df264fcd
Step 2: Create IAM Role
AWS CDK (TypeScript):
const role = new iam.Role(this, 'GitHubActionsRole', {
roleName: 'GitHubActionsRole',
assumedBy: new iam.WebIdentityPrincipal(provider.openIdConnectProviderArn, {
StringEquals: {
'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com'
},
StringLike: {
'token.actions.githubusercontent.com:sub': `repo:${owner}/${repo}:*`
}
}),
inlinePolicies: {
DeploymentPermissions: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['codepipeline:StartPipelineExecution'],
resources: [`arn:aws:codepipeline:*:${this.account}:pipeline-*`]
})
]
})
}
});
Terraform:
resource "aws_iam_role" "github_actions" {
name = "GitHubActionsRole"
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:${var.github_owner}/${var.github_repo}:*"
}
}
}]
})
}
resource "aws_iam_role_policy" "github_actions" {
name = "deployment-permissions"
role = aws_iam_role.github_actions.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "codepipeline:StartPipelineExecution"
Resource = "arn:aws:codepipeline:*:${data.aws_caller_identity.current.account_id}:pipeline-*"
}]
})
}
Step 3: Update GitHub Actions Workflow
Before (Static Credentials):
name: Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- run: aws s3 sync ./dist s3://my-bucket
After (OIDC):
name: Deploy
on: [push]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
- run: aws s3 sync ./dist s3://my-bucket
Step 4: Remove Old Secrets (Cleanup)
# List current secrets
gh secret list
# Remove old credentials
gh secret remove AWS_ACCESS_KEY_ID
gh secret remove AWS_SECRET_ACCESS_KEY
Configuration Management
Avoid Hardcoding Account IDs
Anti-Pattern:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
Better: Use Repository Variables:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/GitHubActionsRole
Best: Read from Configuration File:
- name: Load config
id: config
run: |
echo "account=$(grep '^AWS_ACCOUNT_ID=' .env | cut -d'=' -f2)" >> $GITHUB_OUTPUT
echo "region=$(grep '^AWS_REGION=' .env | cut -d'=' -f2)" >> $GITHUB_OUTPUT
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ steps.config.outputs.account }}:role/GitHubActionsRole
aws-region: ${{ steps.config.outputs.region }}
Deployment Patterns
Pattern A: Pipeline Trigger
GitHub Actions triggers AWS CodePipeline, which handles actual deployment.
Use When:
- Complex multi-stage deployments
- Need AWS-native deployment history
- Want to trigger from multiple sources
- Build requires significant compute resources
Workflow:
permissions:
id-token: write
contents: read
jobs:
trigger:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/GitHubActionsRole
aws-region: ${{ vars.AWS_REGION }}
- run: aws codepipeline start-pipeline-execution --name my-pipeline
IAM Permissions:
{
"Effect": "Allow",
"Action": "codepipeline:StartPipelineExecution",
"Resource": "arn:aws:codepipeline:*:*:pipeline-name"
}
Pattern B: Direct Deployment
GitHub Actions performs full deployment (build + deploy).
Use When:
- Simple static site deployments
- Want fast feedback loops
- Prefer GitHub Actions native features
- Cost-conscious (avoid CodePipeline/CodeBuild costs)
Workflow:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci && npm run build
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/GitHubActionsRole
aws-region: ${{ vars.AWS_REGION }}
- run: |
aws s3 sync dist/ s3://my-bucket/ --delete
aws cloudfront create-invalidation --distribution-id ${{ vars.CF_DIST_ID }} --paths "/*"
IAM Permissions:
{
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::bucket/*", "arn:aws:s3:::bucket"]
},
{
"Effect": "Allow",
"Action": "cloudfront:CreateInvalidation",
"Resource": "*"
}
]
}
Pattern C: Infrastructure Deployment
GitHub Actions deploys infrastructure changes (CDK, Terraform, CloudFormation).
Use When:
- Infrastructure as Code workflows
- Want PR-based infrastructure reviews
- Need to validate changes before merge
Workflow:
permissions:
id-token: write
contents: read
pull-requests: write # For PR comments
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/GitHubActionsTerraformRole
aws-region: ${{ vars.AWS_REGION }}
- run: terraform plan -out=plan.tfplan
- uses: actions/github-script@v7
with:
script: |
// Post plan to PR comment
apply:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/GitHubActionsTerraformRole
aws-region: ${{ vars.AWS_REGION }}
- run: terraform apply -auto-approve
IAM Permissions: Typically requires broad permissions (AdministratorAccess or PowerUserAccess). Consider using separate roles for plan (read-only) vs apply (write).
Multi-Environment Strategy
Separate Roles per Environment
Recommended: Create separate roles for dev/staging/prod with different permissions.
CDK Example:
['dev', 'prod'].forEach(env => {
new iam.Role(this, `GitHubActionsRole-${env}`, {
roleName: `GitHubActionsRole-${env}`,
assumedBy: new iam.WebIdentityPrincipal(provider.openIdConnectProviderArn, {
StringEquals: {
'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com'
},
StringLike: {
'token.actions.githubusercontent.com:sub':
env === 'prod'
? `repo:${owner}/${repo}:ref:refs/heads/main`
: `repo:${owner}/${repo}:ref:refs/heads/dev`
}
})
});
});
Workflow:
- name: Determine environment
id: env
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "name=prod" >> $GITHUB_OUTPUT
else
echo "name=dev" >> $GITHUB_OUTPUT
fi
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/GitHubActionsRole-${{ steps.env.outputs.name }}
aws-region: ${{ vars.AWS_REGION }}
Security Considerations
1. Principle of Least Privilege
Always grant minimum permissions required:
// ❌ Too broad
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
// ✅ Specific
{
"Effect": "Allow",
"Action": "codepipeline:StartPipelineExecution",
"Resource": "arn:aws:codepipeline:us-east-1:123456789012:my-pipeline"
}
2. Repository Scoping
Always restrict to specific repository:
// ❌ Any repository in organization
"token.actions.githubusercontent.com:sub": "repo:my-org/*"
// ✅ Specific repository
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:*"
// ✅ Even more specific (main branch only)
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
3. Audience Validation
Always validate audience:
{
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
}
4. Session Duration
Default: 1 hour (sufficient for most workflows)
Custom duration (if needed):
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
role-duration-seconds: 3600 # 1 hour (default)
aws-region: us-east-1
5. CloudTrail Auditing
Monitor role assumptions:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
--max-results 10
Key fields to monitor:
userIdentity.principalId: GitHub repository and workflowrequestParameters.roleArn: Which role was assumedsourceIPAddress: GitHub Actions IP rangeuserAgent: GitHub Actions user agent
Troubleshooting
Error: "Not authorized to perform sts:AssumeRoleWithWebIdentity"
Cause: Trust policy doesn't match workflow context.
Check:
- Repository name matches trust policy
- Branch/tag matches trust policy condition
- Workflow has
id-token: writepermission
Debug:
- name: Debug OIDC token
run: |
curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq
Error: "OpenIDConnect provider not found"
Cause: OIDC provider not created or wrong ARN.
Fix:
# List providers
aws iam list-open-id-connect-providers
# Check provider details
aws iam get-open-id-connect-provider \
--open-id-connect-provider-arn arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com
Error: "Access Denied" after successful authentication
Cause: Role lacks required permissions.
Fix: Update role's permissions policy to include required actions.
Workflow doesn't request OIDC token
Cause: Missing id-token: write permission.
Fix:
permissions:
id-token: write # Add this
contents: read
Migration Checklist
Migrating from static credentials to OIDC:
- Create OIDC provider in AWS account
- Create IAM role with trust policy
- Attach permissions policy to role
- Test role assumption manually (optional)
- Update workflow to use OIDC
- Add
permissionsblock to workflow - Replace credential secrets with role ARN
- Test workflow in non-production environment
- Verify CloudTrail logs show role assumption
- Deploy to production
- Remove old AWS credential secrets from GitHub
- Revoke/delete old IAM user (if applicable)
- Document role ARN and permissions
References
- GitHub Actions OIDC Documentation
- AWS IAM OIDC Provider Documentation
- aws-actions/configure-aws-credentials
- GitHub OIDC Token Claims
- AWS Security Blog: Use IAM roles to connect GitHub Actions
Progressive Improvement
If the developer corrects a behavior that this skill should have prevented, suggest a specific amendment to this skill to prevent the same correction in the future.
More from loxosceles/ai-dev
static-frontend-hosting
S3 + CloudFront + Lambda@Edge for low-cost global hosting with edge authentication. Apply when setting up frontend hosting infrastructure.
59code-review
Multi-perspective code review strategy covering architecture, security, performance, and quality. Follow when reviewing code or analyzing changes.
46frontend-code-quality
Essential guidelines for clear, maintainable frontend code. Follow when writing or reviewing frontend components, composables, or pages.
46command-execution
Guidelines for executing commands and running scripts. Follow when running shell commands, installing packages, or using project scripts.
45cdk-bootstrap-configuration
CDK synth-time configuration pattern without context caching. Apply when working on CDK infrastructure code or adding new configuration parameters.
45environment-validation
Validate configuration early to fail fast. Apply when writing setup scripts, Lambda cold starts, or any initialization code that depends on environment variables.
45