Quickstart: request a hardened image
The fastest path is: import this flake, wrap a derivation you already build with
mkHardenedImage, nix build it, and then add the CI include to get signing,
attestation, and the scan gate.
1. A minimal consumer flake
You need a derivation for whatever you ship. It can be anything — pkgs.hello
stands in for your real service below. The public API lives at
hardened-builds.lib.${system} and is Linux-only (x86_64-linux,
aarch64-linux).
{
description = "My service, packaged as a hardened image";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
hardened-builds.url = "git+https://gitlab.com/gitlab-org/software-supply-chain-security/hardened-builds?ref=main";
hardened-builds.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
{ self, nixpkgs, hardened-builds }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
hb = hardened-builds.lib.${system};
# Your real package goes here (buildGoModule / buildRustPackage / …).
myService = pkgs.hello;
in
{
packages.${system}.my-service-image = hb.mkHardenedImage {
name = "my-service";
version = "0.1.0";
package = myService;
cmd = [ "/bin/hello" ];
};
};
}
Use the git+https URL as shown. Don’t use a gitlab: URL: Nix’s gitlab:
fetcher resolves a project in a subgroup to the wrong project, even with the
%2F encoding the Nix manual suggests
(NixOS/nix#6435).
hardened-builds is currently a private GitLab project, so nix needs your
GitLab credentials to fetch it. If you already clone from GitLab over SSH, this
makes git fetch this one repository over SSH, without changing the URL in your
flake.nix or flake.lock:
$ git config --global \
url."ssh://git@gitlab.com/gitlab-org/software-supply-chain-security/hardened-builds".insteadOf \
"https://gitlab.com/gitlab-org/software-supply-chain-security/hardened-builds"
In CI the slsa-l1 component handles this for you; see
Fetching hardened-builds in CI.
2. Build it
$ nix build .#my-service-image
That gives you the OCI image. mkHardenedImage also attaches a CycloneDX SBOM
by default; it hangs off passthru.sbom:
$ nix build .#my-service-image.passthru.sbom
That is everything the pure-Nix half produces: the image and its SBOM. It does not sign, attest, scan, or run an SLSA L3 build — those need this project’s signing key, Sigstore identity, and offline runners, which a Nix function you import cannot hold. See Signing & provenance and SLSA Level 3 builds.
3. Add the CI component
Drop this into your .gitlab-ci.yml. It builds .#my-service-image, generates
its SBOM and scans it on your project’s own runners, with SLSA Build L1
provenance written by GitLab Runner. For signed provenance on the offline SLSA
L3 tier, include /ci/components/hardened-artifact-slsa-l3.yml instead:
include:
- project: "gitlab-org/software-supply-chain-security/hardened-builds"
ref: main # pin to a release tag in real use
file: "/ci/components/hardened-artifact-slsa-l1.yml"
inputs:
flake-target: "my-service-image"
artifact-name: "my-service"
flake-target is your flake package output (.#my-service-image). The rest of
the inputs (sbom, scan, fips, …) default sensibly;
Integrate an external project walks through
each one, both build tiers, and which job runs where.
Scaffold instead of hand-writing
If you’d rather start from a working skeleton:
$ nix flake init -t 'git+https://gitlab.com/gitlab-org/software-supply-chain-security/hardened-builds?ref=main#default'
That drops a flake.nix and a starter .gitlab-ci.yml already wired to the
component. Adapt the package and the flake-target/artifact-name inputs.
Where to next
- Building from a source tree instead of a derivation? Pick your ecosystem: Go, Python, Ruby, Rust.
- Unsupported ecosystem or a custom build? Bring your own derivation.
- The exact signatures live in the API reference.