Deploying AWS CDK apps using short-lived credentials and Github Actions
Tags: awscdk, serverless, aws
Published: 2021-12-04 04:14:29

---
title: Deploying AWS CDK apps using short-lived credentials and Github Actions
published: true
description: Stop using IAM users for deploying AWS applications.
tags: awscdk, serverless, aws
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rbt3meemm7lbjrhbhkbw.jpeg
---
There are many ways to deploy an application to AWS. One might use [_AWS CloudFormation_](https://aws.amazon.com/cloudformation/) directly or a service like [AWS CodePipeline](https://aws.amazon.com/codepipeline/). If _AWS CloudFormation_ is not your cup of tea, you have services like [_AWS Elastic Beanstalk_](https://aws.amazon.com/elasticbeanstalk/) at your disposal.

There are a lot of choices.

This blog post will show you **my preferred way** of deploying _AWS CDK_ applications - using [_GitHub Actions_](https://github.com/features/actions) and utilizing **short-lived IAM credentials** for maximum security benefits.

Let us dive in.

> You can find all the code presented in this blog post in [this GitHub repository](https://github.com/WojciechMatuszewski/github-oidc-aws-cdk-example).

## Typical AWS CDK deployment using GitHub Actions

Till recently, a typical AWS deployment using GitHub Actions might have looked similar to the following, **simplified** diagram.

![Typical AWS CDK deployment using GitHub Actions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gctwzfwhg7q9p7o613kb.jpeg)

To deploy the underlying application, one had to keep the long-lived IAM user credentials within the [GitHub repository/organization secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets). While this setup certainly does the job, it leaves a relatively big security gap in our environment.

Let us explore why we should be moving away from IAM users in such scenarios next.

## What is the issue with AWS IAM users

People who know much more about security than I do, like [Ben Kehoe](https://twitter.com/ben11kehoe), have been [advocating for abandoning the use of IAM users for most use-cases a long time](https://twitter.com/ben11kehoe/status/1263876984062513154). I completely agree with their arguments.

Ask any security professional if they would be more comfortable with long-lived or short-lived revokable credentials. I'm willing to bet a large sum of money that they would pick the latter option. The _access keys_ are long-lived! Imagine an attacker getting a hold of those. They would be able to wreck your infrastructure. Not ideal.

Luckily an alternative appeared on the horizon recently. Let us explore that topic further.

## An alternative

We were pretty much stuck with the IAM user setup when deploying _AWS CDK_ applications for the longest time. All has changed when GitHub [announced support for OpenID Connect for GitHub actions](https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/).

Now, instead of relying on the long-lived IAM User credentials, we can **use the [_AssumeRoleWithWebIdentity_](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html) _AWS IAM_ call** to get **short-lived IAM role credentials** to deploy _AWS CDK_ applications. Neat!

![GitHub OIDC flow with AWS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wwhpfvl3iz14x5inl6x4.png)

Let us see, step by step, what goes into making this setup work.

## In Action

Moving to the concrete now – the following is an example of how one might set up GitHub _AWS CDK_ deployment pipeline to leverage the short-lived credentials utilizing GitHub OIDC provider.

> **Important**: The following blog post assumes that you have the `newStyleStackSynthesis` _AWS CDK_ feature flag turned on **or are using _AWS CDK_ v2**. Read more about _AWS CDK_ feature flags [here](https://docs.aws.amazon.com/cdk/latest/guide/featureflags.html).

### Two separate stacks and AWS accounts

I'm a big believer in separating different concerns while writing application code or creating AWS infrastructures. In the spirit of keeping things separated, I will create **two _AWS CDK_ stacks on two separate AWS accounts**.

The AWS account is a great way to reduce the potential blast radius when things go wrong. As a matter of fact, [AWS recommends using multiple AWS accounts to achieve resource independence and isolation](https://aws.amazon.com/organizations/getting-started/best-practices/).

> Please note that **if you are deploying relatively simple architectures, you do not need separate accounts here – it might be an overkill**. If you are not interested in the multi-account setup, read on. I will show you how to do all we discussed today using a single account.

- The **first stack** is called "infrastructure" and will hold the _GitHub OIDC IAM Provider_ as well as the _AWS IAM_ role used to deploy the "application". This stack will be **deployed in account A**.

- The **second stack** is called "application" and will hold the application itself. This stack will be **deployed in account B**.

![Two stacks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vs1a6tnpwbar6eqicaoo.jpeg)

### The infrastructure stack

#### The OIDC provider

Creating a custom OIDC provider using _AWS CDK_ is a breeze. The `iam.OpenIdConnectProvider` class is a great abstraction over the `AWS::IAM::OIDCProvider` _AWS CloudFormation_ resource.

The following _AWS CDK_ code will create a custom _AWS IAM_ OIDC provider that developers can use in the context of GitHub Actions.

```ts
import { aws_iam } from "aws-cdk-lib";

// ...

const gitHubOIDCProvider = new aws_iam.OpenIdConnectProvider(
  this,
  "gitHubOIDCProvider",
  {
    url: "https://token.actions.githubusercontent.com",
    clientIds: ["sts.amazonaws.com"]
  }
);
```

Github has a [great guide](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#adding-the-identity-provider-to-aws) on how to integrate their OIDC provider with AWS. Give it a read!

#### The "deployer" role

As I eluded earlier, we will use the "deployer" role to deploy our main _AWS CDK_ application. This role has to have a trust relationship with the custom OIDC provider we have created earlier – otherwise, we would be unable to assume it during GitHub Actions run.

```ts
import { aws_iam } from "aws-cdk-lib";

// ...

const gitHubOIDCProvider = ...

/**
 * Amend those to your needs.
 */
const yourGitHubUsername = "WojciechMatuszewski";
const yourGitHubRepoName = "github-oidc-aws-cdk-example";
const yourGitHubBranchName = "main";

const applicationDeployerRole = new aws_iam.Role(this, "applicationDeployerRole", {
  assumedBy: new iam.WebIdentityPrincipal(
    gitHubOIDCProvider.openIdConnectProviderArn,
    {
      StringLike: {
        "token.actions.githubusercontent.com:sub":
          // Notice the `ref:refs`. The `s` in the second `ref` is important!
          `repo:${yourGitHubUsername}/${yourGitHubRepoName}:ref:refs/heads/${yourGitHubBranchName}`
      }
    }
  ),
  inlinePolicies: {
    allowAssumeOnAccountB: new aws_iam.PolicyDocument({
      statements: [
        new aws_iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: ["sts:AssumeRole"],
          resources: ["arn:aws:iam::ACCOUNT_B_ID:role/*"]
        })
      ]
    })
  }
});
```

There are two essential things to notice in this code snippet.

1. The **conditions** on the policy are very **important**. They make it so that untrusted repositories can't request the `applicationDeployerRole` access tokens.

1. The `allowAssumeOnAccountB` policy statement. Since we will use this role in a multi-account setup, we need to grant the role permissions to assume roles defined in account A. **This policy statement is not needed when deploying both stacks in the single account**.

---

With the stack deployed, copy the ARN of the `applicationDeployerRole` – you will need it later.

Let us move to the "application" stack.

### The application stack

By default _the AWS CDK_ bootstrapping process creates, amongst other resources, five IAM roles related to the deployment process.
You can learn more about them in the [AWS Documentation](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html) – mainly the "Roles" section.

![AWS CDK bootstrap roles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xjvtjxzh7bfyofayjhps.jpeg)

What is more, those roles have a **trust relationship with the whole AWS account the stack was bootstrapped it** (configurable). This makes it so that, in theory, an untrusted IAM entity could deploy the application at will.

We will change the _AWS CDK_ bootstrapping template so that **only the "deployer" role** can assume the roles _AWS CDK_ creates.

Let us get started.

#### Modifying the bootstrap template

> Check out the [AWS documentation](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html#bootstrapping-customizing) for how to customize _AWS CDK_ bootstrapping process further.

The **first step** is to get the bootstrapping template. Luckily _AWS CDK_ `bootstrap` command exposes the `--get-template` flag.

```shell
npm run cdk bootstrap -- --get-template
```

The **second** step is to amend the _trust relationship_ of the roles in the bootstrap template. Instead of having the whole AWS account as a principal, we will set it to the "deployer" role ARN.

For example, here is the **unmodified** expert from the bootstrapping template containing the _FilePublishingRole_.

```yaml
FilePublishingRole:
  Type: AWS::IAM::Role
  Properties:
    AssumeRolePolicyDocument:
      Statement:
        - Action: sts:AssumeRole
          Effect: Allow
          Principal:
            AWS:
              Ref: AWS::AccountId
```

To amend the trust relationship so that only the "deployer" IAM role can assume it, change the value of the `AWS` key under the `Principal`.

```diff
FilePublishingRole:
  Type: AWS::IAM::Role
  Properties:
    AssumeRolePolicyDocument:
      Statement:
        - Action: sts:AssumeRole
          Effect: Allow
-          Principal:
-            AWS:
-              Ref: AWS::AccountId
+          Principal:
+            AWS: DEPLOYER_ROLE_ARN
```

#### Bootstrapping

With the bootstrap template modified, we can run the `cdk bootstrap` command with a special flag. This flag will tell the _AWS CDK_ to use our modified template as the source of truth for _AWS CloudFormation_.

```shell
npm run cdk bootstrap -- --template YOUR_MODIFIED_TEMPLATE_NAME.yaml
```

When the bootstrapping process is successful, we are ready to move into creating _GitHub Actions_ workflow and deploying our application.

#### Deployment

Okay, it's time we make the last push and deploy our application for the whole world to see. As per the title of this blog post, I will be using _GitHub Actions_ to do that.

Our workflow file will be rather simplistic.

```yaml
# .github/workflows/deployment.yaml
name: deployment

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Pull repository
        uses: actions/checkout@v2
      - name: Install dependencies
        working-directory: ./application
        run: npm install
      - name: Assume deployer role
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: YOUR_DEPLOYER_ROLE_ARN
          aws-region: eu-west-1 # Do not forget about adjusting the region!
      - name: Deploy the application
        working-directory: ./application
        run: npm run deploy:cicd
```

I want to focus on the `Assume deployer role` step. The `aws-actions/configure-aws-credentials@v1` will interact with the GitHub OIDC provider and perform the `sts:AssumeRoleWithWebIdentity` call with the token provided by the GitHub OIDC provider.

Remember specifying conditions on the "deployer" role?

```ts
import { aws_iam } from "aws-cdk-lib";

// ...

const gitHubOIDCProvider = ...

/**
 * Amend those to your needs.
 */
const yourGitHubUsername = "WojciechMatuszewski";
const yourGitHubRepoName = "github-oidc-aws-cdk-example";
const yourGitHubBranchName = "main";

const applicationDeployerRole = new aws_iam.Role(this, "applicationDeployerRole", {
  assumedBy: new iam.WebIdentityPrincipal(
    gitHubOIDCProvider.openIdConnectProviderArn,
    {
      StringLike: {
        "token.actions.githubusercontent.com:sub":
          // Notice the `ref:refs`. The `s` in the second `ref` is important!
          `repo:${yourGitHubUsername}/${yourGitHubRepoName}:ref:refs/heads/${yourGitHubBranchName}`
      }
    }
  ),
  inlinePolicies: {
    allowAssumeOnAccountB: new aws_iam.PolicyDocument({
      statements: [
        new aws_iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: ["sts:AssumeRole"],
          resources: ["arn:aws:iam::ACCOUNT_B_ID:role/*"]
        })
      ]
    })
  }
});
```

This is where they come into play. If we specify a different repository or branch, the `Assume deployer role` step would fail – the `aws-actions/configure-aws-credentials@v1` would not be able to get **short-lived** credentials. The `sts:AssumeRoleWithWebIdentity` call would fail due to conditions mismatch.

If we configure everything correctly, all of our jobs should run successfully.

![Successful deployment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8zo2miu9tgrsjymzgpot.png)

### I do not want to use multiple AWS accounts

> The [example GitHub repository](https://github.com/WojciechMatuszewski/github-oidc-aws-cdk-example) contains the single-account deployment _AWS CDK_ code as well!

You might not want to use a separate account for the "infrastructure" stack, which is entirely understandable. In this case, let us look at what kind of modifications we would have to do to make our deployment work.

![Single AWS account deployment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/idt9qbo969hdhe73q991.jpeg)

1. The "deployer" role does not need the `allowAssumeOnAccountB` policy. The policy is no longer relevant as the `sts:AssumeRoleWithWebIdentity` call will happen in the context of a single account.

```diff
import { aws_iam } from "aws-cdk-lib";

// ...

const gitHubOIDCProvider = ...

const applicationDeployerRole = new iam.Role(this, "applicationDeployerRole", {
  assumedBy: new iam.WebIdentityPrincipal(
    gitHubOIDCProvider.openIdConnectProviderArn,
    {
      StringLike: {
        "token.actions.githubusercontent.com:sub":
          // Notice the `ref:refs`. The `s` in the second `ref` is important!
          `repo:${yourGitHubUsername}/${yourGitHubRepoName}:ref:refs/heads/${yourGitHubBranchName}`
      }
    }
  ),
- inlinePolicies: {
-   allowAssumeOnAccountB: new iam.PolicyDocument({
-      statements: [
-         new iam.PolicyStatement({
-          effect: iam.Effect.ALLOW,
-          actions: ["sts:AssumeRole"],
-          resources: ["arn:aws:iam::ACCOUNT_B_ID:role/*"]
-        })
-      ]
-    })
+  inlinePolicies: {}
  }
});
```

2. Specify [the `qualifier` parameter](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html#bootstrapping-customizing) when bootstrapping the "infrastructure" and "application" stacks.

By default, _AWS CDK_ will re-use resources created by bootstrapping process if you deploy multiple _AWS CDK_ stacks in the same account.
We do not want this to happen as we would not be able to change the trust policies on the roles bootstrapped by the "application" stack.

- To change the qualifier **when bootstrapping**, use the `qualifier` CLI parameter.

```shell
npm run cdk bootstrap -- --qualifier=application --template ./bootstrap-template.yaml
```

- Specify the `synthesizer` property at the _CDK app_ level.

```diff
const app = new cdk.App();
new ApplicationStack(app, "ApplicationStack", {
+  synthesizer: new cdk.DefaultStackSynthesizer({
+    qualifier: "YOUR_QUALIFIER"
+  })
});
```

The rest of the process is the same as in the case of a multi-account setup.

## Closing words

There are many ways for deploying _AWS CDK_ applications. This blog post aims to show you one of them – using GitHub Actions. I hope you found the setup valuable and helpful.

For some serverless/_AWS CDK_ content, follow me on Twitter – [@wm_matuszewski](https://twitter.com/wm_matuszewski)

Thank you for your valuable time.