fix(10-03): unify secret generation to a single producer + self-heal (F-03)
Unify rather than delete. The defect in F-03 was never "a second attempt to
create a key exists" — it was that failure was silent and the completion marker
lied about it. A second attempt is only dangerous when it is an unaudited second
PRODUCER carrying its own idea of success, its own absent retry policy and its
own absent failure record.
Single producer. gen_tls() is now the only code in the ISO build that creates
/etc/archipelago/ssl/archipelago.{key,crt}; gen_ssh() the only code that creates
/etc/ssh/ssh_host_*. Two secondary producers are gone:
- the Dockerfile's `openssl req` layer, which baked a keypair the strip layer
deleted moments later in the same build;
- the installer's "ensure SSL cert exists for nginx HTTPS" block, which before
the strip almost never fired and after it would have fired on every install.
Proof is mechanical, not a claim: every executable `openssl req` / `ssh-keygen
-A` invocation in the builder now lives inside the generator heredoc, and the
test suite fails if one appears outside it.
Build-time assertion. The one realistic total failure is a missing generator
binary, which is deterministic — no retry or reboot fixes it. A rootfs RUN layer
now fails the build if openssl or ssh-keygen is missing or non-executable.
openssl and openssh-server are both already in the package list (and
openssh-server hard-depends openssh-client, which ships ssh-keygen), so today
this is cheap insurance; it earns its place the first time someone edits that
list.
Self-heal, never dead-end. Fail-closed governs SERVING; retry governs
RECOVERING, and they are different things. Adds
archipelago-first-boot-secrets.timer (OnBootSec=5min, OnUnitActiveSec=15min),
installed and enabled with a hand-written symlink fallback because chroot
systemctl enable can fail silently. The service's own ConditionPathExists=!
makes every trigger a no-op once the marker exists, so a healthy node pays
nothing. On success the script now restarts consumers that are in `failed` —
try-reload-or-restart is a no-op on a failed unit, so without this a recovered
node would have valid keys on disk and nginx still down.
Never serve a bogus key. gen_tls parses both halves back with `openssl pkey`
and `openssl x509` before the swap, so a truncated or half-written artefact is
never what nginx reads.
Tests: 6 cases, each with an isolated negative control (transcripts in SUMMARY).
- case 4, TLS fails every attempt on a stripped root -> no key from any source.
Control: reintroduce a fallback key creation -> only case 4 red.
- case 5, self-heal: a failed run then a later successful run -> key present,
marker set, failed units restarted. Control: dead-end on a node that already
failed -> only case 5 red.
- case 6, single-producer invariant. Control: reintroduce the installer block
-> only case 6 red, naming the line.
Residual risk, stated plainly: a machine where generation can never succeed
still ends up with no SSH and no TLS. Build-time assertion removes the
deterministic cause, retry plus timer removes the transient ones, so what
remains is genuinely broken hardware — and it says so on the console and in
/var/lib/archipelago/first-boot-secrets.failed rather than quietly serving a
key nobody audited.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ff6902dd9d
commit
2efab5f219
@@ -456,17 +456,41 @@ RUN ln -sf /etc/nginx/sites-available/archipelago /etc/nginx/sites-enabled/archi
|
||||
# Install nginx snippets (PWA config, HTTPS app proxies)
|
||||
COPY snippets/ /etc/nginx/snippets/
|
||||
|
||||
# Generate self-signed SSL certificate for HTTPS (PWA install + mic/camera
|
||||
# access both require a secure context). SAN covers the install-time default
|
||||
# hostname -- server.set-name regenerates this with the new hostname's SAN
|
||||
# if a node is renamed.
|
||||
RUN mkdir -p /etc/archipelago/ssl && \
|
||||
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
||||
-keyout /etc/archipelago/ssl/archipelago.key \
|
||||
-out /etc/archipelago/ssl/archipelago.crt \
|
||||
-subj "/C=XX/ST=Bitcoin/L=Node/O=Archipelago/CN=archipelago" \
|
||||
-addext "subjectAltName=DNS:archipelago,DNS:archipelago.local,DNS:localhost,IP:127.0.0.1" && \
|
||||
chmod 600 /etc/archipelago/ssl/archipelago.key
|
||||
# The self-signed HTTPS keypair is NOT generated here (audit F-03).
|
||||
#
|
||||
# It used to be: this layer ran "openssl req" and baked one keypair into the
|
||||
# shared image, which meant every node flashed from one ISO — and everyone who
|
||||
# downloaded the ISO — held the same TLS private key. The strip layer at the
|
||||
# end of this Dockerfile would delete it again anyway, so generating it here
|
||||
# now only creates a SECOND piece of code that can mint a TLS key with its own
|
||||
# accounting. There is exactly one producer of this keypair, and it is
|
||||
# first-boot-secrets.sh, which retries and reports.
|
||||
#
|
||||
# The ssl directory is created so that producer's staging swap has somewhere
|
||||
# to land.
|
||||
RUN mkdir -p /etc/archipelago/ssl
|
||||
|
||||
# Fail the BUILD if the rootfs cannot generate per-device secrets.
|
||||
#
|
||||
# The one realistic way first-boot generation fails on every retry is a missing
|
||||
# generator binary, and that failure is deterministic, not transient — retries
|
||||
# and reboots will never fix it. A node in the field must not be where we
|
||||
# discover it. openssl and openssh-server are both in the package list above
|
||||
# (and openssh-server hard-depends openssh-client, which ships ssh-keygen), so
|
||||
# today this assertion is cheap insurance rather than a fix. It earns its place
|
||||
# by turning a silent fleet-wide brick into a loud build failure the first time
|
||||
# anyone edits that package list.
|
||||
RUN set -e; \
|
||||
for bin in /usr/bin/openssl /usr/bin/ssh-keygen; do \
|
||||
if [ ! -x "\$bin" ]; then \
|
||||
echo "FATAL: \$bin missing or not executable in the rootfs." >&2; \
|
||||
echo "first-boot-secrets.sh cannot generate per-device SSH host keys" >&2; \
|
||||
echo "or the TLS keypair without it, and that failure is permanent." >&2; \
|
||||
echo "Restore openssl / openssh-server in the package list above." >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
done; \
|
||||
echo "first-boot secret generators present: openssl, ssh-keygen"
|
||||
|
||||
# Create archipelago systemd service
|
||||
COPY archipelago.service /etc/systemd/system/archipelago.service
|
||||
@@ -586,27 +610,29 @@ RUN apt-get clean && \
|
||||
# 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.
|
||||
# The TLS keypair is no longer generated in this Dockerfile at all (see the
|
||||
# note where that layer used to be). What still gets baked without anyone
|
||||
# asking for it is Debian's openssh-server postinst, which generates
|
||||
# /etc/ssh/ssh_host_* at package install time — i.e. inside this container
|
||||
# build — plus /etc/machine-id, which systemd populates during the build and
|
||||
# which correlates every node flashed from one ISO. This layer removes both.
|
||||
#
|
||||
# archipelago-first-boot-secrets.service recreates all of it per device on
|
||||
# first boot, retrying on a timer until it succeeds. The point of removing it
|
||||
# HERE is to change what a generation 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.
|
||||
# reintroduce host keys. The rm of the TLS keypair is kept as belt-and-braces
|
||||
# even though nothing in this build creates one any more — if a future layer
|
||||
# starts baking a cert, this catches it. Keep the /etc/archipelago/ssl
|
||||
# directory itself so the first-boot script's staging swap has somewhere to
|
||||
# land.
|
||||
RUN rm -f /etc/ssh/ssh_host_* && \
|
||||
rm -f /etc/archipelago/ssl/archipelago.key /etc/archipelago/ssl/archipelago.crt && \
|
||||
mkdir -p /etc/archipelago/ssl && \
|
||||
@@ -1654,6 +1680,36 @@ RemainAfterExit=yes
|
||||
WantedBy=multi-user.target
|
||||
SECRETSSERVICE
|
||||
|
||||
# Self-heal timer. Fail-closed governs SERVING (never present a key we did not
|
||||
# generate); this timer governs RECOVERING (never dead-end a node).
|
||||
#
|
||||
# Without it, a node whose generators failed all their in-boot retries would sit
|
||||
# with no SSH host key and no TLS key until somebody walked to it with a
|
||||
# keyboard. With it, a transient cause that later clears — a full disk that gets
|
||||
# freed, a pool that eventually seeds — repairs the node unattended.
|
||||
#
|
||||
# The service's own ConditionPathExists=! is what stops this: once the marker
|
||||
# exists, every subsequent trigger is a no-op that systemd records as success,
|
||||
# so the timer costs nothing on a healthy node and needs no separate teardown.
|
||||
# This uses systemd's own facilities on purpose; a sleep loop inside the script
|
||||
# would hold a oneshot open for hours and hide the failure from systemctl.
|
||||
cat > "$WORK_DIR/archipelago-first-boot-secrets.timer" <<'SECRETSTIMER'
|
||||
[Unit]
|
||||
Description=Retry per-device secret generation until it succeeds
|
||||
Documentation=man:archipelago-first-boot-secrets.service(8)
|
||||
|
||||
[Timer]
|
||||
# First retry shortly after boot has settled — by then the disk, the entropy
|
||||
# pool and any late-mounting filesystem have had a chance to become healthy.
|
||||
OnBootSec=5min
|
||||
OnUnitActiveSec=15min
|
||||
AccuracySec=30s
|
||||
Unit=archipelago-first-boot-secrets.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
SECRETSTIMER
|
||||
|
||||
cat > "$WORK_DIR/first-boot-secrets.sh" <<'SECRETSSCRIPT'
|
||||
#!/bin/bash
|
||||
# Create this device's own TLS keypair and SSH host keys on first boot.
|
||||
@@ -1666,26 +1722,41 @@ cat > "$WORK_DIR/first-boot-secrets.sh" <<'SECRETSSCRIPT'
|
||||
# host keys, the TLS keypair and machine-id out of the shared image. THIS
|
||||
# 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, 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.)
|
||||
# SINGLE PRODUCER. gen_tls() below is the only code anywhere in the ISO build
|
||||
# that creates /etc/archipelago/ssl/archipelago.{key,crt}; gen_ssh() is the only
|
||||
# code that creates /etc/ssh/ssh_host_*. The Dockerfile no longer bakes a
|
||||
# keypair and the installer's old "ensure SSL cert exists" fallback is gone.
|
||||
# That is the actual lesson of F-03: the bug was never "a second attempt to
|
||||
# create a key exists", it was that failure was silent and the marker lied
|
||||
# about it. A second producer is dangerous precisely because it has its own
|
||||
# accounting — its own idea of success, its own (absent) retry policy, its own
|
||||
# (absent) failure record. One producer means one place that can fail, one
|
||||
# place that retries, one place that reports.
|
||||
#
|
||||
# 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
|
||||
# key and TLS private key that every downloader of the ISO also holds — which
|
||||
# is undetectable host impersonation and transparent MITM of the web UI, on a
|
||||
# node whose operator has no idea.
|
||||
# FAIL CLOSED applies to SERVING: if generation fails, no key exists, so sshd
|
||||
# and the nginx TLS listener refuse to start. They never come up on a
|
||||
# placeholder, a zero-length file, or a key from anywhere else. gen_tls swaps
|
||||
# into place only after openssl has parsed both halves back, so a truncated or
|
||||
# corrupt artefact is never what a service reads.
|
||||
#
|
||||
# So: the completion marker is written ONLY when both generators succeeded. A
|
||||
# failed boot leaves the marker absent, which leaves the unit's
|
||||
# ConditionPathExists=! satisfied, so the whole thing runs again on the next
|
||||
# boot. Each generator is retried with backoff first, so a transient first-boot
|
||||
# condition (slow entropy pool, momentarily full disk) recovers without needing
|
||||
# a reboot at all.
|
||||
# SELF-HEAL applies to RECOVERING, and it is a different thing: a failure must
|
||||
# never dead-end the node. Three attempts with backoff inside the boot, then
|
||||
# archipelago-first-boot-secrets.timer retries every 15 minutes, and every
|
||||
# subsequent boot retries too — all because the marker is never written on
|
||||
# failure. A transient cause that later clears (a full disk that gets freed, a
|
||||
# pool that eventually seeds) repairs the node with nobody at a console. On
|
||||
# success the script restarts whatever refused to start, so recovery is
|
||||
# complete rather than pending-a-reboot.
|
||||
#
|
||||
# The only failure that survives all of that is a deterministic one — a missing
|
||||
# generator binary — and the rootfs build asserts openssl and ssh-keygen are
|
||||
# present and executable, so the build fails rather than the fleet.
|
||||
#
|
||||
# What this replaces was worse in every direction: log a warning, set the
|
||||
# completion marker anyway, never retry, and run forever on the SSH host key and
|
||||
# TLS private key that every downloader of the ISO also holds — undetectable
|
||||
# host impersonation and transparent MITM of the web UI, on a node whose
|
||||
# operator has no idea.
|
||||
#
|
||||
# Testability seam: FIRST_BOOT_SECRETS_ROOT prefixes every absolute path. It is
|
||||
# unset in production — the expansion is empty and behaviour is identical to a
|
||||
@@ -1754,10 +1825,16 @@ retry() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# 1. Self-signed TLS: fresh keypair with this device's hostname in the SAN
|
||||
# (server.set-name regenerates again if the node is renamed later).
|
||||
# Generated to .new and swapped only on success, so the node is never left
|
||||
# holding half a keypair.
|
||||
# THE SINGLE PRODUCER of this node's TLS keypair. No other code in the ISO
|
||||
# build creates /etc/archipelago/ssl/archipelago.{key,crt}. Do not add one; add
|
||||
# a caller of this instead. tests/first-boot-secrets/run-tests.sh case 6 fails
|
||||
# the build's test suite if a second producer appears.
|
||||
#
|
||||
# Fresh keypair with this device's hostname in the SAN (server.set-name
|
||||
# regenerates again if the node is renamed later). Generated to .new, PARSED
|
||||
# BACK, and only then swapped in — so nginx can never be handed a truncated,
|
||||
# zero-length or half-written artefact, which is the "never serve with a bogus
|
||||
# key" half of fail-closed.
|
||||
gen_tls() {
|
||||
mkdir -p "$SSL_DIR" || return 1
|
||||
rm -f "$SSL_DIR/archipelago.key.new" "$SSL_DIR/archipelago.crt.new"
|
||||
@@ -1768,7 +1845,10 @@ gen_tls() {
|
||||
-addext "subjectAltName=DNS:${NODE_NAME},DNS:${NODE_NAME}.local,DNS:archipelago,DNS:archipelago.local,DNS:localhost,IP:127.0.0.1" \
|
||||
>> "$LOG" 2>&1 \
|
||||
&& [ -s "$SSL_DIR/archipelago.key.new" ] \
|
||||
&& [ -s "$SSL_DIR/archipelago.crt.new" ]; then
|
||||
&& [ -s "$SSL_DIR/archipelago.crt.new" ] \
|
||||
&& openssl pkey -noout -in "$SSL_DIR/archipelago.key.new" >> "$LOG" 2>&1 \
|
||||
&& openssl x509 -noout -in "$SSL_DIR/archipelago.crt.new" >> "$LOG" 2>&1; then
|
||||
chmod 600 "$SSL_DIR/archipelago.key.new"
|
||||
mv "$SSL_DIR/archipelago.key.new" "$SSL_DIR/archipelago.key" \
|
||||
&& mv "$SSL_DIR/archipelago.crt.new" "$SSL_DIR/archipelago.crt" || return 1
|
||||
chmod 600 "$SSL_DIR/archipelago.key"
|
||||
@@ -1778,7 +1858,10 @@ gen_tls() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# 2. SSH host keys: generate a full fresh set in staging, then swap.
|
||||
# THE SINGLE PRODUCER of this node's SSH host keys. Same rule as gen_tls: no
|
||||
# second producer anywhere, add a caller instead.
|
||||
#
|
||||
# Generates a full fresh set in staging, then swaps.
|
||||
gen_ssh() {
|
||||
local staging
|
||||
staging=$(mktemp -d) || return 1
|
||||
@@ -1795,6 +1878,31 @@ gen_ssh() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Hand the new material to whatever consumes it.
|
||||
#
|
||||
# Two different situations, and getting this wrong is what would turn the timer
|
||||
# into theatre:
|
||||
# - First boot. We are ordered Before= these units, so they have not started
|
||||
# yet. try-reload-or-restart is a no-op on an inactive unit, which is
|
||||
# exactly right — they will start on their own moments later and read the
|
||||
# keys we just wrote.
|
||||
# - Self-heal, minutes or hours later. The unit already tried to start with no
|
||||
# key and is sitting in `failed`. try-reload-or-restart would be a no-op
|
||||
# there too, which would leave the node broken with valid keys on disk — the
|
||||
# recovery would be "complete" but the service still down. So a failed unit
|
||||
# is explicitly restarted.
|
||||
# --no-block because on first boot we are inside a unit these services are
|
||||
# ordered after; a blocking start could deadlock the boot transaction.
|
||||
refresh_consumer() {
|
||||
local unit="$1"
|
||||
if systemctl is-failed --quiet "$unit" 2>/dev/null; then
|
||||
log "$unit is failed (it started without a key); restarting it"
|
||||
systemctl --no-block restart "$unit" >> "$LOG" 2>&1 || true
|
||||
else
|
||||
systemctl try-reload-or-restart "$unit" >> "$LOG" 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
log "regenerating per-device secrets"
|
||||
|
||||
TLS_OK=0
|
||||
@@ -1803,13 +1911,13 @@ SSH_OK=0
|
||||
if retry "TLS keypair regeneration" gen_tls; then
|
||||
TLS_OK=1
|
||||
log "TLS keypair regenerated (CN=${NODE_NAME})"
|
||||
systemctl try-reload-or-restart nginx >> "$LOG" 2>&1 || true
|
||||
refresh_consumer nginx
|
||||
fi
|
||||
|
||||
if retry "SSH host key regeneration" gen_ssh; then
|
||||
SSH_OK=1
|
||||
log "SSH host keys regenerated"
|
||||
systemctl try-reload-or-restart ssh >> "$LOG" 2>&1 || true
|
||||
refresh_consumer ssh
|
||||
fi
|
||||
|
||||
if [ "$TLS_OK" -eq 1 ] && [ "$SSH_OK" -eq 1 ]; then
|
||||
@@ -1821,8 +1929,12 @@ if [ "$TLS_OK" -eq 1 ] && [ "$SSH_OK" -eq 1 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fail closed. Deliberately NO marker: its absence is what lets the unit run
|
||||
# again on the next boot.
|
||||
# Fail closed on serving, self-heal on recovering.
|
||||
#
|
||||
# Deliberately NO marker. Its absence is the entire retry mechanism: it keeps
|
||||
# the unit's ConditionPathExists=! satisfied, so archipelago-first-boot-secrets
|
||||
# .timer re-runs this in 15 minutes and every subsequent boot re-runs it too.
|
||||
# This is a failure, not a dead end — do not "fix" it by writing the marker.
|
||||
WHICH=""
|
||||
[ "$TLS_OK" -eq 0 ] && WHICH="TLS"
|
||||
[ "$SSH_OK" -eq 0 ] && WHICH="${WHICH:+$WHICH and }SSH"
|
||||
@@ -1831,15 +1943,16 @@ WHICH=""
|
||||
echo "failed=$WHICH"
|
||||
echo "tls_ok=$TLS_OK"
|
||||
echo "ssh_ok=$SSH_OK"
|
||||
echo "detail=per-device secret regeneration failed after all retries; the completion marker was NOT set, so this unit runs again on the next boot"
|
||||
echo "detail=per-device secret generation failed after all retries; the completion marker was NOT set, so archipelago-first-boot-secrets.timer retries in 15 minutes and every subsequent boot retries too"
|
||||
} > "$FAILED"
|
||||
shout "ARCHIPELAGO FIRST BOOT FAILED: could not generate this device's $WHICH key material. Refusing to continue — the affected services will not start. Record: $FAILED Log: $LOG"
|
||||
shout "ARCHIPELAGO: could not generate this device's $WHICH key material. The affected services will NOT start rather than run on a key we did not generate. Retrying automatically every 15 minutes and on every boot. Record: $FAILED Log: $LOG"
|
||||
exit 1
|
||||
SECRETSSCRIPT
|
||||
|
||||
chmod +x "$WORK_DIR/first-boot-secrets.sh"
|
||||
cp "$WORK_DIR/first-boot-secrets.sh" "$ARCH_DIR/scripts/"
|
||||
cp "$WORK_DIR/archipelago-first-boot-secrets.service" "$ARCH_DIR/scripts/"
|
||||
cp "$WORK_DIR/archipelago-first-boot-secrets.timer" "$ARCH_DIR/scripts/"
|
||||
|
||||
# Ship the mesh-radio udev rule at the media root — the embedded installer
|
||||
# searches "$BOOT_MEDIA/99-mesh-radio.rules" first, but nothing ever staged
|
||||
@@ -2764,6 +2877,11 @@ if [ -d "$BOOT_MEDIA/archipelago/container-images" ]; then
|
||||
if [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-first-boot-secrets.service" ]; then
|
||||
cp "$BOOT_MEDIA/archipelago/scripts/archipelago-first-boot-secrets.service" /mnt/target/etc/systemd/system/
|
||||
fi
|
||||
# The self-heal timer. Without it a node whose generators fail every in-boot
|
||||
# retry has no unattended way back — it would need someone at the console.
|
||||
if [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-first-boot-secrets.timer" ]; then
|
||||
cp "$BOOT_MEDIA/archipelago/scripts/archipelago-first-boot-secrets.timer" /mnt/target/etc/systemd/system/
|
||||
fi
|
||||
if [ -f "$BOOT_MEDIA/archipelago/scripts/setup-tor.sh" ]; then
|
||||
cp "$BOOT_MEDIA/archipelago/scripts/setup-tor.sh" /mnt/target/opt/archipelago/scripts/
|
||||
chmod +x /mnt/target/opt/archipelago/scripts/setup-tor.sh
|
||||
@@ -3430,28 +3548,37 @@ RemainAfterExit=yes
|
||||
WantedBy=multi-user.target
|
||||
DIAGSVC
|
||||
|
||||
# Ensure SSL cert exists for nginx HTTPS.
|
||||
# NO install-time TLS certificate is generated here, on purpose (audit F-03,
|
||||
# phase 10 plan 10-03).
|
||||
#
|
||||
# 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.
|
||||
# There used to be an "ensure SSL cert exists for nginx HTTPS" block that ran
|
||||
# its own "chroot /mnt/target openssl req ..." whenever the target had no cert.
|
||||
# Before the identity strip it almost never fired; after the strip it would
|
||||
# have fired on EVERY install.
|
||||
#
|
||||
# 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 \
|
||||
-keyout /etc/archipelago/ssl/archipelago.key \
|
||||
-out /etc/archipelago/ssl/archipelago.crt \
|
||||
-subj "/C=XX/ST=Bitcoin/L=Node/O=Archipelago/CN=archipelago" \
|
||||
-addext "subjectAltName=DNS:archipelago,DNS:archipelago.local,DNS:localhost,IP:127.0.0.1" 2>/dev/null
|
||||
chmod 600 /mnt/target/etc/archipelago/ssl/archipelago.key
|
||||
echo " Generated self-signed SSL certificate"
|
||||
fi
|
||||
# The problem with it was never that a second attempt to create a key existed.
|
||||
# It was that it was a second PRODUCER: its own openssl invocation, its own idea
|
||||
# of success, no retry policy, no failure record, no marker discipline. F-03 was
|
||||
# a silent-failure bug, and a second producer with its own accounting is exactly
|
||||
# how silent failures happen. So the fix is to unify, not to add another
|
||||
# fallback and not to leave the node with no way back.
|
||||
#
|
||||
# What replaces it: archipelago-first-boot-secrets.service is the single
|
||||
# producer of /etc/archipelago/ssl/archipelago.{key,crt}, exactly as it is the
|
||||
# single producer of /etc/ssh/ssh_host_*, and it retries with backoff in-boot,
|
||||
# on archipelago-first-boot-secrets.timer every 15 minutes, and on every boot
|
||||
# until it succeeds. A machine that is merely busy repairs itself unattended; a
|
||||
# machine that is genuinely broken says so loudly on the console and in
|
||||
# /var/lib/archipelago/first-boot-secrets.failed instead of quietly serving a
|
||||
# key nobody audited.
|
||||
#
|
||||
# The deterministic way this could never succeed — a missing openssl or
|
||||
# ssh-keygen — is caught at BUILD time by the assertion layer in STEP 1, so it
|
||||
# cannot reach a node at all.
|
||||
#
|
||||
# Do not reintroduce a producer here. If you need a key earlier, call the same
|
||||
# generator. tests/first-boot-secrets/run-tests.sh case 6 fails if a second
|
||||
# independent key-creating path appears in this file.
|
||||
|
||||
# Enable linger for rootless podman (containers survive logout)
|
||||
mkdir -p /mnt/target/var/lib/systemd/linger
|
||||
@@ -3518,6 +3645,14 @@ chroot /mnt/target systemctl enable archipelago.service 2>/dev/null || true
|
||||
chroot /mnt/target systemctl enable nginx.service 2>/dev/null || true
|
||||
chroot /mnt/target systemctl enable archipelago-load-images.service 2>/dev/null || true
|
||||
chroot /mnt/target systemctl enable archipelago-first-boot-secrets.service 2>/dev/null || true
|
||||
# Self-heal timer: if this does not get enabled, a node whose secret generation
|
||||
# fails every retry has no unattended way back. chroot systemctl enable can fail
|
||||
# silently, so fall back to writing the symlink by hand rather than trusting
|
||||
# `|| true` to have done anything.
|
||||
chroot /mnt/target systemctl enable archipelago-first-boot-secrets.timer 2>/dev/null || \
|
||||
{ mkdir -p /mnt/target/etc/systemd/system/timers.target.wants && \
|
||||
ln -sf /etc/systemd/system/archipelago-first-boot-secrets.timer \
|
||||
/mnt/target/etc/systemd/system/timers.target.wants/archipelago-first-boot-secrets.timer 2>/dev/null || true; }
|
||||
chroot /mnt/target systemctl enable archipelago-setup-tor.service 2>/dev/null || true
|
||||
chroot /mnt/target systemctl enable archipelago-first-boot-containers.service 2>/dev/null || true
|
||||
chroot /mnt/target systemctl enable archipelago-kiosk.service 2>/dev/null || true
|
||||
|
||||
Reference in New Issue
Block a user