all posts
2025-04-08·4 min read·by Darko Gjorgjijoski·in Security

Owning your dependency supply chain

Auditing a dependency tells you about the version you read. It says nothing about the one being published tonight.

I wrote about the supply chain attack on the WordPress.org plugin repository when it was happening. Maintainer accounts were compromised, malicious updates were published under legitimate plugin names, and every site configured to update automatically pulled them. The interval between a bad release existing and a bad release running on your server was measured in hours.

The advice that follows an incident like that is usually to audit your dependencies. I want to be blunt about why that advice is close to useless in this specific shape of attack, and what actually changes the outcome.

Auditing does not operate at the speed of the problem

Auditing a dependency is a point-in-time statement about a version you have looked at. It says nothing about the version that will be published tonight.

The attack does not rely on you having failed to review the package. It relies on the package being fine when you reviewed it and not fine afterwards, combined with a pipeline that fetches whatever is newest without asking. You cannot review your way out of a race you are not present for.

So the useful question is not “is this dependency trustworthy”. It is “what stands between a new upstream release and my running system”, and for a great many setups the honest answer is nothing at all.

What a private registry actually changes

Running your own package registry, with upstream proxied through it, changes one thing: the moment of entry becomes yours to control. Nothing enters your estate because it was published. It enters because it was promoted.

That gives you three things worth having.

  • A gate. A new upstream version exists in the proxy but is not available to builds until something promotes it. That something can be a person, a delay, a scanner, or a policy. Any of them is better than none.
  • Immutability of what you already have. A version you have consumed is stored by you. An upstream package that is deleted, retagged, or republished with different content does not change what your builds resolve.
  • A single place to answer “who is using this”. When the next advisory lands, the question is which of your projects pulled the affected version. A registry you own can answer that. A public one cannot.

What it does not change, which matters more

A private registry does not detect a malicious release. It has no opinion about the contents of a package. If you promote a compromised version, you have distributed a compromised version efficiently to everything you own.

It also does not remove your dependence on upstream maintainers, reduce the number of transitive packages you rely on, or tell you which of your dependencies is one unpaid volunteer away from an incident.

And it adds a component you now own. If it is down, your builds fail. If its storage is lost and you have not kept the artefacts, you have replaced a public single point of failure with a private one that has a smaller operations team, which is you.

So the registry is not a control by itself. It is the place where a control can exist. The control is the promotion policy, and a registry with an auto-promote-everything policy has bought you a mirror and an availability risk, nothing more.

A policy that is actually sustainable

The failure mode of a strict policy is that someone disables it under deadline pressure. So the version I would defend is deliberately modest.

Pin exact versions and commit the lock file. Free, and it is the single largest improvement available. A build that resolves to exactly what it resolved to yesterday is immune to tonight’s release regardless of anything else you do.

Impose a quarantine window rather than a review. Nobody sustains manual review of every dependency bump. A rule that a version must have existed publicly for some number of days before promotion needs no human attention and catches the case that matters, because a malicious release is usually discovered fast and yanked. Time is doing the work that review would not have done.

Turn off automatic updates in production for anything that executes code. This is the one that specifically addresses the incident I started with. Convenience was the vulnerability.

Separate promotion from consumption. Developers pull from the registry. Something else, with different credentials, decides what the registry offers. If the same identity can do both, the boundary is decorative.

Where the effort is worth it

I built a self-hosted Composer registry, so my bias is obvious. It is worth being clear that the case for it is not primarily security.

The case is usually private packages, build reliability when upstream has a bad day, and knowing what you depend on. The supply chain benefit is real but conditional, and it arrives only if you use the gate the registry gives you. For a single small project, pinning versions and disabling automatic updates gets you most of the protection for none of the operational cost, and I would start there rather than with infrastructure.

The thing I would not do is treat the registry as the answer and stop. That is how you end up with a well-run pipeline that distributes whatever it is given, which is a more efficient version of the original problem.

← older
Migrating email from one server to another with ImapSync and Docker
newer →
When Letters Lie: Analyzing a Typical IDN Homograph Attack