AWS Federation With Github Actions

5 minute read

A new feature from GitHub now allows Federation with AWS accounts using Open ID Connect, which allows you to assume an IAM role within your account to deploy services into AWS.

This is a pretty big deal, since if you’re currently using GitHub Actions you’re probably storing credentials using GitHub secrets, which is great but can tend to lead to either long-lived credentials, or having to rotate keys on a regular basis depending on your security policies. And more often than anyone would like to admit, you might be using the same keys for everything, usually with God-like admin powers because someone couldn’t work out exactly what permissions were needed that one time at 3 in the morning on a Sunday.

Grab the code

Everything that I’ll be going through here, including code snippets and Terraform configuration files, can be cloned from this github repository if you’d like to test it out or use it as a base to improve on it.

If you are planning on running it yourself, you’ll need to change the following values:

Sidenote - Getting the Server Thumbprint

As an aside, I’m going to add this here as I wasn’t sure how to get the thumbprint when looking into it. If you want to skip this step, the server thumbprint is included in the code examples below and there’s no need to read this section.

The OIDC provider from Github can be found at https://token.actions.githubusercontent.com/, and as it turns out there is a simple way of querying the server to get the relevant details - you’ll need openssl, which we’ll use here, or a similar tool.

The first thing that we need to do is to append .well-known/openid-configuration to the above URL, giving us

https://token.actions.githubusercontent.com/.well-known/openid-configuration

Visiting that address will return a number of values - the one that you need is jwks_uri, which in this case is “jwks_uri”:“https://token.actions.githubusercontent.com/.well-known/jwks".

You’ll need to remove the https:// prefix and everything after the top-level domain before using the URI in the following command:

openssl s_client -servername token.actions.githubusercontent.com -showcerts -connect token.actions.githubusercontent.com:443

Amongst other things, the command will return the server certificate as shown below (if there are more than one, use the final one that’s returned).

-----BEGIN CERTIFICATE-----
MIIEsTCCA5mgAwIBAgIQBOHnpNxc8vNtwCtCuF0VnzANBgkqhkiG9w0BAQsFADBs
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcDEL
MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
LmRpZ2ljZXJ0LmNvbTEvMC0GA1UEAxMmRGlnaUNlcnQgU0hBMiBIaWdoIEFzc3Vy
YW5jZSBTZXJ2ZXIgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2
4C/CJAbIbQRf1+8KZAayfSImZRauQkCbztyfn3YHPsMwVYcZuU+UDlqUH1VWtMIC
Kq/QmO4LQNfE0DtyyBSe75CxEamu0si4QzrZCwvV1ZX1QK/IHe1NnF9Xt4ZQaJn1
itrSxwUfqJfJ3KSxgoQtxq2lnMcZgqaFD15EWCo3j/018QsIJzJa9buLnqS9UdAn
4t07QjOjBSjEuyjMmqwrIw14xnvmXnG3Sj4I+4G3FhahnSMSTeXXkgisdaScus0X
sh5ENWV/UyU50RwKmmMbGZJ0aAo3wsJSSMs5WqK24V3B3aAguCGikyZvFEohQcft
bZvySC/zA/WiaJJTL17jAgMBAAGjggFJMIIBRTASBgNVHRMBAf8ECDAGAQH/AgEA
MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw
NAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy
dC5jb20wSwYDVR0fBEQwQjBAoD6gPIY6aHR0cDovL2NybDQuZGlnaWNlcnQuY29t
L0RpZ2lDZXJ0SGlnaEFzc3VyYW5jZUVWUm9vdENBLmNybDA9BgNVHSAENjA0MDIG
BFUdIAAwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQ
UzAdBgNVHQ4EFgQUUWj/kK8CB3U8zNllZGKiErhZcjswHwYDVR0jBBgwFoAUsT7D
aQP4v0cB1JgmGggC72NkK8MwDQYJKoZIhvcNAQELBQADggEBABiKlYkD5m3fXPwd
aOpKj4PWUS+Na0QWnqxj9dJubISZi6qBcYRb7TROsLd5kinMLYBq8I4g4Xmk/gNH
E+r1hspZcX30BJZr01lYPf7TMSVcGDiEo+afgv2MW5gxTs14nhr9hctJqvIni5ly
/D6q1UEL2tU2ob8cbkdJf17ZSHwD2f2LSaCYJkJA69aSEaRkCldUxPUd1gJea6zu
xICaEnL6VpPX/78whQYwvwt/Tv9XBZ0k7YXDK/umdaisLRbvfXknsuvCnQsH6qqF
0wGjIChBWUMo0oHjqvbsezt3tkBigAVBRQHvFwY+3sAzm2fTYS5yh+Rp/BIAV0Ae
cPUeybQ=
-----END CERTIFICATE-----

