· 13 min read

12 Steps to Secure GitHub Actions After the Trivy Attack

github-actionssupply-chain-securityawsci-cdsecurityoidc
CI/CD pipeline with a security breach point leading to a compromised cloud - visualizing GitHub Actions supply chain attack path to AWS
Mariusz Gebala AWS SA, Azure Admin, Palo Alto PCNSA - cloud-audit author

It’s March 2026 - a group specializing in security breaches has targeted Trivy, one of the most popular open-source vulnerability scanners. The idea, as always, is innovative, utilizing GitHub Action. Through their exploit, they force-pushed 75 of 76 version tags to malicious commits, stealing AWS credentials, GCP tokens, and SSH keys from every workflow running the compromised action. Over the next five days, the attack spread to Docker Hub, VS Code extensions, and PyPI (CVE-2026-33634, CVSS 9.4). However, a seed appeared in GitHub Actions security.

SHA pinning would have stopped it. But SHA pinning is step 1 of 12.

Here’s what actually went wrong in the Trivy, tj-actions, and TeamPCP attacks - and gives you 12 concrete hardening steps with YAML and Terraform code to harden your GitHub Actions pipelines and protect your AWS accounts.


The timeline nobody told you about

Let’s travel back in time and walk through a timeline that will explain the key stages. Recently, everyone using Trivy at work likely learned about the attack as a final step and might have thought it was a quick, one-time action. Well, no - here’s what happened over the course of 16(!) months.

November 2024: A PAT in a workflow

A SpotBugs maintainer added their Personal Access Token to a CI workflow in spotbugs/sonar-findbugs. A little over a week later, the attacker submitted a malicious pull request to the repository that exploited the pull_request_target trigger. The workflow executed the code, and the code had access to the maintainer’s credentials = PAT stolen.

Source: Palo Alto Unit 42 - Full Chain Analysis

March 2025: tj-actions/changed-files (CVE-2025-30066)

With the stolen PAT, the attacker moved through reviewdog/action-setup (CVE-2025-30154) to tj-actions/changed-files. This action was used by 23,000 repositories. ALL version tags were overwritten, and the malware:

  1. Dumped the memory of the Runner.Worker process
  2. Using regex, it found AWS keys, GitHub PATs, npm tokens, database credentials, and RSA private keys
  3. Printed extracted secrets to workflow logs - publicly visible on a public repository

The primary target was the Coinbase agentkit repository. The attack was detected within 87 minutes. CISA added the CVE to its Known Exploited Vulnerabilities catalog.

Endor Labs estimated that 218 repositories exposed secrets in logs, and StepSecurity’s Harden-Runner confirmed this by detecting unusual behavior.

Sources: Wiz Blog, Semgrep Advisory

August 2025: Nx/s1ngularity - the AI-augmented attack

The nx build system was compromised through the same pull_request_target pattern in GitHub Actions. Malicious scripts harvested cryptocurrency wallets, GitHub tokens, SSH keys, and AWS credentials. The malware used AI CLI tools (Claude, Gemini) for reconnaissance. The attack affected over 5,500 repositories and over 400 users. Some victims’ private repositories were renamed to s1ngularity-repository-##### and made public.

Sources: Wiz Blog, Socket.dev Investigation

September 2025: GhostAction - 3,325 secrets stolen

Compromised GitHub accounts pushed malicious workflow files disguised as “Add Github Actions Security workflow” into 817 repositories. 3,325 secrets were stolen including AWS access keys that were actively exploited post-discovery.

Source: GitGuardian Blog

March 2026: Trivy/TeamPCP (CVE-2026-33634, CVSS 9.4)

The key moment - the most extensive and advanced attack on CI/CD so far. TeamPCP (also known as DeadCatx3) attacker:

Phase 1 - GitHub Actions (March 19): Force-pushed 75 of 76 version tags in aquasecurity/trivy-action and all 7 tags in aquasecurity/setup-trivy. Published malicious Trivy binary v0.69.4 via compromised aqua-bot service account. Window: ~17:43 to ~05:40 UTC. (Aqua Security Advisory)

