production-deb/build.sh
obsidian-ai 0f5bbf004a fork: production-deb v0.1.0 from debian-s8ns-prefs-iso server variant
Server-only canonical production Debian build. Drops laptop/vanilla
variants. Interactive LUKS + hostname at install. user/123 forced rotate.
DVD-1 offline base. S8N_LOGS log-capture partition.

Lineage: forked from s8n/debian-s8ns-prefs-iso commit d4be55f.
2026-05-08 13:53:38 +01:00

427 lines
18 KiB
Bash
Executable file

#!/usr/bin/env bash
# build.sh — Build personal Debian ISO with my prefs + per-variant config.
#
# Base: Debian 13.4 trixie stable netinst. Forky daily was rejected because
# of recurring kernel/udeb-skew bugs (Debian #1106117) producing "no kernel
# modules" failures during install. trixie is a coherent snapshot; we still
# need `intel_iommu=off` for MBA 6,1 SSD detection but bake that into grub.
#
# Apple firmware boots from the ESP partition exposed by the El Torito EFI
# image, NOT from the iso9660 namespace. Custom grub.cfg goes into the FAT
# image inside the ISO via mtools (no root, no loop mount).
#
# Output: out/debian-s8ns-<variant>-<date>.iso
# out/debian-s8ns-<variant>-<date>.iso.sha256
# out/debian-s8ns-<variant>-<date>.creds (mode 0600, install pwds)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="$SCRIPT_DIR/work"
OUT_DIR="$SCRIPT_DIR/out"
# Defaults
VARIANT=""
HOSTNAME_OPT=""
SSH_PUBKEY="${SSH_PUBKEY:-$HOME/.ssh/id_ed25519.pub}"
DISK=""
USERNAME="${USERNAME:-user}"
OUT_ISO=""
TS_AUTH_KEY=""
# Tri-state: "" means "use variant default"; "0"/"1" override variant.
INTERACTIVE_LUKS_OPT=""
INTERACTIVE_HOSTNAME_OPT=""
USER_PW_PLAIN_OPT=""
DEBIAN_VERSION="${DEBIAN_VERSION:-13.4.0}"
# DVD-1 has GNOME + most packages bundled — supports offline install (no mirror).
# netinst is too small for GNOME tasksel; was causing "Bad archive mirror" errors
# on machines without ethernet at install time.
BASE_URL="${BASE_URL:-https://cdimage.debian.org/debian-cd/current/amd64/iso-dvd}"
ISO_NAME="debian-${DEBIAN_VERSION}-amd64-DVD-1.iso"
usage() {
cat <<EOF
Usage: $0 --variant {laptop|server|vanilla} [opts]
Required:
--variant NAME laptop | server | vanilla
Optional:
--hostname NAME hostname for installed system (default: <variant>-host)
--ssh-pubkey PATH SSH pubkey file (default: ~/.ssh/id_ed25519.pub;
falls back to Forgejo API users/s8n/keys if missing)
--disk DEV install target disk (default: variant-specific)
--user NAME first sudoer (default: admin)
--out FILE output ISO path (default: out/debian-s8ns-VARIANT-DATE.iso)
--base-url URL override netinst URL (default: cdimage current)
--ts-auth-key KEY Tailscale auth key (tskey-...) for unattended tailnet join
--interactive-luks omit partman-crypto/passphrase preseed; d-i prompts at console
(default: ON for server, OFF for laptop/vanilla)
--interactive-hostname omit netcfg/get_hostname preseed; d-i prompts (overridable
via kernel cmdline hostname=NAME)
(default: ON for server, OFF for laptop/vanilla)
--user-pw PLAIN bake this plain password (chage -d 0 forces rotate on first
login). Default: random 16-char per-build (or "123" for server).
-h | --help this
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--variant) VARIANT="$2"; shift 2;;
--hostname) HOSTNAME_OPT="$2"; shift 2;;
--ssh-pubkey) SSH_PUBKEY="$2"; shift 2;;
--disk) DISK="$2"; shift 2;;
--user) USERNAME="$2"; shift 2;;
--out) OUT_ISO="$2"; shift 2;;
--base-url) BASE_URL="$2"; shift 2;;
--ts-auth-key) TS_AUTH_KEY="$2"; shift 2;;
--interactive-luks) INTERACTIVE_LUKS_OPT=1; shift;;
--no-interactive-luks) INTERACTIVE_LUKS_OPT=0; shift;;
--interactive-hostname) INTERACTIVE_HOSTNAME_OPT=1; shift;;
--no-interactive-hostname) INTERACTIVE_HOSTNAME_OPT=0; shift;;
--user-pw) USER_PW_PLAIN_OPT="$2"; shift 2;;
-h|--help) usage; exit 0;;
*) echo "ERR: unknown arg: $1" >&2; usage; exit 1;;
esac
done
# Validate
[[ -n "$VARIANT" ]] || { echo "ERR: --variant required" >&2; usage; exit 1; }
VARIANT_FILE="$SCRIPT_DIR/variants/$VARIANT.cfg"
[[ -f "$VARIANT_FILE" ]] || { echo "ERR: variant not found: $VARIANT_FILE" >&2; exit 1; }
# Source variant config (sets VARIANT_NAME, VARIANT_VOLID, GRUB_PARAMS, DEFAULT_DISK,
# TASKSEL_TASKS, PACKAGES_LIST, POST_INSTALL_SCRIPTS; optional: INTERACTIVE_LUKS,
# INTERACTIVE_HOSTNAME, USER_PW_PLAIN_DEFAULT, PRESEED_PRIORITY)
# shellcheck source=/dev/null
source "$VARIANT_FILE"
# Resolve interactive flags: CLI override > variant default > 0
INTERACTIVE_LUKS="${INTERACTIVE_LUKS_OPT:-${INTERACTIVE_LUKS:-0}}"
INTERACTIVE_HOSTNAME="${INTERACTIVE_HOSTNAME_OPT:-${INTERACTIVE_HOSTNAME:-0}}"
PRESEED_PRIORITY="${PRESEED_PRIORITY:-critical}"
# Apply --hostname (HOSTNAME_OPT, NOT $HOSTNAME — bash exposes the system
# hostname via $HOSTNAME so a -n test would always pass).
HOSTNAME_FINAL="${HOSTNAME_OPT:-${VARIANT}-host}"
[[ "$HOSTNAME_FINAL" =~ ^[a-z0-9][a-z0-9-]{0,62}$ ]] \
|| { echo "ERR: invalid hostname '$HOSTNAME_FINAL' (lowercase alnum + hyphens, 1-63 chars)" >&2; exit 1; }
[[ -n "$DISK" ]] || DISK="$DEFAULT_DISK"
[[ -n "$OUT_ISO" ]] || OUT_ISO="$OUT_DIR/debian-s8ns-${VARIANT}-$(date +%Y%m%d).iso"
CREDS_FILE="${OUT_ISO%.iso}.creds"
# Resolve SSH pubkey (-s = file exists AND non-empty)
PUBKEY_CONTENT=""
if [[ -s "$SSH_PUBKEY" ]]; then
PUBKEY_CONTENT="$(cat "$SSH_PUBKEY")"
echo "[*] SSH pubkey: $SSH_PUBKEY"
elif [[ -f "$HOME/.config/veilor-forgejo-pat.txt" ]]; then
echo "[*] SSH pubkey file missing or empty, fetching from Forgejo (user s8n)..."
PAT="$(cat "$HOME/.config/veilor-forgejo-pat.txt")"
PUBKEY_CONTENT="$(curl -sH "Authorization: token $PAT" \
'https://git.s8n.ru/api/v1/users/s8n/keys' \
| python3 -c 'import json,sys; [print(k["key"]) for k in json.load(sys.stdin)]')"
fi
[[ -n "$PUBKEY_CONTENT" ]] || { echo "ERR: no SSH pubkey resolved." >&2; exit 1; }
# Tools
for cmd in curl xorriso sha256sum python3 mkpasswd mcopy mdir; do
command -v "$cmd" >/dev/null \
|| { echo "ERR: missing $cmd. Fedora: dnf install whois mtools xorriso curl python3" >&2; exit 1; }
done
mkdir -p "$WORK_DIR" "$OUT_DIR"
cd "$WORK_DIR"
# === 1. Generate per-build credentials ===
# `head -c N /dev/urandom | base64 | tr -dc '...'` order matters: head reads
# fixed bytes from urandom (no SIGPIPE risk), then base64 + tr clean up.
# Going `tr | head` instead aborts under set -o pipefail because tr gets
# SIGPIPE when head closes its end (exit 141).
echo "[*] Generating per-build credentials..."
# User-pw resolution: --user-pw > variant USER_PW_PLAIN_DEFAULT > random
if [[ -n "$USER_PW_PLAIN_OPT" ]]; then
USER_PW_PLAIN="$USER_PW_PLAIN_OPT"
elif [[ -n "${USER_PW_PLAIN_DEFAULT:-}" ]]; then
USER_PW_PLAIN="$USER_PW_PLAIN_DEFAULT"
else
USER_PW_PLAIN="$(head -c 32 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 16)"
fi
USER_PW_CRYPTED="$(printf '%s' "$USER_PW_PLAIN" | mkpasswd -m yescrypt -s)"
LUKS_INSTALL_PW="$(head -c 48 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 24)"
# LUKS_FINAL_PW is generated and rotated by late_command on the target system.
# We don't store it in the ISO at all — only the throwaway LUKS_INSTALL_PW
# touches the ISO image bytes, and that gets killed in late_command before reboot.
# === 2. Download base ISO (cached) ===
if [[ ! -f "$ISO_NAME" ]]; then
echo "[*] Downloading $ISO_NAME from $BASE_URL ..."
curl -fL --remote-name-all "$BASE_URL/$ISO_NAME" "$BASE_URL/SHA256SUMS"
else
echo "[*] Using cached $ISO_NAME (delete work/$ISO_NAME to refresh)"
curl -fLo SHA256SUMS "$BASE_URL/SHA256SUMS"
fi
# === 3. Verify checksum (anchored — no substring false positives) ===
echo "[*] Verifying SHA256..."
sha256sum -c --ignore-missing SHA256SUMS 2>&1 | grep -q "^${ISO_NAME}: OK$" \
|| { echo "ERR: SHA256 verification failed for $ISO_NAME." >&2; exit 1; }
echo "[OK] checksum"
# === 4. Render preseed.cfg from template ===
# sed-escape pubkey for delimiter `|` and replacement chars `&` `\`
echo "[*] Rendering preseed.cfg for variant=$VARIANT..."
ESCAPED_PUBKEY="$(printf '%s\n' "$PUBKEY_CONTENT" | sed -e 's/[\\&|]/\\&/g')"
ESCAPED_HASH="$(printf '%s' "$USER_PW_CRYPTED" | sed -e 's/[\\&|]/\\&/g')"
ESCAPED_TS_KEY="$(printf '%s' "$TS_AUTH_KEY" | sed -e 's/[\\&|]/\\&/g')"
sed \
-e "s|@HOSTNAME@|$HOSTNAME_FINAL|g" \
-e "s|@USERNAME@|$USERNAME|g" \
-e "s|@DISK@|$DISK|g" \
-e "s|@SSH_PUBKEY@|$ESCAPED_PUBKEY|g" \
-e "s|@USER_PW_CRYPTED@|$ESCAPED_HASH|g" \
-e "s|@LUKS_INSTALL_PW@|$LUKS_INSTALL_PW|g" \
-e "s|@TASKSEL_TASKS@|$TASKSEL_TASKS|g" \
-e "s|@VARIANT@|$VARIANT|g" \
-e "s|@TS_AUTH_KEY@|$ESCAPED_TS_KEY|g" \
"$SCRIPT_DIR/shared/preseed.tpl" > preseed.cfg
# Interactive overrides — strip preseed lines so d-i prompts the operator.
# When INTERACTIVE_LUKS, also strip the late_command luks-rekey step (the
# install passphrase user typed at the prompt is unknown to us — can't rekey).
if [[ "$INTERACTIVE_LUKS" == "1" ]]; then
echo "[*] INTERACTIVE_LUKS=1 — stripping LUKS preseed + rekey from late_command"
# Match passphrase AND passphrase-again (both `partman-crypto/passphrase*`).
sed -i \
-e '/^d-i partman-crypto\/passphrase/d' \
-e '/luks-rekey\.sh /d' \
preseed.cfg
fi
if [[ "$INTERACTIVE_HOSTNAME" == "1" ]]; then
echo "[*] INTERACTIVE_HOSTNAME=1 — stripping hostname preseed (cmdline overrides)"
sed -i \
-e '/^d-i netcfg\/get_hostname /d' \
-e '/^d-i netcfg\/hostname /d' \
preseed.cfg
fi
chmod 644 preseed.cfg
# === 5. Render grub-overlay.cfg ===
echo "[*] Rendering grub-overlay.cfg..."
sed \
-e "s|@GRUB_PARAMS@|$GRUB_PARAMS|g" \
-e "s|@VARIANT@|$VARIANT|g" \
-e "s|@VOLID@|$VARIANT_VOLID|g" \
-e "s|@PRIORITY@|$PRESEED_PRIORITY|g" \
"$SCRIPT_DIR/shared/grub-overlay.cfg.tpl" > grub-overlay.cfg
# === 6. Bundle post-install payload (lands at /cdrom/postinstall/ in ISO) ===
echo "[*] Bundling post-install payload..."
rm -rf payload && mkdir -p payload/scripts
cp "$SCRIPT_DIR/shared/packages/$PACKAGES_LIST" payload/extra.list
for s in "${POST_INSTALL_SCRIPTS[@]}"; do
cp "$SCRIPT_DIR/shared/post-install/$s" "payload/scripts/$s"
chmod +x "payload/scripts/$s"
done
# Tailscale auth-key passes via this file (chmod 600); 30-tailscale.sh reads it.
if [[ -n "$TS_AUTH_KEY" ]]; then
printf '%s\n' "$TS_AUTH_KEY" > payload/ts-auth-key
chmod 600 payload/ts-auth-key
fi
# Driver script — runs in-target via late_command. Already inside chroot.
cat > payload/run.sh <<'RUNSH'
#!/bin/sh
# run.sh — runs inside chroot (via `in-target sh -e`), with /proc /sys /dev
# already bind-mounted by d-i. Loops scripts in lex order.
set -eu
LOG=/var/log/s8n-post-install.log
exec >"$LOG" 2>&1
echo "[s8n] post-install start: $(date -u +%FT%TZ)"
PAYLOAD=/root/s8n-postinstall
if [ ! -d "$PAYLOAD/scripts" ]; then
echo "[s8n] ERROR: $PAYLOAD/scripts missing"
exit 1
fi
for s in "$PAYLOAD"/scripts/*.sh; do
echo "[s8n] running $(basename "$s")"
/bin/sh -e "$s" || { echo "[s8n] ERROR $(basename "$s") exited $?"; exit 1; }
done
echo "[s8n] post-install done: $(date -u +%FT%TZ)"
RUNSH
chmod +x payload/run.sh
# LUKS rekey runs OUTSIDE chroot (cryptsetup needs /target/dev access).
# late_command invokes this with sh -e from the installer's environment.
cat > payload/luks-rekey.sh <<'LUKSSH'
#!/bin/sh
# luks-rekey.sh — runs from late_command in the d-i environment (NOT in-target).
# Adds a fresh random key, verifies it, kills the throwaway slot 0.
# New passphrase written to /target/root/luks-pw.txt mode 0600.
set -eu
LOG=/var/log/s8n-luks-rekey.log
exec >"$LOG" 2>&1
OLD_PW="$1" # passed in by late_command
CRYPT_DEV=$(blkid -t TYPE=crypto_LUKS -o device | head -1)
[ -n "$CRYPT_DEV" ] || { echo "[luks] ERROR: no LUKS device found"; exit 1; }
echo "[luks] rotating keyslots on $CRYPT_DEV"
NEW_PW=$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)
printf '%s\n%s\n%s\n' "$OLD_PW" "$NEW_PW" "$NEW_PW" \
| cryptsetup luksAddKey "$CRYPT_DEV" --key-slot 1
printf '%s' "$NEW_PW" | cryptsetup open --test-passphrase "$CRYPT_DEV" --key-slot 1 -
printf '%s' "$OLD_PW" | cryptsetup luksKillSlot "$CRYPT_DEV" 0 -
umask 077
mkdir -p /target/root
{
echo "LUKS pw (write down NOW, this file is destroyed on first apt upgrade):"
echo "$NEW_PW"
echo "Device: $CRYPT_DEV"
} >/target/root/luks-pw.txt
chmod 0600 /target/root/luks-pw.txt
echo "[luks] rotation complete; new pw in /target/root/luks-pw.txt"
LUKSSH
chmod +x payload/luks-rekey.sh
# === 7. Patch BIOS-mode boot config (isolinux/txt.cfg) for legacy USB boot ===
echo "[*] Patching legacy boot config..."
rm -rf boot-configs && mkdir -p boot-configs
xorriso -osirrox on -indev "$ISO_NAME" \
-extract /isolinux/txt.cfg boot-configs/txt.cfg \
-extract /boot/grub/grub.cfg boot-configs/grub-main.cfg \
-extract /boot/grub/efi.img boot-configs/efi.img \
2>/dev/null || true
chmod -R u+w boot-configs/
# Inject preseed args into legacy isolinux + main grub.cfg
# (isolinux only fires on legacy/CSM boot; main grub.cfg fires on EFI when
# the ESP grub.cfg chains to it. ESP grub.cfg gets fully replaced below.)
# Scrub upstream `priority=critical` first; otherwise our injected priority
# stacks with the existing one in submenus and the duplicate is sketchy.
sed -i "s|priority=critical |priority=$PRESEED_PRIORITY |g" boot-configs/grub-main.cfg boot-configs/txt.cfg
sed -i "s|--- quiet|auto=true priority=$PRESEED_PRIORITY file=/cdrom/preseed.cfg $GRUB_PARAMS --- quiet|g" \
boot-configs/grub-main.cfg
sed -i '1i set default="0"\nset timeout=5\n' boot-configs/grub-main.cfg
sed -i "s|append vga=788 initrd=/install.amd/initrd.gz|append auto=true priority=$PRESEED_PRIORITY file=/cdrom/preseed.cfg $GRUB_PARAMS vga=788 initrd=/install.amd/initrd.gz|g" \
boot-configs/txt.cfg
# === 8. Patch ESP FAT image (this is what Apple firmware actually reads) ===
# Note: paths inside the FAT image are stored as lowercase (/efi/debian/grub.cfg).
# mtools is mostly case-insensitive but verify against the stored case for safety.
echo "[*] Patching ESP FAT image (mtools, no root)..."
mcopy -i boot-configs/efi.img -o grub-overlay.cfg ::/efi/debian/grub.cfg
# Verify the file exists and has our content (size match against our overlay).
EXPECTED_SIZE=$(stat -c%s grub-overlay.cfg)
ACTUAL_SIZE=$(mdir -i boot-configs/efi.img ::/efi/debian/grub.cfg 2>/dev/null \
| awk '/grub *cfg/ { print $3; exit }')
[[ "$ACTUAL_SIZE" == "$EXPECTED_SIZE" ]] \
|| { echo "ERR: ESP grub.cfg size mismatch (expected=$EXPECTED_SIZE got=$ACTUAL_SIZE)" >&2; exit 1; }
echo "[OK] ESP grub.cfg replaced inside efi.img ($EXPECTED_SIZE bytes)"
# === 9. Re-master ISO with overlays + payload ===
echo "[*] Packing $OUT_ISO ..."
# In-place: cp source, then xorriso -dev. -boot_image any keep is REQUIRED to
# preserve the El Torito boot catalog AND keep its pointer to the patched
# /boot/grub/efi.img — without this, UEFI firmware reads the OLD pre-patch
# FAT image even though the iso9660 file is updated.
cp -f "$ISO_NAME" "$OUT_ISO"
xorriso -dev "$OUT_ISO" \
-boot_image any keep \
-volid "$VARIANT_VOLID" \
-map preseed.cfg /preseed.cfg \
-map payload /postinstall \
-map boot-configs/grub-main.cfg /boot/grub/grub.cfg \
-map boot-configs/txt.cfg /isolinux/txt.cfg \
-map boot-configs/efi.img /boot/grub/efi.img \
-map grub-overlay.cfg /EFI/debian/grub.cfg \
-commit
# === 10. Direct-write patched efi.img into the ISO's ESP partition location ===
# xorriso's -dev + -map updates the iso9660 namespace entry for /boot/grub/efi.img,
# but the ACTUAL ESP partition (referenced by MBR partition #2 AND El Torito
# catalog UEFI entry) lives at a fixed LBA in the ISO that doesn't move when
# we replace the iso9660 file. After dd-flashing the ISO to USB, the kernel
# exposes sda2 by reading bytes at the MBR-partition-2 LBA — which is unchanged.
# Fix: direct-dd our patched FAT image into that LBA.
ESP_LBA=$(xorriso -indev "$OUT_ISO" -report_el_torito plain 2>&1 \
| awk '/El Torito boot img/ && /UEFI/ {print $NF; exit}')
[[ -n "$ESP_LBA" ]] || { echo "ERR: cannot find ESP LBA from El Torito catalog" >&2; exit 1; }
echo "[*] Direct-writing efi.img to ISO at LBA $ESP_LBA..."
dd if=boot-configs/efi.img of="$OUT_ISO" \
bs=2048 seek="$ESP_LBA" \
conv=notrunc status=none
echo "[OK] ESP partition now contains patched grub.cfg overlay"
# === 11. Strip APM driver descriptor (single yellow icon on Apple) ===
echo "[*] Stripping APM driver descriptor..."
PT=$(mktemp)
dd if="$OUT_ISO" of="$PT" bs=1 skip=446 count=66 status=none
dd if=/dev/zero of="$OUT_ISO" bs=512 count=16 conv=notrunc status=none
dd if="$PT" of="$OUT_ISO" bs=1 seek=446 count=66 conv=notrunc status=none
rm -f "$PT"
# === 12. Output credentials file (mode 0600) ===
case "$DISK" in
/dev/nvme*|/dev/mmcblk*) LUKS_PART="${DISK}p3" ;;
*) LUKS_PART="${DISK}3" ;;
esac
umask 077
cat > "$CREDS_FILE" <<CREDS
# Bootstrap credentials for $(basename "$OUT_ISO")
# Generated: $(date -u +%FT%TZ)
# This file mode 0600 — destroy after copying to password manager.
variant = $VARIANT
hostname = $HOSTNAME_FINAL
user = $USERNAME
disk = $DISK
luks_part = $LUKS_PART
user_pw = $USER_PW_PLAIN
luks_pw = $(if [[ "$INTERACTIVE_LUKS" == "1" ]]; then echo "(prompted at console — set in person at install time)"; else echo "(rotated by late_command — read /root/luks-pw.txt on first boot)"; fi)
hostname_set = $(if [[ "$INTERACTIVE_HOSTNAME" == "1" ]]; then echo "(prompted at console; or via cmdline 'hostname=NAME')"; else echo "$HOSTNAME_FINAL (baked)"; fi)
priority = $PRESEED_PRIORITY
# The user pw above is yescrypt-hashed in preseed.cfg; \`chage -d 0\` on the
# installed system will force a change on first console/SSH login.
# The LUKS pw inside preseed.cfg is "$LUKS_INSTALL_PW" but it's killed in
# late_command before reboot, so it never persists on disk (non-interactive only).
CREDS
chmod 0600 "$CREDS_FILE"
# === 13. Done ===
SIZE=$(du -h "$OUT_ISO" | awk '{print $1}')
sha256sum "$OUT_ISO" > "$OUT_ISO.sha256"
echo
echo "[OK] Built: $OUT_ISO ($SIZE)"
echo " SHA256: $(awk '{print $1}' "$OUT_ISO.sha256")"
echo " Creds: $CREDS_FILE (mode 0600)"
echo
echo "Flash to USB:"
echo " ./flash.sh /dev/sdX $OUT_ISO"
echo
echo "Variant = $VARIANT"
echo "Hostname = $HOSTNAME_FINAL"
echo "User = $USERNAME"
echo "Disk = $DISK"
echo "Grub params = $GRUB_PARAMS"
echo "Tasks = $TASKSEL_TASKS"
[[ -n "$TS_AUTH_KEY" ]] && echo "Tailscale = auth-key baked, will join on first boot"
echo
echo "User pw = $USER_PW_PLAIN (forced rotate first login)"
if [[ "$INTERACTIVE_LUKS" == "1" ]]; then
echo "LUKS pw = INTERACTIVE — d-i prompts at console (no rekey)"
else
echo "LUKS pw = rotated by installer; read /root/luks-pw.txt on first boot"
fi
if [[ "$INTERACTIVE_HOSTNAME" == "1" ]]; then
echo "Hostname = INTERACTIVE (override via kernel cmdline 'hostname=NAME')"
fi
echo "Priority = $PRESEED_PRIORITY"