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

nordisk/holger

.nornir/appliance-for-idiots.md

holger as an appliance — the identity ceremony, for idiots

What this is. holger built as an appliance: a box (container today, ISO later) that takes the WireGuard hub's PUBLIC key IN as a file, mints its OWN keypair on first start, and gives its public key OUT — on stdout and in a file — so an operator can register it with the hub. The identity survives reboots, container replacement, and image upgrades. One implementation for the whole suite: edda's wg-appliance-core::ceremony (gunnar consumes the same crate); holger implements nothing ceremony-shaped itself. Everything below was run for real on 2026-08-13 (branch feat/appliance-identity); transcripts live in the commits.


§1 The three files and the one line (fixed forever)

Everything lives under the durable root /data (override for tests: HOLGER_APPLIANCE_DATA=<dir>), in wg/:

file direction what
/data/wg/hub.conf IN the hub's side: ONE [Peer] with PublicKey, AllowedIPs, optional Endpoint + PersistentKeepalive. Everything else — including PrivateKey/PresharedKey, by name — is refused with file:line.
/data/wg/identity.key (stays put) the appliance's private key, minted once, 0600. The file IS the identity.
/data/wg/identity.pub OUT the public key, re-derived from the key file whenever missing or stale — read it back without parsing logs.

The ONE canonical line every product prints and every watcher greps (wg_appliance_core::IDENTITY_LINE_REGEX is the published regex; parse_identity_line the shared reader — never write your own):

wg-appliance identity: product=holger pubkey=<44-char-base64>

On the boot that MINTS, one extra loud line precedes it (the word MINTED appears in no other output — a new identity is one grep, and it means the hub must be updated):

wg-appliance identity: MINTED new keypair at /data/wg/identity.key (product=holger) — register the public key below with the hub

§2 The gate — declared, not folklore

The ceremony runs iff env HOLGER_APPLIANCE=1, and only binaries built with the appliance cargo feature carry it (the mini and mvp-server atoms do; nothing else changed — a plain holger-mvp start on a dev box prints nothing appliance-shaped). The container cell DECLARES the env into the image, so "this artifact is an appliance" is a property of the declaration. A binary built without the feature but started with the env set refuses loudly and exits nonzero — never a silent non-appliance that looks healthy.

Durability laws, enforced and robot-proven: an existing key is NEVER silently re-minted; a corrupt/truncated/group-readable key file REFUSES BY NAME (the file is evidence — never rewritten); a declared hub.conf whose tunnel cannot come up (no /dev/net/tun, no CAP_NET_ADMIN) is a loud wg-appliance tunnel: FAILED — … naming the precondition plus a nonzero exit — never half-up. No hub.conf at all is the legitimate first-boot shape: key out, serve on.

§3 The container shape (works today)

The declared cell is [[build.container]] holger-mvp-appliance in .nornir/build-variants.toml: base = "scratch" carrying the mvp-server-musl atom's STATICALLY LINKED binary (the 14th atom, ruled in 2026-08-13 when the musl wall fell — the image is holger and NOTHING else: no libc, no shell, no base pull), HOLGER_APPLIANCE=1 in the image env, /data as the ONE volume (config + registry state + wg identity), archive export .container-out/holger-mvp-appliance.tar so a release carries the image as a file. Building the musl atom needs the declared cross toolchain (.cargo/config.toml §musl — a linker answering to x86_64-linux-musl-gcc; the zig recipe lives beside the cell in build-variants.toml). Two recorded consequences: scratch has no /etc/passwd so the container runs uid 0 (root-in-userns under rootless podman), and the old glibc-floor law (cc-debian13, host-2.39-vs-debian12-2.36 — measured) is moot for a static layer.

Run it (rootless podman; :U chowns the volume to the container user):

podman run --rm -v holger-data:/data:U holger-mvp-appliance:0.6.9

First start prints MINTED + the canonical line; podman logs shows it, and the volume's wg/identity.pub agrees. With a hub declared, add the tunnel preconditions or it fails loudly by design:

podman run --rm -v holger-data:/data:U --cap-add NET_ADMIN --device /dev/net/tun holger-mvp-appliance:0.6.9

§4 Prove it yourself (the robots)

# the ceremony contract on the real binary — 6 robots, fast, no container
cargo test -p holger --no-default-features --features mvp-server,appliance --test appliance_gate

# the container shape end-to-end — heavy, pulls the base once, opt in:
cargo test -p holger-build-space --test appliance_container_robot -- --ignored --nocapture

# the shared ceremony's own suite (in ../edda)
cargo test -p wg-appliance-core

The robots' reds are the point: deleted key → LOUD new mint; corrupt key → named refusal, file byte-identical afterwards; unknown hub.conf key → named with its line; declared hub without tun → the FAILED line; gate off → not one line of ceremony output.

§5 What is NOT done (measured walls, owners named)