
GitHub Actions Workflow Lockfiles Are Coming
On March 19, 2026, somebody used stolen credentials to push a malicious release of Trivy, the open-source vulnerability scanner used by thousands of CI/CD pipelines. The compromised version didn't just fail silently. It actively dumped Runner process memory, swept filesystem paths for AWS credentials, SSH keys, Kubernetes tokens, and cryptocurrency wallets, encrypted everything with AES-256-CBC, and shipped it off to attacker-controlled infrastructure.
The tool designed to find vulnerabilities in your containers became the vulnerability.
This isn't the first time a CI/CD supply chain attack has landed. The tj-actions/changed-files compromise hit 23,000 repositories in March 2025. But the Trivy incident is different in ways that matter. It targeted a security tool specifically, which means the attacker was going after organizations that care about security and have secrets worth stealing.
The attack was a continuation of an earlier supply chain compromise that started in late February 2026. After the initial disclosure on March 1, Aqua Security rotated credentials, but the rotation wasn't atomic. The attacker likely used a still-valid token to exfiltrate newly rotated secrets during the transition window. That gave them access to launch a second, more sophisticated wave on March 19.
The attack hit three components simultaneously:
The Trivy binary itself. The attacker pushed a commit that swapped the actions/checkout reference to an imposter commit containing a composite action that pulled malicious Go source files from a typosquatted domain. They added --skip=validate to goreleaser to bypass binary validation, tagged it as v0.69.4, and let the release pipeline do the rest. The poisoned binary went out through GHCR, ECR Public, Docker Hub, deb/rpm packages, and the get.trivy.dev install script. Exposure window: about 3 hours.
The trivy-action GitHub Action. The attacker force-pushed 76 of 77 version tags to malicious commits that injected an infostealer into entrypoint.sh. The malicious code ran before the legitimate scan, dumped Runner.Worker process memory, swept 50+ filesystem paths for credentials, encrypted everything with AES-256-CBC + RSA-4096, and sent it to attacker infrastructure. If exfiltration failed and a GitHub PAT was available, it created a public repo on the victim's account and uploaded stolen data as a release asset. Exposure window: about 12 hours.
The setup-trivy Action. All 7 existing tags were force-pushed to malicious commits containing the same infostealer, this time as a "Setup environment" step. Exposure window: about 4 hours.
Three days later, on March 22, the attacker came back using separately compromised Docker Hub credentials to publish malicious v0.69.5 and v0.69.6 images directly, bypassing GitHub entirely.
A compromised linter or test runner is bad. A compromised security scanner is worse. Here's why.
Security tools typically run with elevated permissions because they need them. Container scanners need to pull images and inspect layers. SAST tools need read access to your entire codebase. Secret scanners need access to, well, secrets. In a typical GitHub Actions workflow, the security scanning step has access to the GITHUB_TOKEN, any secrets you've injected for registry authentication, and the full checkout of your source code.
There's also a trust problem. Teams that run vulnerability scanners in CI tend to trust those scanners implicitly. Nobody audits the auditor. When Trivy runs successfully and reports zero critical vulnerabilities, the pipeline keeps going. Nobody checks whether Trivy itself did something it shouldn't have, because the whole point of Trivy is to be the thing that checks for bad behavior.
And the targeting is deliberate. An attacker compromising a generic utility action hits a random cross-section of repositories. An attacker compromising a security scanner specifically hits organizations that have secrets worth protecting and infrastructure worth attacking. The victim selection is built into the targeting.
The Trivy compromise shares DNA with the tj-actions/changed-files attack from March 2025, but there are differences worth understanding.
Both attacks used the same core technique: force-pushing version tags to point to malicious commits. Both extracted secrets from the CI runner environment. Both affected thousands of potential targets. CVE-2025-30066 (tj-actions) hit 23,000+ repositories. The Trivy advisory (CVE-2026-33634) doesn't disclose a repository count, but Trivy has over 33,000 GitHub stars and trivy-action is widely used in enterprise CI pipelines.
The differences matter, though. The tj-actions attack was comparatively simple: it extracted secrets from Runner process memory and dumped them into the workflow logs as base64-encoded strings. If your repository was public, those logs were public, and the secrets were exposed. It was crude but effective.
The Trivy attacker was more sophisticated. Instead of dumping to logs, they used encrypted exfiltration to command-and-control infrastructure. They had a fallback mechanism that created a public repository on the victim's GitHub account and uploaded stolen data as release assets. They hit multiple distribution channels simultaneously: GitHub Actions, Docker Hub, GHCR, ECR, package managers. And they came back three days later with separately compromised credentials for a second wave.
The tj-actions incident was an opportunistic smash-and-grab. The Trivy incident looks like a persistent campaign by an attacker who specifically wanted access to the security infrastructure of well-defended organizations.
If you can't trust your security scanner, what can you do? The answer isn't to stop scanning. It's to build pipelines where no single tool compromise gives an attacker the keys to everything.
This is the single most important thing you can do, and both advisories recommend it. Tags are mutable. An attacker with push access can force-push any tag to any commit. SHAs are immutable.
# Bad: mutable tag
- uses: aquasecurity/trivy-action@0.35.0
# Good: pinned to specific commit SHA
- uses: aquasecurity/trivy-action@a11fee7832a1cc18e33a85ad0a0116b230608b44 # v0.35.0There's a nuance here that the Trivy advisory highlights: if you SHA-pinned trivy-action to a commit before April 2025, you were safe from the trivy-action tag hijack, but your safe trivy-action would still pull a malicious setup-trivy during its exposure window, because the action itself didn't pin its own dependencies. SHA pinning only works if it's applied recursively through the dependency chain.
One detail buried in the Trivy advisory: trivy-action@0.35.0 and trivy v0.69.3 were both protected by GitHub's immutable releases feature, which Aqua Security had enabled in early March, before those versions were published. The attacker could force-push older tags, but couldn't touch anything released after immutable releases were turned on. If you maintain a GitHub Action, enable this now.
Trivy publishes sigstore signatures for its releases. The advisory includes verification commands that let you confirm whether a binary was signed by the legitimate release pipeline and check the signing timestamp. A binary signed on March 1 (before the March 19 attack) is safe. Here's what verification looks like:
# Verify signature against Aqua Security's identity
cosign verify-blob \
--certificate-identity-regexp 'https://github\.com/aquasecurity/' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
--bundle trivy_0.69.2_Linux-64bit.tar.gz.sigstore.json \
trivy_0.69.2_Linux-64bit.tar.gz
# Check when it was signed
date -u -d @$(jq -r '.verificationMaterial.tlogEntries[].integratedTime' \
trivy_0.69.2_Linux-64bit.tar.gz.sigstore.json)
# Sat Mar 1 19:11:02 UTC 2026 — before the attackIf you're pulling scanner binaries in CI, add a verification step before running them. It adds a few seconds to your pipeline and catches exactly this kind of attack.
The March 22 follow-up attack pushed malicious images directly to Docker Hub. If your pipeline pulls aquasec/trivy:latest or even aquasec/trivy:0.69.4, you're trusting that whoever controls the Docker Hub repository hasn't replaced the image behind that tag. Pin by digest instead:
# Bad: mutable tag
image: aquasec/trivy:0.69.3
# Good: immutable digest
image: aquasec/trivy@sha256:abc123... # v0.69.3The Trivy infostealer had a fallback exfiltration path: if the C2 connection failed and a GitHub PAT was available, it created a public repository on the victim's account and uploaded stolen data. That fallback only works if the workflow has a PAT with repo scope available in the environment.
Limit blast radius with minimal permissions:
# Set minimal permissions at the workflow level
permissions:
contents: read
security-events: write # Only if uploading SARIF results
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@a11fee7832a1cc18e33a85ad0a0116b230608b44
with:
scan-type: 'fs'
# Don't pass secrets the scanner doesn't need
# Don't set GITHUB_PAT unless absolutely requiredIf your scanner needs to push results to GitHub Security, it needs security-events: write. It doesn't need contents: write, packages: write, or any of the other permissions that would let a compromised tool make persistent changes to your repository or registry.
The Trivy infostealer searched for AWS credentials, GCP service account keys, and Azure tokens on the filesystem. Long-lived cloud credentials stored as GitHub secrets are the highest-value target because they work outside the CI environment and don't expire.
GitHub Actions' OIDC token support lets workflows authenticate to AWS, GCP, and Azure using short-lived tokens that are scoped to the specific workflow run. Even if an attacker extracts one, it expires in minutes and can be scoped to specific resources. If your scanning workflow needs cloud access (to pull images from ECR, for example), use OIDC federation instead of storing access keys in secrets.
The Trivy infostealer worked because it ran in the same process context as the rest of the CI pipeline. It could read /proc/<pid>/mem from the Runner.Worker process and sweep common credential paths on the filesystem.
If you run your scanner as a separate job rather than a step in the same job, it gets its own runner instance with its own isolated filesystem. It won't have access to secrets injected into other jobs unless you explicitly pass them.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
# Build secrets stay in this job's runner
security-scan:
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@a11fee7832a1cc18e33a85ad0a0116b230608b44
with:
scan-type: 'fs'
# Separate runner, no access to build job secretsThis doesn't prevent a compromised scanner from stealing what's available in its own job context, but it limits what's there to steal.
The tj-actions compromise was detected by StepSecurity when their Harden-Runner tool flagged an unauthorized outbound network call to gist.githubusercontent.com. Network monitoring in CI won't prevent an attack, but it can dramatically shorten the detection window. A scanner that suddenly starts making connections to domains it's never contacted before is a strong signal.
If you use Trivy in any CI pipeline, check these things now:
latest during March 19-22, assume compromise.trivy-action by any tag other than 0.35.0, update immediately. Safe versions: trivy-action@0.35.0, setup-trivy@v0.2.6.tpcp-docs. If one exists, the fallback exfiltration mechanism was triggered.The full advisory is at GHSA-69fq-xp46-6x23, including cosign verification commands for both binaries and container images.
The pattern here isn't hard to see. An attacker with compromised maintainer credentials can weaponize any part of the CI/CD pipeline: test runners, build tools, linters, formatters, deployment scripts. Security scanners just happen to be the most rewarding target because the organizations using them have the most to steal.
The fix isn't to stop using Trivy or any other scanner. Aqua Security's response and transparency through the disclosure were solid, and the tooling around sigstore verification actually worked as designed for users who had it in place. The fix is to stop building pipelines that assume any single component is trustworthy. Pin by SHA. Verify signatures. Isolate jobs. Scope permissions. Use short-lived credentials. None of these alone would have fully prevented the Trivy compromise, but together they reduce the blast radius from "attacker gets everything" to "attacker gets very little."
We're now two major GitHub Actions supply chain attacks in two years, both exploiting the same fundamental weakness: mutable version tags. GitHub added immutable releases. Sigstore provides cryptographic verification. The tools exist. The question is whether your pipeline uses them before the next attack, not after.
Recommended for you
What's next in your stack.
GET TENKI