Do the Boring Thing: SME Defence Against Supply-Chain Attacks

Photo by Heather Shevlin

There is a moment in every engineering organisation where something boring becomes existential.
A dependency update, someone runs npm install or docker build . or the CI pipeline quietly pulls the latest version of a package that has been used safely for years.

The build passes, tests pass, deployment is green, everyone moves on.
Except now, somewhere inside the supply chain, a package that used to be trustworthy has become hostile. It is sitting inside your build process with access to environment variables, CI secrets, GitHub/GitLab tokens, cloud credentials, package registry tokens, SSH keys, Docker credentials, and deployment permissions.

That is the terrifying part of modern software supply-chain attacks, They do not need to hack your production env: They wait upstream.
And when your automation does exactly what you trained it to do, it brings the bomb inside.

How do they work?

The pattern is now painfully familiar, an attacker (with country ties or whatever) compromises a maintainer account, they don't need to compromise registries, one trusted maintainer (who might not even be active anymore) would do.

Then they publish a new version, a real package that would probably pass all the unit and integration tests, and would work just fine, but containing a time amount of payload code that can access the goldmine of your environment variables, both in your deployment environment and on developer machines.

Then use trusted platforms such as Github as their means of transferring data out of your systems, Shai-Hulud showed why this is so ugly: malicious package code can execute before tests or security checks, harvest credentials, and exfiltrate them through infrastructure that often looks like normal developer activity.

The attack surface

This is not a NPM problem, the underlying issue exists across almost every software distribution channel, the same class of attacks can as easily hit PyPI, RubyGems, Maven, crates, Goi modules, Homebrew, IDE Extensions, helm charts, Docker / OCI registries, and many more.

The core of the issue is you are pulling executable trust from outside your organisation.

The CI runner is often more dangerous than production. It has access to source code, signing keys, deployment tokens, package publish tokens, cloud credentials, and enough automation privileges to turn a package compromise into an organisation compromise.

Docker is especially interesting because many teams treat container images as cleaner and safer than packages. Sometimes they are. Often they are just a larger box with more surprises inside!

What SMEs should actually do

Large enterprises respond to this with AppSec teams, artifact signing, policy engines, SBOM pipelines, provenance attestations, internal golden images and enough acronyms to induce mild brain damage

Good for them, but, SMEs need something simpler, the goal is to stop pulling neatly packaged vulnerabilities from the internet into your developer/build environments.

For most SMEs, the highest-leverage defence is not an exotic security platform. It is a boring control: do not allow brand-new package versions into production build paths by default.

The practical SME strategy should be this:

Do not consume packages directly from public registries in production paths. Put a controlled layer in the middle. Delay new versions. Pin what you use. Reduce what your builds/devs can directly access.

Use an internal package proxy

Depending on your environment you have a few options, something like Verdaccio, Github packages and pull-through cache for containers should do, there is no shortage of reliable opensource project that can do this.

Your best bet against these attacks is Package ageing, you'll prevent loads of pain by not allowing a newly release package into your environment immediately. Seven days will not stop every attack. It will stop a lot of stupid, fast-moving, high-blast-radius pain.

A package version must be at least 7 days old before normal use

This sounds a bit crude, mostly because it really is, but it's also effective, I would even recommend stronger controls for production dependencies, even up to 30 days to be absolutely sure.

Keep an emergency lane

Age-gating should not block urgent security patches, so you need two lanes:

Normal lane:
Package must be old enough.

Emergency lane:
Package can bypass ageing after explicit approval.


The emergency bypass should require proper 4-eye process, diff reviewed, package hash pinned and an expiry date for the exception.

If I were designing this for a serious SME, I would start with:

1. All production builds use an internal registry/proxy.
2. New package versions are quarantined for 7 days by default.
3. Runtime production dependencies require 14 days.
4. Build/deploy tooling requires 14 to 30 days.
5. Emergency bypass exists, but requires approval and expires.
6. Versions are pinned.
7. Lockfiles are committed.
8. Docker images are pinned by digest.
9. npm install scripts are disabled by default where practical.
10. CI jobs use least-privilege, short-lived credentials.
11. Package install, build, test, publish, and deploy are separated.
12. Scanners run continuously, but they are not the only defence.

Do the boring thing. The internet is a hostile dependency resolver, If your product depends on software you pulled blindly from the public internet five minutes ago, your trust model is mostly vibes.