Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

docs/v2/08-supply-chain.md

8. Supply chain

This chapter says what Holger checks on the way in, what it signs itself, what it exports, and what a downstream checks. Every claim names the code that does it.

On the way in: provenance

The provenance verifier is the crate holger-provenance (provenance/). It runs at the intake's SCANNED step as the scanner holger-provenance (server/lib/src/scanner/provenance.rs), inside the scan sandbox. It makes no network call. The material that came with an artifact waits beside it, by its sha256, in <data_root>/holger-provenance/<sha256>/.

DSSE and in-toto

SLSA Provenance

rules: (
    require_provenance: true,
    keys: [(id: "d24d3785e8281bcd37e8f2c40a3a85e8152d3ce2200a5c929a1a5ca636e8249e", alg: ecdsa_p256, public_key: "<PEM>")],
),
// on a source:
gates: (provenance: require(
    builders: [(id: "https://git.gunnar.rs/nordisk/dwarves/slsa/builders/dwarves-server/v1", level: 2)],
    slsa_min: 2,
)),

Holger's own releases are Build L2 (chapter 7), so the dwarves builder is named at level 2; a builder you trust at L3 is written level: 3 with slsa_min: 3.

Sigstore keyless, offline

For a keyless signature the verifier checks, with no network (provenance/src/verify.rs, provenance/src/tlog.rs):

The pinned root is a signed bundle of the product holger.sigstore-trust/v1 (provenance/src/trust.rs), named by rules.trust_root. It carries trusted_root.json and the pinned APK and OpenPGP keys (apk-keys/<name>, pgp-keys/<name>).

Keyless cosign signatures are verified on intake this way; real cosign 2.4.3 and 3.0.2 verify through Holger with --certificate-identity and --certificate-oidc-issuer against an offline test root.

The estate's trust root (server/lib/src/sigstore_trust.rs):

Not checked offline, and said so in the evidence: a Rekor consistency proof, certificate revocation and RFC 3161 timestamps. If the trusted root pins no CT log, the SCT is recorded as "not checked" instead of refused.

Ecosystem proofs

The material parser (provenance/src/material.rs) reads:

