Create an OpenID Connect for GitHub with the AWS CDK

Photo by Sid Saxena / Unsplash
Photo by Sid Saxena / Unsplash

GitHub Actions is a CI/CD platform that easily automates workflow for building, testing, and deploying your application to production on Cloud Provider.

AWS is the most used Cloud provider, and developers use GitHub Actions to automate the creation, edition, and deployment of applications on AWS services.

To allow a GitHub repository to perform action on AWS services in the GitHub Actions workflow, it is recommended to create an OpenID Connect authentication, which brings the following advantages:

  • Enforce the least privilege: you generate an IAM role with only the permissions required to perform the action on AWS.
  • Filter repository: you define a rule allowing specific repositories and branches to access AWS resources from a GitHub Action running on that repository/branch.
  • Short live token: you define the maximum duration of the session token generated after a successful authentication.
  • Prevent credentials leak: You just need the IAM role the GitHub OIDC provider assumed instead of providing the AWS access ID and secret key.

This post will show you how to use the AWS CDK to create an OpenID Connect provider for GitHub.

Prerequisites

To follow this tutorial, make sure you have the following tools installed on your computer.

Create an AWS CDK v2 project

Run the command below to install the latest version of the AWS CDK and ensure it works by checking the version.


npm install -g aws-cdk

cdk --version

Create a folder that will hold the source code of our project.


mkdir github-oidc

cd github-oidc

Let's initialize a new AWS CDK project with TypeScript by running the command below:


cdk init app --language typescript

The command creates the required files and folders and installs the Node.js modules. The folder structure looks like the one below:

The AWS CDK project structure for TypeScript.
The AWS CDK project structure for TypeScript.

Create an OpenID Connect for GitHub Actions

In the file lib/github-oidc-stack.ts, we will write the code to create the OIDC with the IAM role, which will be done in three steps.

1. Create the GitHub OIDC provider

Replace the content of the file lib/github-oidc-stack.ts with the code below:


import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as iam from 'aws-cdk-lib/aws-iam';

const GITHUB_DOMAIN = 'token.actions.githubusercontent.com';
const CLIENT_ID = 'sts.amazonaws.com'

export class GithubOidcStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const githubProvider = new iam.OpenIdConnectProvider(this, 'OpenIdGitHubProvider', {
      url: `https://${GITHUB_DOMAIN}`,
      clientIds: [CLIENT_ID],
    });
  }
}

We use the class OpenIdConnectProvider to create the Identity Provider which accepts two properties:

  • Provider URL: it is where GitHub generates and stores its OIDC tokens.
  • Client ID: it is the Security Token Service (STS) API used for IAM role authentication.

There is a third optional property named thumbprints you can provide if you have the value (generally when migrating an identity provider created from the AWS Console to IaC).

2. Create the IAM condition for the GitHub repositories

This step configures the GitHub repositories to allow them to perform actions on AWS resources in a GitHub Actions workflow. You can even apply a filter on the branch allowed.

Update the file lib/github-oidc-stack.ts by adding the code below:


const allowedRepositories = [
  'repo:tericcabrel/blog-tutorials:*',
  'repo:tericcabrel/snipcode:main'
];
    
const conditions: iam.Conditions = {
  StringEquals: {
    [`${GITHUB_DOMAIN}:aud`]: CLIENT_ID,
  },
  StringLike: {
    [`${GITHUB_DOMAIN}:sub`]: allowedRepositories,
  },
};

    

An allowed repository follows this pattern: repo:<OWNER>/<REPOSITORY>:<BRANCH>.

Use the symbol * to allow all the repositories or all the branches of a repository.


# Allow all the branches of the GitHub repository 

repo:tericcabrel/blog-tutorials:*

# Allow all the GitHub repositories 

