feat(iso): per-device secrets on first boot + checksum emission and signing
- archipelago-first-boot-secrets.service regenerates the self-signed TLS keypair (device hostname in SAN) and all SSH host keys on the installed system's first boot, before ssh/nginx start — the squashfs bakes one key set at build time, so every flashed device shared them (§F 🔴) - staging-first swaps: a failed regeneration keeps the baked keys rather than leaving the device keyless - the builder now emits <iso>.sha256 next to the ISO, and scripts/sign-iso-checksums.sh signs {artifact,sha256,size} as a JSON doc with the release-root ceremony (verify: ceremony verify) (§F 🟠) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1492,6 +1492,83 @@ mkdir -p "$ARCH_DIR/scripts"
|
|||||||
cp "$WORK_DIR/load-container-images.sh" "$ARCH_DIR/scripts/"
|
cp "$WORK_DIR/load-container-images.sh" "$ARCH_DIR/scripts/"
|
||||||
cp "$WORK_DIR/archipelago-load-images.service" "$ARCH_DIR/scripts/"
|
cp "$WORK_DIR/archipelago-load-images.service" "$ARCH_DIR/scripts/"
|
||||||
|
|
||||||
|
# First-boot per-device secrets: the squashfs bakes one TLS key and one set
|
||||||
|
# of SSH host keys at build time, so every device flashed from the same image
|
||||||
|
# would share them. Regenerate both on the installed system's first boot,
|
||||||
|
# before the network-facing services come up.
|
||||||
|
echo " Creating first-boot secrets regeneration service..."
|
||||||
|
cat > "$WORK_DIR/archipelago-first-boot-secrets.service" <<'SECRETSSERVICE'
|
||||||
|
[Unit]
|
||||||
|
Description=Regenerate per-device secrets (TLS key, SSH host keys)
|
||||||
|
DefaultDependencies=no
|
||||||
|
After=local-fs.target
|
||||||
|
Before=ssh.service nginx.service archipelago.service
|
||||||
|
ConditionPathExists=!/var/lib/archipelago/.secrets-regenerated
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/opt/archipelago/scripts/first-boot-secrets.sh
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
SECRETSSERVICE
|
||||||
|
|
||||||
|
cat > "$WORK_DIR/first-boot-secrets.sh" <<'SECRETSSCRIPT'
|
||||||
|
#!/bin/bash
|
||||||
|
# Replace image-baked secrets with device-unique ones on first boot.
|
||||||
|
# Never leaves the device without working keys: new material is generated
|
||||||
|
# to a staging path first and only swapped in on success.
|
||||||
|
set -u
|
||||||
|
|
||||||
|
LOG=/var/log/archipelago-first-boot-secrets.log
|
||||||
|
MARKER=/var/lib/archipelago/.secrets-regenerated
|
||||||
|
[ -f "$MARKER" ] && exit 0
|
||||||
|
mkdir -p /var/lib/archipelago
|
||||||
|
NODE_NAME=$(hostname 2>/dev/null || echo archipelago)
|
||||||
|
|
||||||
|
echo "$(date): regenerating per-device secrets" >> "$LOG"
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
mkdir -p /etc/archipelago/ssl
|
||||||
|
if openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
||||||
|
-keyout /etc/archipelago/ssl/archipelago.key.new \
|
||||||
|
-out /etc/archipelago/ssl/archipelago.crt.new \
|
||||||
|
-subj "/C=XX/ST=Bitcoin/L=Node/O=Archipelago/CN=${NODE_NAME}" \
|
||||||
|
-addext "subjectAltName=DNS:${NODE_NAME},DNS:${NODE_NAME}.local,DNS:archipelago,DNS:archipelago.local,DNS:localhost,IP:127.0.0.1" \
|
||||||
|
>> "$LOG" 2>&1; then
|
||||||
|
mv /etc/archipelago/ssl/archipelago.key.new /etc/archipelago/ssl/archipelago.key
|
||||||
|
mv /etc/archipelago/ssl/archipelago.crt.new /etc/archipelago/ssl/archipelago.crt
|
||||||
|
chmod 600 /etc/archipelago/ssl/archipelago.key
|
||||||
|
echo "$(date): TLS keypair regenerated (CN=${NODE_NAME})" >> "$LOG"
|
||||||
|
systemctl try-reload-or-restart nginx >> "$LOG" 2>&1 || true
|
||||||
|
else
|
||||||
|
rm -f /etc/archipelago/ssl/archipelago.key.new /etc/archipelago/ssl/archipelago.crt.new
|
||||||
|
echo "$(date): WARNING: TLS regeneration failed, keeping baked key" >> "$LOG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. SSH host keys: generate a full fresh set in staging, then swap
|
||||||
|
STAGING=$(mktemp -d)
|
||||||
|
mkdir -p "$STAGING/etc/ssh"
|
||||||
|
if ssh-keygen -A -f "$STAGING" >> "$LOG" 2>&1 && ls "$STAGING"/etc/ssh/ssh_host_*_key >/dev/null 2>&1; then
|
||||||
|
rm -f /etc/ssh/ssh_host_*
|
||||||
|
mv "$STAGING"/etc/ssh/ssh_host_* /etc/ssh/
|
||||||
|
echo "$(date): SSH host keys regenerated" >> "$LOG"
|
||||||
|
systemctl try-reload-or-restart ssh >> "$LOG" 2>&1 || true
|
||||||
|
else
|
||||||
|
echo "$(date): WARNING: ssh-keygen -A failed, keeping baked host keys" >> "$LOG"
|
||||||
|
fi
|
||||||
|
rm -rf "$STAGING"
|
||||||
|
|
||||||
|
touch "$MARKER"
|
||||||
|
echo "$(date): per-device secrets done" >> "$LOG"
|
||||||
|
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/"
|
||||||
|
|
||||||
# Tor setup: copy torrc and create first-boot setup script
|
# Tor setup: copy torrc and create first-boot setup script
|
||||||
mkdir -p "$ARCH_DIR/scripts/tor"
|
mkdir -p "$ARCH_DIR/scripts/tor"
|
||||||
if [ -f "$SCRIPT_DIR/../../scripts/tor/torrc.template" ]; then
|
if [ -f "$SCRIPT_DIR/../../scripts/tor/torrc.template" ]; then
|
||||||
@@ -2401,6 +2478,13 @@ if [ -d "$BOOT_MEDIA/archipelago/container-images" ]; then
|
|||||||
if [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-load-images.service" ]; then
|
if [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-load-images.service" ]; then
|
||||||
cp "$BOOT_MEDIA/archipelago/scripts/archipelago-load-images.service" /mnt/target/etc/systemd/system/
|
cp "$BOOT_MEDIA/archipelago/scripts/archipelago-load-images.service" /mnt/target/etc/systemd/system/
|
||||||
fi
|
fi
|
||||||
|
if [ -f "$BOOT_MEDIA/archipelago/scripts/first-boot-secrets.sh" ]; then
|
||||||
|
cp "$BOOT_MEDIA/archipelago/scripts/first-boot-secrets.sh" /mnt/target/opt/archipelago/scripts/
|
||||||
|
chmod +x /mnt/target/opt/archipelago/scripts/first-boot-secrets.sh
|
||||||
|
fi
|
||||||
|
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
|
||||||
if [ -f "$BOOT_MEDIA/archipelago/scripts/setup-tor.sh" ]; then
|
if [ -f "$BOOT_MEDIA/archipelago/scripts/setup-tor.sh" ]; then
|
||||||
cp "$BOOT_MEDIA/archipelago/scripts/setup-tor.sh" /mnt/target/opt/archipelago/scripts/
|
cp "$BOOT_MEDIA/archipelago/scripts/setup-tor.sh" /mnt/target/opt/archipelago/scripts/
|
||||||
chmod +x /mnt/target/opt/archipelago/scripts/setup-tor.sh
|
chmod +x /mnt/target/opt/archipelago/scripts/setup-tor.sh
|
||||||
@@ -2942,8 +3026,8 @@ echo "" >> "$LOG"
|
|||||||
|
|
||||||
echo "=== Services ===" >> "$LOG"
|
echo "=== Services ===" >> "$LOG"
|
||||||
for svc in nginx archipelago archipelago-kiosk archipelago-load-images \
|
for svc in nginx archipelago archipelago-kiosk archipelago-load-images \
|
||||||
archipelago-first-boot-containers archipelago-setup-tor \
|
archipelago-first-boot-containers archipelago-first-boot-secrets \
|
||||||
console-setup; do
|
archipelago-setup-tor console-setup; do
|
||||||
STATUS=$(systemctl is-active "$svc" 2>/dev/null || echo "inactive")
|
STATUS=$(systemctl is-active "$svc" 2>/dev/null || echo "inactive")
|
||||||
ENABLED=$(systemctl is-enabled "$svc" 2>/dev/null || echo "disabled")
|
ENABLED=$(systemctl is-enabled "$svc" 2>/dev/null || echo "disabled")
|
||||||
printf " %-40s %s / %s\n" "$svc" "$STATUS" "$ENABLED" >> "$LOG"
|
printf " %-40s %s / %s\n" "$svc" "$STATUS" "$ENABLED" >> "$LOG"
|
||||||
@@ -3073,6 +3157,7 @@ chroot /mnt/target systemctl enable archipelago-bootstrap-switchover.timer 2>/de
|
|||||||
chroot /mnt/target systemctl enable archipelago.service 2>/dev/null || true
|
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 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-load-images.service 2>/dev/null || true
|
||||||
|
chroot /mnt/target systemctl enable archipelago-first-boot-secrets.service 2>/dev/null || true
|
||||||
chroot /mnt/target systemctl enable archipelago-setup-tor.service 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-first-boot-containers.service 2>/dev/null || true
|
||||||
chroot /mnt/target systemctl enable archipelago-kiosk.service 2>/dev/null || true
|
chroot /mnt/target systemctl enable archipelago-kiosk.service 2>/dev/null || true
|
||||||
@@ -3534,6 +3619,16 @@ else
|
|||||||
"$INSTALLER_ISO"
|
"$INSTALLER_ISO"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Checksums: emit next to the ISO so releases can be verified and the
|
||||||
|
# release ceremony can sign them (publish step signs; the build host
|
||||||
|
# never holds the release key)
|
||||||
|
(
|
||||||
|
cd "$(dirname "$OUTPUT_ISO")" || exit 0
|
||||||
|
ISO_NAME=$(basename "$OUTPUT_ISO")
|
||||||
|
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
|
||||||
|
echo " Checksum: $(cat "$ISO_NAME.sha256")"
|
||||||
|
)
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
if [ "$UNBUNDLED" = "1" ]; then
|
if [ "$UNBUNDLED" = "1" ]; then
|
||||||
echo "UNBUNDLED AUTO-INSTALLER ISO CREATED"
|
echo "UNBUNDLED AUTO-INSTALLER ISO CREATED"
|
||||||
|
|||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sign an ISO's checksums with the release root (counterpart to sign-manifest.sh).
|
||||||
|
#
|
||||||
|
# Run: bash scripts/sign-iso-checksums.sh path/to/archipelago-X.Y.Z.iso
|
||||||
|
# Then: paste your 24-word release master mnemonic, press Enter, then Ctrl-D.
|
||||||
|
#
|
||||||
|
# Writes <iso>.sha256.json next to the ISO — a JSON document carrying the
|
||||||
|
# artifact name, size and sha256, signed by the pinned release-root anchor
|
||||||
|
# (same detached-Ed25519 scheme as the OTA manifest and app catalog).
|
||||||
|
# Verify anywhere with: archipelago ceremony verify <iso>.sha256.json
|
||||||
|
#
|
||||||
|
# The mnemonic is read from the terminal only (never stored, never in shell
|
||||||
|
# history). The build host never holds the release key: build emits the plain
|
||||||
|
# <iso>.sha256; the publisher signs with this script during the ceremony.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ISO="${1:-}"
|
||||||
|
[ -n "$ISO" ] || { echo "Usage: $0 path/to/image.iso"; exit 1; }
|
||||||
|
[ -f "$ISO" ] || { echo "Error: $ISO not found"; exit 1; }
|
||||||
|
|
||||||
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
# Use ONLY a prebuilt signer — never compile here (compiling caused hangs in
|
||||||
|
# the earlier catalog ceremony). Prefer the repo's release build.
|
||||||
|
BIN=""
|
||||||
|
for candidate in "$REPO/core/target/release/archipelago" /tmp/archy-sign-bin/release/archipelago; do
|
||||||
|
if [[ -x "$candidate" ]]; then BIN="$candidate"; break; fi
|
||||||
|
done
|
||||||
|
if [[ -z "$BIN" ]]; then
|
||||||
|
echo "⏳ No prebuilt signer found. Build one first:"
|
||||||
|
echo " (cd core && cargo build --release -p archipelago)"
|
||||||
|
echo " Nothing was changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
ISO_NAME="$(basename "$ISO")"
|
||||||
|
ISO_DIR="$(cd "$(dirname "$ISO")" && pwd)"
|
||||||
|
OUT="$ISO_DIR/$ISO_NAME.sha256.json"
|
||||||
|
|
||||||
|
echo "Hashing $ISO_NAME (this can take a minute on a large ISO)..."
|
||||||
|
SHA256="$(sha256sum "$ISO" | awk '{print $1}')"
|
||||||
|
SIZE="$(wc -c < "$ISO" | tr -d ' ')"
|
||||||
|
|
||||||
|
cat > "$OUT" <<EOF
|
||||||
|
{
|
||||||
|
"artifact": "$ISO_NAME",
|
||||||
|
"sha256": "$SHA256",
|
||||||
|
"size_bytes": $SIZE
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
|
echo " Paste your 24-word release master mnemonic below, press Enter,"
|
||||||
|
echo " then press Ctrl-D on a new line."
|
||||||
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
|
"$BIN" ceremony sign "$OUT"
|
||||||
|
|
||||||
|
echo
|
||||||
|
if "$BIN" ceremony verify "$OUT"; then
|
||||||
|
echo "✅ SUCCESS — $OUT signed by the pinned release root."
|
||||||
|
echo " Publish it next to the ISO together with $ISO_NAME.sha256."
|
||||||
|
else
|
||||||
|
echo "❌ Verification failed — do not publish. Re-run the signing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user