The Trivy Supply Chain Attack: How Platform Defaults Failed an Entire Ecosystem

Miguel Martinez
How attackers compromised Trivy via tag poisoning in GitHub Actions, and how continuous verification with Chainloop defends against this class of attack.

We’re shipping software faster than ever. New tools, better CI/CD, automated everything. And somewhere along the way, supply chain security starts feeling like it’s handled. Until an incident reminds us it isn’t.

On March 19, 2026, attackers compromised Trivy — one of the most widely used open-source vulnerability scanners, with over 33,000 GitHub stars. The irony isn’t lost on anyone: a security tool became the attack vector.

This isn’t Trivy’s fault. Getting compromised can happen to anyone. The real issue is the platform defaults that made it possible.

What Happened

The attack came in two waves.

First, an automated bot exploited a pull_request_target workflow in aquasecurity/trivy-action to steal a Personal Access Token. Aqua Security rotated their credentials in response, but the rotation wasn’t atomic, and the attacker captured refreshed tokens before the process completed.

With valid credentials in hand, the attacker force-pushed 75 of 76 version tags in aquasecurity/trivy-action to point at malicious commits. No new release. No branch push. No notifications. The tags just… quietly started pointing somewhere else.

Every CI/CD pipeline referencing those tags was now pulling a credential stealer instead of a vulnerability scanner, one that harvested GitHub Actions secrets, SSH keys, and cloud credentials and sent them to an attacker-controlled server.

The clever part? The malicious code ran before the real Trivy scanner, then let the scanner complete normally. Workflow logs looked fine. Everything passed.

What You Should Do Right Now

If you use Trivy in your CI/CD pipelines, stop and check:

  • Are you affected? Every trivy-action tag except 0.35.0 was compromised. Check Aqua Security’s official advisory for the full list and remediation steps.
  • Rotate your secrets. If you ran a compromised version, treat all pipeline secrets as compromised — GitHub tokens, cloud credentials, SSH keys. Rotate them now.
  • Pin to SHA, not tags. If you’re referencing GitHub Actions by tag (e.g., @0.24.0), switch to full commit SHA pins. Tags are mutable — SHAs aren’t.

Once you’ve done that, keep reading. Understanding why this worked matters just as much as the fix.

Root Cause: Mutable Tags and Missing Verification

The core technique was tag poisoning. Git tags are mutable pointers. A git push -f rewrites where they point, and nothing on GitHub’s release page changes. Same name, same dates, same description. Different code.

The attacker cloned every visible attribute of the original commits: author name, email, timestamps, even the PR number in the commit message. To someone reviewing the repo, nothing looked wrong.

As Dan Lorenc (Chainguard) put it: “GitHub still ships version tags as the default way to pin Actions. They’re mutable. One force-push and you’re running someone else’s code in your pipeline. The entire ecosystem pins to tags because that’s what the docs tell you to do.”

The Smoking Gun: Signatures Were There All Along

Comparing the commit metadata from GitHub’s API for the malicious commit and the legitimate one it replaced (both for tag 0.24.0):

