Terraform Pipelines for Dummies Part2: GitHub Actions AWS Deploy (OIDC)

Terraform Pipelines for Dummies Part1: GitHub Actions AWS Deploy (OIDC)

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

  • No static keys in GitHub repositories.
  • Automatic credential rotation with every workflow run.
  • Simplified role assumption via OIDC.

Workflow

There are 2 pillars to the OIDC authentication using GitHub provider in AWS:

  1. You will first need to set up the OIDC trust in AWS so the pipeline can authenticate using an ID token.
  2. Then you can Update your GitHub actions workflow to authenticate using tokens.

oidc_aws_workflow

GitHub OIDC Keyless Access in a nutshell

  • Set up OIDC trust between AWS IAM role and GitHub Actions.
  • Use configure-aws-credentials to request a signed JWT.
  • GitHub issues a JWT to the workflow.
  • Workflow sends the JWT and requested role to AWS.
  • AWS validates the JWT and returns an Access Token.
  • Workflow uses the Access Token to access AWS resources.

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
Image Not Found
>> git_actions
"aws-labs"
Image Not Found

Workload Directory: Terraform configuration to deploy an AWS VPC is located in the terraform-provider-aws/create-vpc directory of the repository.

Image Not Found
Image Not Found
  • 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)

Image Not Found
  1. Go to IAM > Access Management > Identity Providers.
  2. Create a new provider:Provider Type: OIDC
    • Provider URL: https://token.actions.githubusercontent.com
    • Audience: sts.amazonaws.com
  3. 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

  1. Go to IAM > Roles > Create Role.
  2. 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”
    • See the full policy in the example tab 👉🏼
  3. Add permission to the role: i.e choose AmazonEC2FullAccess
  4. 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.

AWS access action

Image Not Found
The 3 input parameters aren’t secrets but we defined role-to-assume as secret in aws-labs environment
Image Not Found

  • 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

    #  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

GitHub actions terraform workflow


Push changes to the github_actions branch to trigger the GitHub pipeline.

Image Not Found

Go to Actions tab in the GitHub repo & select the latest run. Check the log.

Image Not Found

This needs to be done quickly as terraform apply job is followed by a destroy

Image Not Found

III. How to Fork My Repo to Try OIDC

  1. Visit the Repository:
    Go to https://github.com/brokedba/terraform-examples.
  2. Fork the Repository:
  3. Create an environment called aws_labs
  4. Follow the OIDC steps in this article
  5. Add secrets and variable accordingly
    • secret: AWS_ROLE_TO_ASSUME (from your AWS identity)
    • Variables
      • TF_STACK_DIR: terraform-provider-aws/create-vpc
      • AWS_REGION: i.e us-east-1
  6. 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.

Best Practices

  • Use short-lived credentials with OIDC to improve security.
  • Scope trust policies to specific branches and repositories.
  • Test the IAM role with minimal permissions.

Share this…

Leave a Comment

Your email address will not be published. Required fields are marked *