Secure AWS-CDK deployments with GitHub Actions

GitHub actions enables continuous integration, and the aws-cdk enables infrastructure as code.

This guide provides a secure way to automate the deployments of aws-cdk stacks, from GitHub actions to your AWS account.

It's more secure because you do not have to store long lived credentials in your GitHub account, and, because only the open ID connect with GitHubs fingerprint can assume the deployment role 👍

How it works

We will utilise open ID connect, to grant GitHub a temporary federated identity. This identity will be trusted, to assume a role in your AWS account.

When the identity (GitHub) assumes the roles, we will secure it's access by doing two things:

  • Granting a temporary aws secret key and access key, that expires in an hour.
  • Using claims from the JWT presented by GitHub to AWS to narrow the scope of the allowed identities.

Setup

When we are done we will have:

  • A one-off GitHub action, that creates the identity provider and trust relationship using an aws-cdk stack.
  • Another GitHub action that uses the identity to gain temporary access, and deploy aws-cdk stacks.

Creating the bootstrap stack

We can create a new aws-cdk application:

mkdir bootstrap

npx [email protected] init app --language typescript

After that we will use two components from IAM to create a provider, and a principal.

We will use the principal to create a trust relationship between aws, and GitHub like so.

/**
 * Create an Identity provider for GitHub inside your AWS Account. This
 * allows GitHub to present itself to AWS IAM and assume a role.
 */
const provider = new OpenIdConnectProvider(this, 'MyProvider', {
  url: 'https://token.actions.githubusercontent.com',
  clientIds: ['sts.amazonaws.com'],
});

Then establish the trust relationship by defining the conditions for this provider to act as a principal.

I will provide an example that assumes you are https://github.com/simonireilly and you want to deploy from all repository branches of a repo called awesome-project

const githubOrganisation = "simonireilly"
// Change this to the repo you want to push code from
const repoName = "awesome-project"
/**
 * Create a principal for the OpenID; which can allow it to assume
 * deployment roles.
 */
const GitHubPrincipal = new OpenIdConnectPrincipal(provider).withConditions(
  {
    StringLike: {
      'token.actions.githubusercontent.com:sub':
        `repo:${githubOrganisation}/${repoName}:*`,
    },
  }
);

Finally you want to establish the role that can be assumed by the OIDC principal. This will allow GitHub actions to use the AWS Roles, and mutate the AWS Resources you give it access to.

/**
  * Create a deployment role that has short lived credentials. The only
  * principal that can assume this role is the GitHub Open ID provider.
  *
  * This role is granted authority to assume aws cdk roles; which are created
  * by the aws cdk v2.
  */
new Role(this, 'GitHubActionsRole', {
  assumedBy: GitHubPrincipal,
  description:
    'Role assumed by GitHubPrincipal for deploying from CI using aws cdk',
  roleName: 'github-ci-role',
  maxSessionDuration: Duration.hours(1),
  inlinePolicies: {
    CdkDeploymentPolicy: new PolicyDocument({
      assignSids: true,
      statements: [
        new PolicyStatement({
          effect: Effect.ALLOW,
          actions: ['sts:AssumeRole'],
          resources: [`arn:aws:iam::${this.account}:role/cdk-*`],
        }),
      ],
    }),
  },
});

🚨 These permissions may be too broad for your use case. Consider adding a permissions boundary, or, opting to use a role other than the role automatically created by the cdk for its deployments 🚨

Deploying Bootstrap

With a set of created access keys, you can deploy the bootstrap. This enables someone with higher privilege to setup the link for your team 👍

This keeps you from storing long lived credentials in GitHub.

name: Bootstrap
on:
  workflow_dispatch:
    inputs:
      AWS_ACCESS_KEY_ID:
        description: "Access Key ID with Permissions to deploy IAM, and OIDC"
        required: true
      AWS_SECRET_ACCESS_KEY:
        description: "Secret Access Key with Permissions to deploy IAM, and OIDC"
        required: true
      AWS_REGION:
        description: "Region to deploy to."
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Git clone the repository
        uses: actions/checkout@v1

      - name: Configure aws credentials
        uses: aws-actions/configure-aws-credentials@master
        with:
          aws-access-key-id: ${{ github.event.inputs.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ github.event.inputs.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ github.event.inputs.AWS_REGION }}

      - uses: actions/setup-node@v2
        with:
          node-version: "14"

      - run: yarn install

      - name: Synth stack
        run: yarn --cwd packages/bootstrap cdk synth

      - name: Deploy stack
        run: yarn --cwd packages/bootstrap cdk deploy --require-approval never

When you trigger this action the user must enter aws access keys and aws secrets that have the required privileges.

Post Bootstrap life

With this stack deployed you can now ship any aws-cdk v2 deployments from the trusted repository, to the linked AWS account, without storing long lived credentials.

All you need to do is instruct GitHUb actions to assume the github-ci-role role in your account, and it will get temporary credentials for one hour.

deploy-infrastructure:
    runs-on: ubuntu-latest
    steps:
      - name: Git clone the repository
        uses: actions/checkout@v1

      - name: Assume role using OIDC
        uses: aws-actions/configure-aws-credentials@master
        with:
          role-to-assume: arn:aws:iam::<your-account-id-here>:role/github-ci-role
          aws-region: ${{ env.AWS_REGION }}

      - uses: actions/setup-node@v2
        with:
          node-version: "14"

      - run: yarn install

      - name: Synth infrastructure stack
        run: yarn --cwd packages/infrastructure cdk synth

      - name: Deploy infrastructure stack
        run: yarn --cwd packages/infrastructure cdk deploy --require-approval never

Next steps might to be

Create another bootstrapping aws cdk stack, that allows only deploying from the main branch, and point this one at your production AWS account if you have one 👍

{
  StringLike: {
    'token.actions.githubusercontent.com:sub':
      `repo:${githubOrganisation}/${repository}:ref:/refs/head/main`,
  },
}

Follow up

If you are interested in this stuff, you might like microteams!

A guide I am writing for scale ups, that are growing from one person AWS start-ups to multi-team organisations.

43