SLSA Was On, and It Still Shipped Malware: Enforcing Provenance as a Hard Gate in Your IaC Pipeline

Part 3 of a 4-part series on the software supply chain and your CDE.
- Translating CI/CD Identity into Terraform - kill the standing credentials.
- Zero-CVE Worms in Your CDE Pipeline - why your CVE program cannot see the worm.
- SLSA Was On, and It Still Shipped Malware (this piece) - the provenance control, done honestly.
- Your SBOM Is a Compliance Artifact. Make It a Build Gate - mapping it all to PCI 4.0.1. (Coming soon.)
Every guide to securing your software supply chain ends on the same advice: turn on provenance. Generate signed attestations, adopt SLSA, prove where your artifacts were built. It is good advice, and you should take it. It is also not enough, and in June 2026 two npm incidents proved exactly how it fails, in two opposite directions. Read them together and you get the honest version of the provenance story, which is the version your engineers need before they trust a green check in CI.
Start with the headline that should unsettle anyone who has been told provenance is the answer: Red Hat's npm packages shipped that month with valid, verifiable SLSA provenance attestations. They were also a credential-stealing worm.
Two failures, pointing opposite ways
Mastra: the attestation was missing, and nobody was checking. On June 17, 2026, a dormant maintainer credential republished the entire @mastra scope. The ehindero account had published 15 clean alpha versions of @mastra/core between late 2024 and early 2025, then sat idle for 16 months. It came back to life from a different email address and, between 01:12 and 02:36 UTC, republished 143 @mastra/* packages plus mastra and create-mastra, each with one added dependency on a typosquat called easy-day-js. npm does not expire scope permissions on inactivity, so one stale credential reached the whole scope.
Here is the part that matters for this discussion. Legitimate Mastra releases were built through npm's trusted-publisher flow and carried SLSA provenance. The malicious versions carried none. In the registry the difference was visible in one field: provenance: yes on the real releases, provenance: no on the poisoned ones. Safedep's analysis states the consequence in one sentence: "A signature-verifying install (npm audit signatures, or a policy that requires attestations) would have rejected every package in this wave." Note the "or a policy that requires attestations" clause, because it is doing the real work: the poisoned versions still carried valid npm registry signatures, so catching them meant enforcing a rule that the trusted-publisher attestation had to be present. The defense existed, in the ecosystem, unused at the point of consumption. That is an enforcement gap, not a technology gap.
Red Hat / Miasma: the attestation was present, valid, and meaningless. A day earlier, on June 1, an attacker compromised a Red Hat employee's GitHub account and pushed malicious orphan commits to two RedHatInsights repositories, bypassing code review. The commits added a minimal workflow (ci.yaml) and an obfuscated payload (_index.js). The workflow requested id-token: write, obtained a short-lived OIDC token from GitHub, and authenticated directly to npm's trusted-publishing endpoint. The result was 96 malicious versions across 32 packages, roughly 117,000 downloads, a preinstall worm that harvested GitHub Actions secrets, AWS keys, GCP service accounts, Azure tokens, npm and PyPI tokens, SSH keys, kubeconfig files, and Vault tokens.
And every one of those malicious versions carried legitimate SLSA provenance. The trusted-publishing pipeline did its job: it truthfully attested that these artifacts were built by the RedHatInsights CI, from that repository, at those commits. All true. All signed. All malware. Wiz notes this mirrors TeamPCP's earlier attack on Tanstack, which "also included the production of valid SLSA provenance attestations." This is not a bug in SLSA. It is SLSA working precisely as designed, certifying the output of a build pipeline whose identity the attacker now controlled.
What provenance actually attests, and what it does not
Put the two incidents side by side and the boundary of the control becomes obvious.
Provenance answers exactly one question: was this artifact built by the pipeline I expect, from the source I expect? A SLSA attestation is a signed statement binding an artifact to a builder, a source repository, and a commit. The higher SLSA levels harden that binding against forgery and tampering. That is genuinely valuable, and it is the entire scope of the guarantee.
Provenance does not answer, and was never designed to answer, the two questions the June attacks turned on:
- Is the source benign? Provenance certifies where code came from, not whether that code is safe. Malicious commits pushed to a real repository, then built by the real CI, produce provenance that is valid and damning at the same time. That is the Red Hat case.
- Is the build identity itself still trustworthy? Provenance is only as meaningful as the pipeline it points to. The moment an attacker can push commits and trigger the publish workflow, the attestation certifies their work with your name on it.
So the honest framing, and the one Unit 42 reached in its monitoring guidance, is this: "Provenance verification is necessary but no longer sufficient." Necessary, because the common case is the Mastra shape, an unsigned artifact that any enforcing consumer would have rejected. Insufficient, because the sophisticated case is the Red Hat shape, a signed artifact whose signature is worthless because the pipeline was owned. A serious program does two things: it enforces provenance so the Mastra class dies at install, and it hardens the pipeline identity so the Red Hat class has nothing valid to forge.
Layer one: enforce provenance, so the common attack dies at install
Most supply-chain compromises are the Mastra shape. Enforcement is therefore the highest-leverage thing most teams are not doing, because generating attestations gets celebrated and verifying them gets skipped.
The mechanism has to be a build step that fails closed, and here precision matters, because the obvious command does less than its name suggests. npm audit signatures verifies the npm registry signature on every package it fetched and validates any provenance attestations that are present. It reliably catches a tampered or invalidly-signed artifact. What it does not do, by default, is fail simply because provenance is absent: an unsigned-for-provenance package that still has a valid registry signature passes. That is exactly the Mastra shape, so the raw command alone would not have stopped it. You have to turn "has an attestation" into a requirement:
# In the CI job that builds anything feeding your CDE.
# Files are fetched first, then verified BEFORE anything is built, published, or run.
- run: npm ci --ignore-scripts # fetch with no lifecycle-hook execution (see part 2)
- run: npm audit signatures --json > sig.json
- run: node ci/require-provenance.js sig.json
# exits 1 when ANY package's attestation is missing, not just when a signature is invalid.That closes the window because --ignore-scripts means the fetched payload sits inert in node_modules; nothing has executed, been built, or been published yet when the gate runs. But a per-developer CI step is still discipline that gets forgotten. For a real organizational gate, move enforcement somewhere a developer cannot skip: a private registry or pull-through proxy (Artifactory, GitHub Packages, Verdaccio) configured to reject packages that fail an attestation policy, or a Sigstore policy-controller admission gate that refuses to admit artifacts lacking a required attestation into the environments that touch the CDE. The rule you want is simple to state and hard to bypass: no artifact enters the CDE build without a verified, present attestation.
Two ecosystem changes make this materially easier in the second half of 2026, and both are worth adopting on their own:
- npm v12 stops running
preinstall,install, andpostinstallscripts from dependencies unless they are explicitly approved, and stops auto-resolving Git and remote-URL dependencies without approval. That neutralizes the install-time execution vector both worms relied on. Upgrade to npm 11.16.0 or newer now, so you get warnings for what will break before v12 lands. - A publication cooldown. Unit 42 recommends a registry proxy policy that blocks any version published within the last 24 to 72 hours. A worm's entire advantage is speed; the Mastra scope was poisoned in 84 minutes. A three-day quarantine turns "we pulled it before anyone noticed" into "it was yanked before we were allowed to pull it."
Layer two: harden the identity provenance points at
Enforcement handles Mastra. It does nothing for Red Hat, where the attestation was valid. For that class, your defense is the integrity of the publish identity itself, because in a trusted-publishing world the pipeline is your trust root. Everything provenance asserts reduces to "this came from that workflow, in that repo." If an attacker can quietly become that workflow, the whole chain certifies their malware.
The Red Hat compromise walked through three gaps, and each maps to a concrete control:
- A compromised account could push commits that reached the publish path. Require phishing-resistant MFA on every account with write or publish rights, and enforce branch protection that orphan commits cannot bypass. The malicious
ci.yamlreached a branch that could trigger publishing; it should not have been able to. - The trusted-publisher trust policy was too broad. npm's trusted publishing lets you bind publishing to a specific repository, a specific workflow filename, and optionally a specific deployment environment. Scope it to one protected workflow, and a rogue workflow added by an attacker, under any other filename, cannot mint a valid publishing token at all.
- The publish path had no human gate. Put the publish job behind a protected GitHub Environment with required reviewers, so minting the OIDC token demands an approval that a background orphan-commit push cannot satisfy:
# .github/workflows/publish.yml - the ONLY workflow the npm trust policy allows
jobs:
publish:
environment: release # protected env: required reviewers, gated
permissions:
id-token: write # OIDC token is only issued after approval
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: 22, registry-url: 'https://registry.npmjs.org' }
- run: npm ci --ignore-scripts
- run: npm audit signatures --json > sig.json
- run: node ci/require-provenance.js sig.json # same fail-closed gate as layer one: reject ANY missing attestation
- run: npm publish --provenance # generates provenance that a *human* approvedAnd the control that limits the damage when all of this still fails: the build identity must not be able to reach the CDE. That is the subject of part one of this series, and it is the reason a compromised build pipeline should be an incident, not a breach. Provenance tells you an artifact's origin. It does not contain the blast radius. Scoped, short-lived, CDE-segregated build credentials do.
The same gap, one layer down: your Terraform providers are signed, your modules are not
The two npm incidents are a story about an application build pipeline, but the exact enforcement-versus-identity split runs straight through the infrastructure-as-code that stands up your CDE, and most teams have never looked at it. If you provision cardholder-data infrastructure with Terraform, you are consuming two very different supply chains through one init command, and only one of them is protected.
Providers are the protected half. Every provider release published to the Terraform Registry must be GPG-signed: the release ships a SHA256SUMS file and a SHA256SUMS.sig binary signature over it, and the registry holds the publisher's public key. When terraform init pulls a provider, it verifies that signature automatically, with no flag and no user action, then records the provider version and a set of h1: and zh: SHA-256 checksums in your .terraform.lock.hcl. On every subsequent init, Terraform rejects any provider package whose checksum does not match the locked value. That is provenance enforcement done correctly, on by default. The hashicorp/aws provider you pinned cannot be silently swapped underneath you.
Modules are the unprotected half, and it is not close. Terraform modules have no signing mechanism at all. The lock file, in HashiCorp's own words, "tracks only provider dependencies. Terraform does not remember version selections for remote modules." A module reference therefore has nothing signing it and nothing pinning it. Worse, when you source a module from the public registry, the registry redirects Terraform to fetch a Git tag from a source repository, typically on GitHub, and a Git tag is mutable: anyone who can force-push to that repository can replace the contents of v1.4.0 with entirely different code under the same tag, and the next terraform init in every consuming pipeline pulls the swapped version with no verification failure to stop it. That is the Mastra shape again, an unsigned artifact nothing at the point of consumption is checking, except here it is provisioning your infrastructure rather than running inside your app.
The fix mirrors layer one: turn the source reference into something immutable, then enforce it. For a module, immutability means pinning to a full commit SHA rather than a tag or a version constraint, because a commit hash is content-addressed and cannot be moved the way a tag can:
# Mutable: the `v1.4.0` tag can be force-pushed to point at attacker code.
module "cde_network" {
source = "git::https://github.com/acme/tf-modules.git//network?ref=v1.4.0"
}
# Immutable: a full commit SHA cannot be repointed.
module "cde_network" {
source = "git::https://github.com/acme/tf-modules.git//network?ref=9f2b1c4e8a1d3b7c6f05a2e9d84c1b0f7a6e5d21"
}Be clear-eyed about the tradeoff: pinning to a commit SHA means sourcing the module by a raw Git URL, which gives up the native Terraform Registry syntax (source = "namespace/name/provider" with a version constraint) and its semantic-version resolution. That is a genuine ergonomic cost, and for anything that provisions a CDE it is the correct trade.
For a real organizational gate, do not let CDE infrastructure consume public modules directly at all. Vendor them into a private registry or an internal mirror you control, review changes on the way in, and pin every consumer to an immutable reference. The rule from layer one holds one level down: no artifact enters the CDE build, including the code that builds the environment itself, without a verified, present, immutable reference. The provider half of Terraform already works this way. The module half is waiting for you to make it.
What this means for your PCI program
Skip the reflex to file this under a requirement number and instead ask what the standard is trying to accomplish, because the mapping only helps if it is honest.
The intent behind Requirement 6.3.2 is that you know what is in your software and can act when a component goes bad. An inventory delivers the "know what" half. Provenance enforcement delivers a half the inventory cannot: assurance that the component you resolved is the one your pipeline actually built, not a same-named artifact an attacker republished overnight. Read together, inventory plus enforced attestation is what 6.3.2 is reaching for.
The intent behind the identity requirements in Requirement 8 is that access to in-scope systems is authenticated and attributable. Be careful with the mapping here: PCI does not govern your npm registry token, which is a credential to a third-party service. What it governs is the GitHub repository and CI runner that can publish into your CDE build, which are connected-to systems that impact the security of the CDE, and therefore the accounts that administer them. The GitHub account that can push to the publish branch or trigger the release workflow is exactly such an account, and the Red Hat case is what happens when it is protected like a developer convenience rather than a system that reaches the CDE. Require phishing-resistant MFA on it, and scope and gate the publish path it drives.
One mapping to resist: do not reach for Requirement 6.4.3 here. That control governs payment-page scripts executing in the consumer's browser, an anti-e-skimming concern, and citing it for a build-pipeline compromise is a category error an assessor will catch. Part two covers why, and the final piece in this series covers the one legitimate path from a poisoned dependency to a 6.4.3 obligation.
What a QSA who understands this should now ask to see: the CI step that fails the build on an unverified attestation, the registry or admission policy that enforces it where a developer cannot skip it, the trust policy that binds publishing to one protected workflow, and the review gate on the publish environment. Those are artifacts, checked into a repository, that produce their own evidence.
The takeaway
"Turn on provenance" is where the supply-chain conversation usually ends. It should be where it starts. Generating attestations without enforcing them is theater, and enforcing them without hardening the pipeline they trust is a false floor. The June 2026 attacks were not a failure of SLSA; they were a failure to treat provenance as one layer in a system whose trust root is the build identity itself. Enforce the attestation so the common attack dies at install, harden and gate the publish identity so a valid attestation cannot be forged in your name, and keep the build plane out of the CDE so that when something does slip through, it is contained.
If you want that wired into your pipeline as fail-closed gates and scoped identities rather than a policy document nobody runs, that is the kind of work we do.
Sources
- Wiz - Miasma Supply Chain Attack Targeting Red Hat npm Packages
- Safedep - Mastra npm Scope Takeover Supply Chain Attack
- Aikido - Red Hat npm Packages Compromised by Credential-Stealing Worm
- BleepingComputer - GitHub Announces npm Security Changes to Tackle Supply Chain Attacks
- Unit 42 (Palo Alto Networks) - Monitoring npm Supply Chain Attacks
- npm Docs - Trusted Publishing for npm Packages
- HashiCorp Developer - Dependency Lock File (.terraform.lock.hcl)
- HashiCorp Developer - Publish Providers to the Terraform Registry (release signing / SHA256SUMS)
- GitLab - Terraform in the Software Supply Chain: Modules and Providers
- PCI DSS v4.0.1 - Requirements 6.3.2, 6.4.3, and 8 (requirement intent)
Christopher Callas
Christopher is the Principal at Arbure Inc., leading strategic and technical initiatives that shape the firm's cybersecurity consulting services. With over a decade of experience, he has built a reputation for delivering tailored security solutions that align with business objectives while addressing modern threats. His expertise spans cloud security, compliance, and risk management, guiding organizations through complex regulatory landscapes and securing multi-cloud environments.