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

How it fits together

This page is the map: the detect → emit → image pipeline, the pure-Nix / CI split, and how the whole thing composes builders that already existed in this repo instead of re-implementing them.

The pipeline

The public API (lib/api.nix, ADR-0039) is a thin composition. Nothing in it is new machinery — it wires together stages that already exist and are already tested:

src ──▶ detect ──▶ emit ──▶ mkHardenedImage ──▶ image + SBOM
        (IR)      (derivation)   (buildGitlabImage
                                  + lib/sbom)
  • detect (lib/ecosystem/detect.nix, ADR-0032) — file-presence detection. It looks for a small fixed set of marker files and returns a normalized IR ({ ecosystem; toolchainVersion; primaryMarker; buildConfigFile; }). The markers are go.mod → go, Cargo.toml → rust, pyproject.toml → python, Gemfile.lock → ruby. It is not a general project-type solver; it exists so the dispatch is uniform. If nothing matches, it throws at eval time naming the markers it searched and the escape hatches — no silent empty image.
  • emit (lib/ecosystem/{go,rust,python,ruby}.nix) — per-ecosystem emitters that assemble the arguments for the matching nixpkgs builder (buildGoModule, rustPlatform.buildRustPackage, buildPythonApplication + buildPoetryDeps, bundlerEnv + enrichGemset). They emit build args; they don’t re-implement dependency fetching.
  • mkHardenedImage — wraps the built derivation into a hardened OCI image via buildGitlabImage and attaches its CycloneDX SBOM via lib/sbom.

mkSecureArtifact is the umbrella that runs all three stages, each overridable: force the ecosystem with ecosystem, skip detection-and-build entirely with overrideDerivation, or forward image args through image. mkHardenedImage is the derivation-first core that mkSecureArtifact funnels into once it has a derivation.

Composition, not reimplementation

The API deliberately reuses existing, tested builders:

ConcernDelegated to
OCI imagelib/gitlab-container-builder.nix → pkgs.buildGitlabImage
SBOMlib/sbom (bombon-backed, ADR-0036)
Ecosystem detectionlib/ecosystem/detect.nix (ADR-0032)
Ecosystem build argslib/ecosystem/{go,rust,python,ruby}.nix

buildGitlabImage is the same builder GitLab’s own service images use — no shell in production images, non-root by default, minimal layers, standard OCI labels. Building the image under the FIPS package set is what flips its FIPS marker so FIPS labels appear (see FIPS).

The pure-Nix / CI split

Everything above is pure Nix: deterministic, IFD-free (a consumer’s nix flake check --no-build evaluates cleanly), and free of secrets. It produces the image and the SBOM and nothing more.

The rest — signing, attestation, the offline SLSA L3 build, the scan gate — cannot be a pure function: it needs this project’s signing key, its Sigstore OIDC identity, network, and offline runners. That half lives in the reusable CI components (ci/components/hardened-artifact-slsa-l3.yml, and hardened-artifact-slsa-l1.yml for the unsigned slsa-l1 tier). The slsa-l3 component builds the pure-Nix output on the offline SLSA L3 tier and then signs/attests/scans it on the networked shared runner. The security boundary between those two runner roles is authoritative (ADR-0035):

offline SLSA L3 VM              nix-attic-shared (networked)
──────────────────             ─────────────────────────────
build .#target from source ──▶ substitute closure (--max-jobs 0)
  no secrets, no network         └─ sbom · scan+gate · sign · attest
  no signing key                    key + Sigstore identity live here

A cache miss on the shared runner fails rather than rebuilding off-VM, so the signed artifact is provably the offline VM’s output. Signing never touches the offline VM by construction.

IFD-free discipline

The whole surface is import-from-derivation-free. Lockfiles and gemsets must be vendored relative paths (./Cargo.lock, ./poetry.lock, ./gemset.nix), never read out of a fetched src. That’s what keeps nix flake check --no-build evaluable for consumers, and it’s enforced by the repo’s no-ifd-lockfile lint.

Where to read more

  • The public API and its two-halves rationale: ADR-0039.
  • The auto-ecosystem derivation library (detect + emitters): ADR-0032.
  • The offline SLSA build VMs and shared Attic runner: ADR-0035.
  • The ecosystem-aware SBOM: ADR-0036.
  • Function signatures: the API reference.