repo:tericcabrel/*

3. Create the IAM role with a WebIdentityPrincipal

This part concerns creating an IAM role and attaching policies to grant permission to execute actions on AWS services.

When defining policies on an IAM role, you can use managed policies or create custom policies. A managed policy is a set of policies defined by and maintained by AWS that grant you permission for many services.

One of the commonly managed policies is AdministratorAccess which gives you the right to do almost everything on any service.

Update the file lib/github-oidc-stack.ts by adding the code below:


import * as iam from 'aws-cdk-lib/aws-iam';

const githubActionsRole = new iam.Role(this, 'GitHubActionsRole', {
  roleName: 'gitHub-actions-role',
  description: 'This role is used via GitHub Actions to perform any actions to any AWS services',
  assumedBy: new iam.WebIdentityPrincipal(
    githubProvider.openIdConnectProviderArn,
    conditions,
  ),
  maxSessionDuration: cdk.Duration.hours(1),
  managedPolicies: [
    iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess'),
  ],
});

new cdk.CfnOutput(this, 'GitHubActionsRoleOutputARN', {
  value: githubActionsRole.roleArn
});

The above code does the following:

  • Define the role name and a description of its purpose.
  • Indicates the role is assumed by the GitHub OIDC provider by instantiating a WebIdentityPrincipal, which takes the GitHub OpenID Connect ARN and the IAM conditions we defined earlier.
  • Define the maximum duration of the token generated by the STS.
  • Assign the managed policy "AdministratorAccess" to perform everything on the AWS account.
  • Print the IAM Role ARN in the terminal after it is created on AWS.

3.1. Create the IAM role with a WebIdentityPrincipal and custom policies

In GitHub Actions, we usually want to perform a specific action on one or many AWS services, such as copying static files to AWS S3, deploying a Lambda Function, or publishing a Docker image to Amazon ECR.

Granting an administrator access to perform a specific task like the one enumerated above is not recommended because you are giving too much right, and it is better to give only the required permissions.

Let's say we want to push a Docker image to Amazon ECR from a GitHub Actions; here are the permissions required:

ecr:BatchGetImage, ecr:BatchCheckLayerAvailability, ecr:CompleteLayerUpload, ecr:GetDownloadUrlForLayer, ecr:InitiateLayerUpload, ecr:PutImage, ecr:UploadLayerPart and ecr:GetAuthorizationToken

The code for creating the IAM role with fine-grained permissions is the following:


import * as iam from 'aws-cdk-lib/aws-iam';

const githubActionsRole = new iam.Role(this, 'GitHubActionsRole', {
  roleName: 'github-actions-role',
  description: 'This role is used via GitHub Actions to push a Docker image to the Amazon ECR',
  maxSessionDuration: cdk.Duration.hours(1),
  assumedBy: new iam.WebIdentityPrincipal(
    githubProvider.openIdConnectProviderArn,
    conditions,
  ),
  inlinePolicies: {
    ECRActionPolicy: new iam.PolicyDocument({
      statements: [
        new iam.PolicyStatement({
          actions: [
            'ecr:BatchGetImage',
            'ecr:BatchCheckLayerAvailability',
            'ecr:CompleteLayerUpload',
            'ecr:GetDownloadUrlForLayer',
            'ecr:InitiateLayerUpload',
            'ecr:PutImage',
            'ecr:UploadLayerPart',
          ],
          resources: ['*'],
          effect: iam.Effect.ALLOW,
        }),
      ],
    }),
    ECRAuthPolicy: new iam.PolicyDocument({
      statements: [
        new iam.PolicyStatement({
          actions: ['ecr:GetAuthorizationToken'],
          resources: ['*'],
          effect: iam.Effect.ALLOW,
        }),
      ],
    }),
  },
});

The properties ECRActionPolicy and ECRAuthPolicy can be named as you want.

Deploy a SpringBoot application using GitHub Actions
This post will show how to automatically deploy a SpringBoot application in production when the code changes using a CI/CD pipeline on GitHub Actions.

Deploy the stack to AWS

Here is the complete code of the file lib/github-oidc-stack.ts


import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as iam from 'aws-cdk-lib/aws-iam';

const GITHUB_DOMAIN = 'token.actions.githubusercontent.com';
const CLIENT_ID = 'sts.amazonaws.com'

export class GithubOidcStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const githubProvider = new iam.OpenIdConnectProvider(this, 'OpenIdGitHubProvider', {
      url: `https://${GITHUB_DOMAIN}`,
      clientIds: [CLIENT_ID],
    });

    const allowedRepositories = [
      'repo:tericcabrel/blog-tutorials:*',
      'repo:tericcabrel/snipcode:main'
    ];

    const conditions: iam.Conditions = {
      StringEquals: {
        [`${GITHUB_DOMAIN}:aud`]: CLIENT_ID,
      },
      StringLike: {
        [`${GITHUB_DOMAIN}:sub`]: allowedRepositories,
      },
    };

    const githubActionsRole = new iam.Role(this, 'GitHubActionsRole', {
      roleName: 'github-actions-role',
      description: 'This role is used via GitHub Actions to push a Docker image to the Amazon ECR',
      maxSessionDuration: cdk.Duration.hours(1),
      assumedBy: new iam.WebIdentityPrincipal(
        githubProvider.openIdConnectProviderArn,
        conditions,
      ),
      inlinePolicies: {
        ECRActionPolicy: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: [
                'ecr:BatchGetImage',
                'ecr:BatchCheckLayerAvailability',
                'ecr:CompleteLayerUpload',
                'ecr:GetDownloadUrlForLayer',
                'ecr:InitiateLayerUpload',
                'ecr:PutImage',
                'ecr:UploadLayerPart',
              ],
              resources: ['*'],
              effect: iam.Effect.ALLOW,
            }),
          ],
        }),
        ECRAuthPolicy: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: ['ecr:GetAuthorizationToken'],
              resources: ['*'],
              effect: iam.Effect.ALLOW,
            }),
          ],
        }),
      },
    });

    new cdk.CfnOutput(this, 'GitHubActionsRoleOutputARN', {
      value: githubActionsRole.roleArn
    });
  }
}

Let's deploy it on AWS by running the following command:


cdk bootstrap

cdk synth

cdk deploy

The first command will create the AWS resources required to perform the stack creation on AWS using the CDK.

The second command generates the CloudFormation template from the CDK code

The last command creates the resources on AWS; it takes time, depending on the number of resources to create. You will see the IAM role in the terminal when the deployment is done.

Create the GitHub OIDC provider on AWS with the CDK.
Create the GitHub OIDC provider on AWS with the CDK.

Copy the IAM Role ARN printed in the terminal and use it in your GitHub Actions to push your Docker image to Amazon ECR.

To destroy the stack, run the command below:


cdk destroy

Caveat when creating an Identity provider with the AWS CDK

When deploying the infrastructure in another region on the same AWS account, the AWS CDK cannot skip the Identity Provider creation when it already exists (globally) and instead will throw an error.

The Identity Provider identifier is the URL, and you cannot provide a different one since it is the unique entry point GitHub provides to store and access OIDC tokens.

We deployed the stack on the eu-west-3 region, now let's deploy in eu-west-1 by running the command below:


AWS_REGION=eu-west-1 cdk deploy

We get the following output:

Error when creating an OIDC provider on another AWS region using the CDK.
Error when creating an OIDC provider on another AWS region using the CDK.

Wrap Up

It is the end of this post, where we saw how to create an OpenID Connect for GitHub with the AWS CDK; it can be summarized into these four steps:

  • Create the GitHub OIDC provider using the OpenIdConnectProvider class.
  • Create the IAM conditions for the GitHub repositories where we can perform actions on AWS resources.
  • Create the IAM role with a WebIdentityPrincipal and attach custom policies related to the actions to perform on AWS.
  • Deploy the infrastructure stack on AWS using the CDK.

Once deployed, you can copy the IAM role ARN and use it in your GitHub Actions, for example, to push a Docker image to Amazon ECR.

If you want to create the OIDC provided from the AWS Console, check out the official documentation. If you want it specific to GitHub, check out this article.

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.