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 versions —
lodash@4.17.21, notlodash@^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:
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:
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
latestbecame 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-gypbuilds 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
allowBuildsallowlist for packages that genuinely need to compile. Don't reach fordangerouslyAllowAllBuilds— 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.
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.
-
See SLSA Framework ↩
-
"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/ ↩
-
"Preparing for npm v12: install scripts and non-registry sources become opt-in." GitHub Community Discussion #198547. https://github.com/orgs/community/discussions/198547 ↩