Previously, I used AWS Secrets for authentication in my GitHub Actions workflows. While this method was functional, it required manually managing and rotating credentials. To improve security and automation, I migrated to OpenID Connect (OIDC) authentication with AWS. GitHub provides an official guide on Configuring OpenID Connect in AWS for GitHub Actions, which I followed while implementing this setup.

In this post, I’ll explain how I implemented OIDC using Terraform, making the setup fully Infrastructure as Code (IaC).

Create an OIDC Identity Provider in AWS

As I wanted to put things under laC, l’ve imported existing resources from my earlier proof of concept setup, during which I had created the resources manually via AWS console.

First, I created a github-oidc.tf file. Here’s the full version:

resource "aws_iam_openid_connect_provider" "github_oidc" {
  url            = "https://token.actions.githubusercontent.com"
  client_id_list = ["sts.amazonaws.com"]
}

import {
  to = aws_iam_openid_connect_provider.github_oidc
  id = "arn:aws:iam::xxxxxxxxxxx:oidc-provider/token.actions.githubusercontent.com"
}

resource "aws_iam_role" "github_oidc_role" {
  name = "xxxxxxx"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.github_oidc.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" : "sts.amazonaws.com"
          }
          StringLike = {
            "token.actions.githubusercontent.com:sub" : "repo:xxxxxx/*"
          }
        }
      }
    ]
  })
}

import {
  to = aws_iam_role.github_oidc_role
  id = "xxxxxxxxx"
}

resource "aws_iam_role_policy_attachment" "github_oidc" {
  role       = aws_iam_role.github_oidc_role.name
  policy_arn = aws_iam_policy.this["ecr_pusher"].arn
}

import {
  to = aws_iam_role_policy_attachment.github_oidc
  id = "xxxxxxxxx/arn:aws:iam::xxxxxxxxxxx:policy/ecr_pusher"
}

The following part allows GitHub Actions to authenticate with AWS using OIDC tokens instead of secrets:

resource "aws_iam_openid_connect_provider" "github_oidc" {
  url            = "https://token.actions.githubusercontent.com"
  client_id_list = ["sts.amazonaws.com"]
}

import {
  to = aws_iam_openid_connect_provider.github_oidc
  id = "arn:aws:iam::xxxxxxxxxxx:oidc-provider/token.actions.githubusercontent.com"
}

Create an IAM Role for GitHub Actions

Next, I created an IAM role that GitHub Actions can assume. This role includes a trust policy to:

  • Use the OIDC provider configured in the previous step.
  • Allow only GitHub Actions workflows from neusysadmin/* to assume the role.
  • Ensure that only workflows with a valid OIDC token can authenticate.
resource "aws_iam_role" "github_oidc_role" {
  name = "xxxxxxxxxxxx"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.github_oidc.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" : "sts.amazonaws.com"
          }
          StringLike = {
            "token.actions.githubusercontent.com:sub" : "repo:neusysadmin/*"
          }
        }
      }
    ]
  })
}

import {
  to = aws_iam_role.github_oidc_role
  id = "xxxxxxxxx"
}

Attach the Existing ecr_pusher Policy

Since I already had an IAM policy (ecr_pusher) for pushing images to Amazon ECR, I attached it to the new IAM role.

resource "aws_iam_role_policy_attachment" "github_oidc" {
  role       = aws_iam_role.github_oidc_role.name
  policy_arn = aws_iam_policy.this["ecr_pusher"].arn
}

import {
  to = aws_iam_role_policy_attachment.github_oidc
  id = "xxxxxxxx/arn:aws:iam::xxxxxxxxx:policy/ecr_pusher"
}

Update the GitHub Actions Workflow

To use OIDC authentication, I updated my GitHub Actions workflow by adding the necessary permissions and configuring AWS credentials dynamically.

Below is the full version of my docker-build-scan-push.yaml file:

on:
  workflow_call:
    inputs:
      repository:
        required: true
        type: string
        description: 'Docker image repository'
      dockerfile:
        required: true
        type: string
        description: 'Dockerfile to build'
      imageTag:
        required: true
        type: string
        description: 'Docker image tag'
      dockerContext: 
        required: false
        type: string
        description: 'Docker context'
        default: '.'
permissions:
  id-token: write 
  contents: read
jobs:
  build-and-scan:
    runs-on: ubuntu-latest
    steps:
      # Step 1: Check out the repository
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::xxxxxxxxx:role/xxxxxxxx
          role-session-name: xxxxxxxxxx
          aws-region: eu-central-1

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3


      # Step 2: Log in to Amazon ECR
      - name: Log in to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
        with:
          registries: "xxxxxxxxxxxx"

      - name: Build, tag docker image 
        env:
          REGISTRY: xxxxxxxxxxx.dkr.ecr.eu-central-1.amazonaws.com
          REPOSITORY: ${{inputs.repository}}
          DOCKERFILE: ${{inputs.dockerfile}}
          IMAGE_TAG: ${{inputs.imageTag}} 
          DOCKER_CONTEXT: ${{inputs.dockerContext}}
        id: build_image
        run: |
          docker buildx create --use
          docker buildx build --platform linux/arm64 -f ./$DOCKERFILE -t $REGISTRY/$REPOSITORY:$IMAGE_TAG --load $DOCKER_CONTEXT
          echo "IMAGE=$REGISTRY/$REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT          

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@0.28.0
        with:
          image-ref: '${{ steps.build_image.outputs.IMAGE }}'
          format: 'table'
          exit-code: '1'
          ignore-unfixed: true
          vuln-type: 'os,library'
          severity: 'CRITICAL,HIGH,LOW'

      - name: Push docker image to Amazon ECR (multi-platform)
        id: push_image
        env:
          IMAGE: ${{ steps.build_image.outputs.IMAGE }}
        run: |   
          docker push $IMAGE

First, I added the necessary permissions:

permissions:
  id-token: write 
  contents: read

Then, I edited the jobs Step 1:

- name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::xxxxxxxxxx:role/xxxxxxxxxx
          role-session-name: xxxxxxxx

Migrating to OIDC made authentication in my GitHub Actions workflows easier and more secure, no more static AWS credentials to manage. It’s a simple, automated solution that reduces risk of leaks. If you’re still using long-lived secrets, now’s a great time to switch. Let me know if you have any questions!