fix(10-03): strip fleet-shared identity material from the rootfs tar (F-03)
The rootfs is a container image exported to a tar and extracted verbatim onto every disk flashed from the ISO, and the ISO is published. It baked two things nobody asked for: Debian's openssh-server postinst generates /etc/ssh/ssh_host_* during the container build, and the `openssl req` layer writes the TLS keypair. Both were therefore identical on every node and known to every downloader. Add a final RUN layer to Dockerfile.rootfs that removes /etc/ssh/ssh_host_*, removes the archipelago TLS keypair (keeping the ssl directory so the first-boot staging swap has somewhere to land), truncates /etc/machine-id to systemd's documented "regenerate on next boot" state, and drops a non-shared /var/lib/dbus/machine-id if one exists as a real file rather than a symlink. It also writes /opt/archipelago/rootfs-identity-stripped so a node can answer after the fact whether its rootfs came from a stripped build; no timestamp, so the RECIPE_HASH cache stays reproducible. This is what makes 10-03's fail-closed regeneration structural instead of procedural: with the material gone, a regeneration failure degrades to "no key, service refuses to start" rather than "fleet-shared key, silently". The `openssl req` layer is deliberately left in place — it keeps proving openssl is present and keeps the SAN template next to its consumer; the strip layer is what makes the output non-shared. Two comment corrections that follow from the strip: - The installer's TLS block is no longer a rarely-taken safety net; it now fires on every install. It is per-install and never image-wide, so it does not reopen F-03, but it does mean a first-boot failure still leaves the web UI with a cert while SSH has nothing. Comment updated to say so. - The first-boot script header overstated the fail-closed cost for TLS for the same reason; corrected to claim certainty only for SSH. This edit is inside the RECIPE_HASH region, so the next build is forced to rebuild the rootfs tar — required for the C-4 evidence to mean anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
210430967d
commit
408b328c39
@@ -578,6 +578,42 @@ RUN mkdir -p /var/log/journal && \
|
||||
# Clean up
|
||||
RUN apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# ─── Strip fleet-shared identity material (audit finding F-03) ──────────────
|
||||
#
|
||||
# This image is exported to a tar and extracted VERBATIM onto every disk
|
||||
# flashed from the resulting ISO, and the ISO is a published artefact. Anything
|
||||
# identity-shaped left in here is therefore held by every node AND by every
|
||||
# person who downloaded the ISO.
|
||||
#
|
||||
# Two things get baked without anyone asking for them:
|
||||
# - Debian's openssh-server postinst generates /etc/ssh/ssh_host_* at package
|
||||
# install time, i.e. inside this container build.
|
||||
# - the "openssl req" layer above writes /etc/archipelago/ssl/archipelago.key.
|
||||
# NOTE: this heredoc is UNQUOTED, so backticks here are command substitution
|
||||
# and would run at build time. Never put backticks in these comments.
|
||||
# Plus /etc/machine-id, which systemd populates during the build and which
|
||||
# correlates every node flashed from one ISO.
|
||||
#
|
||||
# archipelago-first-boot-secrets.service recreates all of this per device on
|
||||
# first boot. The point of removing it HERE is to change what a regeneration
|
||||
# failure costs: with the material stripped, a failure degrades to "no key, the
|
||||
# service refuses to start" instead of "fleet-shared key, silently" — which is
|
||||
# the whole of F-03. That makes fail-closed structural rather than procedural.
|
||||
#
|
||||
# This must stay the LAST layer: anything that installs packages after it can
|
||||
# reintroduce host keys. Keep the /etc/archipelago/ssl directory itself so the
|
||||
# first-boot script's staging swap has somewhere to land. The "openssl req"
|
||||
# layer above is deliberately left in place — it keeps proving openssl is
|
||||
# present and keeps the SAN template next to the code that uses it; this layer
|
||||
# is what makes the shipped output non-shared.
|
||||
RUN rm -f /etc/ssh/ssh_host_* && \
|
||||
rm -f /etc/archipelago/ssl/archipelago.key /etc/archipelago/ssl/archipelago.crt && \
|
||||
mkdir -p /etc/archipelago/ssl && \
|
||||
: > /etc/machine-id && \
|
||||
{ [ -L /var/lib/dbus/machine-id ] || rm -f /var/lib/dbus/machine-id ; } && \
|
||||
mkdir -p /opt/archipelago && \
|
||||
printf 'F-03 identity strip: this rootfs was built with the identity-strip layer.\nRemoved:\n /etc/ssh/ssh_host_*\n /etc/archipelago/ssl/archipelago.key\n /etc/archipelago/ssl/archipelago.crt\nTruncated:\n /etc/machine-id\nRecreated per device by archipelago-first-boot-secrets.service on first boot.\n' > /opt/archipelago/rootfs-identity-stripped
|
||||
DOCKERFILE
|
||||
|
||||
# Copy nginx snippets for HTTPS (PWA, app proxies)
|
||||
@@ -1631,9 +1667,12 @@ cat > "$WORK_DIR/first-boot-secrets.sh" <<'SECRETSSCRIPT'
|
||||
# SCRIPT IS THE ONLY THING THAT CREATES THEM. That is deliberate.
|
||||
#
|
||||
# The operational consequence, in plain words: if regeneration fails every
|
||||
# retry, this node has no SSH host key and no TLS key. sshd will not start and
|
||||
# the nginx TLS listener will not start, so the node cannot be reached over
|
||||
# the network and recovery requires the physical console.
|
||||
# retry, this node has no SSH host key, so sshd will not start and the node
|
||||
# cannot be reached over SSH — recovery requires the physical console. Nothing
|
||||
# else on the install path creates host keys, so that outcome is certain.
|
||||
# (TLS is softer: the installer writes a per-node fallback keypair with the
|
||||
# generic CN=archipelago SAN, so the web UI usually still comes up. That
|
||||
# fallback is per install, never image-wide, so it does not reopen F-03.)
|
||||
#
|
||||
# That cost was accepted on purpose. The behaviour it replaces was worse: log
|
||||
# a warning, set the completion marker anyway, and run forever on the SSH host
|
||||
@@ -3391,7 +3430,18 @@ RemainAfterExit=yes
|
||||
WantedBy=multi-user.target
|
||||
DIAGSVC
|
||||
|
||||
# Ensure SSL cert exists for nginx HTTPS (safety net if rootfs build missed it)
|
||||
# Ensure SSL cert exists for nginx HTTPS.
|
||||
#
|
||||
# Since the F-03 identity-strip layer this is no longer a rarely-taken safety
|
||||
# net — the rootfs ships with NO TLS keypair, so this branch fires on every
|
||||
# install. That is fine and deliberate: the installer runs separately on each
|
||||
# target machine, so the key it writes is per node, not image-wide. It uses the
|
||||
# generic CN=archipelago SAN; archipelago-first-boot-secrets.service replaces it
|
||||
# on first boot with one carrying this device's actual hostname.
|
||||
#
|
||||
# Consequence worth knowing: if first-boot regeneration fails, the web UI still
|
||||
# has *a* per-node cert from here, whereas SSH has nothing at all (nothing
|
||||
# recreates host keys at install time) and sshd will refuse to start.
|
||||
if [ ! -f /mnt/target/etc/archipelago/ssl/archipelago.crt ]; then
|
||||
mkdir -p /mnt/target/etc/archipelago/ssl
|
||||
chroot /mnt/target openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
||||
|
||||
Reference in New Issue
Block a user