From 98c71474d3f6b96a4af8c77baf2645b650d7fa07 Mon Sep 17 00:00:00 2001 From: Audiobuzz Date: Fri, 19 Jun 2026 09:59:49 +0000 Subject: [PATCH] Add fleet hostnaming --- diskless.pkr.hcl | 9 ++++ docs/TODO.md | 4 ++ .../NetworkManager/conf.d/20-hostname.conf | 13 ++++++ serving/dnsmasq/Dockerfile | 5 +++ serving/dnsmasq/dhcp-hosts | 23 ++++++++++ serving/dnsmasq/fleet-namer | 45 +++++++++++++++++++ serving/docker-compose.yml | 17 +++++++ 7 files changed, 116 insertions(+) create mode 100644 files/etc/NetworkManager/conf.d/20-hostname.conf create mode 100644 serving/dnsmasq/dhcp-hosts create mode 100755 serving/dnsmasq/fleet-namer diff --git a/diskless.pkr.hcl b/diskless.pkr.hcl index 34b7d4e..3291ea7 100644 --- a/diskless.pkr.hcl +++ b/diskless.pkr.hcl @@ -296,6 +296,15 @@ build { # ---- Diskless adaptations ------------------------------------------ # Per-host machine-id is generated at boot into the overlay "truncate -s 0 /etc/machine-id", + # No per-host hostname in the image (image stays fleet-generic): empty + # /etc/hostname so the static hostname is unset and NetworkManager sets the + # transient hostname from the DHCP lease — DHCP option 12 (the dnsmasq + # fleet-namer assigns a creative name per MAC, server-side) or a reverse-DNS + # PTR lookup. hostname-mode=default makes that explicit so a stricter global + # config can't switch it to 'none' and leave hosts unnamed. + "truncate -s 0 /etc/hostname", + "install -D -m0644 /tmp/etc-stage/NetworkManager/conf.d/20-hostname.conf /etc/NetworkManager/conf.d/20-hostname.conf", + "restorecon /etc/NetworkManager/conf.d/20-hostname.conf", # Read-only erofs/verity root; runtime state on a tmpfs overlay provided # by systemd.volatile=overlay (set in the cmdline by extract-artifacts.sh). # RHEL/Rocky dracut has no 'overlayfs'/'rootovl' module. diff --git a/docs/TODO.md b/docs/TODO.md index 9e5d22e..198d26d 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -17,6 +17,10 @@ flipped back for a secure/production build (consolidated as item #8): test target. Production needs CI/HSM signing. - `enable_kiosk=false` by default (CI may set true); `enable_stig=true`; `boot_splash=true`; NTP via DHCP→serving-host chrony. +- **Hostname:** image is hostname-neutral (empty `/etc/hostname`, + `hostname-mode=default`); the serving dnsmasq's `fleet-namer` (`--dhcp-script`) + assigns a stable creative starship name per MAC (e.g. `nebula-vanguard-07`) via + DHCP option 12 + DNS A/PTR. Host picks it up on next DHCP renewal/reboot. What's working/verified vs awaiting bench is per-item below. Cross-cutting bench-verify checklist lives in `CLAUDE.md` → "Known bench-verify items". diff --git a/files/etc/NetworkManager/conf.d/20-hostname.conf b/files/etc/NetworkManager/conf.d/20-hostname.conf new file mode 100644 index 0000000..1152c89 --- /dev/null +++ b/files/etc/NetworkManager/conf.d/20-hostname.conf @@ -0,0 +1,13 @@ +# Derive the system hostname from the network at boot. +# +# The image carries NO per-host hostname (invariant: no per-host state in the +# artifact) — /etc/hostname is emptied at build, so the static hostname is unset +# and the host comes up as "localhost". With hostname-mode=default, NetworkManager +# sets the *transient* hostname once it has a DHCP lease, in this order: +# 1. DHCP option 12 (host-name) if the server sends one — the lab dnsmasq's +# fleet-namer assigns a creative starship name per MAC, server-side. +# 2. otherwise a reverse-DNS (PTR) lookup of the leased address. +# This is already NM's default, but we set it explicitly so a stricter +# global/STIG config can't silently switch it to "none" and leave hosts unnamed. +[main] +hostname-mode=default diff --git a/serving/dnsmasq/Dockerfile b/serving/dnsmasq/Dockerfile index 540f227..6b40ae8 100644 --- a/serving/dnsmasq/Dockerfile +++ b/serving/dnsmasq/Dockerfile @@ -3,4 +3,9 @@ # entirely as --flags from docker-compose.yml. FROM harbor.bitasylum.org/dockerhub/library/alpine:3.20 RUN apk add --no-cache dnsmasq +# fleet-namer: --dhcp-script that assigns creative starship hostnames per MAC, +# server-side (keeps the workstation image neutral). COPY'd in root-owned, 0755 +# so dnsmasq will execute it. Uses busybox sha256sum/cut/wc — all in the image. +COPY fleet-namer /usr/local/bin/fleet-namer +RUN chmod 0755 /usr/local/bin/fleet-namer ENTRYPOINT ["dnsmasq"] diff --git a/serving/dnsmasq/dhcp-hosts b/serving/dnsmasq/dhcp-hosts new file mode 100644 index 0000000..8d6df51 --- /dev/null +++ b/serving/dnsmasq/dhcp-hosts @@ -0,0 +1,23 @@ +# DHCP reservations for the diskless fleet (dnsmasq --dhcp-hostsfile). +# +# Two ways entries land here, both SERVER-SIDE (the workstation image carries no +# hostname and stays fleet-generic): +# +# 1. AUTO (default): fleet-namer (the --dhcp-script) assigns a stable, creative +# starship-themed name per MAC on first contact (e.g. "nebula-vanguard-07") +# and appends a line below, then reloads dnsmasq. Same MAC -> same name +# forever. The host picks it up on its next DHCP renewal/reboot. +# +# 2. MANUAL (override): add your own line to pin a specific host. fleet-namer +# skips any MAC already listed, so manual entries always win. +# +# Format (fields after the MAC are order-independent): +# , # name only; IP from --dhcp-range +# ,, # also pin the IP +# Names become .${BOOT_DOMAIN} in DNS (A + PTR). +# +# Manual examples (uncomment + edit): +# 52:54:00:11:22:01,192.168.50.101,bridge-command +# 52:54:00:11:22:02,reactor-control +# +# --- fleet-namer auto-assigned entries are appended below this line --- diff --git a/serving/dnsmasq/fleet-namer b/serving/dnsmasq/fleet-namer new file mode 100755 index 0000000..ba51b72 --- /dev/null +++ b/serving/dnsmasq/fleet-namer @@ -0,0 +1,45 @@ +#!/bin/sh +# fleet-namer — dnsmasq --dhcp-script that assigns a stable, creative cyber +# sci-fi hostname per host, SERVER-SIDE, so the workstation image stays neutral. +# +# dnsmasq calls this on every lease event with: $1=action(add|old|del) +# $2=MAC $3=IP [$4=hostname]. For a MAC we haven't named yet, we derive a name +# DETERMINISTICALLY from the MAC (sha256 -> curated wordlists), append a +# reservation (MAC,name) to the dhcp-hostsfile, and SIGHUP dnsmasq so it re-reads +# it. dnsmasq then hands the name out as DHCP option 12 + registers DNS A/PTR; +# NetworkManager on the host sets it as the hostname. +# +# Same MAC -> same name forever (idempotent). The host picks up the name on its +# next DHCP transaction (renewal at ~T1, or reboot) — see the lease-time note in +# docker-compose.yml. Manual reservations in the hostsfile always win (we skip a +# MAC that's already listed). +set -eu + +action="$1"; mac="$2" +case "$action" in add|old) ;; *) exit 0 ;; esac +[ -n "$mac" ] || exit 0 + +HOSTSFILE="${FLEET_HOSTSFILE:-/etc/dnsmasq-hosts}" + +# Already named (auto or manual)? leave it. +grep -qi "^$mac," "$HOSTSFILE" 2>/dev/null && exit 0 + +# Curated DNS-safe wordlists (single line, single-spaced — indexed with cut). +PREFIX="nova nebula quasar pulsar comet vega rigel orion lyra cygnus draco hydra phoenix titan atlas helios aurora zenith vortex ion plasma photon quantum warp aether astral stellar lunar solar cobalt obsidian halcyon" +NOUN="cruiser frigate vanguard sentinel ranger voyager pioneer horizon beacon spectre nomad drifter wraith talon raptor falcon meridian apex helm reactor relay array lance aegis ember corsair seeker harbinger paragon warden monolith" +np=$(echo "$PREFIX" | wc -w) +nn=$(echo "$NOUN" | wc -w) + +# Deterministic indices from the MAC's sha256 (hex chunks mod list length). +h=$(printf '%s' "$mac" | sha256sum | cut -c1-12) +pi=$(( 0x$(echo "$h" | cut -c1-4) % np + 1 )) +ni=$(( 0x$(echo "$h" | cut -c5-8) % nn + 1 )) +num=$(( 0x$(echo "$h" | cut -c9-12) % 100 )) +name=$(printf '%s-%s-%02d' "$(echo "$PREFIX" | cut -d' ' -f"$pi")" "$(echo "$NOUN" | cut -d' ' -f"$ni")" "$num") + +# Register + reload dnsmasq (re-reads --dhcp-hostsfile on SIGHUP). dnsmasq is +# PID 1 in this container; the script runs as root (--user=root in compose). +echo "$mac,$name" >> "$HOSTSFILE" +kill -HUP 1 2>/dev/null || true +echo "fleet-namer: $mac -> $name" >&2 +exit 0 diff --git a/serving/docker-compose.yml b/serving/docker-compose.yml index 6e4a2b7..d702e8e 100644 --- a/serving/docker-compose.yml +++ b/serving/docker-compose.yml @@ -43,6 +43,11 @@ services: - NET_ADMIN # DHCP raw sockets - NET_RAW restart: unless-stopped + volumes: + # DHCP reservations (MAC -> name [-> IP]); drives DHCP option 12 (hostname) + # + DNS A/PTR. Auto-populated by fleet-namer (creative names per MAC); add + # manual lines to pin specific hosts. RW so fleet-namer can append. + - ./dnsmasq/dhcp-hosts:/etc/dnsmasq-hosts command: - --keep-in-foreground # Log to stderr so `docker compose logs` captures output. Without this, @@ -84,6 +89,18 @@ services: # below so they sync time internally and never reach the public NTP pool. # The image's NM dispatcher (10-chrony-dhcp-ntp) feeds this into chrony. - --dhcp-option=option:ntp-server,${SERVER_IP} + # ---- Per-host names (creative starship hostnames, server-assigned) ---- + # The workstation image is hostname-neutral; naming is done HERE. fleet-namer + # (the --dhcp-script) derives a stable themed name per MAC (e.g. + # nebula-vanguard-07), appends it to the hostsfile, and SIGHUPs dnsmasq; + # dnsmasq then sends it as DHCP option 12 (-> NetworkManager sets the + # hostname) and registers DNS A/PTR under ${BOOT_DOMAIN}. The host picks up + # its name on the NEXT DHCP transaction (renewal at ~half lease, or reboot) + # — shorten DHCP_LEASE if you want naming to settle faster on first contact. + # --user=root so the dhcp-script can write the hostsfile + signal dnsmasq. + - --user=root + - --dhcp-hostsfile=/etc/dnsmasq-hosts + - --dhcp-script=/usr/local/bin/fleet-namer # ---- UEFI HTTP Boot ---- # Firmware sends vendor-class "HTTPClient"; we must echo it back (opt 60) # and hand over the UKI URL (opt 67). IP in the URL avoids a DNS round-trip