Skip to main content
  1. Posts/

AWS in GitHub Actions: authenticate with IAM roles and forget about Access Keys

·996 words·5 mins
CICD AWS GitHub Actions IAM roles Terraform OIDC
Alexey Gnetko
Author
Alexey Gnetko
Tech enthusiast. Curious about modern approaches in software engineering

TL;DR
#

If you’re familiar with Terraform and AWS, you can jump right in by checking the code here https://github.com/alexeygnetko/gh-actions-with-iam-role.

⚠️ Quick note: To keep things simple for the demo, I’ve skipped using a backend for Terraform state and hardcoded certain values (like the IAM role ARN). You’ll need minor adjustments before you use it in your project.

Intro
#

GitHub Actions is a widely-used platform for Continuous Integration and Continuous Deployment (CICD). When you use a public cloud like AWS, you often need to communicate with it in your pipelines. Publishing and downloading artifacts, triggering deployments, updating configuration, invalidating CDN cache… You can do everything you want with AWS SDK.

AWS provides a ready-to-use GitHub Action called configure-aws-credentials that allows configuring AWS credentials in GitHub Actions.

The worst (the worst!) way to use it - is to provide AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as parameters. AWS shows warnings in their Management Console when you try to create a set of keys, but it’s still a popular solution.

AWS warning

And I think it’s popular as it looks so easy to start with, but apparently it’s not. You need to create an IAM user, then generate keys, store and inject them as secrets, rotate regularly and we won’t even mention that you authenticate using just keys and not identity…

IAM roles and temporary credentials
#

AWS recommends an alternative method for authenticating programmatic workloads: obtaining temporary credentials via IAM roles. To put it simply, when GitHub Actions interact with AWS, they communicate like “Hello, I am a GitHub Action from repository X currently executing in branch Y. Kindly grant me permission to assume IAM role Z”.

In this post I would like to show how easy it is to use IAM roles and temporary credentials in GitHub Actions.

Prerequisites
#

I assume that you already have a GitHub repository and AWS account or will create them soon.

You will need some basic knowledge in Terraform, but it is certainly worth it. If you have never used infrastructure-as-code tools like Terraform or Pulumi, just start with their documentation and you will never regret it.

Let’s start!

Create an assumable IAM role for GitHub Action
#

The first step is to set up an IAM role and an OpenID Connect (OIDC) provider in our AWS account. For this, we’ll use Terraform to provision AWS resources, organized in the following file structure:

├── terraform/
    ├── config.tf     # Terraform configuration (providers, default tags, etc)
    ├── iam.tf        # IAM role and OIDC provider definitions
    ├── outputs.tf    # Outputs needed for later
    ├── variables.tf  # Global variables

Let’s dive into the iam.tf file that defines IAM role and OIDC provider:

locals {
  provider_url       = "https://token.actions.githubusercontent.com"
  github_thumbprints = ["6938fd4d98bab03faadb97b34396831e3780aea1", "1c58a3a8518e8759bf075b76b750d4f2df264fcd"]
  client_id          = "sts.amazonaws.com"
}

resource "aws_iam_openid_connect_provider" "default" {
  url             = local.provider_url
  client_id_list  = [local.client_id]
  thumbprint_list = local.github_thumbprints
}

module "assumable_role" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-assumable-role-with-oidc"
  version = "5.33.1"

  create_role                    = true
  provider_url                   = local.provider_url
  role_name                      = "${var.app}-${var.env}-AssumableRole"
  oidc_fully_qualified_audiences = [local.client_id]
  oidc_fully_qualified_subjects  = ["repo:${var.repo}:ref:refs/heads/main"]

  # remove `oidc_fully_qualified_subjects` if you want to allow assuming the role from any branch
  #   oidc_subjects_with_wildcards = ["repo:${var.repo}:ref:refs/heads/*"]
}

resource "aws_iam_policy" "example_policy" {
  name        = "example-policy"
  description = "An example IAM policy"

  policy = jsonencode(
    {
      "Version" : "2012-10-17",
      "Statement" : [
        {
          "Effect" : "Allow",
          "Action" : "s3:ListBucket",
          "Resource" : "*"
        }
      ]
  })
}

resource "aws_iam_role_policy_attachment" "example_attachment" {
  policy_arn = aws_iam_policy.example_policy.arn
  role       = module.assumable_role.iam_role_name
}

This is mostly configuration, so let’s break down the essential details.

In locals block we define constants from GitHub and AWS. You can find provider_url and client_id values in GitHub docs. Thumbprints are not there, but I found them in this GH changelog post.

Notice the ${var.repo} variable. It’s defined in variables.tf. Adjust it to match your repository:

variable "repo" {
  type = string
  default = "alexeygnetko/gh-actions-with-iam-role"
}

You can restrict IAM role usage to a single branch, a good idea for a protected main branch requiring a pull request for changes. This prevents leveraging your role in other branches:

oidc_fully_qualified_subjects  = ["repo:${var.repo}:ref:refs/heads/main"]

If you’re okay with using this role in any branch, remove oidc_fully_qualified_subjects and uncomment the oidc_subjects_with_wildcards line:

oidc_subjects_with_wildcards = ["repo:${var.repo}:ref:refs/heads/*"]

You can extend the created role with permissions you need, please use example_policy as a reference.

That’s it. After applying it, we’re done with the IAM role and Identity Provider:

$ cd terraform
$ terraform apply
...
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

iam_role_arn = "arn:aws:iam::912493297952:role/gh-oidc-demo-dev-AssumableRole"

You can see iam_role_arn in the Outputs. We need to save it for later so we can assume this role in GitHub Action.

Verify an IAM role in GitHub Actions
#

Now that we’ve set up our IAM role, let’s confirm it works smoothly in GitHub Actions. The following action verify-iam-role.yml, utilizes aws sts get-caller-identity to check if the role is assigned and we’re authenticated:

name: Verify IAM Role

on:
  workflow_dispatch:
  push:
    branches:
      - main

permissions:
  id-token: write
  contents: read

jobs:
  verify-iam-role:
    runs-on: ubuntu-latest
    steps:
      - name: configure aws credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::912493297952:role/gh-oidc-demo-dev-AssumableRole # the role we created
          role-session-name: verify-iam-role-job
          aws-region: eu-central-1
      - name: Verify IAM Role
        run: aws sts get-caller-identity

Let’s run the action and verify the results:

IAM role is assumed well

It works! 🙂

Conclusion
#

Using temporary credentials and IAM roles for GitHub Actions not only simplifies your workflow but also improves the security of your AWS account. The setup takes just a few minutes, eliminating the need to rotate keys. With Terraform, you can skip manual configurations in the AWS Management Console and effortlessly replicate the setup across different environments and AWS accounts.

You can use the demo code from my repo. However, you can also look for some ready-to-use Terraform modules that will do everything for you. Please keep in mind that you need to trust modules that you bring into your project and review their code, so in this particular case it will take less time to implement it yourself.

Further reading
#