Skip to content

Supply Chain Security Practices

Knowing the threats isn't enough. This chapter covers the practical habits that reduce risk over time—dependency updates, artifact signing, and the security practices that become second nature.

The Boring Part

Updates are the most boring security practice. That's why they matter most. Nobody gets excited about bumping patch versions. But the teams I've seen handle incidents well are the ones who do the boring work consistently. Security isn't a heroic act—it's a thousand small habits.


Dependency Update Strategies

Updates are a double-edged sword: stale dependencies accumulate vulnerabilities, but updates can break things. You need a strategy.

Update Cadence

Update Type Frequency Approach
Security patches Immediately Prioritize over everything
Patch versions Weekly/bi-weekly Usually safe, test and merge
Minor versions Monthly Review changelog, test thoroughly
Major versions Planned Schedule time, expect breaking changes

Automated Update PRs

Dependabot (GitHub) or Renovate (self-hostable) automate the tedious parts:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      development-dependencies:
        dependency-type: "development"
      production-dependencies:
        dependency-type: "production"

Renovate offers more control:

{
  "extends": ["config:base"],
  "schedule": ["before 6am on monday"],
  "automerge": true,
  "automergeType": "branch",
  "packageRules": [
    {
      "matchUpdateTypes": ["patch"],
      "automerge": true
    },
    {
      "matchUpdateTypes": ["major"],
      "automerge": false
    }
  ]
}

The Update Philosophy

Updates aren't free—they cost testing time. But stale dependencies are debt—compounding.

Find a sustainable rhythm:

  • Security updates: Always. No exceptions.
  • Everything else: Regular cadence that your team can sustain
  • Major updates: Planned, not reactive

Never disable automated security updates to reduce noise. The noise is telling you something.

Lock Files

Lock files record exact resolved versions of all dependencies. They're not optional.

What Lock Files Do

  • Capture exact versionslodash@4.17.21, not lodash@^4.17.0
  • Include transitive dependencies — Everything, not just direct
  • Record integrity hashes — Verify downloads haven't been tampered with
  • Enable reproducible builds — Same input → same output

Lock Files by Ecosystem

Ecosystem Lock File Command to Generate
npm package-lock.json npm install
Yarn yarn.lock yarn
pnpm pnpm-lock.yaml pnpm install
pip requirements.txt (weak) pip freeze
pip-tools requirements.txt pip-compile
Poetry poetry.lock poetry lock
uv uv.lock uv lock
Bundler Gemfile.lock bundle install
Cargo Cargo.lock cargo build
Go go.sum go mod tidy

Lock File Rules

1. Lock files go in version control.

# YES
git add package-lock.json
git commit -m "Update dependencies"

# NO
echo "package-lock.json" >> .gitignore  # Don't do this

2. CI installs from lock file.

# npm: use ci, not install
npm ci  # Installs exactly what's in lock file

# pip: don't resolve, use frozen requirements
pip install -r requirements.txt --no-deps

3. Update lock files intentionally.

Don't let CI regenerate lock files. Updates should be explicit:

npm update lodash
git add package-lock.json
git commit -m "Update lodash to 4.17.21"

4. Review lock file changes in PRs.

When a PR includes lock file changes, review them:

  • What versions changed?
  • Were new dependencies added?
  • Did any dependencies disappear?

Time as a Control: Caching and Cooldowns

Lock files answer what am I installing? This section answers a different question that most teams never ask: when am I allowed to install it?

That turns out to be the control that actually works against a worm.

Why time is the right axis

Look at how these incidents actually end. The September 2025 debug/chalk compromise was resolved in roughly two and a half hours. Shai-Hulud was detected in about twelve. The August 2026 keyv worm had clean releases restored as latest the same day it started.2

Now look at how fast they spread. The keyv worm crossed nine organizations in about thirty minutes, because self-propagation runs at registry API speed.

Detection is measured in hours. Propagation is measured in minutes. You cannot win that race by reacting — by the time an advisory exists, your build either pulled the bad version or it didn't. But you can decline to enter the race. If your builds simply don't install anything published in the last twenty-four hours, every one of those incidents becomes something you read about rather than something you clean up.

That's the whole idea. You're not detecting malice. You're letting the rest of the internet detect it on your behalf, and arriving late on purpose.

Minimum release age

Every major package manager now ships this control. Annoyingly, each one picked a different name and a different unit — check yours rather than copying from memory:

Manager Config key Unit File Since Default
npm min-release-age days .npmrc 11.10.0 none (opt-in)
pnpm minimumReleaseAge minutes pnpm-workspace.yaml 10.16 1440 (1 day) since pnpm 11
Yarn Berry npmMinimalAgeGate minutes .yarnrc.yml 4.10.0 none (opt-in)
Bun minimumReleaseAge seconds bunfig.toml 1.3.0 none (opt-in)

