jasonbutz.info

Secure Deploys from GitHub with the AWS CDK

AWS, GitHub, Security, OIDC, AWS CDK

I recently started consolidating the DNS for all the domains I own to AWS Route 53. During that change, I had a few sites that used root CNAME capabilities of different services, but that isn’t something Route 53 provides. It was easy enough to switch to deploying the site on AWS using the CDK and hosting it with S3 and CloudFront. The whole setup took me about an hour to get everything deployed from my machine. When it came time to add a deployment through a GitHub Workflow, I decided to stop copying and pasting credentials into workflow secrets in GitHub. Instead, I set up OpenID Connect (OIDC).

What is OIDC?

In general, OpenID Connect is an authentication layer built on OAuth 2.0. It is a mechanism to verify an identity based on authentication performed by an authorization server and provide access to basic information about that identity. Focusing on this particular use case, OIDC is a mechanism that enables GitHub to receive temporary credentials from AWS. We configure information about GitHub and define an IAM Role GitHub can assume, then without needing any additional credentials from us, GitHub can use that Role while executing our workflow. Configuring OIDC means we trust GitHub, and there is a degree of risk but less risk than sharing access credentials. We can adhere to least-privilege access guidelines by properly configuring our IAM Role.

Establishing the Trust Relationship

The first step is to indicate to AWS that we want to trust GitHub. GitHub provides documentation focused on AWS to help through this process.

Working through the AWS Console, this process is straightforward. First, you go to the Identity and Access Management (IAM) service; then you select Identity providers, and then Add provider.

Select OpenID Connect and then put https://token.actions.githubusercontent.com as the Provider URL. Then click Get thumbprint. That should load information about GitHub’s public-key certificate. The thumbprint value at the time of writing should be 6938fd4d98bab03faadb97b34396831e3780aea1. Enter sts.amazonaws.com for the audience and click the Add provider button.

AWS Console screenshot showing the IAM service's 'Add an Identity provider' form with the details filled in to add GitHub as an OIDC identity provider

Once you have done that, you should see token.actions.githubusercontent.com in your list of identity providers. A given provider can only be defined once per AWS account. If you plan to configure the identity provider via automation, you’ll need to account for that.

Building the IAM Role

Building our IAM Role for GitHub to use is simple but does require careful attention to the trust policy and permissions. In the AWS Console, go to the Identity and Access Management (IAM) service; select Roles, then click the Create role button. For the Trust entity type choose Web identity. For the Identity provider field, select token.actions.githubusercontent.com, and for the Audience, select sts.amazonaws.com. Click the Next button.

AWS Console screenshot showing the IAM service's create role, select trusted entity form with 'Web identity' selected and the GitHub identity provider selected

If you want to define policies here, feel free. Be sure you keep your policies as restricted as possible. I use the AWS CDK and will define the permissions using an inline policy. Click the Next button. On the next page, enter a role name and a description. Click Create role.

Pull up the IAM Role you created in the console. The trust policy needs to be updated. Any GitHub repository could use this IAM Role if they know the ARN for it. We will add a second condition and restrict access to a single repository.

Your trust policy should look similar to this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::000000000000:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        }
      }
    }
  ]
}

Add a StringLike property for the operator that checks the token.actions.githubusercontent.com:sub condition key against a value like repo:jbutz/example-repo:*. You’ll end up with something like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::000000000000:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:jbutz/example-repo:*"
        }
      }
    }
  ]
}

In this example policy, any branch from the GitHub repository named example-repo attached to my GitHub username (jbutz) can assume this IAM Role.

If I wanted only to allow the main branch to deploy, I could move the expression to the StringEquals object and use repo:jbutz/example-repo:ref:refs/heads/main as the value. The values present in the subject property will vary based on the GitHub Workflow. GitHub has details in its documentation. You do need to be aware that there is a limited set of OIDC claims AWS supports, AWS has the details in the IAM service documentation. I have an environment configured, which changes the sub value, and tried to use the ref claim. Unfortunately, AWS doesn’t support arbitrary OIDC claims. I hope that is a feature added in the future.

As I said, my goal was to enable the CDK to deploy; this means the permissions I need to grant are relatively limited. The CDK assumes specific IAM Roles during deployment, so I can focus on allowing only those actions. The CDK uses four IAM Roles, one for deployment, one for lookups, one for publishing files, and one for container image publishing. The role names follow a predictable pattern; this enables the CDK to know what roles to look up by default. The only action required is sts:AssumeRole. An example policy that provides access to all four roles is below:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Resource": [
        "arn:aws:iam::000000000000:role/cdk-hnb659fds-deploy-role-000000000000-us-east-1",
        "arn:aws:iam::000000000000:role/cdk-hnb659fds-file-publishing-role-000000000000-us-east-1",
        "arn:aws:iam::000000000000:role/cdk-hnb659fds-image-publishing-role-000000000000-us-east-1",
        "arn:aws:iam::000000000000:role/cdk-hnb659fds-lookup-role-000000000000-us-east-1"
      ],
      "Effect": "Allow"
    }
  ]
}

GitHub

Now that you have your IAM Role, you can update your GitHub Workflow to use this new IAM Role to provide your AWS credentials. The repository for AWS’s configure AWS credentials GitHub Action provides ample examples, as well as a sample CloudFormation template to configure an IAM Role.

I’ve put together the CDK construct I created for myself and my example GitHub Workflow in a GitHub Gist. You can also find the Gist at the bottom of the page.

Quick Setup, Better Security

Once you have the OIDC identity provider configured in your account and know how to define the IAM Role’s trust policy, improving your security and credential handling becomes much easier when deploying from AWS to GitHub. You can further customize the sub claim in the tokens sent to AWS and better restrict access to AWS using GitHub’s OIDC API. They explain this functionality in the OpenID Connect documentation.