Malicious (e0198fd)Original (6e7b7d1)
AuthorVinayak S (Dr-DevOps)Vinayak S (Dr-DevOps)
Date2024-07-09T06:19:25Z2024-07-09T06:19:25Z
MessageIdentical (PR #369)Identical
Signatureverified: falseunsignedverified: true — valid GPG

The original commit was signed by GitHub’s merge key (created through a PR merge). The malicious replacement had no signature at all. The cryptographic tooling to detect this attack already existed. GitHub just doesn’t enforce it. The “Verified” badge is cosmetic, not a gate.

How We Use Chainloop for Chainloop

We use Chainloop to secure Chainloop Open Source releases. Same platform, same policies, same foundations our customers use. No tool can guarantee 100% security, but we can stack multiple layers of defense so that no single failure is enough to compromise a release.

  • We collect every interaction from tools, humans, and agents as cryptographically signed, tamper-proof records
  • We check branch and tag protection settings on every build — force push blocked, deletion blocked, rules change restricted
  • We verify signature information on each commit at merge time
  • We ensure all artifacts are signed — every container and Helm chart before release
  • We gate releases — blocked if compliance requirements aren’t met
  • We declare policies, contracts, and requirements as code — preventing drift
  • We monitor and get notified in real time about any drift in our compliance and security posture

Continuously Verifying Repository Guard Rails

The Trivy attack relied on force-pushing tags. That’s only possible if the repository allows it.

Chainloop’s workflows run gather-runner-context, which queries GitHub’s API for the repo’s protection settings at every build. This covers both branches and tags:

  • Branch protection: branch-force-push-blocked, branch-deletion-blocked, repository-rules-change-restricted — protecting source code integrity
  • Tag protection: tag-force-push-blocked, tag-deletion-blocked, tag-rules-change-restricted — protecting release references from being silently rewritten
Branch Protection Configuration requirement evaluation showing 4 of 4 checks passing

The tag protection policies are directly relevant here. tag-force-push-blocked checks that tags have force-push protection via GitHub rulesets, which is exactly how the Trivy attacker rewrote 75 version tags.

If someone disabled any of these protections (intentionally or not), the next workflow run would flag it. This doesn’t prevent the attack at the Git level (that’s GitHub’s job), but it continuously verifies the guard rails exist and alerts when they don’t.

Verifying Commit Signatures at Release Time

Our release gate contract enforces the source-commit policy with signature and author verification:

policies:
  attestation:
    - ref: source-commit
      with:
        check_signature: yes
        check_author_verified: yes

At attestation time, Chainloop queries GitHub’s commit API for the signature verification status. Unsigned commit or key mismatch? The policy fails, the gate fails.

Release Gate attestation run showing source-commit and compliance requirement checks passing In-toto attestation statement showing commit signature verification fields

In the Trivy case, the malicious commits were unsigned: verified: false. This check would have caught them and blocked the release.

One caveat: this checks that the committer signed with a key in their GitHub profile, not whether they’re authorized on this repo. A verified user signing from a fork would still pass — the imposter commit gap Chainguard flagged in 2023. But the Trivy attacker impersonated maintainers without their keys, so commit verification catches this specific attack.

Signing Every Artifact

The compromised Trivy binary v0.69.4 was published to GitHub Releases, Docker Hub, GHCR, and ECR. Our artifact-signed policy requires every container image and Helm chart to have a Cosign or Notary signature before it’s included in an attestation. An unsigned or differently-signed malicious binary would fail this check, assuming the release goes through our instrumented pipeline.

The Release Gate

All of these checks feed into a release gate, the contract that decides whether a release can proceed:

- ref: check-compliance-requirement
  with:
    requirement_name: no-vulnerabilities-high,sbom-compliance
  gate: true

gate: true is a hard stop. Compliance requirements not met? Release blocked.

Declarative Configuration — Preventing Drift

All of the above (contracts, policies, requirements, gates) are defined declaratively, as code. They’re not one-time settings someone configures in a UI and forgets about. They live in version control, go through code review, and apply consistently across every workflow run.

This matters because security drift is silent. A branch protection rule gets relaxed for a hotfix and is never restored. A signing requirement gets skipped “just this once.” Declarative configuration means the expected state is always defined, always versioned, and always enforced. If reality drifts from the declaration, the next attestation catches it.

Real-Time Compliance Visibility

Policies and gates catch problems at build and release time. But you also need to know where you stand right now — across all projects, all frameworks, at a glance.

SLSA v1.2 Compliance dashboard showing 90% compliance with requirement status overview

Chainloop’s compliance dashboard gives that view. Every policy evaluation, every requirement, every framework mapping, aggregated and live. Not after an audit. Continuously.

For the Trivy-relevant controls, our dashboard shows branch protection, tag protection, commit signing, and artifact signatures all passing across the Chainloop project, updated with every attestation.

Closing

None of this is perfect. Chainloop can’t prevent credential theft. That’s how this whole chain started. And GitHub still doesn’t expose whether a committer is authorized on a specific repo, so the fork imposter commit gap remains open. But Chainloop can limit the blast radius: even with stolen credentials, you can’t release unsigned code, from an unverified author, through a pipeline that continuously attests its own integrity.

We’re working to close these gaps. Contributor verification — checking not just that a commit is signed, but that the signer is an authorized contributor on that repo — is on our roadmap. So are policies that detect when repository protection settings are weakened or removed, catching configuration drift before it becomes an attack surface.

The Trivy attack didn’t require novel techniques. It worked because the basic guard rails weren’t enforced. Best practices aren’t a checkbox you tick once — they’re continuous verification. That’s what we try to do at Chainloop, and it’s what we’d encourage every team shipping software to consider.

And this is only going to get harder. Every engineering organization we talk to is using AI coding agents — and almost none of them can answer three basic questions. What was the agent configured to do? What was it permitted to do? And what did it actually do? If mutable tags and unsigned commits are a governance gap, ungoverned AI agents are an open door. We’ve been working on something to address this. Stay tuned.

; ---