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

Go

mkSecureArtifact detects a Go project by the presence of a go.mod file at the root of src. It then builds the module with buildGoModule and hands the result to mkHardenedImage.

The marker

Detection is file-presence based: a go.mod at the top of src means “go”. If a .goreleaser.yml / .goreleaser.yaml is also present, detect records it as the buildConfigFile, but it does not act on it automatically — you opt into the goreleaser path explicitly (below).

Required and optional ecosystemArgs

  • vendorHash (required) — the buildGoModule vendor hash. There is no way around this; a missing vendorHash throws at eval time.
  • subPackages (optional) — which packages to build (default [ "." ]).
  • goreleaser (optional) — a parsed goreleaser config; when present, the build args (subPackages, ldflags, CGO_ENABLED) come from it instead of subPackages.
  • goVersion, buildId (optional) — only meaningful on the goreleaser path.

Plain buildGoModule (subPackages)

hb.mkSecureArtifact {
  name = "my-tool";
  version = "1.2.0";
  src = ./.;
  ecosystemArgs = {
    vendorHash = "sha256-…";
    subPackages = [ "cmd/my-tool" ];
  };
  image.cmd = [ "/bin/my-tool" ];
}

Anything under image.* is forwarded to mkHardenedImage — that is how you set cmd, entrypoint, env, and so on.

The goreleaser path

Many gitlab-org/* Go tools drive their release builds with goreleaser. Parse the config and pass it through so the resulting binary carries goreleaser’s ldflags (version stamping, CGO_ENABLED, the right main package):

let
  gorel = hardened-builds.lib.${system}.parseGoreleaser ./.goreleaser.yml; # see note
in
hb.mkSecureArtifact {
  name = "glab";
  version = "1.40.0";
  src = ./.;
  rev = self.rev or "0000000000000000000000000000000000000000";
  ecosystemArgs = {
    vendorHash = "sha256-…";
    goreleaser = gorel;
    goVersion = "1.22.3";
  };
}

The emitter resolves goreleaser’s {{.Version}}, {{.ShortCommit}}, and {{.Env.GO_VERSION}} template placeholders by plain string substitution against version, rev, and goVersion. rev must be the real full commit SHA — {{.ShortCommit}} is the first 7 characters of it — even if you fetch the source by tag elsewhere. Any other unresolved {{…}} placeholder throws at eval time rather than baking a literal template string into your binary.

Note: the goreleaser parser lives in lib/ecosystem/goreleaser.nix. Wire it into your consumer flake the same way you wire the rest of the API; the exact attribute name is in the API reference.

Derivation-first alternative

If the source-first dispatch doesn’t fit — a monorepo with several binaries, an unusual build, or you already have a package — build the buildGoModule derivation yourself and pass it straight to mkHardenedImage:

let
  myTool = pkgs.buildGoModule {
    pname = "my-tool";
    version = "1.2.0";
    src = ./.;
    vendorHash = "sha256-…";
    subPackages = [ "cmd/my-tool" ];
  };
in
hb.mkHardenedImage {
  name = "my-tool";
  version = "1.2.0";
  package = myTool;
  cmd = [ "/bin/my-tool" ];
}

You get the same image and SBOM either way; mkSecureArtifact is just the convenience wrapper around detect-then-build-then-image.

SBOM

The Go module set and the Go stdlib are folded into the CycloneDX SBOM automatically (the SBOM builder reads them off the finished buildGoModule derivation’s goModules). No extra configuration needed. See SBOMs.

FIPS

Go’s FIPS story is BoringCrypto (ADR-0026). Pass fips = true only if you built the package under the FIPS package set — the flag applies FIPS labels but does not convert a non-FIPS build. See FIPS.