💸 Catch expensive AWS mistakes before deployment! See cost impact in GitHub PRs for Terraform & CDK. Join the Free Beta!
AWS CDK GitHub OIDC: Complete Setup and Troubleshooting Guide

AWS CDK GitHub OIDC: Complete Setup and Troubleshooting Guide

Set up GitHub OIDC with AWS CDK to eliminate access keys. Step-by-step guide with OidcProviderNative, trust policies, and troubleshooting.

February 3rd, 2026
0 views
--- likes

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:

  1. Your GitHub Actions workflow requests an OIDC token from GitHub's provider
  2. The token contains claims including aud (audience) and sub (subject identifying org/repo/branch)
  3. The aws-actions/configure-aws-credentials action calls AWS STS AssumeRoleWithWebIdentity with the token
  4. AWS STS validates the token signature using GitHub's public keys, checks expiration (within 5-minute window), and verifies trust policy conditions match
  5. STS returns temporary credentials (access key ID, secret key, session token)
  6. 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 bootstrap in 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.

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.com is 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 sub claim

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 KeyGitHub JWT ClaimPurpose
token.actions.githubusercontent.com:audaudAudience - ensures token was issued for AWS
token.actions.githubusercontent.com:subsubSubject - 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:

  1. JWKS endpoint inaccessible: GitHub's .well-known/openid-configuration endpoint must be reachable from AWS
  2. High latency: More than 5 seconds latency causes timeouts
  3. 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:

  1. Incorrect role ARN: Typo in the workflow configuration
  2. Trust policy mismatch: The sub claim doesn't match your condition
  3. Session duration too long: Maximum for AssumeRoleWithWebIdentity is 12 hours

Resolution:

  1. Double-check the role ARN in your workflow matches exactly
  2. Use CloudTrail to see the actual sub claim value from the failed request
  3. Compare the PrincipalId in CloudTrail with your trust policy conditions
  4. 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:

  1. Obtain the current thumbprint using the OpenSSL method described earlier
  2. Update your CDK code with the new thumbprint
  3. 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 OidcProviderNative for 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.

Share this article on ↓

Subscribe to our Newsletter

Join ---- other subscribers!