feat(platform): ID names the machine — a namespaced sha256 of /etc/machine-id, computed at run time, stored nowhere (#95)

Option A per triage's normalization on #95: derive, never mint. The
derivation is pinned — sha256("rig-machine-id:<machine-id>"), first 32 hex
as 8-4-4-4-12 — and the prefix is the contract that keeps the id
uncorrelatable with other tools' derivations, per machine-id(5)'s own
guidance not to expose the raw value.

Missing, empty and 'uninitialized' machine-id files degrade loudly to an
'unavailable (reason)' line: hashing nothing would hand every such machine
the same identity, the worst possible failure for an identity field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
claude-bot-andresmgsl 2026-07-24 01:59:45 +00:00
parent f00c90738b
commit f26ed51938

View file

@ -2,7 +2,8 @@
# rig platform — what is this machine? Calculated at run time, stored nowhere. # rig platform — what is this machine? Calculated at run time, stored nowhere.
# #
# Read-only in the strongest sense rig has: it reads /proc, uname, # Read-only in the strongest sense rig has: it reads /proc, uname,
# /etc/os-release, df and systemd-detect-virt, and writes NOTHING, ever. That # /etc/os-release, /etc/machine-id, df and systemd-detect-virt, and writes
# NOTHING, ever. That
# is the design, not an implementation detail — specs change without rig doing # is the design, not an implementation detail — specs change without rig doing
# anything (RAM added, root disk resized, unattended-upgrades patching the # anything (RAM added, root disk resized, unattended-upgrades patching the
# kernel), so a stored spec is stale the moment the machine changes, and # kernel), so a stored spec is stale the moment the machine changes, and
@ -24,13 +25,19 @@ usage() {
cat <<'EOF' cat <<'EOF'
usage: rig platform usage: rig platform
Describes the machine you are on: hostname, OS, kernel, CPU, memory, disk Describes the machine you are on: hostname, a stable machine ID, OS, kernel,
and virtualization, then rig's own provenance (which rig, when, and the role CPU, memory, disk and virtualization, then rig's own provenance (which rig,
marker bootstrap wrote). when, and the role marker bootstrap wrote).
Computed at run time from /proc, uname, /etc/os-release, df and ID names the machine where HOSTNAME names the slot: it is derived from
systemd-detect-virt. Writes nothing, needs no root, makes no network call — /etc/machine-id (a namespaced sha256, never the raw value, which machine-id(5)
so it also works on a pristine Debian box rig has never bootstrapped, where asks tools not to expose). Two machines reporting the same ID were cloned
from one image — actionable information, not a coincidence: no identity that
lives in the filesystem survives the filesystem being copied.
Computed at run time from /proc, uname, /etc/os-release, /etc/machine-id, df
and systemd-detect-virt. Writes nothing, needs no root, makes no network call
— so it also works on a pristine Debian box rig has never bootstrapped, where
the provenance block reads 'not bootstrapped'. the provenance block reads 'not bootstrapped'.
EOF EOF
} }
@ -53,6 +60,61 @@ field() { printf '%-10s %s\n' "$1" "$2"; }
# before anything has been installed. # before anything has been installed.
HOSTNAME_V="$(hostname 2>/dev/null || uname -n)" HOSTNAME_V="$(hostname 2>/dev/null || uname -n)"
# --- identity (#95) -----------------------------------------------------------
# HOSTNAME names the slot; ID names the machine. rig itself sets the hostname
# during bootstrap and reuses it across rebuilds ('hetzner-cp-1' is a role, not
# hardware), so nothing above answers "is this the same machine I converged in
# June, or its replacement?". /etc/machine-id does — but machine-id(5) asks
# that the raw value not be exposed (it is a stable correlator across every
# tool that leaks it), and its documented remedy is an application-specific
# derivation. So: THE PINNED DERIVATION, fixed by #95 so two implementations
# can never disagree —
#
# printf 'rig-machine-id:%s' "$(cat /etc/machine-id)" | sha256sum
# → first 32 hex chars, rendered 8-4-4-4-12
#
# The 'rig-machine-id:' prefix is the contract, not decoration: it is what
# keeps this id uncorrelatable with any other tool's derivation of the same
# machine-id. sha256sum is coreutils, which this command is restricted to.
# Derived, computed here, stored nowhere — #64's thesis — so it exists before
# bootstrap and needs no write path.
#
# What this deliberately does NOT fix: a host cloned from a golden image
# carries the clone's /etc/machine-id, so two machines reporting the same ID
# means a cloned image. That is surfaced (help text, README) rather than
# defended against — no identity that lives in the filesystem survives the
# filesystem being copied.
#
# RIG_MACHINE_ID overrides the path so the harness can drive the present,
# absent, empty and uninitialized cases against fixtures (repo precedent:
# RIG_MANIFEST / RIG_ROLE_MARKER below).
MID_FILE="${RIG_MACHINE_ID:-/etc/machine-id}"
ID_V=""
if [ ! -r "$MID_FILE" ]; then
# Never an empty string: an ID field that renders blank looks like a bug,
# and a missing file is a fact worth naming.
ID_V="unavailable (no $MID_FILE)"
else
# $(...) strips the trailing newline — that is part of the pinned derivation
# above, not an accident of shell.
MID="$(cat "$MID_FILE")"
if [ -z "$MID" ]; then
# NEVER a hash of nothing: hashing the empty string would hand every such
# machine the SAME id — the worst possible failure for an identity field.
# Images do ship the file empty (that is first-boot semantics per
# machine-id(5)), so this path is real, not defensive.
ID_V="unavailable ($MID_FILE is empty)"
elif [ "$MID" = "uninitialized" ]; then
# machine-id(5)'s other not-yet-set sentinel — same collision failure as
# empty if hashed, so same loud degradation.
ID_V="unavailable ($MID_FILE is uninitialized)"
else
MID_HASH="$(printf 'rig-machine-id:%s' "$MID" | sha256sum)"
MID_HASH="${MID_HASH%% *}"
ID_V="${MID_HASH:0:8}-${MID_HASH:8:4}-${MID_HASH:12:4}-${MID_HASH:16:4}-${MID_HASH:20:12}"
fi
fi
# --- OS --------------------------------------------------------------------- # --- OS ---------------------------------------------------------------------
# THE os-release TRAP: /etc/os-release defines VERSION, NAME and ID, so # THE os-release TRAP: /etc/os-release defines VERSION, NAME and ID, so
# sourcing it in the MAIN shell silently clobbers same-named script variables. # sourcing it in the MAIN shell silently clobbers same-named script variables.
@ -112,6 +174,7 @@ VIRT="$(systemd-detect-virt 2>/dev/null || true)"
printf '%s\n' "PLATFORM" printf '%s\n' "PLATFORM"
field HOSTNAME "$HOSTNAME_V" field HOSTNAME "$HOSTNAME_V"
field ID "$ID_V"
field OS "${OS:-unknown}" field OS "${OS:-unknown}"
field KERNEL "$KERNEL" field KERNEL "$KERNEL"
field CPU "${CPU_MODEL:-unknown}${CORES:+ ($CORES cores)}" field CPU "${CPU_MODEL:-unknown}${CORES:+ ($CORES cores)}"