One day, expressed four ways:

# .npmrc — npm, in days
min-release-age=1
# pnpm-workspace.yaml — pnpm, in minutes
minimumReleaseAge: 1440
# .yarnrc.yml — Yarn Berry, in minutes
npmMinimalAgeGate: 1440
# bunfig.toml — Bun, in seconds
[install]
minimumReleaseAge = 86400

If you use automated update PRs, set the same floor there — Renovate calls it minimumReleaseAge. A cooldown in your installer and a bot that opens a PR the instant a version publishes are working against each other.

The objection, and the honest answer

"So I'm deliberately not getting security patches for a day?"

Yes. Take the trade anyway, and understand why it's asymmetric. A malicious release is typically discovered in hours — the cooldown catches it almost every time. A legitimate security patch is racing an exploit window that's usually longer than a day, and if it isn't — an actively-exploited critical — you override the cooldown deliberately for that one package. Every implementation supports an exception path. What you're buying is that the default is safe and the exception is a decision someone makes on purpose.

The failure mode you're preventing is the one where nobody made any decision at all.

Where caching fits

A pull-through cache or private registry mirror — Artifactory, Nexus, Verdaccio, devpi — gives you the same time buffer from a different direction, plus a policy chokepoint:

  • Caching — faster builds, and a stable build environment stops chasing whatever latest became overnight
  • Availability — your build doesn't break when upstream does, and a package that gets unpublished is still there (left-pad is the reason this bullet exists)
  • Control — you can approve packages before use, and block a compromised scope for the whole org with one change during an incident

But be precise about what a cache does and doesn't do, because it's easy to over-claim:

A cache is a time defense, not an integrity defense

Garbage in, garbage out. A cache that warms before a compromise protects every build behind it — there's nothing to fetch, so the poisoned version never enters your environment. A cache that warms after the compromise faithfully stores the malware and serves it to everyone. The cache has no opinion about what it's holding.

And the part nobody plans for: your cache does not receive the unpublish. When the registry yanks a malicious version, your mirror keeps its copy. Upstream is clean, advisories say resolved, and your proxy is still the one place in the world happily serving keyv@6.0.0. Caching converts a fast-moving infection into a slow-burning persistence problem.

Which means the control isn't the cache — it's the purge runbook. Know today, before you need it, how to evict a specific package version from your proxy and how to prove it's gone. If nobody on your team has ever done that, you don't have this defense. You have a copy of the problem.

Blocking install scripts

The other half of what stops this class of attack. Most registry malware executes through lifecycle hooks — preinstall, install, postinstall — which run automatically when you resolve a package. You don't have to import it. You don't have to run your app. Resolution is execution.

The ecosystem finally moved on this:

  • npm 12 disables install scripts by default; lifecycle scripts and implicit node-gyp builds are opt-in.3
  • npm 11 and earlier run them. Set it yourself: npm config set ignore-scripts true
  • pnpm 10+ blocks them by default, with an allowBuilds allowlist for packages that genuinely need to compile. Don't reach for dangerouslyAllowAllBuilds — the name is doing you a favor.

The catch with npm's flag is that it's blunt: ignore-scripts disables all scripts, including your own project's, and there's no per-package allowlist. Packages that legitimately compile native code — node-gyp consumers, sharp, and friends — need an explicit npm rebuild <package> after install. Do that work once and keep the safe default.

ignore-scripts is not a force field

It blocks lifecycle hooks. It does not block malicious code in the module body.

If the payload sits in the package's actual export rather than a postinstall, it runs the moment your application calls require() or import. The keyv worm used a preinstall hook, so blocking scripts would have stopped it cold — but that's a fact about that attacker's choice, not a property of the defense.

Turn scripts off. It's the single highest-value line of configuration in this guide. Then keep the lock file, the cooldown, and the SBOM, because it only closed one door.

Artifact Signing

Code signing verifies authenticity: this artifact came from who it claims to come from, and hasn't been modified.

Why Sign?

  • Authenticity — Verify the claimed source
  • Integrity — Detect tampering
  • Non-repudiation — Prove who published what
  • Chain of custody — Traceable provenance

Signing Mechanisms

Traditional (GPG):

# Sign a file
gpg --armor --detach-sign artifact.tar.gz

# Verify
gpg --verify artifact.tar.gz.asc artifact.tar.gz

GPG works but has usability issues—key management is painful.

Modern (Sigstore/cosign):

# Sign a container image (keyless)
cosign sign myregistry/myimage:v1.0

# Verify
cosign verify myregistry/myimage:v1.0

Sigstore uses OpenID Connect for identity—no key management required. Sign with your GitHub/Google/Microsoft identity.

npm Provenance

