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

nordisk/holger

docs/v2/06-pairing.md

6. Pairing

Pairing is how the boxes of an estate (front, main, twin) and the downstreams find each other and decide whom to trust. The pairing code is the crate nordisk-pairing (Skidbladnir, pinned in Cargo.lock), shared with Gunnar. Holger's side is server/cli/src/paired.rs, holger-estate-front/ and server/lib/src/sync/.

The boxes and their links

                      the ONE box (installer)
                             │ udp/51821 wg-installer → tcp/8474 bootstrap (mTLS, installer CA)
                             ▼
 internet ──443──► FRONT 10.44.0.1 ──── wg0 udp/51820 (+ PSK) ────┬──► MAIN 10.44.0.2/.3
                     │                                           └──► TWIN 10.44.0.2/.3
                     │                                                 ▲
                     │                          replication door (gRPC port + 1, TLS 1.3, client cert)
                     │
                     └── holger-ds 10.46.0.1/24, udp/51830 ──► DOWNSTREAMS (sync door tcp/50070)
port what trust
udp/51821 → tcp/8474 the installer link wg-installer (10.45.0.2/30, installer 10.45.0.1) and the bootstrap listener mTLS under the installer CA
udp/51820 wg0, the estate overlay 10.44.0.0/24 (front .1, the boxes .2 and .3) WireGuard keys and a pre-shared key
tcp/50051, tcp/8443 gRPC control and the HTTP repositories, over wg0 the front's console bearer
gRPC port + 1 (50052) the replication door between main and twin TLS 1.3, a client certificate from the main's own issuer
80, 443 the public web door on the front Let's Encrypt (ACME)
udp/51830, tcp/50070 the downstream hub holger-ds (10.46.0.1/24) and the sync door WireGuard keys and a pre-shared key

The front dials the boxes only at their overlay addresses (holger-estate-front/src/wg_only.rs).

Seeds and certificates

The ONE box makes one seed per box. A seed is a TOML file, <root>/pairing/seed.toml, where <root> is HOLGER_APPLIANCE_DATA or /data. It holds (Seed, nordisk-pairing seed.rs):

It holds no per-box value and no pre-shared key. The front's tunnel key comes in the join reply, and the pre-shared keys come in the feed (below).

How the seed reaches the box. For a medium built with a seed, the seed is a staged file (the installer parameter pairing_seed, never a kernel command line; holger-recipe/src/packaging.rs). The media the ONE box lays from carry no seed: their seed.toml holds only the marker nordisk-pairing-seed-from = "upcloud-metadata". At first boot the box reads its seed from the cloud's metadata service (169.254.169.254/metadata/v1/user_data, 36 tries 5 s apart) and writes it over the marker, mode 0600. A seed of another kind than Holger is refused ([pairing-seed-kind]).

The installer's own material (the CA certificate and key, the installer's certificate and key, the link file) is never on a medium. A sold estate keeps it as the bundle installer-material/1, sealed in the main's admin-only raw repository monetize-backup.

The ceremony at first boot

The installer dials out to every box; a box never dials the installer.

  1. The box raises wg-installer and serves its bootstrap listener on tcp/8474.

  2. Both ends show certificates under the installer CA: the box its bootstrap certificate (SAN bootstrap.installer); the installer its client certificate (CN installer). The box serves no other client.

  3. The installer calls, in order:

    • GET /install/hello: the box answers a JoinRequest: its kind, its WireGuard public key, its capacity, and a CSR for a key the box made itself;
    • POST /install/feed: the installer sends the JoinReply: the box's name, the overlay, its own certificate, its role, its peers (the front first) and the feed, which carries the box's WireGuard pre-shared key (wg_psk; the front's feed holds wg_psk.<box>);
    • GET /install/offer, GET /install/secret-offer: what each box offers the others, fed on to every box;
    • GET /install/status, then POST /install/close.
  4. The bootstrap certificate may join only a slot that has not joined yet (join-spent), and it is withdrawn once every slot has joined (join-withdrawn).

  5. The weakest box becomes the twin. The role is written once, in holger-role.ron.

  6. The box keeps pairing/own.key, own.pem, joined.toml (sealed at rest), signal.toml and closed. A fresh install clears them.

Trust rests on the pinned installer CA. There are no fingerprints to compare by hand.

After the close the installer can still call /install/refeed, /install/cert, /install/renew and /install/bootstrap on the same mTLS link.

The twin's replication certificate comes from the front: TwinCsr → IssueTwinCertificate → InstallTwinCertificate, recorded in pairing/certified.toml (holger-estate-front/src/certify.rs). The console bearer is derived on both ends from an x25519 key each side makes under console/ (holger-estate-front/src/console_key.rs).

Downstreams: signed invites

A downstream is not laid by the ONE box. It is enrolled by an invite from its main.

There is no CLI verb for invites yet. (holger pair downstream|upstream|repair|list is a different thing: it pins the mirror mTLS certificates of a peer in holger-peers/*.ron.)

The front polls GET /-/sync/downstreams/wg every 5 s and lists each enrolled downstream on its hub holger-ds, with its pre-shared key (RelayPeer); it lists a downstream again when its key changes and refuses a blank one (holger-estate-front/src/downstreams.rs). The console's list (WgPeer) never carries the key.

WireGuard

Certificate renewal

What a twin rebuild keeps

A dead main is rebuilt from its twin: the front re-admits the box with a fresh seed from the kept bundle, the ONE box lays it again, and the new main starts empty and held, then recharges from the twin's replication door as holger-recharge (holger-estate-front/src/recharge.rs).

Kept (the ESTATE list, server/lib/src/replication.rs):

Not kept, made new on the new box: the role record, the box's own TLS material (holger-replication-issuer, holger-twin-tls, holger-recharge-tls), the recharge markers, fleet/, and the pairing state.

Still in v1