A hands-on technical reference for platform engineers operationalizing Nix-based SBOMs, CVE remediation, and supply chain controls in real C

Nix Derivation Hash Inputs and Build Traceability

Nix's reproducibility guarantee has a blind spot that breaks supply chain integrity on purpose.

· 10 min read · Updated
Cover illustration for “Nix Derivation Hash Inputs and Build Traceability”
Nix Derivation Anatomy · September 10, 2026 · 10 min read · 2,184 words

Nix derivation hashes turn a build recipe into a cryptographic commitment, binding builder, arguments, environment, and every input path into one value before a single byte of output exists. That commitment is the mechanism behind Nix's reputation for reproducibility, and anyone relying on it as a supply chain guarantee needs to know it is, by design, incomplete.

Nix computes a derivation hash over the full build recipe before execution begins: the builder, its arguments, the environment variables it sees, and the store path of every declared input. That hash becomes the output store path, so two derivations with identical recipes land at the same location and Nix treats them as the same build, skipping redundant work and fetching pre-built results from a binary cache instead. External sources are anchored through fixed-output derivations, where Nix fetches the content, hashes it, and fails if the hash does not match the declared value; everything built on top of that layer inherits its integrity guarantees by depending on the layer beneath it. The limit of this guarantee is the completeness of the recipe: inputs the derivation does not declare, such as sandbox paths mounted at build time, affect what the build produces without appearing in the hash at all.

How does Nix ensure reproducible builds and cache correctness using store paths?

A Nix expression does not build anything directly. It evaluates into a derivation, which is a precise recipe: the builder to invoke, the arguments to pass it, the environment variables it sees, and the exact store path of every input it depends on. The derivation hash is computed over that recipe, not over whatever bytes the build eventually produces, and the output store path is itself derived from the derivation hash rather than from the output's contents. In Nix's default input-addressed model, two derivations with the same recipe land at the same store path regardless of what the build actually emits.

That design choice is what makes Nix's caching and sharing model work. Because the hash commits to the full recipe before execution, Nix can recognize a derivation it has already built and skip rebuilding it, and it can fetch a binary built on one machine and trust it on another through a binary cache. External sources, the ones Nix doesn't control the production of, get anchored through fixed-output derivations: Nix fetches the content, hashes it, and fails outright if the hash doesn't match what was declared. That single check is the supply chain anchor for every upstream dependency a Nix build touches. The layering compounds from there: a fetchgit derivation pinned to a declared hash can feed into an mkDerivation that has no network access at all, so the integrity of the whole chain rests on each layer trusting the one beneath it, all the way down to that first fixed-output hash check.

The strength of this chain depends entirely on one condition: that the derivation is a complete description of everything the build actually uses. If the recipe leaves something out, the hash has nothing to say about it.

The hidden input: sandbox paths the derivation hash does not see

That condition fails in at least one well-documented way. Sandbox paths, meaning additional filesystem paths mounted into the build sandbox at build time, are a real build input that no derivation records, so two builds run from the identical .drv file can produce different outputs while landing at the same output hash.

