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

Hermetic Nix Builds Without a Global Nix Installation

Developers can now build with Nix in CI without pre-installing it globally.

· 10 min read
Cover illustration for “Hermetic Nix Builds Without a Global Nix Installation”
Hermetic Build Patterns · October 13, 2026 · 10 min read · 2,334 words

Nix's reproducibility guarantees always cost you something, and you pay it before the first build ever runs. Every Nix derivation writes its outputs under /nix/store, a path compiled directly into binaries at link time. That path has to exist on the host before Nix can build or run anything. That architectural fact created a loop that every new Nix user ran into: to get the reproducibility Nix promises, you first needed root access, an installer, and a daemon kept alive in the background. You installed Nix once onto a machine, like a system dependency, instead of declaring it once inside a repository like a project dependency. For CI pipelines and shared infrastructure, this pushed real cost onto platform teams: every runner, every fresh developer laptop, every ephemeral container needed Nix pre-installed before it could do useful work, and that prerequisite made adoption fragile wherever infrastructure churned quickly.

What "Hermetic" Requires

A hermetic build produces outputs that depend only on declared, content-addressed inputs. No network access happens mid-build, no ambient host state leaks in, and no compiler or library sneaks in from whatever happens to be installed on the machine that day. Nix enforces this with sandboxed build environments that cannot reach the network or the host filesystem, paired with fixed-output derivations (FODs), whose results must match a prerecorded cryptographic hash before the build is allowed to succeed. Any impurity that wants into the build has to pass through that hash check. The flake.lock file carries the rest of the contract: it pins exact commit hashes for nixpkgs and every other input, so a build run five months from now reconstructs the identical derivation graph as a build run today. The /nix/store path sits at the center of both halves of this story. Its fixed location is what makes build outputs relocatable across any machine sharing that store, and it's the same fixed location that historically tied Nix to a host-level install. SLSA Level 4, which requires hermetic builds plus two-person review, maps directly onto what Nix flakes with pinned inputs already deliver at the build layer. That is the supply chain standard this approach can credibly claim, and the rest of this piece tests each proposed solution against the standard just described.

Bazel-managed ephemeral Nix stores on Linux

The requirement for a /nix/store does not demand that store live on the host permanently. It can just as well live inside a user-namespace sandbox that the build tool creates and tears down on its own terms, provided that build tool is willing to bootstrap the store itself. bazel_local_nix, released as rules_nix at version 0.1.0 in May 2026, does that. At bootstrap time it downloads a pinned, checksummed Nix release along with a pinned, statically-linked bwrap (bubblewrap) binary, then runs Nix unprivileged inside a bubblewrap user namespace, working against a persistent store kept under Bazel's own output root. No host-provided bwrap binary is required anywhere in this chain, since the binary itself gets fetched during bootstrap. The only permission the host needs to grant is the ability to create unprivileged user namespaces, a capability most modern Linux kernels already expose without special configuration.

Two rules do the real work once the store exists. nix_package calls nix build inside the wrapper and packages the resulting runtime closure, along with relocatable bin/ shims, into a single artifact. nix_toolchain then extracts that bundle, and the shim inside it re-executes the real binary under bwrap with the bundle's store bound at /nix/store. The host machine never sees a /nix directory at any point in this process. Reproducibility gets pinned at the installable reference itself: a flake URL carrying a commit hash, such as github:NixOS/nixpkgs/<rev>#hello, guarantees a deterministic build, and unpinned references are explicitly flagged as non-deterministic and unsuitable for released rules. The bazel-nix-flakes example repository demonstrates the entire chain end to end: install Bazelisk, run bazel run:hello, and Bazel self-installs an ephemeral Nix store and compiles a hello-world binary, with no prior Nix installation anywhere on the machine. That store persists across builds once seeded, so only the first build absorbs the bootstrap cost, and every build after that hits the local cache.

The scope of this pattern has firm edges. It works on Linux x86_64 only, and remote execution is unsupported because the store it builds is mutable, shared, and accessed locally without sandboxing. macOS developers and Windows users working through WSL still need a genuine Nix installation to get full toolchain access under this pattern.

The NEAR MPC project's hermetic Nix builds for identical Docker image digests across CI and macOS

