DEV Community

Danny Steenman for AWS Community Builders

Posted on • Originally published at towardsthecloud.com on

AWS CDK GitHub OIDC: Complete Setup and Troubleshooting Guide

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.

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.

```typescript showLineNumbers
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.



```typescript showLineNumbers
// Legacy approach - use OidcProviderNative for new implementations
const githubProvider = new iam.OpenIdConnectProvider(this, 'GitHubProvider', {
  url: 'https://token.actions.githubusercontent.com',
  clientIds: ['sts.amazonaws.com'],
});
Enter fullscreen mode Exit fullscreen mode

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 ':'
Enter fullscreen mode Exit fullscreen mode

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.

```typescript showLineNumbers
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 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)**:



```typescript
StringEquals: {
  'token.actions.githubusercontent.com:sub': 'repo:YourOrg/YourRepo:ref:refs/heads/main',
}
Enter fullscreen mode Exit fullscreen mode

Any branch in repository:

StringLike: {
  'token.actions.githubusercontent.com:sub': 'repo:YourOrg/YourRepo:*',
}
Enter fullscreen mode Exit fullscreen mode

Any repository in organization (use with caution):

StringLike: {
  'token.actions.githubusercontent.com:sub': 'repo:YourOrg/*',
}
Enter fullscreen mode Exit fullscreen mode

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.

```yaml showLineNumbers
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:



```yaml showLineNumbers
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
Enter fullscreen mode Exit fullscreen mode

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:

```typescript showLineNumbers
// 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:



```typescript showLineNumbers
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' },
// ]
Enter fullscreen mode Exit fullscreen mode

Pull Request Deployments

For preview environments or PR-based testing, restrict access to pull request events:

```typescript showLineNumbers
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:



```typescript showLineNumbers
// 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-*'],
  })
);
Enter fullscreen mode Exit fullscreen mode

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': '*' }
Enter fullscreen mode Exit fullscreen mode

This will work:

// CORRECT - includes organization
StringLike: { 'token.actions.githubusercontent.com:sub': 'repo:YourOrg/*' }
Enter fullscreen mode Exit fullscreen mode

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:

```python showLineNumbers
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"
    )
Enter fullscreen mode Exit fullscreen mode



## 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](https://github.com/towardsthecloud/aws-cdk-examples/tree/main/openid-connect-github).

For more CDK patterns, check out the [AWS CDK best practices guide](https://towardsthecloud.com/blog/aws-cdk-best-practices) or explore [multi-account strategies](https://towardsthecloud.com/blog/aws-multi-account-strategy) for enterprise deployments. Teams using Bitbucket Pipelines can implement the same OIDC pattern with our [Bitbucket OIDC guide](https://towardsthecloud.com/blog/aws-cdk-openid-connect-bitbucket).



---

> Written by Danny, Founder @ [towardsthecloud](https://towardsthecloud.com) → Helping startups cut costs and [ship faster on AWS](https://towardsthecloud.com/services/aws-landing-zone).
>
> FYI; I'm also building [cloudburn.io](https://cloudburn.io) → Help developers catch expensive infra in PR's before they deploy on AWS Cloud.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)