7 min readJohnny UnarJohnny Unar

Tag Pinning Is Not Supply Chain Security

The TeamPCP compromise of trivy-action rewrote 76 of 77 tags overnight. If your CI pins to @v0.34.2, you already lost.

what actually happened

On March 19, 2026, whoever controls the TeamPCP handle got write access to the aquasecurity/trivy-action repository and did something that most people assumed was impossible in practice, they force-pushed new commits and then moved 76 of the 77 version tags to point at that malicious code. Not one tag. Not the latest tag. Seventy-six of them, going back years. So if your workflow had uses: aquasecurity/trivy-action@v0.34.2 sitting in a file you wrote in 2024 and never touched again, your next pipeline run pulled the rewritten tag, executed a credential-harvesting shim, and phoned home your cloud tokens, SSH keys, and any Kubernetes service account tokens mounted into the runner, all before the actual Trivy scan even started. The scan still ran. Your dashboards stayed green. The exfiltration happened in a pre step that most people never look at because who reads the action.yml of a scanner they've used for three years.

The clever part, and I use clever the way you'd use it for someone who mugged you efficiently, is that the payload didn't break anything. A broken action gets noticed in about four minutes because the red X shows up in the PR. A working action that quietly copies $AWS_SESSION_TOKEN into an HTTP POST before doing its real job gets noticed when someone else is spending your money. TeamPCP understood that the whole value of hitting a security tool is that nobody expects the security tool to be the vector.

why everyone thought they were safe

After the tj-actions/changed-files mess in March 2025, where an attacker modified the action to dump CI secrets into build logs and then retroactively updated the tags, the entire industry converged on one piece of advice that got repeated in every conference talk and every internal Slack security channel for the next twelve months. Pin your tags. Stop using @main, stop using @v4, pin to a specific version like @v4.2.1 so you know exactly what you're running.

That advice was wrong, or more precisely it was a half-measure dressed up as a solution, and the trivy-action compromise is the proof that a lot of very smart DevSecOps teams shipped a control that never controlled anything. A git tag is a mutable pointer. It is a ref, a text file under .git/refs/tags/ that contains a hash, and whoever has push access to the repo can change what that file points at whenever they want. There is no signature requirement, no immutability, nothing at the protocol level stopping a maintainer, or someone who compromised a maintainer, from moving v0.34.2 to point at whatever commit they like. GitHub will happily accept the force-push. Your runner will happily resolve the tag to its current target at execution time, not at the time you wrote the workflow.

So when you wrote @v0.34.2 you did not pin anything. You wrote down a name and trusted that the name would keep meaning the same thing forever, which is exactly the trust assumption that got detonated. The version number in your workflow file is documentation, not enforcement.

the only thing that is actually immutable

A commit SHA is content-addressed. The hash is derived from the contents of the commit, the tree, the parent, the metadata, all of it, so if you pin to aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 you are pinning to a specific object that cannot be swapped out from under you without changing the hash, and if the hash changes then the thing you referenced simply does not resolve anymore. That is a trust boundary. Everything softer than that is a suggestion.

GitHub's own hardening guide has said this for years, pin third-party actions to a full-length commit SHA, and most people read it, nodded, and then pinned to a tag anyway because SHAs are ugly and hard to read and you can't tell at a glance what version you're on. I get it. @a1b2c3d tells you nothing. But the ugliness is the point, a SHA is not human-memorable precisely because it's tied to bytes and not to a human-assigned label that a human can reassign.

Here's the annoying reality that nobody wants to hear. Pinning to a SHA only pins that one action. It doesn't pin the actions that action calls internally, and a huge number of composite actions do exactly that, they invoke other uses: steps in their own action.yml, which resolve at runtime, which means your carefully SHA-pinned top-level action can still pull a mutable tag two layers down and you'd never see it in your own repo. You solved one edge of the graph and left the rest wide open.

auditing the whole actions graph

The mental model that actually protects you treats your CI not as a list of steps but as a dependency graph, the same way you'd think about your npm or Go module tree, because that's what it is. Every action you call can call others, those can call others, and the transitive set of code executing on a runner with access to your secrets is almost always ten times larger than the handful of uses: lines you personally wrote.

So the work is to walk that graph. For every third-party action, pin the SHA, then open its action.yml and check whether it's a composite action that calls further steps, and if it does, either those are also SHA-pinned or you have a hole. Tools like pin-github-action and StepSecurity's Harden-Runner help with the first layer, and Harden-Runner in particular is worth running because it monitors outbound network traffic from the runner and would have flagged trivy-action making an unexpected POST to an unknown host, which is the kind of behavioral detection that catches the thing your static pinning missed. Dependabot can bump SHAs for you and it'll show you the diff of what changed between the old commit and the new one, which is the reproducibility check most people skip, you're supposed to actually read what changed before you merge the bump.

We do this for clients as part of security audits, and the pattern is depressingly consistent, a team that believes they solved supply chain security in 2025 has beautifully tag-pinned workflows and zero visibility into the composite actions three layers deep, plus a pre and post step problem nobody's looked at. The graph is the boundary. Anything you didn't audit inside it is trusted whether you meant to trust it or not.

what to change on monday

Convert every uses: reference that points at a tag or branch to a full commit SHA, and put the human-readable version in a trailing comment so you keep the documentation without the false pin, like uses: aquasecurity/trivy-action@6c175e9c... # v0.34.2. Do it for actions you wrote too, not just third-party ones, because internal repos get compromised the same way external ones do.

Turn on Harden-Runner in audit mode across your critical pipelines and let it baseline the outbound traffic for a week, then flip it to block mode once you know what normal looks like, so the next time an action tries to exfiltrate to a host that isn't in your allowlist the run fails loudly instead of succeeding quietly. Add a CI check that fails any PR introducing a uses: line pinned to anything that isn't a 40-character SHA, because the discipline erodes the moment it depends on a human remembering, and someone will paste @v1 from a README six weeks from now if nothing stops them.

And scope your runner permissions down hard, because SHA pinning limits what code runs but permission scoping limits what that code can steal when your pinning eventually fails, and it will fail somewhere, on some transitive dependency you didn't get to. Short-lived OIDC tokens instead of long-lived AWS keys, permissions: read-all as the default with explicit grants only where needed, no broad Kubernetes tokens mounted into jobs that only need to run a linter. The teams that came out of March 19 fine weren't the ones with perfect pinning, they were the ones whose stolen credentials expired in fifteen minutes and couldn't touch production anyway. Defense in depth is boring and it's the only thing that works when your trust boundary turns out to be a text file someone else can edit.

Johnny Unar

Written by

Johnny Unar

Want to work with us?

The TeamPCP compromise of trivy-action rewrote 76 of 77 tags overnight. If your CI pins to @v0.34.2, you already lost.