Your CI Runner Is the Target: Hardening Against npm Worms
The keyv compromise reached 444 packages and over two billion monthly installs through preinstall scripts. The controls that actually stop it are boring and mostly free.
Key takeaways
- The keyv compromise reached 444 packages and over two billion monthly installs through preinstall scripts.
- The controls that actually stop it are boring and mostly free.
On this page
Your CI Runner Is the Target: Hardening Against npm Worms#
On 4 August 2026, an attacker compromised the GitHub account of the maintainer behind keyv, a key-value library pulling roughly 127 million weekly npm downloads. What followed was not a single malicious release. It was a worm: at least 444 packages across 1,381 versions were compromised, with a combined total exceeding two billion monthly installs. The payload ran on install, harvested credentials from developer machines and CI runners, and used what it found to publish itself further.
The important detail for anyone running a pipeline is what it targeted. This class of attack is no longer trying to reach your production servers. It is trying to reach the credentials sitting in your build environment, because those credentials are the mechanism for the next hop.
What the worm actually did#
Three mechanisms did the work, and each one abuses a feature that exists for good reasons.
Preinstall scripts. npm lets a package run arbitrary code during installation. That is how native modules compile. It also means that adding a dependency, or a dependency of a dependency, grants code execution on whatever machine runs the install, before a single line of your application has run.
Trusted provenance. Provenance attestation exists to prove a package was built from the source repository it claims. Because the attacker held the maintainer's account and the legitimate publishing pipeline, the malicious versions carried valid provenance. The signal that was supposed to establish trust was produced correctly for a malicious artifact.
IDE hooks. The payload installed itself into editor configuration so it survived the obvious remediation of reinstalling dependencies. Cleaning node_modules did not clean the machine.
This is a sharp escalation from the era when supply chain attacks were typo-squatted packages waiting for someone to fumble a name. The pattern has been building for a year, through the Shai-Hulud worm in late 2025 and the Axios maintainer account compromise in July 2026, and the direction is consistent: compromise a maintainer, inherit their trust, self-propagate through whatever credentials the payload finds. We covered the broader category in software supply chain attacks.
Why provenance did not save anyone#
It is worth being precise, because "use provenance" is common advice and this incident shows its boundary. Provenance answers the question "was this artifact built from that repository by that pipeline". It does not answer "was the person driving that pipeline supposed to be". When an attacker controls the maintainer account, every cryptographic check downstream returns exactly the answer it should, for an artifact you do not want.
Attestation still belongs in your pipeline, because it closes off a different and real attack path, the tampered artifact in transit or in a registry mirror. Our notes on SBOM and attestation cover where it does pay. It is not a control against a compromised publisher, and treating it as one leaves the actual gap open.
The controls that actually work#
Four changes, in rough order of how much they buy you per minute spent.
Stop running install scripts in CI. Most builds do not need them. This one line removes the entire preinstall execution path:
# In CI. Fails loudly if something genuinely needs a build step,
# which is the point: you then allowlist that one package deliberately.
$ npm ci --ignore-scripts
If a dependency truly requires a native build, run that package's scripts explicitly rather than granting execution to the whole tree.
Add a cooldown on new versions. Nearly every one of these compromises is detected within hours to days. A package manager configured to refuse versions published in the last few days converts most of these incidents into a non-event for you:
# .npmrc
minimum-release-age=4320 # minutes, roughly three days
Kill long-lived tokens. The worm's propagation depended on finding publishing credentials in environments it reached. A static NPM_TOKEN in CI secrets is exactly that. Short-lived credentials minted per run through OIDC cannot be exfiltrated usefully, because they expire before the attacker can use them and they are scoped to one job. This is the same change we recommend for cloud credentials in OIDC federation beyond GitHub and GitLab, and the reasoning transfers directly.
Separate build from publish. A job that compiles untrusted third-party code should not hold the credentials that publish your packages or deploy your infrastructure. Split them into two jobs with different permissions, so that code execution during npm install lands somewhere that holds nothing worth stealing.
Egress filtering on runners is the fifth control and the one most teams skip. A build that only needs the registry and your VCS has no business reaching an arbitrary host, and a default-deny egress policy turns credential theft into a failed connection you can alert on.
The decision, concretely#
- Running
npm installornpm ciin CI without--ignore-scripts? Change that today. It is one flag, it takes an afternoon to shake out the exceptions, and it closes the path every one of these worms has used. - Holding a static publish token in CI secrets? Replace it with OIDC. Static tokens are the reason a dependency compromise becomes a company compromise.
- Pinning with a lockfile and calling it done? Lockfiles pin versions, which does not help when the attack ships as a new version you then resolve. Add the release-age cooldown, which is the control that actually addresses this shape.
- Relying on provenance or an SBOM as your defence against malicious packages? Keep them, but move them out of that column. They verify build integrity, not publisher intent.
The call we'd make#
Assume your CI runner will execute hostile code at some point, and design for what it can reach when it does. That framing gets you to the right controls quickly: no install scripts, no long-lived credentials, no publish rights in a build job, no unrestricted egress. None of it requires a vendor, none of it is expensive, and all of it would have reduced the keyv worm from an incident to a log line. The scanning and alerting layer is worth having on top, but it is a detection control, and detection alone arrives after the credential has already left.
Get the DevOps Troubleshooting Cheat Sheet
Subscribe and get our free one-page reference for the errors that eat an afternoon — CrashLoopBackOff, OOMKilled, Terraform state locks, and more — plus new guides as we publish them.
Three LLM Providers, One Cloud Region: The September 3 Outage
ChatGPT, Claude, and Grok degraded together when Azure East US failed. Gemini stayed up. Multi-provider failover does not help when your providers share a substrate.
Best Log Management Tools in 2026: What You Actually Pay For
Every log platform looks affordable at proof-of-concept volume and expensive at production volume. The pricing model, not the feature list, decides which one you can live with.
More from DevOps
Explore more articles in this category
Best Managed Kubernetes in 2026: EKS vs GKE vs AKS vs DOKS
The control plane fee is the least interesting number. What separates managed Kubernetes providers is upgrade cadence, how much they run for you, and where the node bill lands.
Best Log Management Tools in 2026: What You Actually Pay For
Every log platform looks affordable at proof-of-concept volume and expensive at production volume. The pricing model, not the feature list, decides which one you can live with.
Kubernetes 1.37 Garhwal: What Actually Changes for You
Sixty-seven enhancements shipped on August 26, but only a handful change how you run clusters. Scale-to-zero autoscaling, rootless kubelet, and a GA metrics API are the ones to read.
You might have missed
Evergreen posts worth revisiting.