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

Signing & provenance

Signing and provenance are delivered by the CI component, not the Nix API. This page explains what gets signed, why it can’t be pure Nix, where it runs, and how to verify it.

Of the two CI components, only slsa-l3 (hardened-artifact-slsa-l3.yml) signs. The slsa-l1 component signs nothing; its SLSA provenance is the unsigned artifacts-metadata.json GitLab Runner writes for the build job. See Integrate an external project.

What gets signed

The component’s hardened:sign job produces three keyless Sigstore signatures (ADR-0015):

  1. The artifact — cosign sign-blob over the built image path.
  2. The SBOM — when sbom = true, the CycloneDX *.cdx.json.
  3. A SLSA v1.0 provenance statement — when attest = true, an in-toto Statement with a https://slsa.dev/provenance/v1 predicate naming the flake target, the builder, and the artifact digest, itself signed.

Each produces a .sigstore.bundle artifact. Signing is keyless: the job requests a Sigstore OIDC token (SIGSTORE_ID_TOKEN, aud: sigstore) and cosign records the signature in the transparency log against that CI identity.

Why it can’t be pure Nix

mkHardenedImage / mkSecureArtifact produce the image and SBOM as deterministic, IFD-free Nix — no secrets, no network. Signing is the opposite: it needs an OIDC identity, network access to the Sigstore infrastructure, and the transparency-log upload. A Nix function you import into your own flake can’t hold any of that, so it doesn’t pretend to. The honest boundary is: pure Nix makes the thing; CI signs the thing.

Where it runs, and why it’s a boundary

Signing runs only on the networked nix-attic-shared runner, never on the offline SLSA L3 build VM (ADR-0035). The build VM has no key, no network, and no secrets by construction — that’s what makes its build trustworthy. Adding a signing step, a registry login, or any secret to the build job would break that invariant, so the component never does. The signing job resolves the already-built image with nix build … --max-jobs 0 (substitute-only, so it signs exactly what the offline VM built) and signs it there.

See SLSA Level 3 builds for the full isolation model.

Verifying

With cosign, verify a bundle against the CI identity that produced it:

$ cosign verify-blob \
    --bundle my-service.sigstore.bundle \
    --certificate-identity-regexp 'https://gitlab\.com/.*' \
    --certificate-oidc-issuer https://gitlab.com \
    "$IMAGE_PATH"

Use the SBOM bundle to verify the SBOM the same way, and the provenance bundle to verify provenance.json. Match --certificate-identity / --certificate-oidc-issuer to the project and issuer that ran the pipeline.

Reference

Sigstore-over-Nix and the SBOM/provenance shape are ADR-0015; the offline-build / signing-on-shared-runner boundary is ADR-0035.