Supply-chain safety for small teams
A practical guide to software supply-chain safety for small teams: lockfiles, pinning, fewer dependencies, install scripts, typosquatting, CI secrets, SBOMs and signed releases.
Software supply-chain safety is about the code you did not write. A modern app pulls in dozens or hundreds of packages, most of them chosen by other packages, and any one of them can run code on your laptop, in your CI and in front of your users. Big companies have teams for this. Small teams have a few habits, and in our experience the habits cover most of the risk.
This is the list we use on our own apps and on client projects. None of it is exotic. The hard part is doing it every time, including on the Friday afternoon when a dependency update is blocking a release.
How attacks actually arrive
It helps to be specific about the threat, because it shapes which defences matter. Most supply-chain incidents we read about fall into a few patterns.
A trusted package ships a bad release. A maintainer's account is phished, a token leaks from their CI, or a new co-maintainer turns out to be hostile. The next version contains something extra, often in an install script so it runs before anyone looks at the code. Everyone whose version range allows the new release picks it up automatically.
A lookalike name. Typosquatting registers a package one letter away from a popular one and waits for someone to mistype it. A related trick, dependency confusion, publishes a public package with the same name as a company's internal one, betting that a build tool will prefer the public registry.
The build pipeline itself. A third-party CI action or plugin is updated, and because it was referenced by a moving tag, the new version runs with access to your secrets.
In the diagram above, the bad release sits three hops away from the app. Nobody on the team chose it. It arrived because a URL parser used it, and an HTTP client used the parser. That is the normal case, and it is why reviewing only your direct dependencies is not enough.
Lockfiles and pinning
A lockfile records the exact version of every package in the tree, direct and transitive, usually with a hash of its contents. It is the single most useful supply-chain control, and it is free. Commit it. Review changes to it. Make CI fail if it is out of date.
Every ecosystem has a command that installs strictly from the lockfile and refuses to quietly update it. Use those in CI and in release builds:
npm ci # never rewrites package-lock.json
pnpm install --frozen-lockfile
cargo build --locked
pip install --require-hashes -r requirements.txt
go mod verify # checks downloaded modules against go.sum
Pinning means your version ranges stop moving on their own. With a lockfile you already get reproducible installs, but a loose range in the manifest still invites a surprise the next time someone regenerates the lock. We keep ranges tight for anything that touches the network, crypto, parsing or the build.
Pin CI actions and plugins too, and pin them to a full commit hash rather than a tag. A tag like v4 can be moved to point at new code. A commit hash cannot.
Fewer, better dependencies
The cheapest dependency to secure is the one you did not add. Before adding a package, we ask three plain questions. Could we write this in under a hundred lines? How many transitive dependencies does it bring? Who maintains it, and how many people would notice if a release went wrong?
Small utility packages are the classic trap. A package that pads a string or checks whether a value is a number costs almost nothing to write yourself, but as a dependency it is a whole new publisher you now trust with install-time code execution. Standard libraries have also grown a lot. Things that needed a package ten years ago, such as fetching over HTTP or parsing a URL, are often built in now.
For the dependencies that stay, prefer ones that are widely used, actively maintained by more than one person, and published from CI with provenance rather than from someone's laptop. None of these guarantee anything. They make a bad release more likely to be noticed quickly.
Updating without panic
Never updating is its own risk, because old versions collect known vulnerabilities. The goal is to update deliberately rather than automatically.
- Batch updates. Take dependency updates on a regular schedule, in their own pull request, not mixed into feature work.
- Wait a few days. Most malicious releases are found and pulled within days. Tools like Renovate and Dependabot can hold off on brand-new versions for a set period, and that delay alone avoids a lot of trouble.
- Read the diff for small packages. For a small dependency, looking at what changed between versions takes minutes. New network calls, new install scripts or obfuscated code are all reasons to stop.
- Check advisories. Run
npm audit,cargo audit,pip-auditor an equivalent in CI, and treat a high-severity finding in production code as a bug.
Install scripts deserve special attention. In npm, a package can run arbitrary commands during installation, before your code ever imports it. pnpm now declines to run dependency build scripts unless you allow them by name, which is a sensible default. With npm you can install with --ignore-scripts and allow-list the few packages that genuinely need a build step. Most do not.
CI secrets and releases
Your CI system is the most attractive target you own. It has your signing keys, your deploy tokens and your publishing credentials, and it runs third-party code on every push.
Give each job the least it needs. Workflows should default to read-only permissions and request write access only in the job that publishes. Keep signing keys and deploy tokens out of jobs that run on pull requests from forks. Where the platform supports it, publish with short-lived OIDC tokens instead of long-lived secrets. PyPI and npm both support trusted publishing from CI, so there is no token sitting in your settings for someone to steal.
Then sign what you ship. For desktop apps that means platform code signing, such as Apple Developer ID with notarisation and Authenticode on Windows. For packages, container images and plain downloads, Sigstore tools can sign and record a public, verifiable signature. Publish checksums next to downloads. Users rarely check them, but security-minded ones do, and the signature is what lets you prove later that a file came from you.
SBOMs and reproducible builds
A software bill of materials is a machine-readable list of everything inside a release, usually in SPDX or CycloneDX format. Its value shows up on a bad day. When a vulnerability is announced in some library, an SBOM for every release lets you answer "are we affected, and which versions" in minutes instead of by memory. Tools such as Syft generate them from a built artifact, and it is worth attaching one to every release.
Reproducible builds go one step further. If the same source always produces a byte-identical output, anyone can rebuild a release and check that the published binary matches the code. That closes the gap where a compromised build machine inserts something that is not in the repository. Full reproducibility is hard for desktop apps, because timestamps, file ordering and signing all get in the way. Setting SOURCE_DATE_EPOCH, sorting inputs and building in a pinned container gets you most of the way. We treat it as a direction rather than a box to tick.
The checklist
This is the short version we keep in project templates. It fits on one screen on purpose.
- Lockfile committed, CI installs with the frozen or locked flag.
- Version ranges tight for network, crypto, parsing and build tooling.
- CI actions and plugins pinned to commit hashes.
- New dependency needs a reason, an owner and a look at its tree.
- Private package names scoped or reserved on the public registry.
- Install scripts off by default, allow-listed by name.
- Dependency updates batched, delayed a few days, diffs read for small packages.
- Audit tool in CI, high-severity findings block the release.
- CI permissions read-only by default, secrets never exposed to fork pull requests.
- Publishing through short-lived tokens where the registry supports it.
- Releases signed, checksums published, SBOM attached.
- Builds as reproducible as the platform allows.
If you are building security tools or anything that runs with high privileges, go further than this. For everyone else, the list above is roughly an afternoon to set up and a few minutes a week to keep. The one item we would do first, today, is the frozen lockfile in CI, because it turns every silent change to your dependency tree into a line in a pull request that a person has to read.