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

SBOM Generation for Nix-Built Container Images

Nix's content-addressed builds produce reliable software bills of materials automatically.

· 11 min read
Cover illustration for “SBOM Generation for Nix-Built Container Images”
SBOM Generation Workflows · September 26, 2026 · 11 min read · 2,574 words

Most organizations that generate software bills of materials for their containers say the practice pays off: 73% report that SBOMs make vulnerability mitigation faster and more precise. And yet 86% of organizations say generating an SBOM in the first place is still hard. That gap, between the acknowledged value of the output and the difficulty of producing it, is the real starting point for any conversation about container security today.

Why SBOM generation is hard for container images

Diagram: The SBOM Value-Difficulty Gap. Visualizes: Show the tension between two statistics from the article: 73% of organizations say SBOMs make vulnerability mitigation faster and more precise, yet 86% say generating an SBOM in the first place is…

The difficulty isn't conceptual. Engineers understand what an SBOM is supposed to do: list every component in a piece of software, so that when a new vulnerability disclosure drops, someone can check in minutes rather than weeks whether it applies. The trouble is operational. Most organizations run different scanners across artifact types, cobbling together tools that produce inconsistent output across pipelines. Each tool emits its own format, its own confidence levels, and its own blind spots. Engineering time that should go toward triaging vulnerabilities instead goes toward reconciling five different reports that all claim to describe the same image.

The tooling sprawl produces a deeper quality problem. An SBOM that lists declared versions instead of the versions actually resolved at build time isn't just incomplete, it's misleading, because it tells a story about the software that doesn't match what's running in production. An SBOM that misses transitive dependencies, the packages pulled in indirectly by something you did declare, hides exactly the class of risk that catches teams off guard. An SBOM with no cryptographic binding to the artifact it describes can drift from that artifact without anyone noticing, which defeats the point of generating one. Every downstream decision, a go/no-go on a deployment, an answer to an auditor, a patch prioritization call, inherits whatever gaps existed in the SBOM. Garbage in, garbage out, except the garbage here has a severity score attached to it.

So a lot of teams treat SBOM generation as a box to check: satisfy a customer's procurement questionnaire, produce a document for a compliance audit, move on. That's a missed opportunity. Handled properly, the SBOM is a live operational asset, not a PDF that gets filed away.

What makes container SBOMs structurally harder than source-code SBOMs

Source-code SBOMs have it comparatively easy. Point a tool at a package.json or a requirements.txt, and you get a fairly clean picture of declared dependencies. Container SBOMs are a different animal entirely, because a container image is a build artifact, not a source tree. To describe it accurately, an SBOM has to account for base OS packages, application-level dependencies, and runtime libraries, all layered across however many stages the container build definition sets out.

Three problems compound here, and none of them are edge cases.

First, compiled binaries copied straight into a final image stage are close to invisible to standard scanning tools. Most SBOM generators work by querying a package database (dpkg, rpm, apk), not by inspecting the filesystem contents directly. If a binary was compiled elsewhere and just COPY'd in, there's no database entry to find, so essentially no scanner will catch it. Second, multi-stage builds actively hide information by design: the point of a multi-stage build is to discard the intermediate stage once its output has been copied forward. The build-time dependencies that produced that output are gone by the time anyone runs a scan. Third, distroless and minimal base images, increasingly the standard for production containers because they shrink attack surface, don't even have a package manager for a post-build tool to query. There's nothing to interrogate.

Package database integrity is a subtler failure mode underlying all three. Post-build scanning trusts that the package database on the image reflects installed reality. If that database is incomplete, out of date, or tampered with, the resulting SBOM looks complete while quietly being wrong, and there's no signal to tell the difference. This is the core limitation of the entire post-build scanning approach: it's heuristic. It infers what's in the image after the fact, rather than deriving that answer from the actual build graph that produced the image. Detection quality varies by language ecosystem, some are well-supported, others barely, and that variance is never structurally guaranteed to close.

Nix's Content-Addressed Build Model and SBOM Contents

Nix approaches the problem from the opposite direction: instead of inferring dependencies after the build, it derives the SBOM from the exact graph that produced the build. Every Nix build is a content-addressed derivation. Every input, source code, compiler version, build script, even environment variables, gets hashed, and that hash becomes part of the resulting output's store path. A package's store path includes a hash prefix that is a direct function of everything that went into building it.