Phase 2 - Docker Hub (March 22): Uploaded malicious Trivy Docker images v0.69.5 and v0.69.6 (no corresponding GitHub releases - a red flag).

Phase 3 - Checkmarx (March 23): Hijacked 35 tags in checkmarx/kics-github-action and checkmarx/ast-github-action. Compromised Checkmarx VS Code extensions. (Sysdig Analysis)

Phase 4 - PyPI (March 24): Published malicious LiteLLM versions 1.82.7 and 1.82.8.

The “TeamPCP Cloud stealer” payload:

  1. Dumped Runner.Worker process memory
  2. Queried AWS IMDS (169.254.169.254) for IAM role credentials
  3. Queried ECS task metadata (169.254.170.2)
  4. Scraped AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN from environment
  5. Targeted ~/.aws/credentials and 50+ sensitive file paths
  6. Encrypted stolen data with AES-256 + RSA-4096
  7. Exfiltrated to a typosquatted domain (scan.aquasecurtiy.org)
  8. Fallback: created a public repo named tpcp-docs using victim’s GitHub PAT for data drops

10,000+ CI/CD workflows were affected. Unlike tj-actions (which leaked secrets to logs), TeamPCP encrypted and exfiltrated in real-time.

Sources: CrowdStrike, Microsoft, GitHub Advisory GHSA-69fq-xp46-6x23

March 2026: prt-scan - AI-generated payloads

Running concurrently with TeamPCP: 500+ malicious PRs targeting pull_request_target workflows. AI-generated, language-aware payloads in conftest.py, package.json, Makefile, build.rs. Exfiltrated through GitHub’s own API - no external servers contacted. At least 50 repos confirmed compromised.

Source: Wiz Blog


The pattern you should see

Three things are escalating:

  1. Attack sophistication. 2025: dump secrets to logs. 2026: encrypt with AES-256+RSA-4096, exfiltrate to C2, cascade across 4 ecosystems in 5 days.
  2. Security tools are targets. Trivy (vulnerability scanner), Checkmarx KICS (IaC scanner), reviewdog (linter). The tools meant to protect your pipeline are being weaponized. I compared several of these tools in my AWS security CLI tools roundup - the Trivy compromise puts that comparison in a new light.
  3. The CI/CD-to-cloud lateral movement is the goal. Every one of these attacks specifically targeted cloud credentials. The pipeline is not the target - your AWS account is.

Your GitHub Actions pipeline is an attack vector into AWS

Most organizations treat GitHub Actions security and AWS security as separate domains. Attackers see them as one continuous path:

Compromised Action or Workflow Injection
         |
         v
Secret Extraction (memory dump, env vars, IMDS, temp files)
         |
         v
AWS Credential Theft (static keys, OIDC tokens, instance roles)
         |
         v
AWS Lateral Movement (AssumeRole, cross-account, S3/RDS access)
         |
         v
Impact (data exfiltration, supply chain poisoning, cryptomining)

The Trivy attack specifically queried the AWS Instance Metadata Service at 169.254.169.254 and the ECS task metadata endpoint at 169.254.170.2. It was not looking for GitHub tokens - it was looking for your AWS credentials.


GitHub Actions security checklist: 12 steps to harden your pipeline

1. Pin all actions to full commit SHA

Git tags are mutable - an attacker with write access can move a tag to a malicious commit. SHA references are immutable.

Before (vulnerable):

steps:
  - uses: actions/checkout@v4
  - uses: aquasecurity/trivy-action@0.28.0

After (hardened):

steps:
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
  - uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b960b396a2f37a0 # v0.29.0

Find the SHA for any tag:

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

Or use the bulk converter:

npm install -g pin-github-action
pin-github-action .github/workflows/ci.yml

Would have prevented: tj-actions (CVE-2025-30066), Trivy (CVE-2026-33634), reviewdog (CVE-2025-30154).

Sources: GitHub Docs, StepSecurity Pinning Guide

2. Auto-update pinned SHAs with Renovate

