‹ some kind of sorcery

Connecting Github Actions to AWS S3 using OIDC

Oct 26, 2021

Typically, connecting Github Actions to AWS requires storing an AWS Access Key ID and Secret Access Key as environment variables. A more secure way is to use federated access, or in other words, provision something in AWS that tells it to trust calls coming from your Github Actions.

Inspired by Aidan Steele’s AWS federation comes to GitHub Actions , I want to show how to achieve the same result with Terraform. In addition, this also shows how to attach some permissions to allow your Github Actions to upload files to S3.

This guide assumes you are familiar with Terraform, Github, and AWS.

Step 1.0: Set up your backend and AWS provider

This first step is a bit opinionated. If you know what you’re doing when setting up your Terraform backend and AWS provider, feel free to skip this step. Note in this example to fill in the following placeholder values:

  • <state bucket name goes here>
    • The name of the S3 bucket that contains your Terraform state.
  • <your AWS region>
    • The AWS region in which you deploy your infrastructure. For instance, I use us-east-1.
resource "aws_s3_bucket" "terraform_state" {
  bucket = "<state bucket name goes here>"
  acl    = "private"

  versioning {
    enabled = true
  }

  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        sse_algorithm = "AES256"
      }
    }
  }
}

terraform {
  required_providers {
    # https://registry.terraform.io/providers/hashicorp/aws/latest/docs
    aws = {
      source  = "hashicorp/aws"
      version = "3.63.0"
    }
  }

  backend "s3" {
    bucket = "<state bucket name goes here>"
    key    = "terraform/tfstate"
    region = "<your AWS region>"
  }
}

# Access key id and secret access key are pulled in from AWS environment variables
# that are set using `aws configure`.

provider "aws" {
  # alternatively, `export AWS_DEFAULT_REGION="<your AWS region>"` in your shell environment
  region = "<your AWS region>"
}

Step 2.0: Create the requisite AWS components with Terraform

In this step, put the following code into a file called oidc.tf. Note that this step assumes you already have one or two S3 buckets; the code snippets here will not create them.

You will create:

  • aws_iam_openid_connect_provider.github
    • This component lets AWS trust Github Actions initiated from your repo.
  • aws_iam_policy_document.github_actions
    • This policy document is the assume_role_policy for the role assumed by Github Actions.
  • aws_iam_policy_document.github_s3_upload_policy
    • This policy document is attached to the role assumed by Github Actions. In this example, it permits read & write actions on the buckets specified in the local.s3_buckets variable.
  • aws_iam_role.github_actions
    • This is the AWS role that can be assumed from within Github Actions.

Remember to fill in the following placeholder values:

  • <your github organization>
    • This value is your Github account name.
  • <your github repo>
    • This is the repository from which your Github Actions will execute.
  • <your bucket name here>
    • This is the S3 bucket with which Github Actions will interact.
  • <another bucket name if you have multiple>
    • This code snippet supports allowing Github Actions to interact with multiple S3 buckets, but leave this out if you’re only using one bucket.
locals {
  github_organization = "<your github organization>"
  github_repo         = "<your github repo>"
  s3_buckets          = ["<your bucket name here>", "<another bucket name if you have multiple>"]
}

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

  client_id_list = [
    "https://github.com/${local.github_organization}",
    "sigstore" # https://github.com/aws-actions/configure-aws-credentials/issues/271#issuecomment-931272443
  ]

  thumbprint_list = ["a031c46782e6e6c662c2c87c76da9aa62ccabd8e"]
}

data "aws_iam_policy_document" "github_actions" {
  statement {
    actions = [
      "sts:AssumeRoleWithWebIdentity",
    ]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github.arn]
    }

    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"

      values = [
        "repo:${local.github_organization}/${local.github_repo}:*"
      ]
    }
  }
}

data "aws_iam_policy_document" "github_s3_upload_policy" {
  dynamic "statement" {
    for_each = local.s3_buckets
    content {
      actions = [
        "s3:Get*",
        "s3:List*",
        "s3:Put*"
      ]

      resources = [
        "${statement.value}",
        "${statement.value}/*",
      ]
    }
  }
}

resource "aws_iam_role" "github_actions" {
  name               = "github-actions"
  assume_role_policy = data.aws_iam_policy_document.github_actions.json
  inline_policy {
    name   = "github-s3-upload-policy"
    policy = data.aws_iam_policy_document.github_s3_upload_policy.json
  }
}

Step 3.0: Creating a Github workflow to upload files to S3

Create a file at this location in your Git repo: .github/workflows/s3_workflow.yml with the following contents. Remember to change the following placeholder values:

  • <your AWS account id>
    • The 12-digit identifier for your AWS account.
  • <your AWS region>
    • The AWS region in which you deploy your infrastructure. For instance, I use us-east-1.
  • <your branch name>
    • The branch for which a git push will trigger an S3 upload
  • <source directory of files to upload>
    • The directory to upload to S3, relative to the root directory of your Git repo.
  • <your bucket name here>
    • This is the S3 bucket with which Github Actions will interact.
  • <another bucket name if you have multiple>
    • This code snippet supports allowing Github Actions to interact with multiple S3 buckets, but leave this out if you’re only using one bucket.
  • <another source directory of files to upload>
    • The directory to upload to S3, relative to the root directory of your Git repo.
    • Leave this out if you’re only using one bucket.
name: Upload sites to S3

on:
  push:
    branches:
      - <your branch name>

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v1

      - name: Configure AWS Credentials
        # https://github.com/aws-actions/configure-aws-credentials/issues/271#issuecomment-931272443
        uses: aws-actions/configure-aws-credentials@b8c74de753fbcb4868bf2011fb2e15826ce973af
        with:
          aws-region: <your AWS region>
          role-to-assume: arn:aws:iam::<your AWS account id>:role/github-actions
          role-session-name: github-actions

      - name: Upload files to <your bucket name here>
        run: aws s3 sync <source directory of files to upload> s3://<your bucket name here>

      - name: Upload files to <another bucket name if you have multiple>
        run: aws s3 sync <another source directory of files to upload> s3://<another bucket name if you have multiple>

Step 4.0: Try it out!

If everything went well, you should now be able to sync files to S3 after running git push, and you don’t have to store any AWS access credentials as environment variables.

Summary

In this guide, you learned how to:

  • Create an AWS OIDC provider to allow Github Actions to assume an AWS role in your account
  • Create a role that has permissions to read and write files in one or more S3 buckets
  • Set up a Github workflow to sync files from a directory in your repo to a bucket in S3 on every push to a particular branch

Next Steps

You can give Github Actions access to do anything in AWS as long as you attach the appropriate permissions to the role that Github Actions assumes. With this guide as a foundation, you can expand further and create a tighter integration between your Github repositories and AWS.

🏷