Skip to content

How to Secure GitHub Actions Against Supply Chain Attacks

Problem

I woke up one morning to find my CI/CD logs exposing secrets that should never have been visible. The tj-actions/changed-files action I had trusted for years was compromised.

# What I saw in my build logs
2025-03-15T08:23:45.123Z ##[warning]Unexpected output detected
2025-03-15T08:23:45.456Z AWS_ACCESS_KEY=AKIA...
2025-03-15T08:23:45.789NPM_TOKEN=npm_...

This was the tj-actions supply chain attack (CVE-2025-30066). Attackers compromised a maintainer account and injected malicious code that dumped CI/CD secrets into build logs. Over 23,000 repositories were affected.

The root cause? I was using mutable version tags instead of immutable commit SHAs.

What Happened

GitHub Actions supports three ways to reference an action:

1. Branch reference (MOST VULNERABLE):

vulnerable-workflow.yml
- uses: actions/checkout@main # Points to latest commit on main

This points to whatever commit is at the branch tip. Any new commit changes what code runs in my workflow.

2. Tag reference (STILL VULNERABLE):

still-vulnerable-workflow.yml
- uses: tj-actions/changed-files@v41 # Tag can be moved

Tags can be moved, deleted, or force-pushed. Attackers who gain maintainer access can retag malicious code. This is exactly what happened in the tj-actions attack.

3. Commit SHA (MOST SECURE):

secure-workflow.yml
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # Immutable

This is an immutable cryptographic reference. It cannot be changed without detection.

The attackers moved existing version tags (v41, v42, etc.) to point to malicious commits. Any workflow using tj-actions/changed-files@v41 automatically received the malicious code.

Why This Matters

The irony struck me hard. The attackers made their malicious releases immutable, something the legitimate maintainers had failed to do.

From Reddit discussion:
"Trivy is a reputable provider, but they fucked up. All the more ironic that Trivy are a security vendor..."
"What's additionally ironic is that the attacker made their compromised tags/releases immutable, something that Trivy should have done"

This attack showed that:

  • Even security vendors can have weak release processes
  • Reputation means nothing without verifiable security controls
  • Mutable references are a single point of failure
  • Transitive dependencies multiply the risk

The Fix: Pin to Commit SHA

I needed to replace all my tag references with commit SHAs. Here is the vulnerable workflow I had:

vulnerable-ci.yml
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # VULNERABLE
- uses: actions/setup-node@v4 # VULNERABLE
with:
node-version: '20'
- uses: tj-actions/changed-files@v41 # COMPROMISED
id: changed-files
- run: npm ci
- run: npm test

I converted it to this secure version:

secure-ci.yml
name: CI
on: [push]
permissions:
contents: read # Principle of least privilege
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: '20'
- uses: tj-actions/changed-files@d6e91a2266cdb9d620100ce78a7624c1e4a4178c # v41.0.1
id: changed-files
- run: npm ci
- run: npm test

Notice I added the version as a comment after the SHA. This keeps the code readable while maintaining security.

How to Find Commit SHAs

I use three methods to find the correct SHA:

Method 1: GitHub API

find-sha-api.sh
curl -s https://api.github.com/repos/actions/checkout/tags | jq -r '.[] | select(.name == "v4.2.2") | .commit.sha'

Method 2: Git ls-remote

find-sha-git.sh
git ls-remote https://github.com/actions/checkout refs/tags/v4.2.2

Method 3: GitHub Releases Page

Navigate to the releases page of the action repository. The commit SHA is shown in the release details.

Set Up Dependabot for Updates

I do not want to manually track updates. Dependabot can automate this:

.github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
reviewers:
- "your-security-team"

This creates pull requests when actions update. I can review the changes before merging.

Restrict Workflow Permissions

Even with SHA pinning, I limit what actions can do:

deploy-with-permissions.yml
name: Deploy
on:
push:
branches: [main]
# Default to read-only for all jobs
permissions: read-all
jobs:
deploy:
runs-on: ubuntu-latest
# Grant write permissions only where needed
permissions:
contents: write
id-token: write # For OIDC authentication
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Deploy
run: ./deploy.sh

This follows the principle of least privilege. If an action is compromised, the damage is limited.

Common Mistakes to Avoid

1. Assuming β€œVerified” Actions Are Safe

Verification only confirms the publisher identity. The tj-actions publisher was legitimate; their account was compromised.

2. Using Branch References for Third-Party Actions

never-do-this.yml
- uses: some-org/some-action@main # NEVER do this

Branch references provide zero protection against supply chain attacks.

3. Ignoring Transitive Dependencies

An action I use may depend on other actions. I check the action.yml file and any composite actions it references.

4. Not Auditing Release Processes

Before adopting any action, I now:

  1. Check the .github/workflows directory
  2. Verify they use SHA pinning themselves
  3. Look for OIDC-based releases with signed artifacts
  4. Review their security policy

5. Granting Excessive Permissions

bad-permissions.yml
jobs:
build:
runs-on: ubuntu-latest
# Implicitly gets write permissions!

Instead, I explicitly declare minimal permissions:

good-permissions.yml
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest

Summary

The tj-actions attack and Trivy incident proved that supply chain attacks target the publishing infrastructure, not just the code. The single most effective protection is pinning all GitHub Actions to immutable commit SHA hashes.

Key actions I took:

  1. Audited all workflow files for tag references
  2. Replaced all @v* references with full commit SHAs
  3. Configured Dependabot for automated, reviewed updates
  4. Restricted workflow permissions to minimum required
  5. Started auditing action release processes before adoption

The attackers made their malicious releases immutable. The legitimate maintainers did not. That irony should be a wake-up call for every developer using GitHub Actions.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments