Storing AWS access keys in GitHub Secrets is a security risk waiting to happen. Every week, leaked credentials lead to compromised accounts and unexpected bills. Even with rotation policies, long-lived credentials remain a liability.
OIDC (OpenID Connect) eliminates this risk entirely. Instead of storing secrets, GitHub Actions can assume IAM roles with temporary credentials that expire automatically. No secrets to leak, no keys to rotate.
In this GitHub OIDC AWS CDK guide, you'll learn to implement secure authentication, from basic setup to production-ready configurations. Plus, you'll get troubleshooting solutions for the errors that trip up most developers. This is a CDK best practice for secure CI/CD deployments. If you're looking to configure OpenID Connect for Bitbucket instead, check out the Bitbucket OIDC guide.
Why Use GitHub OIDC Instead of Access Keys?
Long-term IAM user access keys present multiple security risks that make them unsuitable for CI/CD pipelines:
- Credential exposure: Can be uploaded to public repositories, shared via insecure channels, or embedded in code
- No automatic expiration: Remain valid until explicitly rotated or deleted
- Manual management overhead: Require periodic rotation, secure storage, and distribution
- Broad compromise window: If exposed, can be used until detected and revoked
- Difficult auditing: Harder to attribute actions to specific workflows or runs
- Scalability issues: Managing credentials across multiple teams and repositories becomes complex
OIDC temporary credentials solve these problems. Credentials are generated dynamically for each workflow run and automatically expire (typically within 1 hour, maximum 12 hours). No secrets to manage, rotate, or secure. CloudTrail logs include federated identity information, making it easy to trace actions back to specific repositories and branches.
The AWS Well-Architected Framework Security Pillar (SEC02-BP02) explicitly recommends using temporary credentials instead of long-term credentials. For machine identities like CI/CD systems, the guidance is clear: use IAM roles with temporary credentials.
How GitHub OIDC Authentication Works
Understanding the authentication flow helps you debug issues when they arise. Here's what happens when GitHub Actions authenticates to AWS:
The key steps:
- Your GitHub Actions workflow requests an OIDC token from GitHub's provider
- The token contains claims including
aud(audience) andsub(subject identifying org/repo/branch) - The
aws-actions/configure-aws-credentialsaction calls AWS STSAssumeRoleWithWebIdentitywith the token - AWS STS validates the token signature using GitHub's public keys, checks expiration (within 5-minute window), and verifies trust policy conditions match
- STS returns temporary credentials (access key ID, secret key, session token)
- Credentials are valid for the configured session duration
Now that you understand the flow, let's set up the infrastructure.
Prerequisites
Before implementing GitHub OIDC with CDK, ensure you have:
- AWS CDK v2 installed: See the CDK installation guide if needed
- AWS account with IAM permissions: You need permissions to create OIDC providers and IAM roles
- CDK bootstrapped: Run
cdk bootstrapin your target account/region. See CDK bootstrap guide - GitHub repository: For testing the workflow after deployment
With prerequisites in place, let's create the OIDC provider.
Step 1: Create the OIDC Provider with CDK
AWS CDK provides two constructs for creating OIDC providers. For new implementations, use OidcProviderNative as it's the recommended approach with simpler architecture.
Using OidcProviderNative (Recommended)
OidcProviderNative uses the native CloudFormation AWS::IAM::OIDCProvider resource instead of Lambda-backed custom resources. This means simpler deployments and fewer moving parts.
import * as iam from 'aws-cdk-lib/aws-iam';
const githubProvider = new iam.OidcProviderNative(this, 'GitHubProvider', {
url: 'https://token.actions.githubusercontent.com',
clientIds: ['sts.amazonaws.com'],
thumbprints: ['6938fd4d98bab03faadb97b34396831e3780aea1'],
});
The configuration requires:
- url: GitHub's OIDC provider URL where tokens are generated
- clientIds:
sts.amazonaws.comis the audience value that identifies AWS STS as the intended recipient - thumbprints: The SHA-1 hash of GitHub's JWKS endpoint certificate (required for
OidcProviderNative)
Using OpenIdConnectProvider (Legacy)
The original OpenIdConnectProvider construct uses Lambda-backed custom resources. It's maintained for backward compatibility only, and the CDK team explicitly discourages adding new features to it.
// Legacy approach - use OidcProviderNative for new implementations
const githubProvider = new iam.OpenIdConnectProvider(this, 'GitHubProvider', {
url: 'https://token.actions.githubusercontent.com',
clientIds: ['sts.amazonaws.com'],
});
When to use the legacy construct: Only if you have existing stacks using it and migration isn't practical. For new stacks, always use OidcProviderNative.
Obtaining the GitHub Thumbprint
The thumbprint is the SHA-1 hash of the certificate used by GitHub's JWKS endpoint. While the thumbprint value 6938fd4d98bab03faadb97b34396831e3780aea1 is current as of this writing, GitHub may rotate their certificates.
To obtain the current thumbprint manually:
# Get the JWKS URI from the discovery document
curl -s https://token.actions.githubusercontent.com/.well-known/openid-configuration | jq -r '.jwks_uri'
# Then use OpenSSL to get the certificate thumbprint
openssl s_client -servername token.actions.githubusercontent.com -connect token.actions.githubusercontent.com:443 \
< /dev/null 2>/dev/null | openssl x509 -fingerprint -sha1 -noout | cut -d'=' -f2 | tr -d ':'
With the provider created, let's create the IAM role that GitHub Actions will assume.
Step 2: Create the IAM Role with Trust Policy
The IAM role needs a trust policy that allows GitHub's OIDC provider to assume it. The trust policy is your security boundary, so getting it right is critical.
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
const githubActionsRole = new iam.Role(this, 'GitHubActionsRole', {
roleName: 'GitHubActionsDeployRole',
description: 'Role assumed by GitHub Actions for deployments',
maxSessionDuration: cdk.Duration.hours(1),
assumedBy: new iam.WebIdentityPrincipal(
githubProvider.openIdConnectProviderArn,
{
StringEquals: {
'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
},
StringLike: {
'token.actions.githubusercontent.com:sub': 'repo:YourOrg/YourRepo:*',
},
}
),
});
The WebIdentityPrincipal creates a trust policy that:
- Only accepts tokens from your GitHub OIDC provider
- Validates the audience matches
sts.amazonaws.com - Restricts access to specific repositories via the
subclaim
Understanding Trust Policy Conditions
GitHub is a shared OIDC provider, meaning multiple organizations use the same issuer URL. IAM automatically enforces that your trust policy includes the sub claim with a specific value (not just wildcards).
If you try to create a role without a proper sub condition, you'll get a MalformedPolicyDocument error. This is a security-by-default mechanism that prevents accidental overly-permissive configurations.
The condition keys map to JWT claims from GitHub:
| AWS Condition Key | GitHub JWT Claim | Purpose |
|---|---|---|
token.actions.githubusercontent.com:aud | aud | Audience - ensures token was issued for AWS |
token.actions.githubusercontent.com:sub | sub | Subject - identifies org/repo/branch |
Repository and Branch Filtering Patterns
The sub claim format from GitHub follows this pattern: repo:OrgName/RepoName:ref:refs/heads/BranchName
You can restrict access at different levels of specificity:
Most restrictive (specific branch):
StringEquals: {
'token.actions.githubusercontent.com:sub': 'repo:YourOrg/YourRepo:ref:refs/heads/main',
}
Any branch in repository:
StringLike: {
'token.actions.githubusercontent.com:sub': 'repo:YourOrg/YourRepo:*',
}
Any repository in organization (use with caution):
StringLike: {
'token.actions.githubusercontent.com:sub': 'repo:YourOrg/*',
}
Never use a wildcard-only pattern like *. It would allow any GitHub organization to assume the IAM role, defeating the purpose of OIDC security.
Step 3: Configure GitHub Actions Workflow
With the AWS infrastructure deployed, configure your GitHub Actions workflow to use OIDC authentication.
Required Workflow Permissions
The workflow must have id-token: write permission to request JWT tokens from GitHub's OIDC provider. Without this, the workflow cannot authenticate.
permissions:
id-token: write # Required for OIDC authentication
contents: read # Required for checking out code
Place the permissions block at the job level or workflow level depending on your needs.
Complete Workflow Example
Here's a complete workflow that authenticates using OIDC and deploys with CDK:
name: deploy-production
on:
push:
branches:
- main
workflow_dispatch: {}
jobs:
deploy:
name: Deploy CDK stacks
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
aws-region: eu-west-1
role-session-name: GitHubActions-${{ github.run_id }}
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: CDK deploy
run: npx cdk deploy --all --require-approval never
Key configuration points:
- role-to-assume: The ARN of the IAM role you created in Step 2
- aws-region: The region where you want to operate
- role-session-name: Optional but useful for CloudTrail auditing. Including the run ID makes it easy to trace actions back to specific workflow runs.
Advanced Trust Policy Patterns
For production environments, you'll often need more sophisticated trust policies than basic repository filtering.
Environment-Based Restrictions
GitHub Environments let you add protection rules like required reviewers and deployment branches. You can restrict your IAM role to only be assumable from specific environments:
// Only allow deployments from the 'production' environment
StringLike: {
'token.actions.githubusercontent.com:sub': 'repo:YourOrg/YourRepo:environment:production',
}
This pattern is particularly powerful when combined with GitHub's environment protection rules. You can require manual approval before production deployments while allowing automated deployments to staging.
Multiple Repository Support
A single OIDC provider can serve multiple repositories. Expand your trust policy to include additional patterns:
export interface GitHubOidcStackProps extends cdk.StackProps {
readonly repositoryConfig: { owner: string; repo: string; filter?: string }[];
}
// In your stack
const repoPatterns = props.repositoryConfig.map(
(r) => `repo:${r.owner}/${r.repo}:${r.filter ?? '*'}`
);
const githubActionsRole = new iam.Role(this, 'GitHubActionsRole', {
assumedBy: new iam.WebIdentityPrincipal(
githubProvider.openIdConnectProviderArn,
{
StringEquals: {
'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
},
StringLike: {
'token.actions.githubusercontent.com:sub': repoPatterns,
},
}
),
});
// Usage:
// repositoryConfig: [
// { owner: 'YourOrg', repo: 'app-frontend' },
// { owner: 'YourOrg', repo: 'app-backend', filter: 'ref:refs/heads/main' },
// ]
Pull Request Deployments
For preview environments or PR-based testing, restrict access to pull request events:
StringLike: {
'token.actions.githubusercontent.com:sub': 'repo:YourOrg/YourRepo:pull_request',
}
Security consideration: Be cautious with PR deployments. Untrusted code from forks could run with the assumed role's permissions. Consider using separate roles with minimal permissions for PR workflows versus production deployments.
Security Best Practices
Making your OIDC setup production-ready requires attention to permissions and auditing.
Least-Privilege Permissions
The current example uses a placeholder for permissions. In production, never use AdministratorAccess. Instead, grant only the permissions your deployment actually needs:
// Example: Permissions for CDK/CloudFormation deployments
githubActionsRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'cloudformation:*',
'ssm:GetParameter',
's3:*',
],
resources: ['*'],
})
);
// Even better: Scope to specific resources
githubActionsRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['lambda:UpdateFunctionCode', 'lambda:UpdateFunctionConfiguration'],
resources: ['arn:aws:lambda:*:*:function:my-app-*'],
})
);
Start with the minimum permissions and add more as needed. CloudTrail logs will show you which permissions are actually being used.
Auditing with CloudTrail
Every AssumeRoleWithWebIdentity call is logged in CloudTrail. The role session name includes the federated identity information, making it easy to trace actions back to specific GitHub repositories and workflow runs.
Look for these events in CloudTrail:
- AssumeRoleWithWebIdentity: When GitHub Actions assumes the role
- Subsequent API calls: Will show the role session name that includes your configured
role-session-name
Set up CloudTrail alerts for unusual patterns, such as role assumptions from unexpected repositories or at unusual times.
Troubleshooting Common Errors
Even with correct configuration, you may encounter errors. Here's how to diagnose and fix the most common issues.
InvalidIdentityToken Error
Symptoms: Error stating InvalidIdentityToken when the workflow attempts to assume the role.
Common causes:
- JWKS endpoint inaccessible: GitHub's
.well-known/openid-configurationendpoint must be reachable from AWS - High latency: More than 5 seconds latency causes timeouts
- JWT format issues: Token is malformed or encrypted
Resolution:
- Verify GitHub's OIDC endpoints are accessible (unlikely to be the issue)
- Use regional STS endpoints instead of the global endpoint to reduce latency
- Check the GitHub Actions logs for the actual error message
AccessDenied on AssumeRoleWithWebIdentity
Symptoms: AccessDenied error when attempting to assume the role.
Common causes:
- Incorrect role ARN: Typo in the workflow configuration
- Trust policy mismatch: The
subclaim doesn't match your condition - Session duration too long: Maximum for
AssumeRoleWithWebIdentityis 12 hours
Resolution:
- Double-check the role ARN in your workflow matches exactly
- Use CloudTrail to see the actual
subclaim value from the failed request - Compare the
PrincipalIdin CloudTrail with your trust policy conditions - Reduce session duration if it exceeds 12 hours
MalformedPolicyDocument Error
Symptoms: Role creation fails with MalformedPolicyDocument error.
Cause: For GitHub (a shared OIDC provider), IAM requires the sub claim condition to have a specific value, not just wildcards.
Resolution: Ensure your trust policy includes:
- The condition key
token.actions.githubusercontent.com:sub - A value that specifies at least the organization:
repo:YourOrg/*
This won't work:
// WRONG - wildcard only
StringLike: { 'token.actions.githubusercontent.com:sub': '*' }
This will work:
// CORRECT - includes organization
StringLike: { 'token.actions.githubusercontent.com:sub': 'repo:YourOrg/*' }
Certificate Thumbprint Mismatch
Symptoms: Error stating the OIDC provider's certificate doesn't match the configured thumbprint.
Cause: GitHub rotated their certificate and the thumbprint in your CDK code is outdated.
Resolution:
- Obtain the current thumbprint using the OpenSSL method described earlier
- Update your CDK code with the new thumbprint
- Deploy the changes
For providers created via the AWS Console, AWS automatically handles thumbprint updates. CDK-managed providers need manual updates.
Python CDK Example
If you're using Python CDK, here's the complete implementation:
from aws_cdk import (
Stack,
Duration,
CfnOutput,
aws_iam as iam,
)
from constructs import Construct
class GitHubOidcStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Create OIDC Provider
github_provider = iam.OidcProviderNative(
self, "GitHubProvider",
url="https://token.actions.githubusercontent.com",
client_ids=["sts.amazonaws.com"],
thumbprints=["6938fd4d98bab03faadb97b34396831e3780aea1"],
)
# Create IAM Role
github_actions_role = iam.Role(
self, "GitHubActionsRole",
role_name="GitHubActionsDeployRole",
description="Role assumed by GitHub Actions",
max_session_duration=Duration.hours(1),
assumed_by=iam.WebIdentityPrincipal(
github_provider.open_id_connect_provider_arn,
conditions={
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:YourOrg/YourRepo:*"
}
}
)
)
# Add deployment permissions (customize as needed)
github_actions_role.add_to_policy(
iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"cloudformation:*",
"ssm:GetParameter",
],
resources=["*"]
)
)
# Output the role ARN for use in GitHub Actions
CfnOutput(
self, "GitHubActionsRoleArn",
value=github_actions_role.role_arn,
description="ARN for GitHub Actions role"
)
Conclusion
You've now implemented secure GitHub OIDC authentication with AWS CDK. The key takeaways:
- Use
OidcProviderNativefor new implementations. It's simpler and recommended by the CDK team. - Always restrict trust policies to specific organizations, repositories, and branches. IAM enforces this for GitHub as a shared provider.
- OIDC eliminates credential management burden. No more rotation, no more secrets to leak, and better auditability through CloudTrail.
Next step: Deploy the complete example and verify authentication using a test workflow. You can find a working example in my GitHub repository.
For more CDK patterns, check out the AWS CDK best practices guide or explore multi-account strategies for enterprise deployments. Teams using Bitbucket Pipelines can implement the same OIDC pattern with our Bitbucket OIDC guide.
Build Scalable CDK Apps That Are Easy to Maintain
Transform your complex CDK codebase into a structured, reusable architecture. Get real-world expertise from someone who's built production CDK at scale.

