Files
diskless-workstation/serving
Audiobuzz c40dd5ac93 fleet-manager: checkpoint 5 — admin WebAuthn/passkey auth (replaces browser mTLS)
The human->UI hop now authenticates with a hardware passkey (YubiKey), not an
mTLS client cert: phishing-resistant, origin-bound, better UX. (manager->broker
stays mTLS — service-to-service.)

- webauthn.go: credential store (registered admins + passkeys + a one-time-token
  jti replay ledger; JSON file, atomic writes) + the webauthn.User impl + RP setup.
- cli.go: 'fleet-manager register-admin <name>' mints a short-lived HMAC-signed
  enrolment link over a shared enrol key; the browser completes the WebAuthn
  create ceremony. No anyone-can-register endpoint; single-use via the ledger.
- authapi.go: register/login ceremonies (usernameless discoverable login,
  userVerification required) + logout + whoami; short-lived in-memory ceremony
  store between begin/finish.
- session.go: opaque server-side sessions (Secure/HttpOnly/SameSite=Strict cookie)
  + requireSession middleware gating /api/*, with a CSRF header check on mutations.
  adminName now comes from the session (audit attribution).
- main.go: register-admin subcommand dispatch; admin TLS switched to SERVER-AUTH
  (no client cert) with a HOT-RELOADING cert (certReloader) so a Let's Encrypt
  DNS-01 renewal is picked up without a restart. RP = diskless-server.bitasylum.org.
  /api/* mounted behind requireSession; /auth/* + /healthz public.

Cert strategy (per design decision): the UI HTTPS cert is a publicly-trusted LE
cert via DNS-01 (mgmt-network UI isn't internet-reachable, so HTTP-01/TLS-ALPN-01
can't validate; DNS-01 proves control of bitasylum.org via TXT + supports
wildcards). External ACME client issues+renews to the cert path; manager reloads.

Deps: github.com/go-webauthn/webauthn (server-side). Tests: enrol-token
mint/verify/tamper/expiry, session middleware (401/403/CSRF/expiry), cred-store
persistence + jti ledger. build/test/vet/gofmt clean. Live passkey ceremony
(virtual authenticator / real YubiKey) is a bench item.
2026-07-04 04:26:30 +00:00
..
2026-06-27 08:52:17 +00:00
2026-06-19 04:00:47 +00:00
2026-06-28 10:33:14 +00:00
2026-06-28 18:32:36 +09:30
2026-06-27 08:52:17 +00:00
2026-06-28 08:44:49 +00:00
2026-06-18 12:03:45 +00:00
2026-06-27 19:29:19 +09:30

Serving stack — deploy & test (lab)

Stands up DHCP/DNS + HTTP(UKI) + NVMe/TCP(rootfs) so a diskless client can network-boot the release built by the pipeline, and walks through testing it. This targets the open-network test path (rd.dot1x=0, unsigned UKI) — see the repo README for the secure path.

0. Prerequisites

  • A serving host on the same L2 as the test client (the boot VLAN / test bridge). Note its interface (BOOT_IFACE) and address (SERVER_IP).
  • Docker Engine ≥ 28 (for the type: image artifact volume; otherwise use the commented bind-mount fallback in docker-compose.yml).
  • Hugepages for SPDK: sudo sysctl vm.nr_hugepages=1024.
  • Registry creds (docker login git.bitasylum.org) — pulls both the artifact image and the SPDK target image.
  • The SPDK image is prebuilt by CI (.gitea/workflows/spdk-image.yaml → ${SPDK_IMAGE}:${SPDK_VERSION}); the serving host just pulls it. To roll the SPDK version, bump SPDK_VERSION (or run that workflow manually with a tag) so the image exists in the registry, then update .env. (dnsmasq is the only service still built locally — a tiny alpine image.)

1. Build with a real NVMe discovery address ⚠ required

The UKI's rd.nvmf.discover address is baked in at build time (immutable once signed). The default is the DNS name nvme.fleet.lab, which this stack's dnsmasq resolves to SERVER_IP. If you built before nvmet_traddr existed, the UKI still contains the literal NVMET_TRADDR placeholder and will not connect NVMe — rebuild:

packer build \
  -var image_version=<ver> \
  -var disable_dot1x=true \
  -var nvmet_traddr=nvme.fleet.lab \
  -var oci_image=git.bitasylum.org/audiobuzz/fleet-boot -var oci_push=true \
  .

Keep nvmet_traddr (UKI) == NVME_HOST (this stack's .env).

2. Deploy the serving stack

cd serving
cp .env.example .env      # edit IMAGE_VERSION, BOOT_IFACE, SERVER_IP, DHCP range
docker login git.bitasylum.org
docker compose up -d --build
docker compose ps

IMAGE_VERSION must match the built tag (e.g. a branch build is 0.0.0-<short-sha>; a tag push is the clean version).

Verify each service:

# HTTP: UKI is served
curl -sI http://${SERVER_IP}/fleet-${IMAGE_VERSION}.efi | head -1   # 200 OK

# HTTP: kiosk SPA is served (reverse-proxied to the kiosk-app container —
# the spaceship terminal the diskless kiosk loads by default)
curl -sI http://${SERVER_IP}/kiosk/ | head -1                       # 200 OK

# NTP: lab time server answers (diskless hosts get it via DHCP option 42)
chronyc -h ${SERVER_IP} tracking 2>/dev/null || ntpdate -q ${SERVER_IP}

# NVMe/TCP: target is listening + discoverable
ss -lntp | grep 4420
sudo nvme discover -t tcp -a ${SERVER_IP} -s 4420                   # lists the subsystem

# DNS: discovery name resolves to the serving host
dig +short @${SERVER_IP} nvme.fleet.lab                             # -> ${SERVER_IP}

# Audit collector: rsyslog receiver is listening on :514 (booted hosts offload
# auditd here; records land in audit-collector/logs/<sender-ip>/)
ss -lntp | grep ':514'
logger -n ${SERVER_IP} -P 514 -T -t audispd "collector smoke test"  # -> logs/<ip>/audit.log

# DHCP/boot: watch leases + bootfile handout
docker compose logs -f dnsmasq

3. Boot a test client

Create a diskless client VM (or bare metal) on the boot LAN:

  • UEFI firmware (OVMF) with HTTP Boot enabled.
  • Secure Boot DISABLED — the lab UKI is unsigned; firmware will reject it otherwise. (Re-enable Secure Boot only once you ship a signed UKI.)
  • No disk (root comes from NVMe/TCP), NIC on the boot bridge.
  • No TPM/swtpm needed in this mode (rd.dot1x=0 skips 802.1X/MACsec).

qemu example (adjust bridge/OVMF paths):

qemu-system-x86_64 -machine q35 -accel kvm -m 4096 \
  -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE.fd \
  -drive if=pflash,format=raw,file=OVMF_VARS.fd \
  -netdev bridge,id=n0,br=<boot-bridge> -device virtio-net-pci,netdev=n0 \
  -nographic

(In the OVMF menu enable HTTP Boot / pick the NIC if it doesn't auto-boot.)

4. Expected sequence & success criteria

  1. DHCP lease from dnsmasq; firmware gets option 67 → http://${SERVER_IP}/fleet-<ver>.efi.
  2. Firmware fetches the UKI over HTTP and boots it (no GRUB/shim).
  3. initramfs: rd.dot1x=0 bypass → DHCP on the NIC.
  4. nvme discover to nvme.fleet.lab:4420 (→ SERVER_IP) → connect namespace.
  5. dm-verity maps /dev/mapper/root; erofs root mounts read-only.
  6. systemd.volatile=overlay adds the tmpfs overlay; systemd reaches multi-user.

Pass: the client reaches a login/multi-user state with the root served over NVMe/TCP. Confirm on the client console: findmnt / (erofs, ro), nvme list (the connected namespace), lsblk (dm-verity root).

5. Troubleshooting

Symptom Likely cause
Firmware won't load the UKI Secure Boot still enabled (UKI is unsigned), or wrong boot file URL
No DHCP / no boot menu client not on the same L2 as BOOT_IFACE; dnsmasq not bound to that iface
Boots, hangs at NVMe nvmet_traddr ≠ NVME_HOST, or UKI still has the NVMET_TRADDR placeholder (rebuild)
nvme discover empty SPDK not listening (check docker compose logs nvmeof; hugepages allocated?)
SPDK won't start hugepages not allocated, or bdev_aio_create -r unsupported on the pinned SPDK tag
verity mapping fails rootfs.erofs / verity.json mismatch — must be the same release as the UKI (they are, from one build)