Copy the certificate to certificate.crt, and run the following command:

openssl x509 -in certificate.crt -fingerprint -noout

This returns the following,

SHA1 Fingerprint=A0:31:C4:67:82:E6:E6:C6:62:C2:C8:7C:76:DA:9A:A6:2C:CA:BD:8E

which, once you remove all of the colon characters will give you the thumbprint that you need:

A031C46782E6E6C662C2C87C76DA9AA62CCABD8E

Terraform Code

The Terraform code that makes this possible is pretty straightforward. We need to create an assumable IAM role with the relevant permissions that you’ll require for deployments, and define the Open ID Connect Provider, which in this case is GitHub.

IAM Role

resource "aws_iam_role" "ExampleGithubOidcRole" {
  name = "ExampleGithubOidcRole"
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/ReadOnlyAccess"
    ]

  assume_role_policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": {
        "Sid": "ExampleGithubOidcRole",
        "Effect": "Allow",
        "Principal": {"Federated": "arn:aws:iam::1234567890:oidc-provider/token.actions.githubusercontent.com"},
        "Action": "sts:AssumeRoleWithWebIdentity",
        "Condition": {"StringLike": {"token.actions.githubusercontent.com:sub": "repo:github_account/github_repo:*"}}
    }
})

As you can see, we’re attaching a policy to the role that will allow it to create a VPC. The other things to call out in this Terraform block are:

"Principal": {"Federated": "arn:aws:iam::1234567890:oidc-provider/token.actions.githubusercontent.com"},

where 1234567890 is your AWS account number, and

"Condition": {"StringLike": {"token.actions.githubusercontent.com:sub": "repo:github_account/repo_name:*"}}

where github_account and repo_name correspond to the repository in your GitHub account - in my example, this would be thetestlabs and aws-federation-with-github-actions respectively.

The “StringLike” condition is particularly important, as it ensures that the role can only be assumed from that particular repository rather than from any repository on GitHub.

The condition can be manipulated in a number of ways, for example for any repo in your GitHub account:

token.actions.githubusercontent.com:sub: repo:github_account/*

Or scoped to a specific branch:

token.actions.githubusercontent.com:sub: repo:github_account/*:ref:refs/heads/main

Identity Provider

The final resource that needs to be defined for Federation to work is the OIDC provider, and this is where we will use the thumbprint that we got from the server earlier on in this post.

resource "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"

  client_id_list = [
    "https://github.com/repo_name",
  ]

  thumbprint_list = [
    "a031c46782e6e6c662c2c87c76da9aa62ccabd8e"
  ]
}

So now that we have all of that defined in Terraform, and presuming that we’ve run terraform apply to deploy them, we can create a GitHub Action to deploy a service into our AWS account.

GitHub Action

Obviously, if you’re doing anything other than playing around with this functionality, you’ll want to configure all of the niceties around Terraform such as a remote backend and structuring your code, but we’re not going to cover those types of things in this post.

Since we have set the role up to have permission to deploy a VPC into the account, we’ll do that. The following is just lifted wholesale from the Terraform Registry, and we’re saving it in a subfolder called terraform_vpc_example.

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = true
  enable_vpn_gateway = true

  tags = {
    Terraform = "true"
    Environment = "dev"
  }
}

Of course, we will also need to define a GitHub Action in order to make this all work. The workflow here looks like this:

name: Example
on:
  push:

jobs:
  Build:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:

      - name: Checkout
        uses: actions/checkout@v2
        
      - name: Configure AWS
        run: |
          export AWS_ROLE_ARN=arn:aws:iam::1234567890:role/ExampleGithubOidcRole
          export AWS_WEB_IDENTITY_TOKEN_FILE=/tmp/awscreds
          export AWS_DEFAULT_REGION=aws_region

          echo AWS_WEB_IDENTITY_TOKEN_FILE=$AWS_WEB_IDENTITY_TOKEN_FILE >> $GITHUB_ENV
          echo AWS_ROLE_ARN=$AWS_ROLE_ARN >> $GITHUB_ENV
          echo AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION >> $GITHUB_ENV

          curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL" | jq -r '.value' > $AWS_WEB_IDENTITY_TOKEN_FILE

      - name: Run Terraform
        run: |
          cd terraform_vpc_example/
          terraform init
          terraform plan
          terraform apply -auto-approve

That’s all the code that we need, so let’s see it in action. Now that we have a GitHub Workflow defined which runs on push events, all we need to do is commit a change to our repository and push it to the main branch to kick it off. And there we have it - deploying to AWS using GitHub Actions without the need for storing any credentials anywhere!