Pinning to SHAs without automation means you either never update or give up and revert to tags. Renovate solves this.

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended",
    "helpers:pinGitHubActionDigests"
  ]
}

On first run, Renovate opens PRs converting all tag references to SHA pins. Future PRs update both the SHA and the version comment.

Dependabot also supports GitHub Actions updates but does not auto-pin to SHAs - Renovate is better for this use case.

3. Set GITHUB_TOKEN to read-only

Every workflow gets a GITHUB_TOKEN. By default, it may have write permissions. A compromised action step with a write-capable token can push code, modify releases, and more.

permissions: read-all

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - run: npm test

  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write    # only for OIDC
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

Also set at org level: Settings > Actions > General > Workflow permissions > “Read repository contents and packages permissions.”

Source: GitHub Changelog

4. Replace static AWS keys with OIDC

Static AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY stored as GitHub Secrets are permanent until manually rotated. OIDC tokens expire within minutes. After the tj-actions attack dumped secrets to logs, static AWS keys remained valid until someone noticed and rotated them. OIDC tokens would have been useless by then.

Terraform setup:

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

resource "aws_iam_role" "github_actions" {
  name = "github-actions-deploy"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        Federated = aws_iam_openid_connect_provider.github.arn
      }
      Action = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = {
          "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
        }
        StringLike = {
          "token.actions.githubusercontent.com:sub" = "repo:YOUR_ORG/YOUR_REPO:ref:refs/heads/main"
        }
      }
    }]
  })
}

Critical: Lock the sub claim to a specific repo AND branch. Never use wildcards like repo:org/*. Datadog Security Labs found 500+ vulnerable role ARNs across 275+ AWS accounts with missing or overly broad sub conditions. One UK Government role granted full CodeCommit access to any GitHub repo in the world. This kind of IAM misconfiguration is exactly what shows up in a proper AWS security audit.

I wrote more about this in my GitHub Actions OIDC AWS Backdoor article.

Workflow:

permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0
    with:
      role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
      aws-region: eu-central-1

5. Add egress monitoring with Harden-Runner

Even if a compromised action runs, it cannot phone home if outbound traffic is blocked. StepSecurity Harden-Runner monitors network egress, file integrity, and process activity.

Start with audit mode:

steps:
  - name: Harden Runner
    uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
    with:
      egress-policy: audit

After a few runs, switch to block mode:

  - name: Harden Runner
    uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
    with:
      egress-policy: block
      allowed-endpoints: >
        github.com:443
        api.github.com:443
        registry.npmjs.org:443

Harden-Runner detected the tj-actions compromise, the Trivy v0.69.4 malicious release, and the Nx supply chain attack. It secures 25+ million workflow runs per week.

Would have prevented: The Trivy exfiltration to scan.aquasecurtiy.org.

6. Scan your workflows for dangerous patterns

Zizmor is a security-focused static analyzer for GitHub Actions (23 rules, 10 weakness categories). Actionlint catches syntax issues. Run both in CI.

steps:
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
  - name: Run zizmor
    uses: zizmorcore/zizmor-action@cd0be69f6905cbc363e1b549640b4023f0774e93 # v1.6.0
  - name: Run actionlint
    run: |
      bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
      ./actionlint -color

Zizmor catches: script injection via ${{ }}, unpinned actions, excessive permissions, dangerous pull_request_target patterns. Grafana mandated Zizmor across all repositories after their April 2025 incident (CVE-2025-54313). For a broader look at scanning tools, see my comparison of AWS security scanners.

7. Fix or remove pull_request_target workflows

pull_request_target is the most exploited trigger in GitHub Actions history. It runs in the context of the base repository with access to secrets. SpotBugs, Ultralytics, Grafana, Nx, and prt-scan all exploited it.

Dangerous:

on: pull_request_target
steps:
  - uses: actions/checkout@v4
    with:
      ref: ${{ github.event.pull_request.head.sha }}  # runs attacker code with secrets

Safe:

on: pull_request
permissions:
  contents: read
steps:
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

If you must use pull_request_target, never checkout the PR head. The December 2025 GitHub update enforces that pull_request_target workflows execute code from the default branch only.

Also: Settings > Actions > General > “Require approval for all outside collaborators.”

Sources: GitHub Security Lab, Palo Alto Networks

8. Use environment protection rules

Require human approval before production secrets are accessible.

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://myapp.example.com
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: eu-central-1

Configure production with: 2 required reviewers, branch restriction (only main), 5-minute wait timer.

9. Protect workflow files with CODEOWNERS

# .github/CODEOWNERS
.github/workflows/ @your-org/security-team
.github/actions/   @your-org/security-team

For stronger enforcement, use Push Rulesets restricting .github/**/* file paths with a bypass list limited to authorized users.

