I’m done with AWS keys.
Keeping AWS keys around is like having a key to your house. You have to keep the keys safe, make sure you’ve got access to them any time you need them, and if somebody ever managed to steal a key or even a copy of that key, they could walk straight in whilst you’re out, and the next thing you know your sofa is gone, your freezer is blasting hot air, and your TV is running up thousands in bitcoin mining costs.
… or something like that.
Anyway, that’s why I moved all of my pipelines to OIDC.
What is OIDC?
OpenID Connect (OIDC) is an authentication protocol that allows users to authenticate with an identity provider (IDP) and obtain a token that can be used to access protected resources. OIDC is built on top of the OAuth 2.0 protocol and provides additional features such as user authentication and user information.
In the above analogy, OIDC is kind of like having a friend standing at the door of your house. That friend checks to make sure you’re really you before they let you in by checking with somebody you’ve both agreed you can trust.
When you’re building a pipeline, a lot of the time you have to give your tooling some pretty uncomfortable access. At the very least, it’ll likely be able to build and push to an artifact repository such as ECR or S3, and may even be able to start processes in your cloud environment. You don’t want malicious actors having access to these sorts of roles.
In this post I’m going to run through how to set up an IDP using Terraform to work with GitHub Actions, and a sample GitHub Action workflow that authenticates with our AWS environment using OIDC.
How to use OIDC for AWS in GitHub Actions
Assumptions I make about the reader
Expanding out on any topic could take us all the way to the absolute foundations of concepts, and neither of us want me to write that much, so I’m including some basic assumptions about you, the reader. If these assumptions are wrong then let me know, and maybe I’ll write about that next:
- You’re familiar with Terraform
- You’re familiar with GitHub Actions
Setting up an OIDC IDP
The Terraform block for setting up an IDP is actually pretty simple:
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [""]
}
But before we can run it, we do need to fill out the thumbprint_list variable. These thumbprints are how AWS verifies that an OIDC token has actually been provided by GitHub (as they’re actually the thumbprint of a certificate that the provider will advertise).
AWS provide a very handy guide for getting the thumbprint for a given url here - you can either follow these instructions using the url in the above terraform block OR you can use a little docker image I created just for you!*
As we’re talking about security in our build tooling and supply chains, I’m going to take a moment to remind folks to always double check before running / trusting a tool, especially if it’s in the security space! You can find the repository for the Docker image I’m about to show you here
If you have Docker installed, you can simply run:
docker run snufflufagus/oidc <URL>
# in this instance, it would be
docker run snufflufagus/oidc https://token.actions.githubusercontent.com
Once you have your thumbprint, throw it in your Terraform block (this thumbprint is correct at the time of publishing - 17th March, 2023).
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
Finally:
terraform apply
If you want to check, you'll be able to find your new IDP in the IAM service in AWS, under Identity Providers.
Creating an IAM role to use our OIDC token with
Now that we’ve got our IDP set up, we need an IAM role to authenticate as. For the purposes of this tutorial, we’re going to push a file into an existing S3 bucket, but ultimately anything that you want to do can be achieved by swapping out the permissions in the IAM policy document we create.
First, we need a couple of data blocks:
data "aws_iam_policy_document" "assume_role_policy" {
statement {
effect = "Allow"
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:<YOUR_GITHUB_USERNAME_OR_ORG_NAME_HERE>/<YOUR_REPO_NAME_HERE>:*"]
}
}
}
So what does this do?
The actions
block tells AWS that some actors may assume the role. We don't want to make it so that anybody can do this, so we narrow it down using our principal
and condition
blocks:
principals - this block specifies that the thing trying to assume the role has to be via our IDP, which is a good start - but if we just left it at that, then anybody could use GitHub Actions to authenticate with this role. That's not good - so we use condition blocks to narrow it down.
condition - this particular block uses the StringLike condition (read more about conditions here) against the claims that our OIDC token will make. You find a full list of things you could test against in a GitHub OIDC token here, and change out sub
for whatever value you'd like to test against.
In this block, we're specifying that only a specific GitHub repo can use this role, but the Action could come from any branch or Git ref (as dictated by the wildcard *). You could swap out the wildcard to specify a branch (which is useful if you do branch based deployment), or other Git compatible reference.
Now, by itself, this will do nothing - we need to attach it to a role!
resource "aws_iam_role" "deploy_role" {
name = "deploy-role"
assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}
resource "aws_iam_role_policy_attachment" "attach_deploy_policy" {
role = aws_iam_role.deploy_role.name
policy_arn = aws_iam_policy.ecs_deploy_policy.arn
}
resource "aws_iam_policy" "ecs_deploy_policy" {
name = "deploy-policy"
policy = data.aws_iam_policy_document.deploy_document.json
}
data "aws_iam_policy_document" "deploy_document" {
statement {
effect = "Allow"
actions = [
"s3:PutObject"
]
resources = [
"${aws_s3_bucket.test.arn}"
]
}
}
resource "aws_s3_bucket" "test" {
bucket = "test.man-yells-at.cloud"
acl = "private"
versioning {
enabled = true
}
}
As this is just a demonstration, I've created an S3 bucket and given our role permissions to put objects in that bucket.
Now we've created a role, we've given it some permissions, and we've specified what can assume that role. Let's give it a whirl in a workflow!
Testing our new role
name: Test our new OIDC role!
on:
push:
branches: [main]
workflow_dispatch:
permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout
jobs:
oidc-test:
runs-on: ubuntu-latest
steps:
- name: Configure aws credentials
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: eu-west-1
- name: Sync the repo with S3
id: sync-s3
run:
echo "testdata" > my-test-file.txt
aws s3 cp my-test-file.txt s3://${{secrets.S3_BUCKET_NAME}}
We give the GitHub Action block the ARN of the role that we just created, and the region that we want to assume the role in, and then that session will persist through our workflow, which means in the next step we can create a file and copy it into S3!
It's important to note the permissions at the top of our flow - this tells Actions that this workflow is allowed to generate a token.
Finally, we're going to push that into our GitHub repository and see what happens. If you've been following along with my steps, you'll notice that I've used a couple of secrets for my AWS_ROLE_ARN and S3_BUCKET_ARN - you'll want to set those secrets up in GitHub before running this workflow!
Hopefully you've reached the end of this with a GitHub Action that's just done something in AWS without you having to give GitHub a set of credentials!
If you're looking to expand upon this, it's worth taking a look at what condition keys are available, and what claims you can use from GitHub's OIDC token for more secure / complex combinations and workflows.
It's also worth remembering that this isn't the pinnacle of security, it's just better in most cases. By implementing OIDC in my pipelines, I'm giving some degree of control over my security mechanisms to another entity, so I need to make sure I trust that entity before implementing this, I need to review that trust on a frequent basis, and I should absolutely combine it with other tools and mechanisms. There's a whole world of security tools within AWS and that's before even taking a look at the sea of vendors who all offer their own solutions.
I'll take my tin foil hat off now.
Top comments (1)
Dang dude. This will super charge my dev teams productivity when I share it. Thanks so much.