Intro
Did you know that 12 millions secrets were publicly exposed in GitHub in 2023 alone? This is additional evidence that leaked secrets rhyme with financial and reputation loss for users, organizations and even states. The worst thing to do is make it easy for hackers to infiltrate your company’s system. This is where workload identity federation like OIDC rose to the challenge to make all your Cloud authentication seamless with 0 secrets stored.
What is OIDC ?
OpenID Connect (OIDC) is an authentication protocol built on top of OAuth 2.0 which adds an ID layer (ID token) to enable user authentication and Single Sign-On on top of OAUTH’s access delegation. If you want to learn more about both OAuth and OIDC check out our Blog-post
Keyless AWS deployment with OIDC
Struggling with AWS credential management in your CI/CD pipelines? Both AWS and GitHub Actions now support OpenID Connect (OIDC) for secure deployments by simplifying the process, and aligning with modern security practices.
With GitHub’s OIDC provider, you can authenticate directly from your workflows without the need for static access keys.
Key Benefits
Workflow
There are 2 pillars to the OIDC authentication using GitHub provider in AWS:
- You will first need to set up the OIDC trust in AWS so the pipeline can authenticate using an ID token.
- Then you can Update your GitHub actions workflow to authenticate using tokens.
GitHub OIDC Keyless Access in a nutshell
JWT (Jason Web Token): is a core component of OIDC containing information about the signed identity
Getting started
I. My GitHub Terraform Pipeline
To demonstrate how to configure OIDC in AWS, I’ve set up a GitHub repository with a Terraform pipeline (see below):
Repo: https://github.com/brokedba/terraform-examples
>> git_actions
"aws-labs"
Workload Directory: Terraform configuration to deploy an AWS VPC is located in the terraform-provider-aws/create-vpc
directory of the repository.
- Environment variables and secrets will be required for our git pipeline.
Repository: github.com/brokedba/terraform-examples
Branch: github_actions
Environment: aws-labs
What Are We Doing ?
In this scenario, we’re setting up a Terraform pipeline to deploy a VPC in AWS—but without using any access keys or secrets. Instead, we’ll use GitHub Actions workload identity to assume an AWS IAM role directly via OIDC.
How Does It Work?
We’re leveraging OpenID Connect (OIDC) to create a trust relationship between your GitHub pipeline and your AWS account. This means that when you push code to your repo, GitHub Actions will authenticate itself to AWS without requiring static credentials (access keys/secrets).
II. Configuring OIDC in AWS
Step 1: Set Up the OIDC Identity Provider (Trust)
- Go to IAM > Access Management > Identity Providers.
- Create a new provider:Provider Type: OIDC
- Provider URL:
https://token.actions.githubusercontent.com
- Audience:
sts.amazonaws.com
- Provider URL:
- Save the
ARN
of the identity for later.
aws iam create-open-id-connect-provider \
--url "https://token.actions.githubusercontent.com" \
--thumbprint-list "6938fd4d98bab03faadb97b34396831e3780aea1" \
--client-id-list "sts.amazonaws.com"
Step 2: Create an IAM Role and map it to the GitHub Identity
- Go to IAM > Roles > Create Role.
- Select Trusted Entity:
Custom Trust Policy
.- Paste the trust policy statement customized for your GitHub repo/branches
- Use the condition key
token.actions.githubusercontent.com:sub
to limit access to our github repo/environment/branch - In our case we select our GitHub environment aws_labs to specify our branch + repo in one claim
- Principal:
Federated
> ARN of the GitHub Identity created earlier - Action: “
sts:AssumeRoleWithWebIdentity
“ - Condition: “StringEquals” :
{sub & audience}
- sub: “
repo:brokedba/terraform-examples:environment:aws-labs
“ - aud: “sts.amazonaws.com”
- sub: “
- Principal:
- See the full policy in the example tab 👉🏼
- Add permission to the role: i.e choose AmazonEC2FullAccess
- Give a Role a Name: GitAction-oidc-role, and hit create
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::645884190840:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:brokedba/terraform-examples:environment:aws-labs",
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
}
}
]
}
Step 3: Configure GitHub Actions Workflow
Once OIDC trust is set, create a workflow where the configure-aws-credentials
action will receives a JWT from the GitHub OIDC provider, then requests an access token from AWS.
The 3 input parameters aren’t secrets but we defined role-to-assume
as secret in aws-labs
environment
- Create a workflow triggered by any push on our repo branch “git_actions” from any tf file under create-vpc folder
name: 'Terraform_aws_vpc'
on:
push:
branches: [ "git_actions" ]
paths:
- 'terraform-provider-aws/create-vpc/*tf' # tf files trigering the pipeline
env:
TF_VAR_aws_region: "${{ vars.AWS_REGION }}"
STACK_DIR: ${{ vars.TF_STACK_DIR }}
permissions:
id-token: write # This is required for requesting the JWT
- Add the
configure-aws-credentials
action to Authenticate to AWS from the pipeline.
# Authenticate with AWS using OIDC Workload Federated Identiry
- name: 'Configure AWS credentials'
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
# arn:aws:iam::1234567890:role/example-role
role-session-name: MySessionName # Default : Githubactions
aws-region: ${{ vars.AWS_REGION }}
Prerequisites
o Set permissions: id-token: write
to allow this action to generate the JWT Token.
link: terraform_aws_vpc
name: 'Terraform_aws_vpc'
on:
push:
branches: [ "git_actions" ]
paths:
- 'terraform-provider-aws/create-vpc/*tf'
env:
TF_VAR_aws_region: "${{ vars.AWS_REGION }}"
STACK_DIR: ${{ vars.TF_STACK_DIR }}
permissions:
id-token: write
jobs:
# ############
# INIT
# ############
terraform_setup:
name: 'Terraform Init-Validate'
runs-on: ubuntu-latest
environment: aws-labs
# Use default shell and working directory regardless of the os of the GitHub Actions runner
defaults:
run:
shell: bash
working-directory: ${{ env.STACK_DIR }}
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout
uses: actions/checkout@v3
# Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 1.0.3
terraform_wrapper: false
# cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }} --> Terraform cloud
# Create a cache for the terraform pluggin and copy tf binary
- name: Config Terraform plugin cache
run: |
echo 'plugin_cache_dir="$HOME/.terraform.d/plugin-cache"' >~/.terraformrc
mkdir --parents ~/.terraform.d/plugin-cache
terra_bin=`which terraform`
cp $terra_bin .
# Initialize a new or existing Terraform working directory(creating initial files, loading any remote state, downloading modules..)
- name: Terraform Init
id: init
run: |
echo ====== INITIALIZE terraform provider plugins : $GITHUB_WORKSPACE/${{ vars.TF_STACK_DIR }} ======
pwd
echo terra_bin=$terra_bin >> "$GITHUB_OUTPUT"
# echo the temp directory path is : $RUNNER_TEMP
terraform init
terraform -v
- name: Terraform format
run: |
echo ====== FORMAT the Terraform configuration in ${{ env.STACK_DIR }} ======
terraform fmt
- name: Terraform Validate
run: |
echo ====== VALIDATE the Terraform configuration in ${{ env.STACK_DIR }} ======
terraform validate
# Authenticate with AWS using OIDC Workload Federated Identiry
- name: 'Configure AWS credentials'
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
# arn:aws:iam::1234567890:role/example-role
role-session-name: MySessionName #${{ secrets.My_sessionName }}
aws-region: ${{ vars.AWS_REGION }}
- name: Print assumed Role
run: aws sts get-caller-identity
# PLAN
- name: Terraform Plan
id: plan
run: |
echo ====== PLAN execution for the Terraform configuration in ${{ env.STACK_DIR }} ======
terraform plan -input=false -no-color -out tf.plan
# Save all plugin files and working Directory in a cache
- name: Cache Terraform
uses: actions/cache@v3
with:
path: |
~/.terraform.d/plugin-cache
./*
key: ${{ runner.os }}-terraform-${{ env.STACK_DIR }}
restore-keys: |
${{ runner.os }}-terraform-${{ env.STACK_DIR }}
# ${{ hashFiles('**/.terraform.lock.hcl') }}
outputs:
terra_path: ${{ steps.init.outputs.terra_bin }}
# ############
# APPLY
# ############
Terraform_Apply:
name: 'Terraform Apply'
runs-on: ubuntu-latest
environment: aws-labs
needs: [terraform_setup]
# Use default shell and working directory regardless of the os of the GitHub Actions runner
defaults:
run:
shell: bash
working-directory: ${{ env.STACK_DIR }}
steps:
# Checkout the repository to the GitHub Actions runner not necessary. The cache has it
- name: Cache Terraform
uses: actions/cache@v3
with:
path: |
~/.terraform.d/plugin-cache
./*
key: ${{ runner.os }}-terraform-${{ env.STACK_DIR }}
restore-keys: |
${{ runner.os }}-terraform-${{ env.STACK_DIR }}
# Configure terraform pluggin in the new runner
- name: Config Terraform plugin cache
run: |
echo 'plugin_cache_dir="$HOME/.terraform.d/plugin-cache"' >~/.terraformrc
# terraform init not needed here . int files are already in the cache
# TERRAPATH="${{ needs.terraform_setup.outputs.terra_path }}"
# echo old terraform binary location: $TERRAPATH
# Authenticate with Azure using OIDC Workload Federated Identiry (i.e User Manged Identity)
- name: 'Configure AWS credentials'
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
# arn:aws:iam::1234567890:role/example-role
role-session-name: MySessionName #${{ secrets.My_sessionName }}
aws-region: ${{ vars.AWS_REGION }}
# APPLY
- name: Terraform Apply
id: plan
if: github.event_name == 'push'
run: |
echo ====== APPLY execution for the Terraform configuration in ${{ env.STACK_DIR }} ======
echo "== Reusing cached version of terraform =="
sudo cp ./terraform /usr/local/bin/
terraform -v
terraform plan -input=false -no-color -out tf.plan
terraform apply --auto-approve tf.plan
# Create a cache for the terraform state file
- name: Cache Terraform statefile
uses: actions/cache@v3
with:
path: |
${{ env.STACK_DIR }}/terraform.tfstate
key: ${{ runner.os }}-terraform-apply-aws-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-terraform-apply-${{ github.run_id }}
# ############
# DESTROY
# ############
Terraform_Destroy:
name: 'Terraform Destroy'
runs-on: ubuntu-latest
environment: aws-labs
permissions: write-all
needs: [Terraform_Apply]
# Use default shell and working directory regardless of the os of the GitHub Actions runner
defaults:
run:
shell: bash
working-directory: ${{ env.STACK_DIR }}
steps:
# Checkout the repository to the GitHub Actions runner not necessary. The cache has it
- name: Cache Terraform
uses: actions/cache@v3
with:
path: |
~/.terraform.d/plugin-cache
./*
key: ${{ runner.os }}-terraform-${{ env.STACK_DIR }}
restore-keys: |
${{ runner.os }}-terraform-${{ env.STACK_DIR }}
# Configure terraform pluggin in the new runner
- name: Config Terraform plugin cache
run: |
echo 'plugin_cache_dir="$HOME/.terraform.d/plugin-cache"' >~/.terraformrc
# Restore a cache for the terraform state file
- name: Cache Terraform statefile
uses: actions/cache@v3
with:
path: |
${{ env.STACK_DIR }}/terraform.tfstate
key: ${{ runner.os }}-terraform-apply-aws-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-terraform-apply
# terraform init not needed here . int files are already in the cache
# ls terraform.tfstate
# Authenticate with Azure using OIDC Workload Federated Identiry (i.e User Manged Identity)
- name: 'Configure AWS credentials'
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
# arn:aws:iam::1234567890:role/example-role
role-session-name: MySessionName #${{ secrets.My_sessionName }}
aws-region: ${{ vars.AWS_REGION }}
# clean terraform cache after destroy completion
# DESTROY
- name: Terraform Destroy
id: destroy
run: |
echo ====== Destroy the Terraform configuration in ${{ env.STACK_DIR }} ======
echo "== Reusing cached version of terraform =="
sudo cp ./terraform /usr/local/bin/
terraform -v
terraform destroy --auto-approve
# clean terraform cache after destroy completion
- name: clean cache
id: cache_deletion
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh extension install actions/gh-actions-cache
echo " deleting tfstate caches"
gh actions-cache list
gh actions-cache delete ${{ runner.os }}-terraform-apply-aws-${{ github.run_id }} --confirm
gh actions-cache delete ${{ runner.os }}-terraform-${{ env.STACK_DIR }} --confirm
How to check my git actions claims?
The actions-oidc-debugger action, is the perfect tool to print your ID token (JWT) claims.
Here’s an excerpt of my repo claims:
// Example: OIDC Token claims for “brokedba”s github actions workflow
{ "actor": "brokedba",
"aud": "https://github.com/brokedba",
"environment": "gcp-labs",
"event_name": "push",
"iss": "https://token.actions.githubusercontent.com",
"job_workflow_ref": "brokedba/terraform-examples/.github/workflows/..",
"ref": "refs/heads/git_actions",
"ref_type": "branch",
"repository": "brokedba/terraform-examples",
"repository_owner": "brokedba",
snip ...
"sub": "repo:brokedba/terraform-examples:environment:gcp-labs", <<--- used in AWS
"workflow": "Terraform_gcp_vpc",
snip ...
}
Final Step: Test the Pipeline
Once all the configurations are completed, the last step is to test the pipeline to ensure everything works as expected.
Note: Our test Workflow has 3 different Jobs 1. Terraform init-validate 2. Terraform Apply 3. Terraform Destroy
Push changes to the github_actions
branch to trigger the GitHub pipeline.
Go to Actions tab in the GitHub repo & select the latest run. Check the log.
This needs to be done quickly as terraform apply job is followed by a destroy
III. How to Fork My Repo to Try OIDC
- Visit the Repository:
Go to https://github.com/brokedba/terraform-examples. - Fork the Repository:
- Create an environment called aws_labs
- Follow the OIDC steps in this article
- Add secrets and variable accordingly
- secret:
AWS_ROLE_TO_ASSUME
(from your AWS identity) - Variables
TF_STACK_DIR
: terraform-provider-aws/create-vpcAWS_REGION
: i.e us-east-1
- secret:
- Switch to the Git Actions Branch
Conclusion
Integrating GitHub Actions with AWS using OIDC provides a seamless, secure, and keyless authentication workflow. By leveraging OIDC trust, you eliminate the need for long-term credentials, reducing secret management overhead (storage, duplication, rotation) and simplifying cloud access.
We can clearly conclude the below best practices that should be your baseline from now on.