Two builds with identical inputs land on the exact same store path, every time. That's the property that makes it safe to share build outputs across machines without re-verifying them by hand, and it's what makes the entire dependency graph machine-verifiable rather than just docum... It's the property that makes it safe to share build outputs across machines without re-verifying them by hand, and it's what makes the entire dependency graph machine-verifiable rather than just documented. You don't have to trust a changelog or a package maintainer's word about what went into a build. The hash is the proof.

Structurally, the Nix store behaves like a Merkle tree. Changing a dependency changes the store path for that dependency, which invalidates every path downstream that depends on it. An attacker who swaps in a tampered dependency doesn't get to hide that swap, because the hash simply won't match anymore, and neither will anything built on top of it. The same logic applies to build scripts: modify one, and the output hash changes. Every package definition in Nix carries a cryptographic hash of its source, so if that source changes after the fact, whether from a maintainer's mistake, a compromised upstream repository, or a deliberate attack, the hash no longer matches and the build simply fails. That failure is the whole point. It converts an invisible tampering event into a loud, blocking error.

Reproducibility at scale

Content-addressing is a strong theoretical guarantee, but it's fair to ask how well it holds up across a package repository as large as nixpkgs, which spans hundreds of thousands of packages maintained by a distributed community. Julien Malka, Stefano Zacchiroli, and Théo Zimmermann ran the first large-scale empirical study of this question, rebuilding 709,816 packages from historical nixpkgs snapshots to check whether the same inputs really do produce bitwise-identical outputs.

The result: bitwise reproducibility rates landed between 69% and 91%, with an upward trend over time. That's a meaningful number at that scale, not a rounding error, and it's evidence that the theoretical model actually holds up under real-world conditions most of the time. But it's not universal, and the honest reading of that data is that reproducibility is a strong, improving property of the Nix ecosystem, not an absolute guarantee for every package in every snapshot.

The xz supply-chain incident serves as a concrete stress test of what content-addressing does and does not protect against. A malicious maintainer spent roughly two to three years building trust within the xz project, a core compression library, before inserting a backdoor that enabled remote code execution via SSH. The backdoor was discovered essentially by chance, not by any automated defense. Julien Malka later published analysis examining how NixOS's build model could have helped detect an attack of that shape.

That case study points directly at the limits of what content-addressing solves. Nix stops someone from tampering with a source after its hash has been recorded, but it does nothing to stop a malicious package from being defined correctly in the first place, hash and all, if the source itself is already compromised at the moment it's pinned. Nix also doesn't analyze build artifacts for malware or known vulnerabilities; it's a control at the build integrity layer, not a substitute for a full supply chain security program. The entire chain of trust in nixpkgs eventually terminates at a binary seed, the bootstrap-tools tarball, which is checked into the repository, built once, and hashed. That seed asks for trust rather than proving it, unlike the rest of the system. It's a foundation the whole edifice rests on, and it's fair to treat it as the one link in the chain that asks for trust rather than proving it.

sbomnix: the primary tool suite for extracting Nix dependency graphs as SBOMs

Given all that structural advantage, the practical question is how to actually get an SBOM out of a Nix build. The primary answer is sbomnix, an open-source project from the Technology Innovation Institute (tiiuae), hosted on a code-hosting platform under the Apache 2.0 license. It's not a single command-line tool so much as a small suite built around a common idea: derive the bill of materials from the actual Nix graph rather than from a post-hoc scan.

The suite breaks into four pieces. sbomnix itself generates an SBOM from a given Nix derivation or output path. nixgraph queries and visualizes the dependency graph directly, useful when you need to see the shape of what you're shipping rather than just a flat list. nixmeta summarizes meta-attributes across a given nixpkgs version, things like licensing and maintainer data at the ecosystem level. And vulnxscan demonstrates what an SBOM is actually good for once you have one: it runs vulnerability scans against the generated bill of materials, closing the loop between "here's what's in the image" and "here's what's wrong with what's in the image."

The tooling accepts Nix flake references, raw derivations, and store paths as input, and it exports to the two formats that matter for interoperability with the rest of the security tooling ecosystem: CycloneDX and SPDX. One detail matters operationally more than it might first appear: sbomnix defaults to generating an SBOM of runtime dependencies. The tool can also be directed to build-time dependencies instead, since build-time information can be derived from the derivation graph alone. Which mode you want depends entirely on the question you're asking: what ships in the container, or what went into producing it.

Where nixpkgs metadata is available, typically for targets referenced by flake, sbomnix enriches each component with descriptions, license information, maintainer names, and homepage links. That's a meaningful step up from a bare list of package names and version numbers. It turns the SBOM into something a security reviewer or an auditor can actually read and act on, not just a machine-parseable artifact.

