Bring it together: pin dependencies by hash, defeat dependency confusion and typosquatting, use a private proxy registry, and enforce "only signed, verified artifacts run" at cluster admission.
A version range like ^1.2.0 means "trust whatever they publish next" — which is exactly what a hijacked package exploits. Commit a lockfile, install in the frozen/CI mode that refuses to deviate from it, and pin critical inputs (base images, GitHub Actions) by immutable digest/SHA. Reproducibility is a security property: if the same inputs always produce the same build, tampering has nowhere to hide.
# install strictly from the lockfile — fail if it would change
npm ci # not 'npm install'
pip install -r requirements.txt --require-hashes
# pin base images by digest, not a moving tag
# BAD: FROM node:20
# GOOD: FROM node:20-slim@sha256:5e6b...
# pin GitHub Actions to a commit SHA, not @v4 (a tag can be moved)
# uses: actions/checkout@b4ffde65... # v4.1.1
# ranges = "trust their next release blindly". Pins = "trust these
# exact bytes I reviewed".Dependency confusion exploits resolvers that prefer a public package over your private one of the same name; typosquatting exploits fat fingers. Defend by scoping and reserving your internal package names, pinning the registry so private names never resolve publicly, and reviewing new dependencies before they enter the tree. A new dependency is a new trust decision.
Dependency confusion:
you have internal pkg "billing-utils" (private registry)
attacker publishes "billing-utils" to the PUBLIC registry, v99.0.0
naive resolver picks the higher public version -> runs attacker code
Defenses:
- scope internal packages (@my-org/billing-utils) and RESERVE the scope
- configure the client so @my-org/* ONLY resolves to the private registry
- never let public + private share a flat namespace
Typosquatting: enforce an allowlist / review for NEW deps; a human
approves the first time a package enters the tree.Pulling directly from public registries on every build ties your security and availability to systems you do not control. A private proxy/pull-through registry (Artifactory, Nexus, GitHub/GitLab package registry) caches approved versions, lets you block known-malicious packages, scans on ingest, and keeps builds working when upstream is down or a package is yanked. It is the single choke point where supply-chain policy is enforced.
# point package managers at the private proxy, not the public internet
# .npmrc
registry=https://nexus.internal.example.com/repository/npm-proxy/
@my-org:registry=https://nexus.internal.example.com/repository/npm-private/
# pip.conf
[global]
index-url = https://nexus.internal.example.com/repository/pypi-proxy/simple
# the proxy: caches approved versions, quarantines/blocks malicious ones,
# scans on ingest, and survives upstream outages/yanks. One place to
# enforce "what may enter our builds".All the signing and provenance work pays off only if something refuses to run unverified artifacts. A cluster admission policy verifies signatures and provenance at the moment a pod is scheduled, so an unsigned or wrongly-sourced image is rejected even if it reached your registry. This is the final gate — the DevSecOps engineer's "fail closed" for the supply chain.
# Sigstore policy-controller (or Kyverno) — reject unverified images
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata: { name: require-signed-provenance }
spec:
images:
- glob: "registry.example.com/**"
authorities:
- keyless:
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: https://github.com/my-org/.+
attestations:
- name: must-have-slsa-provenance
predicateType: slsaprovenance
# Result: a pod referencing an unsigned or wrongly-built image is
# DENIED at admission. The chain is closed end to end.