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.
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:
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.