10. Enable secret scanning + push protection

Block commits containing secrets before they reach the repository. As of April 2026, covers 200+ secret types.

Settings > Code security > Secret scanning > Enable > Push protection > Enable.

For CI-level scanning, add Gitleaks:

steps:
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
    with:
      fetch-depth: 0
  - uses: gitleaks/gitleaks-action@ff98106e84b2c5df1deeec5b383e8a42a6e8d8a6 # v2.3.9
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

11. Use reusable workflows for centralized controls

Define hardened CI once, call it everywhere. No more copy-paste drift.

Central workflow (in your-org/.github):

name: Reusable CI
on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: "20"

permissions:
  contents: read

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
        with:
          egress-policy: audit
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - run: npm ci && npm test

Caller:

jobs:
  ci:
    uses: your-org/.github/.github/workflows/ci-template.yml@main

Source: GitHub Well-Architected

12. Run OpenSSF Scorecard

Automated security score across 18+ checks: pinned dependencies, token permissions, branch protection, dangerous workflows.

jobs:
  scorecard:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      id-token: write
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          persist-credentials: false
      - uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
        with:
          results_file: results.sarif
          publish_results: true
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif

What GitHub is building next

GitHub’s 2026 Actions Security Roadmap (published March 26, 2026) introduces:

Workflow-level dependency locking (preview in 3-6 months): A dependencies: section that locks all direct and transitive action dependencies by commit SHA. Hash mismatches halt execution before jobs run.

Native Layer 7 egress firewall (preview in 6-9 months): Operates outside the runner VM - immutable even if an attacker gains root inside the runner. Domain allowlists, monitor and enforce modes.

Immutable Actions (GA): Actions published as immutable OCI images to GitHub Packages. Cannot be retroactively modified.

Scoped Secrets (preview in 3-6 months): Bind secrets to specific repos, branches, environments, or trusted reusable workflows.

The Shai-Hulud 2.0 attack (November 2025, 25,000+ repos) was directly cited by GitHub as the motivation for accelerating this roadmap.


What would have prevented each attack

Steptj-actions (2025)Trivy/TeamPCP (2026)prt-scan (2026)GhostAction (2025)
1. Pin to SHAYESYESNoNo
3. Read-only tokenPartialPartialPartialPartial
4. OIDCPartialPartialPartialPartial
5. Egress blockingNoYESNoYES
7. Fix pull_request_targetOrigin of chainOrigin of chainYESNo
8. Environment protectionPartialPartialNoNo

No single measure stops everything. Defense in depth is the point.


Start here (20 minutes)

If you do nothing else today:

  1. Pin all actions to SHA - 5 min per workflow
  2. Set GITHUB_TOKEN to read-only - 5 min at org level
  3. Enable secret scanning + push protection - 5 min
  4. Add harden-runner in audit mode - 5 min per workflow

Then schedule time for OIDC migration (Step 4) and pull_request_target audit (Step 7). Those two changes close the attack vectors that started both the tj-actions and Trivy chains. If you want to validate your AWS-side IAM configuration is not exposing overly broad trust policies, I wrote about automating CIS benchmark checks that catch exactly these issues.


If you want to check what your AWS account looks like after your CI/CD pipeline has been running with overly broad permissions, try cloud-audit. It scans your AWS environment in under 60 seconds and shows you attack chains - including chains that start from overprivileged IAM roles your CI/CD creates.