npm supports provenance attestations for packages published from CI:

# GitHub Actions
- name: Publish with provenance
  run: npm publish --provenance --access public
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Consumers can verify packages were built from claimed source code.

Verification in CI

# Verify signatures before deployment
- name: Verify image signature
  run: |
    cosign verify \
      --certificate-identity=ci@myorg.com \
      --certificate-oidc-issuer=https://token.actions.githubusercontent.com \
      myregistry/myimage:${{ github.sha }}

SLSA Framework

SLSA (Supply-chain Levels for Software Artifacts, pronounced "salsa") is a framework for supply chain integrity.1

SLSA Levels

Level Requirements What It Means
1 Documented build process You can explain how artifacts are built
2 Hosted build, signed provenance Builds run on service infrastructure, provenance is tamper-evident
3 Hardened build, non-falsifiable provenance Build service is hardened, provenance can't be faked
4 Two-party review, hermetic builds All changes reviewed, builds are fully reproducible

Practical SLSA

Most projects should target Level 2-3:

  • Use a CI service (GitHub Actions, GitLab CI)
  • Generate provenance attestations
  • Sign artifacts

Level 4 is for high-security contexts—it requires hermetic builds (no network access during build) and two-party review for all changes.

GitHub Actions SLSA Generator:

- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.0.0
  with:
    artifact-name: my-artifact

This generates SLSA Level 3 provenance for your artifacts.

Repository Security

For Package Maintainers

If you maintain packages others depend on:

Enable 2FA. Required on npm and PyPI for popular packages. Do it anyway.

Use fine-grained tokens. Don't use personal tokens for CI. Create scoped tokens with minimal permissions.

Require review for releases. Tag protection, branch protection, and release approval workflows.

Sign your releases. Use Sigstore, GPG, or your registry's provenance features.

Publish a SECURITY.md. Tell people how to report vulnerabilities.

# Security Policy

## Reporting a Vulnerability

Please report security vulnerabilities to security@example.com.
Do not open public issues for security problems.

We aim to respond within 48 hours and patch within 7 days.

For Package Consumers

Use lock files. Already covered—don't skip this.

Verify checksums. Lock files include hashes. CI should verify them.

npm ci  # Verifies integrity hashes automatically

Set a release cooldown. The highest-leverage line of config you can add today — see Time as a Control. One day of delay retroactively neutralizes most registry compromises of the last two years.

Turn off install scripts. npm 12 and pnpm 10+ do this for you. Everything older needs npm config set ignore-scripts true.

Consider private registries. For organizations, mirror approved packages with Artifactory, Nexus, Verdaccio (npm), or devpi (Python). Caching, availability, and a single chokepoint where you can block a compromised scope org-wide — with the caveats covered above, including the purge runbook you need before you need it.

Monitor for anomalies. Watch for:

  • New maintainers on critical dependencies
  • Unusual release patterns
  • Suspicious new dependencies

Defense Checklist

Minimum Viable Security

  • Lock files in version control
  • CI installs from lock file
  • Install scripts disabled (npm 12 / pnpm 10+ by default, otherwise ignore-scripts)
  • Minimum release age set (1 day floor)
  • Automated vulnerability scanning
  • 2FA on package registry accounts
  • Secrets not in source code

Better Security

  • Automated dependency update PRs — with the same cooldown as your installer
  • SBOM generation on every build
  • Lock file changes reviewed in PRs
  • Private registry mirror
  • A tested runbook for purging a poisoned version from that mirror
  • Signed artifacts

Advanced Security

  • SLSA Level 2+ compliance
  • Hermetic builds
  • Provenance verification in deployment
  • Anomaly detection on dependencies
  • Regular security audits

Habits, Not Projects

Security practices are habits, not projects. You don't "do security" once—you build it into how you work.

The teams I've seen handle incidents well have these habits deeply embedded. Lock files are always committed. Vulnerability scans run on every PR. Updates happen on a predictable cadence. When something goes wrong, they're responding from a position of knowledge, not scrambling to figure out what they're even running.

The teams that struggle treat security as an obstacle. They disable scanning because it's noisy. They skip lock files because they're confusing. They defer updates because they might break things.

Both teams eventually face the same incidents. One is prepared. One isn't. Build the habits now, before you need them.



  1. See SLSA Framework 

  2. "Mitigating supply chain attacks." pnpm documentation. https://pnpm.io/supply-chain-security — and "Locking down dependency installs across npm, pnpm, yarn, and bun." Craigory Coppola. May 2026. https://craigory.dev/blog/2026-05-29/package-manager-release-cooldown/ 

  3. "Preparing for npm v12: install scripts and non-registry sources become opt-in." GitHub Community Discussion #198547. https://github.com/orgs/community/discussions/198547