proof what it is
PyPI PEP 740 the attestations PyPI publishes beside a file
npm provenance the dist.attestations of a package, and the registry's dist.signatures
cosign / Chainguard the .sig and .att attachments of an OCI image
.intoto.jsonl DSSE envelopes, one per line (dwarves' release bundles are this)
Sigstore bundles v0.1 to v0.3
APK the signed APKINDEX.tar.gz of Wolfi, Chainguard and Alpine, against a pinned apk key (provenance/src/apk.rs)
Maven .asc OpenPGP signatures against a pinned key, opt-in feature maven-pgp (provenance/src/pgp.rs)

Scanners in a sandbox

The server never unpacks, parses or scans an artifact itself. The scanners (server/lib/src/scanner/):

kind scanner
CVE modgunn-osv, against a local advisory database (OSV, NVD, KEV); a scan never dials
malware holger-inspect (archive bombs, path traversal, nesting, executables), YARA, ClamAV
licence holger-license
secrets holger-secrets, rules as data (server/lib/src/scanner/secrets-rules.ron)
provenance holger-provenance (above)
SBOM holger-wheel-sbom (Python wheels), holger-image-sbom (container images)
models holger-model (GGUF, Safetensors, ONNX; the pickle family is blocked)

The INGEST walk opens every container it meets (holger-inspect, server/inspect/src/walk.rs, magic.rs): zip, tar and gzip; bzip2; xz (decoded in memory and held to max_nested_bytes, past it NestedTooLarge); zstd; ar (a .deb, GNU and BSD long names); rpm (the lead and headers, and the payload in gzip, xz, zstd, bzip2 or plain cpio newc); conda (.conda, a zip around tar.zst, and the old .tar.bz2). Compression around a tar is opened one level. A 7z, or anything else the walk cannot read, blocks as not-unpacked. An absolute symlink inside a deb or rpm payload is reported as Info (it is a root file system); anywhere else it blocks. Known limit: a .deb whose data.tar.xz decodes past 256 MiB blocks.

The scanners and the walk run in holger-scan-worker, one rootless podman container per artifact (server/sandbox/, image localhost/holger-scan-worker:<Holger release>, :3.1.0 for Holger 3.1.0, which must be this server's build: chapter 5): --network none, --pull never, the artifact read-only, a read-only root, a capped tmpfs, --cap-drop all, uid 65532, a deadline. ClamAV is clamd in its own container (holger-clamd). On the OS-less appliance the engine is a pinned static crun that Holger drives itself (server/sandbox/src/crun.rs). The worker returns one JSON report; the server keeps the advisory match and the evidence.

The worker's root filesystem is hashed again at every start, and a changed one is laid again (holger-sandbox-14); podman runs the worker image by its digest. A real build talks to ClamAV only over the sandboxed clamd's unix socket: a TCP or host clamd, clamscan, or no socket refuses the start (holger-sandbox-15). The sandboxed clamd runs with AlertExceedsMax yes, so a file past its scan limits is reported, not passed.

promotion.intake.rules: (sandbox: required) is the default. off exists only in a test build; a release build refuses it (holger-sandbox-6). No engine means every scan is refused (holger-sandbox-1), never an in-process fallback.

Signed scanner signatures

ClamAV databases and YARA rules reach Holger as a signed bundle of the product holger.scanner-sigs/v1 (intel/src/sigs.rs). It holds ClamAV database files (payload/clamav/<name>) or YARA files (payload/yara/*.yar, *.yara) and nothing else (holger-sigs-1).

Coming: a bundle that withdraws a file; the scanners reading the installed YARA rules.

Untrusted work runs as jobs

Every piece of untrusted work is a job: every scan, and every bundle import (the inbox bundle.inbox, POST /-/bundle/import, gRPC ImportBundle, holger bundle import, holger bundle allow-first-import --from-bundle). The job API is Heimdall's nordisk-heimdall-core 0.2 from crates.io; the runners are holger-job-runner (jobs/runner).

OpenZL bundles verify inside the Heimdall guest

In Heimdall (heimdall main 6579b33, docs/OPENZL-WASM.md), the verify-bundle guest carries OpenZL's C decoder compiled to wasm32: decoder only, vendored from openzl-sys-rs 0.4.1+openzl.0.3.0 with one patch. The guest imports nothing but the WIT emit-file.

Multi-core bundle verification in Heimdall

In Heimdall since 0.2 (docs/GATLING-WASM.md in the heimdall repository); Holger vendors the released guests (heimdall-guests-0.2.1) under guests/heimdall/0.2.1/, with their pins, SLSA statement and signatures.

With engine: heimdall and its guests: (the estate and the appliance medium), the import door verifies every bundle in this split guest inside the worker (server/lib/src/job_runner/verify_bundle.rs, feature heimdall-wasm). Without it the verify job runs Holger's native verifier inside the container.

Coming: the fetching downstream's fetch as a job; intel import as a job.

SBOM

The SBOM is CycloneDX 1.5 (server/lib/src/sbom.rs). It is filed as a signed DSSE in-toto statement with the predicate https://cyclonedx.org/bom, beside the scan evidence (server/lib/src/scanner/attestation.rs).

Two kinds of artifact get a generated SBOM:

Other formats have no generated SBOM yet.

Holger signs its own evidence

Scan results (predicate HOLGER_SCAN_EVIDENCE_V1) and the SBOM are DSSE envelopes signed by Holger's attestation signer (server/lib/src/attest.rs, AttestSigner). The key is Ed25519 (the default) or ECDSA P-256, given by the operator as PKCS#8 with an X.509 certificate: attestation: (signer_key, signer_cert, signer_alg), or held on a hardware token over PKCS#11 (attestation.signer_hsm, chapter 10). Holger mints no key of its own. The same signer signs the checkpoints of the hash-chained audit log.

The export: one signed bundle

holger export (gRPC BundleService.Export) takes the AVAILABLE set of the production repository and writes one bundle (server/lib/src/promote/airgap.rs, update-bundle/):

What crosses: the export profile. Every way out (holger export, BundleService.Export, the diode push) carries only what decides (server/lib/src/export_profile.rs, a guard over 15 structures). There is no RON switch.

The same bundle is carried on a USB stick, pushed through a one-way diode, or fetched online:

The downstream

The downstream (on-prem) runs the same checks whether the bundle came over the network or on a stick:

It does not re-check the original package's SLSA or Sigstore proofs, and it does not scan again. The upstream did those checks, and the import records the upstream-signed evidence. The proofs do travel: an export from a Holger with the provenance scanner puts each artifact's provenance material in the bundle (<artifact>/provenance/<file>, the index's provenance field), and the downstream build links the verifier (holger-provenance, through server-lib intake). But the downstream has no scanners, so its judge_carried_provenance returns nothing (server/lib/src/import_door.rs). A full Holger with scanners that imports a bundle does verify the carried provenance again, under its own policy, and re-runs the malware scans.

The first import

A downstream with no accepted bundle yet has no rollback floor, so its first import is a secure-side admin's decision:

Signatures through Holger: OCI referrers

Signing tools find an image's signatures, attestations and SBOMs through Holger as a registry (server/lib/src/referrers.rs, server/lib/src/pull_intake/):

cosign verify-attestation --key https://<holger>/-/evidence-key \
  --type https://holger.rs/attestation/scan-evidence/v1 --insecure-ignore-tlog=true <image>

Tested with real clients: cosign 1.13.6, 2.4.3 and 3.0.2, notation 1.3.2, oras 1.2.3 and kyverno 1.13.4 (kyverno apply with verifyImages) (server/lib/tests/real_signing_clients_verify_through_holger.rs, server/cli/tests/downstream_serves_the_build_systems.rs; CI steps real-clients and real-clients:downstream).

Gone since the lean cut

The lean cut of 2026-10-17 (holger 9521bf06) removed:

Open questions

  1. Should the downstream re-check the original SLSA and Sigstore proofs? The material is already in the bundle and the verifier is already linked; it needs the provenance scanner turned on in the downstream build, and a trust root and policy on the downstream.
  2. A USB stick from a source that is not Holger: accept only Holger-signed bundles, or also verify raw SLSA and DSSE files on import?

Moved from v1

Still in v1