Introduction
Project link: https://github.com/realexcel2021/oidc-terraform/
GitHub Actions OIDC... What's the Thingy? okay so here's the new gist. Back in time yea? before one could interact with AWS programmatically or in a CI/CD pipeline using our favorite GitHub actions, we used the wonderful Access keys and Secret Access keys which we try our best to hide from the outside world and keep rotating these keys chronologically for security reasons.
These Access keys are so valuable and vital that if by any means someone unauthorized gets access to those keys, the person also gets access to the AWS account associated with the keys. Now that we have DevOps engineers, and companies practicing DevOps that are now putting up their CI/CD pipelines in Github, how do we manage security with these keys.... π€ and we have to keep changing them π€·ββοΈπ€·ββοΈ.
OIDC
That's where our new guy comes in. OIDC stands for Open ID Connect, this is an authentication protocol based on the Oauth2 framework. A perfect example of this is when you want to log in to any website or mobile application, and then you'll find an option saying "Login with Facebook" or "Log in with Google". Basically, when you do that, the OIDC is the underlying technology beneath that process. It is designed to allow standard and secure authentication for mobile apps.
How does the OIDC work?
The OIDC works by providing user authentication and secure identity information to applications that need the authentication, when a user then tries to access the application, they are redirected to an Identity Provider for authentication. The user provides authentication (email and password) to the Identity Provider. Upon successful authentication from the user to the Identity Provider, the Identity Provider issues an ID Token encoded as JWT (JSON Web Token) that contains info about the user to the application that needs this authentication. let's have an example event of this.
Let's say you want to sign up for a banking app and click "Sign up with Google" Now in this process, you are the user, the banking application is the Application and Google is the Identity Provider. When you click that button, it takes you to Google Sign In and you choose your account or you sign into your Google account. Upon successfully signing in to Google, Google provides an ID token to the bank application which is the JWT token providing secure access to the bank app without managing passwords or use info.
GitHub OIDC and AWS
Now that we have an overview of how OIDC works, we can then understand that AWS can authenticate our GitHub actions to the resources in AWS. Let's give an example. We have a GitHub actions script that will deploy some frontend code in an EC2 instance. Now in this scenario, the user is the GitHub actions script and in our AWS account, the Identity Provider in this case that will authenticate our GitHub Actions workflow will be IAM to give access to our Account. I know you might think about AWS Cognito but in this case, we are not creating OIDC for an application in AWS, we are creating OIDC for our whole AWS account which in this case will be IAM. So in IAM, to create an Identity Provider, that identity provider yea, will assume a role. Now what's this role for?
This role the Identity provider assumes is the list of services that the user (Github Actions) is allowed to use after successful authentication e.g. EC2. So since we want our GitHub actions to deploy some frontend code to a web server (EC2) we'll give the Identity provider a role that has permission to EC2. Consider the diagram below for the scenario I explained above
Hands-on Demo with Github OIDC and AWS using Terraform
In this demo, we'll create an EC2 instance with Terraform and then configure the OIDC with GitHub actions or authentication to AWS without access keys. Project link: https://github.com/realexcel2021/oidc-terraform
First of all, let's write the terraform script. On your local machine on any of your text editor, create a folder and add a main.tf
file. In the file, paste in the code below.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
# Web server
resource "aws_instance" "nginx" {
ami = "ami-053b0d53c279acc90"
instance_type = "t2.micro"
security_groups = [aws_security_group.nginx_sg.name]
user_data = file("./script.sh")
tags = {
Name = "NGINX"
}
}
# Security group for the web server
resource "aws_security_group" "nginx_sg" {
name = "Allow Public Access on ssh and web server"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [ "0.0.0.0/0" ]
}
tags = {
Name = "Nginx Web Server-sg"
}
}
This code provisions an ec2 instance and a security group that is open to the public, this instance serves as a web server. If you look closely, you'll find that the terraform code requires a user data file, this is a bash script that should be executed after the instance is created and in the running state. Create a script.sh
file and paste the code below.
#!/bin/bash
sudo apt update -y &&
sudo apt install -y nginx
echo "Our Web server is running" > /var/www/html/index.html
This script updates the provisioned machine, installs Nginx on the machine and overwrites the default Nginx page. With these two files, we are ready for deployment on AWS. Before deploying to AWS, let's, first of all, Head over to github and create a repository and push these two files we have created. Note that when creating this repository, be sure to set it to a private repo. If you're following along, you should have a repo similar to this
Copy your github user name and the name of the repo from the repo link. Something linked to this realexcel2021/oidc-terraform
we'll be using this info in our Identity provider which is IAM.
Now head over to your AWS account in IAM and click on "Identity Provider" then click on "Add provider"
Select OpenID Connect. In the provider url, put in https://token.actions.githubusercontent.com
. This URL specifies where the request for authentication will come from. In the Audience, put in sts.amazonaws.com
.
Click on "Get thumbprint" and click "Add provider"
We now have the provider configured.
Now Let's create the Role that Github actions will assume after successful authentication into our AWS account. We'll head over to IAM and create a role.
In the Create Role page, select Custom Trust policy. You'll find an editor below, paste the trust policy below this image into the editor.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456123456:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:<GITHUB_USERNAME>/<REPO_NAME>:*"
},
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
}
}
]
}
Edit the JSON policy. In the "Federated"
edit 123456123456
to your AWS account ID that you are deploying to. In token.actions.githubusercontent.com:sub
edit it to your github username and the repo name you wish to authenticate into AWS, this repo is where your GitHub actions and terraform scripts are. After editing these in the editor, click Next.
Next, we want to add permissions that github actions will assume. Using the least privilege strategy, since we only want to deploy an EC2 instance on the AWS account, we'll grant permission for EC2FullAccess. You can select these permissions based on your terraform use cases. After selecting the useful permissions, click Next.
We will also attach policy for AmazonS3FullAccess. This is because we want to store our terraform state files in an s3 bucket.
Name the Role and click "Create role"
You should have two policies attached to the IAM role for github actions.
Now that this role is created, copy the ARN of the Role. You can find it when you click on the role. Keep the ARN we'll use it later in GitHub.
Before we add the details of the role in our github repo, let's create an s3 bucket for terraform. This s3 bucket will hold our terraform state files. Head over to s3 and create a bucket. Give it whatever name you wish. Ensure that encryption is enabled for the bucket.
I have my bucket created and specified the name
Let's head over to our GitHub repo to configure some settings for the GitHub actions. We'll add the role that the GitHub actions should assume to the secrets (we don't want the public to have access to our role ARN). To do this, go to your repo and then click on settings
Click on "Secrets and Variables" then click on "Actions"
Click on "New Repository Secret" and let's add the role ARN as a secret, you can name it anything, but for this demo, we'll be naming it AWS_ROLE paste the ARN of the secret and click "Add secret".
We'll also add an Actions secret, which is the name of the s3 bucket. replace this with the name of the bucket you created earlier.
Also, add a secret for the Bucket key name. The bucket key name is the directory in the bucket where terraform should store the states.
Finally, you should have 3 secrets stored in your github actions secrets.
Now let's write the GitHub action for this terraform script that will automatically deploy this for us in a CICD pipeline. In your local file where you have your terraform code, create a file in the directory .github/workflows/deploy.yml
In the deploy.yml
file paste the github actions code below.
name: 'Deploy Terraform Script'
on:
workflow_dispatch:
push:
branches:
- master
pull_request:
permissions:
id-token: write # This is required for Github Actions to request JWT
contents: read # This is required for actions checkout
pull-requests: write
jobs:
deploy-terraform:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: .
steps:
- name: Checkout Git code
uses: actions/checkout@v4
- name: Configure credentials for the OIDC connections
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: ${{secrets.AWS_ROLE}}
role-session-name: GITHUB-ACTIONS-OIDC
aws-region: us-east-1
- name: Install Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.2.5
- name: Clean Terraform Code
id: fmt
run: terraform fmt -check
continue-on-error: true
- name: Initialize Terraform
id: init
env:
AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }}
AWS_BUCKET_KEY_NAME: ${{ secrets.AWS_BUCKET_KEY_NAME }}
run: terraform init
- name: Validate Terraform Code
id: validate
run: terraform validate -no-color
- name: Plan terraform code
id: plan
if: github.event_name == 'pull_request'
run: terraform plan -no-color
- uses: actions/github-script@v6
if: github.event_name == 'pull_request'
env:
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Format and Style π\`${{ steps.fmt.outcome }}\`
#### Terraform Initialization βοΈ\`${{ steps.init.outcome }}\`
#### Terraform Validation π€\`${{ steps.validate.outcome }}\`
<details><summary>Validation Output</summary>
\`\`\`\n
${{ steps.validate.outputs.stdout }}
\`\`\`
</details>
#### Terraform Plan π\`${{ steps.plan.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`\n
${process.env.PLAN}
\`\`\`
</details>
*Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: Check if terraform plan succeeds
if: steps.plan.outcome == 'failure'
run: exit 1
- name: Terraform Apply
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
run: terraform apply -auto-approve -input=false
This actions code, triggers either when manually triggered in github, when there's a push in the github repo, or whenever there is a pull request. For this example, we'll be creating a pull request to validate the terraform code that we intend to deploy.
The actions code needs permission for id-token
this is permission for GitHub actions to request for authentication in AWS. Read permission for content is needed for actions checkout, this is needed for GitHub actions to read the code we have in our repository.
We have one job in our GitHub actions that run on Ubuntu. this job is where our terraform code is going to be executed. This ubuntu machine has steps that will run in it, like installing Terraform on the Ubuntu machine, applying the code, and only displaying the Terraform plan when a pull request is merged.
Note that in the OIDC connection step in the ubuntu machine, we configured the AWS region that we want to authenticate to using the OIDC connection. You can edit the aws-region
to any AWS region of your choice. The three parameters, role-to-assume
role-session-name
aws-region
are required.
The way the actions script works is if there is a pull request, it outputs the terraform plan only. Then if the pull request is merged into the master branch, terraform apply will then happen. terraform apply
command only works if there is a push only on the master branch.
So to demonstrate this, a new branch called test-PR-1
was created and i pushed it to my github code base.
Creating the pull request, triggers the actions script to display a plan of the terraform code.
I'll head over to the pull request, and merge the pull request to the master branch. This will trigger the actions script to apply the terraform code to the AWS account with no access keys.
The merge pull request triggered the github actions to apply the terraform code.
All checks passed
Authentication to AWS was successful.
The instance is created successfully
The Nginx server successfully installed
The tfstate file for terraform files is also available in our s3 bucket
This demo explains how GitHub OIDC authentication works and how you can authenticate GitHub actions to AWS without access keys. This is a great innovation when keeping security in mind.
If you followed along with this demo, please ensure to destroy the resources with the steps below.
If you created a new branch and merged the pull request on your github repo, you should still have that branch named test-PR-1
. In that branch on your local machine, in the .github/workflows
directory, add a new file called destroy.yml
and paste in the code below.
name: 'Destroy Terraform Infrastructure'
on:
workflow_dispatch:
permissions:
id-token: write # This is required for Github Actions to request JWT
contents: read # This is required for actions checkout
pull-requests: write
jobs:
destroy-terraform:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: .
steps:
- name: Copy repository code
uses: actions/checkout@v4
- name: Configure OIDC connection
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: ${{secrets.AWS_ROLE}}
role-session-name: GITHUB-ACTIONS-OIDC
aws-region: us-east-1
- name: install terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.2.5
- name: Initialize Terraform
id: init
env:
AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }}
AWS_BUCKET_KEY_NAME: ${{ secrets.AWS_BUCKET_KEY_NAME }}
run: terraform init -backend-config="bucket=${AWS_BUCKET_NAME}" -backend-config="key=${AWS_BUCKET_KEY_NAME}"
- name: destroy terraform
id: destroy
run: terraform destroy --auto-approve
This GitHub actions script runs a job only on a work flow dispatch. This means that the pipeline can only be triggered manually whenever you want to destroy the terraform infrastructure.
Commit and push the new file to your git repository.
Compare and make a pull request to merge the branch. Upon creating the pull request, the master branch github actions will display a terraform plan
Merge the pull request
The pull request merge will trigger the deploy github actions again. Which will not deploy anything because there is no change in our terraform code.
In the actions tab, you'll find a workflow named Destroy Terraform Infrasructure
You can then manually trigger the workflow just as we have specified it in the GitHub actions by clicking the run workflow
button.
The job was successfull
The terraform infrastructure has been destroyed!!
Thank you very much for the read! Please if anything is unclear to you or you find any information wrong in the article, feel free to reach out to me πon Twitter .