The clearest evidence that this pattern holds up in production comes from a blockchain infrastructure project, where hermetic Nix builds now produce identical Docker image digests whether the build runs in CI or on a developer's laptop. Every step that executes build code there, rustc, build.rs, proc-macros, the C/C++ compilers, image assembly, and layer compression through skopeo inside the sandbox, runs with no network access. Only inputs pinned by hash, meaning Cargo.lock, flake.lock, and specific git revisions, get fetched during the build.

That result looks very different from what the project ran before. The old build path kept the network open throughout, with rustup fetching the toolchain mid-build because of a version mismatch between repro-env.toml and rust-toolchain.toml, and apt-get pulling packages inside Dockerfiles. Under that setup, the digest just reflected whatever the network happened to serve at build time, not anything declared in the source tree. The new path instead treats sha256sum result/manifest.json as the digest of the pushed Nix tag, and that digest gets published unchanged with skopeo copy --preserve-digests (the project's votes have not yet moved over to the Nix digests). Adopting hermetic Nix builds let the team delete a substantial amount of surrounding machinery: the node Dockerfiles (Dockerfile-node and Dockerfile-node-gcp) are gone, along with the standalone nix-build-mpc-node workflow and build-images.sh. repro-env, the launcher Dockerfile, podman, buildx, and the AppArmor sysctl steps still remain in place, with their removal planned as a follow-on step once Nix digests have matched independent rebuilds across several releases. The hermetic approach eliminated the global Nix install requirement and, alongside it, a whole stack of privileged build tooling that the old pipeline depended on.

What Hermetic Builds Give You at the Supply Chain Layer

When a Docker build calls apt-get or rustup during the build layer, the image's contents end up depending on whatever the network happened to serve at that moment, not on what the source tree actually declares. That gap matters because it means two builds from identical source can legitimately produce different artifacts, and there's no way to tell from the digest alone whether that difference reflects a deliberate change or a compromised package feed.

Nix's sandboxed build environment closes off the two most common routes attackers use against the supply chain. It verifies the hash of every source before using it, and it builds inside an environment cut off from both the network and the host filesystem, so a compromised mirror has no opening to inject code after a hash has already been defined. Fixed-output derivations handle the narrow set of cases where a build legitimately needs to touch the network: the output hash is declared ahead of time, and any deviation from that hash fails the build, making network-based exfiltration or substitution something the build system catches.

None of this closes every gap. Nix cannot stop someone from defining a malicious package in nixpkgs with a hash that correctly matches the malicious content, and it scans no build artifacts for known vulnerabilities. Hermeticity is a strong control at the build layer, not a full supply chain program on its own. It pairs naturally with provenance tooling: the nix-sbom-helper project from Cyberus Technology produces SPDX and CycloneDX SBOMs as first-class Nix build outputs, with each input generating its own set of files named from the output store path. Provenance here is a build artifact, not something bolted on afterward through a separate scan, because hermeticity gives you that same property one layer up. The EU Cyber Resilience Act, starting in September 2026, will require a machine-readable SBOM covering at least the most important dependencies for products with digital elements, and hermetic Nix builds that generate SBOMs as outputs offer a direct route to meeting that requirement.

The offline and closure-completeness problems that trip up teams attempting hermetic builds in air-gapped or restricted environments

The hardest practical obstacle to hermetic Nix builds appears in air-gapped and restricted environments, where closure completeness becomes an unforgiving requirement. If even one transitive dependency is missing when you pre-populate the Nix store offline, Nix has no substitute to fetch, so it has to build that piece from source instead. Because dependencies have their own dependencies, one missing small package can cascade into rebuilding a large share of the entire dependency graph, so what should have been a quick offline build turns into an unplanned compile marathon.

The mitigation lives in the same persistent store mechanism that makes ordinary builds fast. That store, kept under <output_user_root>/rules_nix_store and overridable through RULES_NIX_CACHE, gets seeded once on the first build and reused on every build after that. For air-gapped work, the same mechanism becomes the pre-population strategy: seed the store on a networked machine, archive it, and restore the archive onto the restricted host. Teams following this pattern need to test the archived closure for completeness before depending on it in production.

The microVM pattern runs each job in its own virtual machine with a fresh guest kernel per build, so it handles isolation differently and avoids some of the state-leakage concerns that come with shared persistent stores. It trades that isolation for a different cost: there's no persistent box accumulating a warm cache, so every job pays the full bootstrap cost unless the team builds and maintains its own store-snapshot management outside the build system itself.

Structuring the Bazel workspace, pinning references, and configuring the store

Getting a working hermetic setup under Bazel starts with the registry configuration, since rules_nix resolves against it before anything else can happen. You need the.bazelrc file to point at the custom registry ahead of the default public registry:

common --registry=
common --registry=

From there, MODULE.bazel declares the dependency itself, with bazel_dep(name = "rules_nix", version = "0.2.1") invoking the nix_bootstrap repository rule, the step responsible for downloading the pinned Nix release and the statically-linked bwrap binary.

Inside BUILD files, nix_package takes a pinned installable reference in the form github:NixOS/nixpkgs/<rev>#<attr>. The commit hash in that reference is what makes the reproducibility claim hold; leave it out, and the reference becomes non-deterministic, which invalidates the hermetic guarantee the rest of the setup depends on. nix_toolchain extracts the bundle that nix_package produces, and any target consuming that toolchain needs build --spawn_strategy=local set in.bazelrc, because the compiler runs under bwrap and cannot nest inside Bazel's own sandboxing.

C and C++ toolchains follow the same pattern through the nix_cc extension: declaring nix_cc.configure(attribute = "gcc", nixpkgs = "github:NixOS/nixpkgs/<rev>") and registering the resulting toolchain lets ordinary cc_binary and cc_library targets compile through the Nix-backed compiler with no host compiler involved at any point. The binaries from this pipeline link against /nix/store paths inside their bundle, so on a host with no /nix directory they run correctly only when you execute them under bwrap with that bundle's store bound at /nix/store, and the relocatable shim in bin/ handles that automatically.

This pattern alongside declarative environment managers that abstract Nix further

The Bazel and rules_nix combination makes the most sense when a project is already built on Bazel and needs Nix packages as build inputs without polluting the host, not as a general developer environment solution. Teams chasing the same hermetic guarantees across local development, CI, and production, without writing Bazel rules themselves, tend to reach for higher-level declarative environment tools that manage the Nix layer on their behalf. These tools treat the manifest, not the container image, as the actual unit of promotion between environments.

FloxHub illustrates what that collaborative layer looks like: environments get named, versioned, and shared by name, teams pull complete stacks on demand, and each environment carries an input-derived hash that can be attached to deployments for CVE discovery and hash-level blocking. That turns the SBOM and provenance story hermetic builds make possible into something actionable at the operations layer, not just a build-time artifact. The two approaches don't compete for the same job. A team can run rules_nix inside Bazel actions for hermetic compilation while relying on a declarative environment manager for the surrounding developer environment, CI preamble, and production toolchain, and choosing one does not rule out the other.

Honest limits: what hermetic Nix builds do not solve

The ephemeral-store-in-bwrap pattern works on Linux x86_64 only, so macOS developers and Windows users on WSL still need a genuine Nix installation, and any team running a mixed OS fleet has to plan explicitly for that asymmetry rather than assume uniform tooling across every machine. Remote execution is also out of reach under this pattern: because the rules_nix store is mutable, shared, and accessed locally without sandboxing, it cannot plug into Bazel's remote execution infrastructure, and large distributed build farms would need real architectural changes before this pattern could serve them. Nix itself does not scan build artifacts for vulnerabilities, and it does not stop a malicious package from being defined in nixpkgs with a hash that correctly matches malicious content, so hermetic builds guard against tampering after a hash has been set, not against what gets defined first. Closure completeness in offline environments remains a genuine operational burden, one that demands investment in store pre-population tooling tested thoroughly before any production build depends on it.

These limits mark the edges of where this approach applies. The bootstrapping paradox that once made Nix a system prerequisite rather than a project dependency is solvable now, through unprivileged user namespaces and bootstrapping managed by the build tool itself. Teams that understand where the edges are, Linux-only scope, no remote execution, no vulnerability scanning, can adopt hermetic Nix builds today without ever touching the host system.

Sources

  1. Hermetic stand-alone nix+bazel combo
  2. More airgap questions - Help - NixOS Discourse
  3. Nix: A Solution With Problems

More in Hermetic Build Patterns