bombon and nix-sbom-helper: complementary tools and format-specific considerations

sbomnix isn't the only tool in this space, and the alternatives matter because they target different compliance needs and different gaps in ecosystem coverage. bombon, maintained by nikstur, generates CycloneDX v1.5 SBOMs specifically aimed at compliance with BSI TR-03183 v2.0.0, the technical guideline from Germany's Federal Office for Information Security, as well as alignment with US Executive Order 14028 on software supply chain security. It targets specific compliance requirements including US Executive Order 14028 on software supply chain security.

Several language ecosystems packaged within nixpkgs, most notably Rust and Go, vendor their dependencies rather than expressing each one as its own individual Nix derivation. Since bombon's approach relies on walking the derivation graph, it can't record vendored dependency information the same way it handles a standard Nix package tree. A Rust binary built in nixpkgs might have dozens of crates baked into its vendor directory, and none of them appear as distinct nodes bombon can enumerate.

Work is underway to close that gap rather than accept it as permanent. A fork hosted on another code-hosting platform (gitlab.com/gitlab-org/nix/bombon) extends coverage to Ruby's gem ecosystem, Python's PyPI packages, Go's standard library, and yarn2nix-managed dependencies from a widely used scripting language, all producing CycloneDX output. This is an active front of development, not a finished answer. Separately, there is also a CLI and library built on top of a project called Nixtract that generates SBOMs directly from Nix expressions without requiring the derivation to be built first. That's a meaningfully different tradeoff: useful for teams that need SBOM output before build artifacts exist, at the cost of not reflecting what a completed build actually produced.

Attaching SBOMs to OCI images: attestation, signing, and registry integration

None of this matters if the SBOM isn't tied to the artifact it claims to describe. An SBOM sitting in a separate file, unsigned and unlinked to the image it documents, is just a document someone could swap out or fall out of sync with reality. Verifiability requires binding: the SBOM has to travel with cryptographic proof that it belongs to this specific image and no other.

The standard mechanism here is build-time attestation. Container build systems with native attestation support can attach the generated SBOM as an in-toto attestation and push both the image and its SBOM to the registry in a single operation, so the image digest and the SBOM are cryptographically linked from the moment either one exists publicly. Because Nix produces a deterministic image digest given a fixed set of inputs, that attestation becomes something a CI pipeline can actually verify against, rather than just trust. Because that digest is deterministic given fixed inputs, it is something a CI pipeline can actually verify against.

sbomnix extends into this territory directly, offering SLSA provenance attestation tooling for Nix flakes and derivations that runs alongside its SBOM generation. That means provenance data and the bill of materials aren't two separate artifacts a team has to remember to keep in sync by hand. They travel together, generated from the same underlying graph, verified against the same hash.

Declarative Environment Management, SBOM Generation, and the Development Lifecycle

Treating SBOM generation as something that happens at image-build time is a reasonable default, but it draws the boundary in the wrong place. The dependency closure that Nix exposes doesn't come into existence when the container gets assembled. It exists from the moment the environment itself is declared, whether that's a developer's local shell, a CI runner, or a production host.

A declarative, version-controlled, pinned manifest makes that dependency graph auditable at every one of those stages, using the same inputs and producing the same hashes everywhere it's evaluated. That consistency is what makes the resulting SBOM trustworthy in the first place: the document only means something if the environment it describes is the same environment that's actually running.

Environment drift, a package version that differs between staging and production, an undeclared transitive dependency that snuck in through a side channel, a tool installed by hand on one engineer's laptop and nowhere else, is a provenance gap. It's a provenance gap. An SBOM generated from a drifted environment describes a system that no longer exists, which makes it inaccurate the moment it's produced, regardless of how carefully the generation tooling was built. Reproducibility and SBOM accuracy aren't two separate goals here; they're the same property viewed from two different angles.

When environments are versioned and addressable by name, with generation-based rollbacks available if something breaks, the SBOM for any given deployment is no longer a static artifact frozen at build time but something reproducible on demand, at any point in the system's history, from the same declarative source. That's the real destination this whole approach points toward: not a better scanner bolted onto the end of a pipeline, but a build model where the bill of materials was never separate from the build.

Sources

  1. Generate SBOM from OCI container made with Nix
  2. sbomnix - generate Software Bills of Materials for Nix - LinuxLinks
  3. Nix State of the SBOM | Engelen Open Source
  4. github.com
  5. SBOMs in 2026: Some Love, Some Hate, Much Ambivalence
  6. fosdem.org
  7. github.com
  8. github.com

More in SBOM Generation Workflows