Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SBOMs

Every hardened image comes with a CycloneDX SBOM. This page covers what it covers, how to get it, and the one rule you have to follow to keep it honest.

What it covers

The SBOM builder (lib/sbom, ADR-0036) wraps bombon to produce a complete inventory:

  • The Nix runtime closure — every store path the image needs at runtime, the same walk sbomnix did (ADR-0015).
  • The language-ecosystem dependencies that Nix vendors into a single store path and that a closure-only walk therefore misses: Go modules plus the Go stdlib, Rust crates, Ruby gems, Python wheels/sdists, and yarn/npm packages.

That second half is the part that matters for vulnerability matching. A CVE against a specific gem, crate, module, or wheel only shows up if that dependency is named in the SBOM — a closure-only SBOM can’t match it, so the scan gate would silently miss it. See VEX & vulnerability scanning.

How to get it

In pure Nix, the SBOM hangs off the image’s passthru:

$ nix build .#my-service-image.passthru.sbom

mkHardenedImage (and therefore mkSecureArtifact) attaches it by default (sbom = true). Set sbom = false to skip it, in which case passthru.sbom is absent.

In CI, the component’s hardened:sbom job builds passthru.sbom. Under slsa-l1 it substitutes from the build job’s cached closure where it can, and builds or fetches anything missing. Under slsa-l3 it runs on the shared runner with --max-jobs 0 (substitute-only — the closure comes from the offline build’s Attic push, never a rebuild), copies out the *.cdx.json, and — when sign = true — signs it. See Integrate an external project.

Auto-detection vs the sbomEcosystems override

The builder auto-detects go and rust from robust markers on the finished derivation (goModules, cargoDeps) and consumes them without rebuilding.

Ruby and yarn/npm can’t be auto-detected — their inventory lives in a gemset.nix or a yarn.lock, not in anything readable off the built derivation. For those you declare the ecosystem explicitly with sbomEcosystems, which is threaded onto the package before imaging:

hb.mkHardenedImage {
  name = "my-rails-app";
  version = "3.1.0";
  package = myRailsApp;
  sbomEcosystems = [
    { kind = "ruby"; gemset = ./gemset.nix; }
    { kind = "yarn"; yarnLock = [ ./yarn.lock ]; }
  ];
}

The no-IFD vendored-path rule

Every lockfile, gemset, or yarn.lock path in sbomEcosystems must be a vendored relative path (./gemset.nix, ./yarn.lock) — never an interpolated read out of a fetched source like "${src}/yarn.lock". Reading a lock file out of a fetched derivation is an import-from-derivation, which nix flake check cannot evaluate and the repo’s no-ifd-lockfile lint rejects. Copy the lock/gemset into your repository and reference it relatively.

This is the same discipline the Rust cargoLock, Python lock, and Ruby gemset args follow throughout the ecosystem emitters — see those pages for the per-ecosystem detail.

Reference

The design and rationale (why bombon, why our own consuming emitters rather than bombon’s rebuild-the-package emitters) are in ADR-0036.