← Back to Blog
DevSecOps Advanced 14 min

DevSecOps Pipeline Hardening: Hardening GitHub Actions & OIDC Federated Auth

Eliminate long-lived cloud credentials in CI/CD by configuring GitHub Actions OIDC federated authentication with AWS IAM, securing workflow triggers, and preventing script injection.

GitHub ActionsOIDCAWSIAMDevSecOpsCI/CD Security
DevSecOps OIDC Pipeline Security Architecture

Modern CI/CD automation pipelines are one of the most high-value targets in cloud environments. Hardcoded AWS access keys (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY) stored in repository secrets pose a severe threat: if compromised or leaked via a PR build, attackers gain persistent access to your infrastructure.

In this first part of our DevSecOps Zero-to-Hero Pipeline Security playlist, we cover how to completely eliminate static cloud credentials using OpenID Connect (OIDC) federated authentication, enforce least-privilege IAM boundary policies, and remediate GitHub Actions workflow vulnerabilities like pull_request_target injection.


1. The Risk of Long-Lived CI/CD Credentials

Storing static credentials inside GitHub Secrets introduces critical risks:

  1. Credential Drift & Theft: Static keys do not rotate automatically. If a pipeline step prints environment variables or leaks an artifact, the key remains active indefinitely.
  2. Over-privileged Access: Developers often attach broad policies (AdministratorAccess or s3:*) to pipeline IAM users to avoid deployment permission errors.
  3. No Short-lived Revocation: Disabling access requires manual intervention in the AWS IAM Console.

2. Architecting OIDC Federated Authentication

OIDC allows GitHub Actions workflows to request short-lived JSON Web Tokens (JWT) signed by GitHub’s Identity Provider (https://token.actions.githubusercontent.com). AWS verifies the token signature against GitHub’s OpenID discovery endpoint and issues temporary STS credentials (aws:AssumeRoleWithWebIdentity).

┌─────────────────┐       1. Request JWT Token       ┌────────────────────────┐
│  GitHub Actions │ ───────────────────────────────> │ GitHub OIDC Provider   │
│    Workflow     │ <─────────────────────────────── │ (token.actions.github) │
└────────┬────────┘       2. Signed OIDC Token       └────────────────────────┘

         │ 3. AssumeRoleWithWebIdentity(Token, RoleArn)

┌─────────────────┐
│     AWS STS     │ ───────────────────────────────┐
│ (Security Token)│                                │ 4. Validate Claims
└────────┬────────┘                                ▼
         │                                ┌────────────────────────┐
         │ 5. Short-Lived Credentials     │   AWS IAM OIDC Provider    │
         └──────────────────────────────> │  & Trust Policy Check  │
                                          └────────────────────────┘

3. Configuring AWS IAM OIDC Provider & Trust Policy

Step 1: Create the OIDC Identity Provider in AWS

Using Terraform to register GitHub’s OIDC Provider:

resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"] # GitHub OIDC CA thumbprint
}

Step 2: Define the Scoped IAM Trust Policy

Never grant wildcard access (repo:org/*:*). Strictly scope the sub (subject) claim to your specific organization, repository, and deployment branch:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          "token.actions.githubusercontent.com:sub": "repo:Dynamo2k1/Portfolio:ref:refs/heads/main"
        }
      }
    }
  ]
}

4. Hardening the GitHub Actions Workflow

Below is a production-grade hardened workflow that requests minimal permissions and assumes the short-lived AWS IAM role:

name: Secure Production Deployment

on:
  push:
    branches: [ main ]

# Require minimal top-level permissions (Principle of Least Privilege)
permissions:
  id-token: write  # Mandatory for requesting the OIDC JWT token
  contents: read   # Read repository contents only

jobs:
  deploy:
    name: Build & Deploy Infrastructure
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Configure AWS Credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActions-Production-Deployer
          aws-region: us-east-1
          audience: sts.amazonaws.com
          role-session-name: GitHubActionsDeployment

      - name: Verify AWS Identity
        run: |
          aws sts get-caller-identity

5. Preventing Workflow Injection Vulnerabilities

The pull_request_target Trap

Using pull_request_target triggers the workflow in the context of the base branch while retaining access to repository secrets. If combined with explicit checkout of untrusted code from a fork:

# ❌ VULNERABLE WORKFLOW - DO NOT USE
on: pull_request_target

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }} # Checks out malicious PR code!
      - run: npm install && npm run build               # Executes arbitrary attacker code!

Remediation:

  1. Always run untrusted PR code under pull_request (which has zero secret access and read-only token permissions).
  2. If pull_request_target is strictly necessary (e.g. for labeling PRs), never check out or execute code from github.event.pull_request.head.sha.

In Part 2 of this playlist, we explore Automated SAST, DAST & Secret Scanning with Semgrep and TruffleHog.

// Discussion

Enjoyed this? Let us work together.

Available for Security Engineering, DevSecOps, and Penetration Testing engagements.