Dependency integrity
Pinning, lockfiles, and dependency confusion.
You inherit the security of every dependency you pull in, so dependency integrity is about controlling exactly what enters your build and proving it did not change. The first tool is the lockfile — package-lock.json, go.sum, Cargo.lock — which records the exact version and a cryptographic hash of every dependency, direct and transitive. Commit it, and every build (and every developer) resolves to the identical, verified bytes instead of "whatever the latest matching version is today."
# the lockfile pins exact versions AND hashes of the whole tree$ npm ci # installs strictly from package-lock.json, fails if it drifts$ go mod verify # checks every module against the hashes in go.sum# never "npm install" in CI (it can update the lockfile); always the locked/verified install
Pin, and pin the base image too
Pinning extends beyond app dependencies. Pin your container base image by digest (FROM image@sha256:...), not a floating tag, so a rebuild uses the exact bytes you reviewed and cannot silently pull a re-pushed or tampered base. Pin CI action/plugin versions the same way — a GitHub Action referenced by a mutable tag can change under you. The principle is uniform: mutable references are trust holes; pin everything that goes into the build to immutable identifiers.
FROM gcr.io/distroless/static@sha256:d71f4f2a9c... # base pinned by digest# GitHub Actions pinned by full commit SHA, not a tag:# uses: actions/checkout@8f4b7f8... # not @v4 (a mutable tag)
Defend against dependency confusion
Internal package names are a specific risk: if your build can fall back to a public registry, an attacker publishing a public package with your internal name and a higher version can hijack the resolution. Two defenses: namespace internal packages under a scope you own (@acme/...) so a public name cannot collide, and configure the resolver to fetch internal scopes only from your private registry, with no public fallback. Never let a build silently reach the public internet for a package that should be internal.
@acme:registry=https://registry.acme.internal/ # internal scope → private only# and crucially: do not configure a public fallback for @acme/*# a request for @acme/auth can therefore never resolve to a public impostor