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):
- The artifact —
cosign sign-blobover the built image path. - The SBOM — when
sbom = true, the CycloneDX*.cdx.json. - A SLSA v1.0 provenance statement — when
attest = true, an in-toto Statement with ahttps://slsa.dev/provenance/v1predicate 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.