Automating SBOM Generation in GitHub Actions With Nix Flakes
Nix Flakes and automation ensure reproducible, audit-ready dependency inventories at every build.

In December 2021, the question "do we use Log4j, and where?" turned out to be unanswerable for most organizations running services in that language. Log4Shell exposed a basic inventory failure: companies could not enumerate their own dependency trees fast enough to know if they were exposed. That failure is the reason SBOMs (software bills of materials) moved from a niche compliance artifact to a wired-in requirement for continuous delivery, and Nix Flakes, paired with automated generation tools in GitHub Actions, offer one of the more rigorous ways to close that gap.
The industry data on where things stand is not encouraging. A large majority of organizations that generate SBOMs, 73%, report that doing so makes vulnerability mitigation more efficient. Yet 86% of organizations say SBOM generation is still difficult. That gap between "it helps" and "it's hard" is not a conceptual problem. Nobody is confused about what an SBOM is supposed to do. The difficulty is operational: generation is manual, it happens once per release cycle instead of continuously, or it lives outside the CI pipeline entirely, disconnected from the artifact it is supposed to describe. Roughly half of organizations that generate SBOMs do so only case-by-case, usually to satisfy a specific audit or customer request. A one-time SBOM filed away for compliance purposes creates a false sense of coverage, because the software described in that document is frozen at a single point in time while the deployed artifact keeps moving.
Regulatory pressure that makes per-build SBOMs a practical necessity rather than a best practice
The EU Cyber Resilience Act sets the nearest hard deadline that turns this from an efficiency argument into a legal one. Starting September 11, 2026, manufacturers of products with digital elements must report actively exploited vulnerabilities and severe incidents to regulators. Reporting on a clock like that requires knowing, in advance and without delay, what is inside a given product. An organization that has to go spelunking through build logs and package manifests after a vulnerability disclosure has already lost the race.
Full CRA application lands December 11, 2027, and at that point the obligations widen considerably: secure-by-design requirements, conformity assessment, technical documentation, CE marking, SBOM generation, vulnerability handling, and security update obligations all become enforceable simultaneously. The CRA's SBOM requirements are specific on a few points that matter for tooling decisions. The SBOM must be machine-readable, it must cover at least the top-level dependencies, it has to stay current rather than reflecting some earlier release, and it needs to live in technical documentation that's available to authorities on request. Crucially, the standard expects an SBOM built from what actually ships.
The penalty structure gives this teeth: up to €15 million or 2.5% of total worldwide annual turnover, whichever figure is higher. For a mid-sized software vendor, that percentage-of-turnover clause can exceed the fixed cap by a wide margin, which makes SBOM generation a board-level line item rather than an engineering nicety.
What Nix Flakes guarantee, and why that guarantee matters for SBOM accuracy
Nix treats every package as a pure function of its inputs. Give it the same inputs, get the same outputs, every time, on every machine. That property, reproducibility, means each package in a Nix build carries its exact dependency chain down to the specific version of the C library it was linked against. This is a different guarantee than what most package managers offer, where "gcc 12" might resolve to slightly different binaries depending on when and where the install happened.
Before Flakes existed, Nix's channel mechanism pinned configurations to a URL that could change underneath you. A nix-channel reference was mutable at the source, which undermined reproducibility exactly where it mattered most: at the boundary where external inputs entered the build. Flakes fixed this with a lockfile. A flake.lock file records the exact commit hash of every external input a project depends on, the same way a package-lock.json or Cargo.lock pins versions, with Nix extending that pinning deep through the build closure.
A flake itself is just two files: flake.nix, which declares inputs and outputs, and flake.lock, which pins those inputs to exact revisions. Together they form a hermetically sealed description of a build, whether that build produces a development shell, a container image, or an entire NixOS system configuration. As of early 2024, roughly 97% of nixpkgs, the central package repository for the Nix ecosystem, builds bit-for-bit identically across machines and across time. That figure is the highest reproducibility rate of any major Linux distribution, and it is precisely the property an SBOM needs in order to be trustworthy rather than aspirational. An SBOM generated from a non-reproducible build describes a moving target. An SBOM generated from a flake describes something fixed.
The three tools available for generating SBOMs from Nix Flakes
Three tools currently handle SBOM generation from Nix flakes, and they diverge on scope: whether they cover a single derivation or an entire flake's outputs, whether they default to runtime or buildtime dependencies, and which output formats they support.
sbomnix (upstream: tiiuae/sbomnix) generates an SBOM from either a flake reference or a Nix store path, with flake references recommended as the target. By default it captures runtime dependencies. It has to build the target first. Passing --buildtime skips that requirement and works from evaluation alone. When the target is a flake reference, sbomnix resolves the nixpkgs version behind it and enriches the output with descriptions, license data, maintainer information, and homepage links; that enrichment step is skipped when the target is a raw store path, since there's no flake metadata to resolve against. Output comes in both CycloneDX and SPDX formats, and the document metadata records provenance details like nixpkgs:rev, nixpkgs:flakeref, and nixpkgs:version. There's also an experimental feature where sbomnix will prefer a package URL (PURL) sourced directly from meta.identifiers.purl in the derivation's JSON output, when that field is populated, over generating its own.
sbomnix ships alongside a set of companion tools in the same repository: nixgraph for visualizing dependency graphs, vulnxscan for scanning the generated SBOM against known vulnerabilities, repology_cli and repology_cve for cross-referencing package databases, nix_outdated for flagging stale dependencies, and a provenance tool for generating SLSA v1.0 attestations. There's also flakevuln, a reusable GitHub Action bundled in the same repo, built for running automated daily vulnerability scans against a Nix flake project. sbomnix can be invoked directly without installation via nix run github:tiiuae/sbomnix#sbomnix -- --help. It fits teams that want a single-derivation or narrowly targeted SBOM with rich metadata, vulnerability scanning, and provenance attestation bundled into one toolkit.
FlakeBOM takes a different approach. It exists because per-derivation tools like sbomnix (and a tool called bombon) require some integration work to cover an entire flake's output set; FlakeBOM instead runs against any flake with no special wiring and generates an SBOM spanning all of that flake's outputs. It's written in Rust and integrates directly with Determinate Nix's C API, and it's built for scale: dependency trees involving tens of thousands of components, including call stack frames. Run with no arguments, it generates an SBOM from the flake in the working directory, recursively walking every derivation in the flake's schema and representing each as a component with its store path, package name, and version. Output is CycloneDX, and it includes Vulnerability Exploitability eXchange (VEX) metadata, which flags whether a known vulnerability in a component is actually exploitable in the context it's deployed in, rather than just present. FlakeBOM fits teams already on Determinate Nix who want whole-flake coverage and VEX data without hand-wiring per-derivation scans.
nix-sbom-helper, from cyberus-technology, takes a third path: it produces CycloneDX JSON, CSV, and SPDX JSON all from a single nix-build invocation, rather than requiring separate runs per format. For flakes specifically, nix-sbom-helper.sbomsForFlakeOutputs self generates SBOM attributes for every packages and nixosConfigurations output in the flake with minimal setup. For standard Nix usage or when finer control over a single derivation is needed, buildSbom { input = pkgs.hello; } handles it with one argument. It also supports full NixOS system SBOMs by targeting the top-level system build output. On reproducibility, when no external metadata is supplied, the tool's output is itself reproducible, because it wraps sbomnix internally (which uses a stable UUIDv5) and forces a fixed timestamp at build time. This tool suits teams that want all three output formats from a single invocation, or teams targeting full NixOS system configurations rather than individual packages.
Buildtime vs. runtime dependencies in Nix
The distinction between buildtime and runtime dependencies in Nix is not academic, and it changes what an SBOM actually certifies.
The buildtime closure includes everything listed in a derivation's .drv file: compilers, build tools, standard libraries, and the bootstrap infrastructure Nix uses to construct anything. Even a trivial hello-world C program pulls in more than 150 packages once gcc, stdenv, glibc, and bash are all accounted for. The useful property here is that computing the buildtime closure only requires evaluating the derivation, not building it. That makes buildtime SBOMs cheap and fast, which is what you want for a pre-build gate in CI that needs to check dependency data before spending compute on an actual compile.
Runtime closure is narrower: it's the subset of buildtime dependencies that the built output actually references once it exists, determined by Nix scanning the output for store path references. That distinction matters directly for security triage, because runtime dependencies are what actually gets deployed and exposed to a network or an end user, while the compilers and build scaffolding sitting in the buildtime closure generally don't ship. The tradeoff is that runtime dependency data requires the target to actually be built first. Any tool defaulting to runtime scope, sbomnix included, will force a full build (a "force-realise" in Nix terminology) of the target before it can produce output, which has real time and compute cost in a CI pipeline.
Wiring sbomnix into a GitHub Actions workflow step by step
Two prerequisites come before any of this: the repository needs a committed flakelock file, and GitHub Actions needs to be enabled on the repo. On the lockfile point there's no flexibility. Floating references defeat the entire premise of a reproducible SBOM, so a committed lockfile is a security requirement here.
Step 1: install Nix. A GitHub Action installs Nix on the runner with flakes support enabled, so flake-based workflows are ready to use.
Step 2: restore the binary cache. A binary cache action makes previously built paths available to the runner, paths that may have been built in prior runs. Without this step, every CI run rebuilds dependencies from scratch, and since runtime SBOM generation with sbomnix requires a full build, an unmediated cache miss can turn a fast SBOM step into a significantly slower one. Restoring the cache keeps that cost from compounding on every push.
Step 3: validate the lock file. A validation step can verify that the root nixpkgs inputs meet whatever policy the project has set. It can be configured with custom policy logic to catch supply-chain drift before it reaches the build step. This validation should run before the build itself, so that a compromised or drifted lockfile fails the pipeline immediately instead of getting baked into an SBOM that looks clean but describes a tampered dependency tree.
Once those three steps are in place, sbomnix runs against the flake reference and writes a CycloneDX or SPDX document as a build artifact, on every push, without anyone opening a terminal. That output can then be stored, diffed against the previous run, and queried directly when a CVE drops. Vulnerability response no longer requires a scramble to reconstruct what shipped: it becomes a lookup against a document that was already generated, timestamped, and tied to a specific commit hash, precisely the posture Log4Shell, and the regulatory reporting obligations that followed, made clear was overdue.