A technical analysis made the problem concrete rather than theoretical. A derivation with no declared dependencies at all can read a file at /truth if one happens to be mounted into the sandbox. Run with an honest sandbox, the build reports 2 + 2 = 4. Run with a poisoned extra sandbox path, it reports 2 + 2 = 5. Both runs produce outputs at the same store path with the same hash. --option extra-sandbox-paths causes this: it mounts additional paths into the build environment without touching the .drv file in any way, so running nix derivation show on the recipe confirms those paths appear nowhere in it.drv file in any way; running nix derivation show` on the recipe confirms those paths appear nowhere in it.

The problem goes a layer deeper than a flag someone forgot to document. Default sandbox paths are a compile-time property baked into the Nix binary itself, not a fixed, universal list, so two machines reporting the identical nix --version string can carry different default sandboxes without either user ever touching a configuration setting. That means a derivation can pass Nix's own --check reproducibility flag on one machine and produce a different result on another because the sandbox underneath it changed, even though nothing about the derivation itself did.

The same analysis points at a structural reason this gap exists rather than treating it as a bug. Folding sandbox paths into the derivation hash would break binary cache sharing outright, because different machines would then compute different store paths for what is logically the same package. The property that lets Nix share build results across machines is the same property that leaves this gap open. Sandbox paths are a side effect of a trade-off the input-addressing model makes on purpose, and the result is a gap in what the recipe describes, not a crack in the cryptography computing the hash over it.

Why the hash proves the recipe, not the execution

Diagram: Three Gaps That Stack in the Nix Trust Chain. Visualizes: Show three distinct attack surfaces that compound rather than overlap in Nix's supply chain model: (1) Sandbox paths — extra-sandbox-paths mounts paths into the build environment…

A derivation hash guarantees only that a given result is consistent with a particular Nix evaluation, which is a narrower guarantee than it may appear. It says nothing about whether the evaluator, the builder, or the host machine actually carried out that evaluation honestly, and that is a separate and more difficult trust problem to solve.

A compromised build host can tamper with the build process, or swap the output after evaluation finishes, all while leaving the derivation itself untouched. The hash proves what was specified. It does not prove what was run. The Kettle paper, published by Confidential.ai in April 2026, states this gap in plain terms: the evidence a software consumer normally gets, things like CI logs, signed checksums, and provenance documents the pipeline itself emits, proves only that some system produced a statement, not that the statement describes reality. A compromised build runner can inject code during compilation and leave the source repository completely clean.

Kettle's answer to that gap is to move the trust anchor off the build infrastructure. Builds run inside a Trusted Execution Environment, and the source commit, dependency set, toolchain, build environment, and output artifact digests all get recorded in a provenance document generated inside a measured confidential VM. The SHA-256 digest of that document is then committed to the TEE platform's attestation report, which is signed by the TEE manufacturer's hardware root of trust rather than by whoever operates the build infrastructure. Verification under this model collapses to one signature check against the vendor root plus a handful of digest comparisons, with no need to re-run the build at all, which takes the build infrastructure and its operators out of the trust surface entirely.

A second upstream gap sits even further back in the chain, at the Git commit hashes that Nix flake locks depend on. A Carnegie Mellon paper from July 2026 showed that signed Git commits are malleable: an attacker who doesn't have the signing key can still produce a second, distinct commit with an identical tree, identical metadata, a valid signature, and a "Verified" badge from a Git forge, differing from the original only in its commit hash. Three routes get there: ECDSA algebraic inversion, OpenPGP unhashed subpacket insertion, and non-canonical DER re-encoding. That has direct consequences for Nixpkgs dependency pinning and for any reproducible-build system treating a commit hash as a stable, content-addressable key. For Nix specifically, a flake.lock pinning a commit hash gives strong protection against accidental drift, version skew, someone pushing a different commit by mistake, but weaker protection against a targeted attacker exploiting that malleability on purpose.

These three gaps stack rather than overlap. Sandbox paths can change what a build does. A compromised host can change what a build produces. A malleable commit hash can change what source a build consumes. Each one operates at a different point in the chain. Closing one doesn't close the others.

Drift elimination and the flake lock

None of that undercuts what the derivation hash does guarantee for the inputs it actually describes. Within that scope, Nix doesn't just discourage environment drift, it makes drift structurally impossible, and flakes extended that same guarantee to cover the full dependency graph rather than just a single package.

Conventional package managers resolve dependencies at install time, and that resolution can differ across machines, across time, and across CI runners even when the stated requirements look identical. Nix replaces that resolution step with evaluation of a graph that's already fixed before anything runs. The channel system used to undercut this guarantee somewhat, since nix-channel was mutable and a team pulling from different channel snapshots could get different packages from the same expression, a weakness flakes were built to close.

The practical payoff appears at the point where developer machines and CI meet. One team's experience from 2025 captures the scale of what that buys: after months spent fighting Docker cache invalidation, the team replaced a 47-line Dockerfile with a 43-line flake.nix, and the flake handled dependency isolation and caching without any manual layer optimization at all.

That same input-addressed structure has a second benefit beyond eliminating drift: it makes the build graph inspectable before a single build runs. Generating the complete list of build-time dependencies for a derivation doesn't require executing the build at all, and that property is what makes supply chain analysis at scale possible rather than merely convenient.

Turning the build graph into auditable supply chain records

Because Nix's build graph can be fully enumerated before anything builds, that graph can be turned directly into auditable supply chain records, SBOMs, vulnerability reports, SLSA provenance attestations, without a separate scanning pass over finished artifacts. The provenance tool generates SLSA v1.0 compliant provenance attestation files in JSON format for any Nix flake or derivation. vulnxscan demonstrates vulnerability scanning using the SBOMs that sbomnix generates.

The scale this exposes is larger than it first appears. Even a simple "hello world" C program typically pulls in over 150 packages in its build-time dependency list, and a conventional scanner working only against the finished artifact would miss most of them. A September 2026 pull request merged into the Semwright project shows what this looks like in practice: it pins nixpkgs to an exact revision, builds five release executables through a Nix derivation, generates reproducible per-binary CycloneDX 1.5 SBOMs twice and compares them byte-for-byte, and produces deterministic, non-admitted tar and deb certification bundles on both x86_64 and aarch64. That byte-for-byte comparison only works because the build graph is fully pinned down by the derivation hash in the first place. A FOSDEM 2026 talk framed the stakes for the ecosystem directly: Nix and Nixpkgs are seeing ever-broader adoption at the same time SBOMs are becoming the standard format for demonstrating provenance, and connecting the two is becoming necessary work for the Nix community.

On the monitoring side, a system called Lila, presented at MSR '26 in April 2026 in Rio de Janeiro, aggregates reproducibility results from independent builders into a shared reproducibility database, which lets transparent monitoring scale across large package collections rather than requiring one-off audits. The same model supports a practical CVE-to-patch workflow: because every deployment can be tied to the input-derived hash of its environment, a newly disclosed CVE can be traced to every affected store path without grepping logs or rescanning container images, with the hash itself serving as the index. The graph being enumerable and stable in the first place is what creates the value here, not scanning harder after the fact. That same hash-indexed model for identifying a deployment carries over cleanly from a single build to an entire fleet of environments.

Hash-indexed environments across architectures, CI, and production

That portability is the argument's natural endpoint. The same derivation hash that anchors a supply chain record for one build also keeps developer laptops, CI runners, and production targets synchronized across architectures, which closes off the most common source of "it worked somewhere else" failures.

The target system is itself an input to the derivation hash, so the same declarative specification produces verified, independently auditable environments on Linux and on macOS, on x86_64 and on aarch64, without rebuilding or rewriting the recipe for each one. Nix flakes can declare cross-platform support directly in the configuration rather than through separate build scripts per target. MCP server configurations managed through Nix's Home Manager illustrate the pattern concretely: platform differences, macOS's launchd against NixOS's systemd, are handled at the module level, with a shared base configuration plus platform-specific pieces layered on top, so one manifest governs both targets. The derivation hash is what makes that single manifest trustworthy on either platform, because it encodes the target system as part of what it commits to, not as an afterthought bolted on after the fact.

Sources

  1. Kettle: Attested builds for verifiable software provenance
  2. Lila: Decentralized Build Reproducibility Monitoring for the Functional Package Management Model
  3. Git Hash Chain Malleability
  4. The Nix sandbox is a hidden input - Links - NixOS Discourse

More in Nix Derivation Anatomy