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.
This commit is contained in:
obsidian-ai 2026-05-08 13:53:38 +01:00
commit 0f5bbf004a
15 changed files with 1604 additions and 0 deletions

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
work/
out/
*.iso
*.iso.sha256
*.deb
boot-configs/
.secrets
*.swp

157
CHANGELOG.md Normal file
View file

@ -0,0 +1,157 @@
# Changelog (production-deb)
Forked from `s8n/debian-s8ns-prefs-iso` (commit d4be55f) on 2026-05-08.
Server variant only; laptop/vanilla stripped.
## [0.1.0] — 2026-05-08
### Added
- Initial fork from debian-s8ns-prefs-iso server variant
- Same hardening, interactive LUKS+hostname, DVD-1 offline base, S8N_LOGS partition
---
# Upstream changelog (debian-s8ns-prefs-iso, kept for lineage)
All notable changes to debian-s8ns-prefs-iso. Format inspired by Keep a Changelog.
## [0.4.0] — 2026-05-08
### Added
- **USB log-capture partition** for offline diagnostics. flash.sh creates a
3rd MBR partition (vfat, label `S8N_LOGS`) using all remaining USB free
space (~27 GiB on a 32 GiB stick). preseed `early_command` mounts it at
`/tmp/s8n-logs`; `late_command` writes a per-run directory with:
- `syslog`, `installer/` (full d-i logs)
- `s8n-luks-rekey.log` and `s8n-post-install.log`
- `lsblk`, `lspci`, `dmesg`, `mount`, `df`, `exit-status`, `build-info`
Even if late_command's main block fails, the trap-style outer `sh -c`
still copies whatever logs exist before unmounting.
- **`scripts/read-usb-logs.sh`** — dumps the latest run's logs from the
S8N_LOGS partition. Auto-detects by label or takes `/dev/sdX` as arg.
`--copy` flag rsyncs the entire S8N_LOGS contents to `out/usb-logs-<ts>/`.
- **flash.sh `--yes` flag** for non-interactive use.
### Changed
- preseed.tpl `late_command` wrapped in trap-style `sh -c` so log-collect
runs even if the install body fails. Exit status preserved via captured
`$STATUS`.
### Fixed
- (none new — 0.4 is purely additive)
### Operator workflow change
After a failed install, instead of photographing the d-i screen:
1. Pull the USB from the target machine
2. Plug into the build host (onyx)
3. Run `sudo scripts/read-usb-logs.sh /dev/sdX` (or just `--copy` to grab everything)
## [0.3.0] — 2026-05-07
### Added
- **install.sh** companion script — apply same shared/post-install/ scripts to an
existing Debian system (curl|bash compatible via Forgejo tarball API)
- **Per-build randomized credentials.** User pw `mkpasswd -m yescrypt` 16-char
random; LUKS install pw 24-char random. Written to `<iso>.creds` mode 0600.
- **LUKS rekey in late_command.** New 32-char random pw added via
`cryptsetup luksAddKey`, throwaway slot 0 killed before reboot. Final pw
written to `/target/root/luks-pw.txt` mode 0600.
- **Tailscale `--ts-auth-key` build flag.** Bakes one-time tskey into ISO;
installed system auto-joins tailnet via systemd oneshot on first boot, then
shreds the key file.
- **VM smoke-test harness** at `scripts/test-vm.sh`. QEMU+OVMF, virtio disk +
cdrom, 30 GiB qcow2, headless. Phase 1 unattended install + Phase 2
first-boot SSH check.
- **MBA 6,1 driver superset in laptop variant:**
- `mbpfan` (applesmc fan control), `bluez` `blueman` (BCM4360 BT)
- `tlp tlp-rdw powertop lm-sensors` (battery / thermals)
- `firmware-sof-signed` (Haswell HDA SOF fallback)
- GRUB cmdline: `acpi_backlight=vendor`, `snd_hda_intel.model=mba6`
- `/etc/modprobe.d/hid_apple.conf` with `fnmode=2 iso_layout=0`
- `/etc/modules-load.d/apple.conf` for applesmc + coretemp
- `update-initramfs -u` after modprobe.d edits
### Changed
- **Base ISO: trixie 13.4 stable** (was forky daily). Reason: Debian bug
[#1106117](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1106117) —
forky weekly netinsts ship with kernel/udeb skew producing "no kernel
modules" install failures. trixie is a coherent snapshot; intel_iommu=off
in grub overlay still solves MBA 6,1 SSD detection on stable kernel 6.12.
- **late_command rewritten** to use `in-target` (with bind-mounted /proc /sys
/dev) instead of nested `chroot` (which had no bind-mounts → apt failed).
Wrapped in `sh -c 'set -e ; ...'` so partial failures abort install.
- **Postinstall payload moves to `/cdrom/postinstall/`** in the ISO; copied
to installed system at `/root/s8n-postinstall/` during late_command.
- **ESP image patching via mtools.** Previous v0.2 wrote grub-overlay.cfg
into iso9660 namespace at `/EFI/debian/grub.cfg`, which Apple firmware
doesn't read. v0.3:
1. `xorriso -extract /boot/grub/efi.img` to grab the embedded FAT image
2. `mcopy -i efi.img grub-overlay.cfg ::/efi/debian/grub.cfg` to patch FAT
3. `xorriso -dev <iso> -boot_image any keep -map ... -commit` to splice back
4. **Direct-dd patched efi.img into the ISO at the El Torito EFI image LBA**
because xorriso's `-map` only updates iso9660 namespace, not the
ESP-partition data range that MBR partition #2 points to
- **post-install split:** `40-mba61.sh``40-broadcom-wl.sh` (any laptop with
broadcom-sta-dkms) + `50-mba61.sh` (only Apple Toshiba [1179:010b])
- **install.sh idempotence:** `00-base.sh` skips ufw reset if already active;
`10-dark.sh` grep-guards `/etc/environment` append; sshd_config not modified
on rerun if already includes our drop-in.
### Fixed (CRITICAL bugs from v0.2 audit)
- **A2-1** `build.sh:65` Bash `HOSTNAME` builtin shadowing — renamed to
`HOSTNAME_OPT`, default-variant-host now applies correctly
- **A2-2** `build.sh:71` empty SSH pubkey file → SSH lockout. Use `-s` not `-f`
- **A2-3** `build.sh:103` sha256 grep regex unanchored. Now `^${ISO_NAME}: OK$`
- **A2-4** `build.sh:148` chroot-empty branch removed (single in-target path)
- **A2-5** late_command bind-mounts via in-target (apt now works inside)
- **A2-6** flash.sh sed strip → `lsblk -no PKNAME` (handles nvme/mmcblk/RAID)
### Fixed (HIGH security from v0.2 audit)
- **A1-3** late_command wrapped in `sh -c 'set -e'` — fail loud, no silent
half-installs claiming success
- **A1-1 / A3-creds** plaintext `changeme` removed; yescrypt hash via
`mkpasswd -m yescrypt` baked, plain pw printed once to .creds file 0600
- **A1-2** LUKS pw auto-rotation in late_command; throwaway slot 0 killed
before reboot
### Fixed (MED from v0.2 audit)
- **flash.sh** refuses `/dev/mmcblk*`, `/dev/vd*` in addition to nvme/sda
- **build.sh** validates hostname format before render
- **build.sh** drops unused 7z dep check; adds mtools + mkpasswd checks
- **Forgejo URL** `s8n-ru``s8n` (user renamed 2026-05-07)
### Known limits / deferred to v0.4+
- VM smoke test currently can't proceed past GRUB on headless QEMU because
d-i graphical-install requires VGA framebuffer; no `console=ttyS0`
redirection in our overlay. Either: add a "VM serial install" menuentry
with console=ttyS0,115200n8, or run VM test with VNC display. **Build-time
content checks (xorriso -extract + mdir verify + direct-dd ESP read-back)
are the current correctness gate.**
- SHA256SUMS not yet GPG-verified (Debian release key pin pending) — v0.4
- Forgejo SSH key fingerprint not pinned — v0.4
- `install.sh` GRUB persist still uses substring match (not word-boundary) — v0.4
- Server hardening superset (auditd rules, apparmor enforce, faillock,
login.defs YESCRYPT, journald persistent+seal, KSPP cmdline, etc.) — v0.5
- Reproducible builds (SOURCE_DATE_EPOCH, --modification-date) + Forgejo CI — v1.0
## [0.2.0] — 2026-05-07
### Added
- Smoke-tested 3 variants build clean
- `install.sh` companion script
- Wider firmware coverage in shared pkgsel/include
- post-install split 40-mba61 → 40-broadcom-wl + 50-mba61
### Fixed
- xorriso extract chmod u+w
- ESP grub.cfg uses overlay direct (not extracted)
- LUKS partition path NVMe vs SATA (no more `sdap3`)
- MBR PT preservation via cp + xorriso -dev (was zeroed by `-indev/-outdev`)
## [0.1.0] — 2026-05-07
### Added
- Initial scaffold: build.sh, flash.sh, 3 variants (laptop/server/vanilla)
- shared/preseed.tpl, grub-overlay.cfg.tpl, post-install/, packages/
- APM strip step for single-icon Apple boot
- Forgejo private repo at git.s8n.ru/s8n/debian-s8ns-prefs-iso

83
README.md Normal file
View file

@ -0,0 +1,83 @@
# production-deb
Production Debian server ISO builder. Hardened headless. Interactive LUKS +
hostname at install time. user pw `123` (forced rotate first login). SSH key
pre-placed for instant remote access.
Forked from `s8n/debian-s8ns-prefs-iso` server variant; non-server variants
(laptop/vanilla) stripped. This repo is the canonical production server build.
## Install behavior
Boot the flashed USB. Two prompts at console:
1. **Hostname** — typed (default = `server-host`)
2. **LUKS passphrase** — set in person, NOT preseeded
Everything else unattended:
- Debian 13.4 trixie (DVD-1 base, offline-capable — no mirror needed)
- LUKS+LVM atomic, encrypted root
- User `user`, pw `123`, sudo, forced rotate on first SSH login (`chage -d 0`)
- SSH ed25519 key from `~/.ssh/id_ed25519.pub` pre-placed in `/home/user/.ssh/authorized_keys`
- sshd hardened: pubkey-only, no root, no password auth
- ufw default-deny, allow 22/tcp
- fail2ban + auditd + apparmor enforce + libpam-pwquality
- dropbear-initramfs + cryptsetup-initramfs (LUKS unlock-via-SSH on boot)
- Tailscale client installed, manual `tailscale up --login-server=https://hs.s8n.ru` post-boot
## Usage
```
./build.sh --variant server --hostname <name> --disk /dev/nvme0n1
sudo ./flash.sh --yes /dev/sdX out/debian-s8ns-server-DATE.iso
```
(Variant is always `server` in this repo; flag retained for compat with the
parent project's build.sh.)
After install completes:
```
ssh user@<hostname>
# PAM forces pw change → set new pw → shell
```
## Layout
```
build.sh ISO factory (DVD-1 base, hd-media kernel, mtools ESP edit)
flash.sh safe USB flash + adds S8N_LOGS log-capture partition
install.sh curl|bash post-install applier for existing systems
variants/server.cfg hardening prefs, INTERACTIVE_LUKS=1, INTERACTIVE_HOSTNAME=1
shared/
preseed.tpl offline preseed + late_command (LUKS rekey, post-install)
grub-overlay.cfg.tpl ESP grub.cfg with priority=high
packages/server.list ufw fail2ban auditd apparmor dropbear-initramfs etc.
post-install/
00-base.sh extra pkgs, sysctl hardening, ufw, unattended-upgrades
20-ssh.sh sshd hardening (key-only, no root)
30-tailscale.sh install client only (manual login post-install)
scripts/
test-vm.sh QEMU+OVMF smoke test harness
read-usb-logs.sh offline log dump from S8N_LOGS partition (post-failure)
```
## Hardening posture
Currently: G1-G6 + B1-B4 from veilor-server-bootstrap baseline. Per
`debian-s8ns-prefs-iso/AUDIT.md` the v0.5 superset is queued — covered in the
parent repo's roadmap.
## Defaults
- Locale: `en_GB.UTF-8`, keymap `gb`, timezone `Europe/London`
- Mirror: cdrom-only at install (DVD-1 has all packages; no internet required)
- Logs: install-time logs land on the USB's `S8N_LOGS` (sda3) partition for
offline diagnostics — `sudo scripts/read-usb-logs.sh /dev/sdX3 --copy`
## Why a separate repo
`debian-s8ns-prefs-iso` is the multi-variant playground (laptop/server/vanilla).
`production-deb` is the production-server-only canonical path. Changes here
should preserve the "boot, prompt 2 things, walk away" property.

427
build.sh Executable file
View file

@ -0,0 +1,427 @@
#!/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"

143
flash.sh Executable file
View file

@ -0,0 +1,143 @@
#!/usr/bin/env bash
# flash.sh - Safe USB flash with explicit confirm.
# Refuses internal disks (NVMe + first SATA + boot devices).
set -euo pipefail
YES=0
ARGS=()
for a in "$@"; do
case "$a" in
-y|--yes) YES=1 ;;
*) ARGS+=("$a") ;;
esac
done
set -- "${ARGS[@]+${ARGS[@]}}"
DEV="${1:-}"
ISO="${2:-}"
if [[ -z "$DEV" || -z "$ISO" ]]; then
echo "Usage: $0 [--yes] /dev/sdX path/to.iso"
exit 1
fi
[[ -f "$ISO" ]] || { echo "ERR: iso not found: $ISO" >&2; exit 1; }
[[ -b "$DEV" ]] || { echo "ERR: not a block device: $DEV" >&2; exit 1; }
# Refuse obvious internal disks (nvme, first SATA, eMMC/SD, virtio).
case "$DEV" in
/dev/nvme*|/dev/sda|/dev/mmcblk*|/dev/vd*)
echo "ERR: refusing to write to likely-internal device: $DEV" >&2
echo " If you really mean it, use dd manually." >&2
exit 2;;
esac
# Refuse if device is mounted as / or /boot.
# Use lsblk -no PKNAME to map partition → parent disk; sed 's/[0-9]*$//' breaks
# for /dev/nvme0n1pN and /dev/mmcblk0pN (would yield /dev/nvme0n / /dev/mmcblk0).
ROOT_PART=$(findmnt -n -o SOURCE /)
ROOT_PKNAME=$(lsblk -no PKNAME "$ROOT_PART" 2>/dev/null | head -1)
[[ -n "$ROOT_PKNAME" ]] && ROOT_SRC="/dev/$ROOT_PKNAME" || ROOT_SRC="$ROOT_PART"
[[ "$DEV" != "$ROOT_SRC" ]] || { echo "ERR: $DEV is the root device. Aborting." >&2; exit 2; }
SIZE=$(lsblk -bndo SIZE "$DEV" | head -1)
SIZE_GB=$((SIZE / 1024 / 1024 / 1024))
MODEL=$(lsblk -ndo MODEL,VENDOR "$DEV" | xargs)
cat <<EOF
=== About to wipe and flash ===
Device: $DEV
Size: ${SIZE_GB} GiB
Model: $MODEL
ISO: $ISO
This DESTROYS all data on $DEV. Type 'yes' (lowercase) to continue.
EOF
if [[ "$YES" -eq 1 ]]; then
echo "[--yes] skipping interactive confirm"
else
read -r ANS
[[ "$ANS" == "yes" ]] || { echo "Aborted."; exit 1; }
fi
echo "[*] Unmounting any partitions on $DEV..."
for p in $(lsblk -nro NAME "$DEV" | tail -n +2); do
sudo umount "/dev/$p" 2>/dev/null || true
done
echo "[*] Flashing $ISO -> $DEV ..."
sudo dd if="$ISO" of="$DEV" bs=4M status=progress conv=fsync oflag=direct
sudo sync
# === Add a 3rd partition for install logs ===
# The flashed ISO is iso9660 + ESP (sda1, sda2) totaling ~759 MB on a much
# larger USB. Carve a 3rd MBR partition out of the remaining free space and
# mkfs.vfat as label S8N_LOGS. preseed early_command mounts this partition
# during install and writes logs to it; after a failed install, pull the USB,
# plug into onyx, and run scripts/read-usb-logs.sh /dev/sdX.
echo "[*] Re-reading partition table..."
sudo partprobe "$DEV" 2>&1 || true
sleep 2
# Find the highest end-sector of existing partitions; new partition starts after
ISO_END=$(sudo sfdisk -l -o End "$DEV" 2>/dev/null | awk '/^[ ]*[0-9]+/ {print $1}' | sort -n | tail -1)
ISO_END="${ISO_END:-0}"
[[ "$ISO_END" -gt 0 ]] || { echo "ERR: cannot read partition table on $DEV after dd" >&2; exit 3; }
LOG_START=$((ISO_END + 1))
# Round up to 1 MiB alignment (2048 sectors of 512B)
LOG_START=$(( (LOG_START + 2047) / 2048 * 2048 ))
TOTAL_SECTORS=$(sudo blockdev --getsz "$DEV")
LOG_SIZE=$((TOTAL_SECTORS - LOG_START - 2048)) # leave 1 MiB tail for safety
[[ "$LOG_SIZE" -gt 102400 ]] || { echo "ERR: not enough space for log partition (<50 MiB)" >&2; exit 3; }
LOG_SIZE_MB=$((LOG_SIZE / 2048))
echo "[*] Adding partition 3 at sector $LOG_START, size ${LOG_SIZE_MB} MiB ..."
# Use sfdisk to add a 3rd MBR entry without disturbing existing ones.
# `--no-reread` because the kernel already has the iso9660 partition open.
EXISTING_PT=$(sudo sfdisk -d "$DEV")
{
echo "$EXISTING_PT"
echo "${DEV}3 : start=$LOG_START, size=$LOG_SIZE, type=c"
} | sudo sfdisk --no-reread --no-tell-kernel "$DEV" || {
# Fallback: write only the new entry by editing in place
echo "[*] sfdisk batch failed; trying single-entry append"
printf 'n\np\n3\n%s\n+%sK\nt\n3\nc\nw\n' "$LOG_START" "$((LOG_SIZE_MB * 1024))" \
| sudo fdisk "$DEV" || true
}
sudo partprobe "$DEV" 2>&1 || true
sleep 2
PART3="${DEV}3"
case "$DEV" in
/dev/nvme*|/dev/mmcblk*) PART3="${DEV}p3" ;;
esac
if [[ -b "$PART3" ]]; then
echo "[*] Formatting $PART3 as vfat (label S8N_LOGS)..."
sudo mkfs.vfat -F 32 -n S8N_LOGS "$PART3"
# Drop a README into the log partition so it's discoverable
TMPDIR=$(mktemp -d)
sudo mount "$PART3" "$TMPDIR"
sudo tee "$TMPDIR/README.txt" >/dev/null <<EOF
S8N_LOGS partition
==================
This partition collects install logs during preseed/late_command on Debian
ISOs built by s8n/debian-s8ns-prefs-iso. If install fails on the target
machine, pull this USB, plug into another machine, and read /var/log/* +
/postinstall.log files here for diagnostics.
Companion script: scripts/read-usb-logs.sh /dev/sdX
EOF
sudo umount "$TMPDIR"
rmdir "$TMPDIR"
echo "[OK] Log partition $PART3 ready (S8N_LOGS, vfat)"
else
echo "[!] WARN: $PART3 didn't appear; log capture WILL NOT work on this USB"
fi
sudo sync
echo
echo "[OK] Done. Eject: sudo eject $DEV"
echo "[i] After install fail/success, mount $PART3 (label S8N_LOGS) to read logs"

119
install.sh Executable file
View file

@ -0,0 +1,119 @@
#!/usr/bin/env bash
# install.sh — apply my prefs to an existing Debian system.
# Sister to build.sh: build.sh produces a fresh-install ISO; this runs against
# an already-installed system and applies the same post-install tweaks.
#
# Usage on the target box (as root):
#
# git clone ssh://git@192.168.0.100:222/s8n-ru/debian-s8ns-prefs-iso /tmp/s8n
# sudo /tmp/s8n/install.sh --variant laptop
#
# Or via Forgejo tarball API (needs PAT in env):
#
# curl -fsSL -H "Authorization: token $GIT_PAT" \
# https://git.s8n.ru/api/v1/repos/s8n-ru/debian-s8ns-prefs-iso/archive/main.tar.gz \
# | sudo tar xz -C /tmp
# sudo /tmp/debian-s8ns-prefs-iso/install.sh --variant laptop
#
# Variants: laptop | server | vanilla (matches variants/*.cfg)
set -euo pipefail
if [[ $EUID -ne 0 ]]; then
echo "ERR: must run as root (or via sudo)" >&2
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VARIANT=""
HOSTNAME_OVERRIDE=""
SKIP_PKGS=0
usage() {
cat <<EOF
Usage: sudo $0 --variant {laptop|server|vanilla} [opts]
Required:
--variant NAME laptop | server | vanilla
Optional:
--hostname NAME override hostname
--skip-pkgs skip apt install of extra.list (testing only)
-h | --help show this
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--variant) VARIANT="$2"; shift 2;;
--hostname) HOSTNAME_OVERRIDE="$2"; shift 2;;
--skip-pkgs) SKIP_PKGS=1; shift;;
-h|--help) usage; exit 0;;
*) echo "Unknown arg: $1" >&2; usage; exit 1;;
esac
done
[[ -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
# shellcheck source=/dev/null
source "$VARIANT_FILE"
echo "[install] variant=$VARIANT"
echo "[install] grub_params=$GRUB_PARAMS"
echo "[install] post-install scripts: ${POST_INSTALL_SCRIPTS[*]}"
# Set hostname if requested
if [[ -n "$HOSTNAME_OVERRIDE" ]]; then
echo "[install] setting hostname to $HOSTNAME_OVERRIDE"
hostnamectl set-hostname "$HOSTNAME_OVERRIDE"
fi
# Stage payload like the chroot install would see it
PAYLOAD=/tmp/s8n-payload
rm -rf "$PAYLOAD"
mkdir -p "$PAYLOAD/post-install" "$PAYLOAD/packages"
cp "$SCRIPT_DIR/shared/packages/$PACKAGES_LIST" "$PAYLOAD/packages/extra.list"
for s in "${POST_INSTALL_SCRIPTS[@]}"; do
cp "$SCRIPT_DIR/shared/post-install/$s" "$PAYLOAD/post-install/$s"
chmod +x "$PAYLOAD/post-install/$s"
done
# Persist GRUB cmdline additions (for future kernel updates)
GRUB_FILE=/etc/default/grub
if [[ -f "$GRUB_FILE" && -n "$GRUB_PARAMS" ]]; then
echo "[install] persisting GRUB params: $GRUB_PARAMS"
# Strip any already-present tokens to avoid duplication
CURRENT=$(grep -E '^GRUB_CMDLINE_LINUX_DEFAULT=' "$GRUB_FILE" | sed -E 's/.*"(.*)"/\1/')
NEW="$CURRENT"
for tok in $GRUB_PARAMS; do
if ! grep -qF -- "$tok" <<<"$NEW"; then
NEW="$NEW $tok"
fi
done
NEW="$(echo "$NEW" | sed -E 's/ +/ /g; s/^ //; s/ $//')"
sed -i "s|^GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"$NEW\"|" "$GRUB_FILE"
command -v update-grub >/dev/null && update-grub || true
fi
# Run scripts in order. Skip pkg-install step if --skip-pkgs.
LOG=/var/log/s8n-install.log
echo "[install] log -> $LOG"
{
echo "==== s8n install start: $(date -u +%FT%TZ) variant=$VARIANT"
for s in "${POST_INSTALL_SCRIPTS[@]}"; do
SCRIPT_PATH="$PAYLOAD/post-install/$s"
if [[ "$SKIP_PKGS" -eq 1 && "$s" == "00-base.sh" ]]; then
echo "==== SKIP $s (--skip-pkgs)"
continue
fi
echo "==== RUN $s"
/bin/sh "$SCRIPT_PATH" || echo "==== WARN $s exited $?"
done
echo "==== s8n install done: $(date -u +%FT%TZ)"
} 2>&1 | tee -a "$LOG"
echo
echo "[install] DONE. Reboot if scripts touched modprobe / GRUB / kernel modules."

78
scripts/read-usb-logs.sh Executable file
View file

@ -0,0 +1,78 @@
#!/usr/bin/env bash
# read-usb-logs.sh — read install logs from S8N_LOGS partition on the USB.
# Use after a failed install: pull USB from target, plug into onyx (or wherever
# you have this repo cloned), run this script.
#
# Usage:
# sudo scripts/read-usb-logs.sh /dev/sdX
# sudo scripts/read-usb-logs.sh # auto-detect by label
#
# Outputs:
# - Lists all run-* directories
# - Cats the latest run's exit-status, last 50 lines of syslog, post-install log
# - On --copy: rsyncs the entire S8N_LOGS contents to ./out/usb-logs-<date>/
set -euo pipefail
DEV="${1:-}"
COPY=0
[[ "${1:-}" == "--copy" ]] && { COPY=1; DEV="${2:-}"; }
if [[ -z "$DEV" ]]; then
DEV="$(blkid -L S8N_LOGS 2>/dev/null || true)"
[[ -n "$DEV" ]] || { echo "ERR: no partition labeled S8N_LOGS found. Pass /dev/sdXN explicitly." >&2; exit 1; }
fi
[[ -b "$DEV" ]] || { echo "ERR: not a block device: $DEV" >&2; exit 1; }
MOUNT=$(mktemp -d)
trap 'sudo umount "$MOUNT" 2>/dev/null || true; rmdir "$MOUNT" 2>/dev/null || true' EXIT
echo "[*] Mounting $DEV at $MOUNT (read-only)..."
sudo mount -o ro "$DEV" "$MOUNT"
echo "[*] Contents:"
ls -la "$MOUNT"
LATEST=$(ls -1d "$MOUNT"/run-* 2>/dev/null | sort | tail -1 || true)
if [[ -z "$LATEST" ]]; then
echo "[!] No run-* directories found. Either install never reached late_command, or log capture failed."
exit 0
fi
echo
echo "[*] Latest run: $(basename "$LATEST")"
echo " contents:"
ls -la "$LATEST"
echo
echo "=== build-info.txt ==="
cat "$LATEST/build-info.txt" 2>/dev/null || echo "(missing)"
echo
echo "=== exit-status.txt ==="
cat "$LATEST/exit-status.txt" 2>/dev/null || echo "(missing — install may have crashed before late_command finished)"
echo
echo "=== syslog (last 80 lines) ==="
tail -80 "$LATEST/syslog" 2>/dev/null || echo "(missing)"
echo
echo "=== s8n-post-install.log (full) ==="
cat "$LATEST/s8n-post-install.log" 2>/dev/null || echo "(missing — postinstall didn't run)"
echo
echo "=== s8n-luks-rekey.log (full) ==="
cat "$LATEST/s8n-luks-rekey.log" 2>/dev/null || echo "(missing — luks-rekey didn't run)"
echo
echo "=== installer/ (debian-installer logs) ==="
ls "$LATEST/installer/" 2>/dev/null || echo "(missing)"
if [[ "$COPY" -eq 1 ]]; then
DEST="$(pwd)/out/usb-logs-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$DEST"
echo
echo "[*] Copying full log set to $DEST ..."
sudo cp -r "$MOUNT"/run-*/. "$DEST/"
sudo chown -R "$USER:$USER" "$DEST"
echo "[OK] $DEST"
fi

150
scripts/test-vm.sh Executable file
View file

@ -0,0 +1,150 @@
#!/usr/bin/env bash
# test-vm.sh — VM smoke test harness for built ISOs.
# Boots the ISO via qemu+OVMF, runs unattended preseed install on a fresh
# qcow2 disk, then boots the installed system and verifies success criteria
# (sshd listening, broadcom-sta-dkms package present if laptop, dark theme
# default, etc.).
#
# Usage: scripts/test-vm.sh out/debian-s8ns-VARIANT-DATE.iso [VARIANT]
#
# What it does:
# 1. Create 30 GiB qcow2 in /tmp/s8n-vmtest/
# 2. Boot ISO with OVMF UEFI, preseed runs unattended, expects ~20-40 min
# 3. After install completes, ISO ejects, system reboots
# 4. Boot installed system, capture serial console
# 5. Run verification checks via SSH (qemu user-mode net 22→2222 fwd)
# 6. Report PASS / FAIL with what was checked
#
# Requires: qemu-system-x86_64, OVMF firmware. KVM if available.
set -euo pipefail
ISO="${1:-}"
VARIANT="${2:-}"
[[ -f "$ISO" ]] || { echo "Usage: $0 path/to/iso [variant]" >&2; exit 1; }
# Auto-detect variant from filename if not given
if [[ -z "$VARIANT" ]]; then
case "$ISO" in
*laptop*) VARIANT=laptop ;;
*server*) VARIANT=server ;;
*vanilla*) VARIANT=vanilla ;;
*) echo "ERR: cannot detect variant from filename, pass as 2nd arg" >&2; exit 1;;
esac
fi
VMDIR="${VMDIR:-/tmp/s8n-vmtest}"
mkdir -p "$VMDIR"
DISK="$VMDIR/disk.qcow2"
VARS="$VMDIR/OVMF_VARS.fd"
INSTALL_LOG="$VMDIR/install.log"
BOOT_LOG="$VMDIR/firstboot.log"
# Fresh state per run
rm -f "$DISK" "$VARS" "$INSTALL_LOG" "$BOOT_LOG"
qemu-img create -f qcow2 "$DISK" 30G >/dev/null
cp /usr/share/OVMF/OVMF_VARS.fd "$VARS"
KVM_FLAG=""
[[ -r /dev/kvm ]] && KVM_FLAG="-enable-kvm -cpu host"
QEMU_BASE=(
qemu-system-x86_64
-m 2048 -smp 2
$KVM_FLAG
-drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE.fd
-drive if=pflash,format=raw,file="$VARS"
-netdev user,id=n0,hostfwd=tcp:127.0.0.1:2222-:22
-device virtio-net-pci,netdev=n0
-drive file="$DISK",format=qcow2,if=virtio
-display none
-nodefaults
)
echo "[test] === Phase 1: unattended install from $ISO ==="
echo "[test] log: $INSTALL_LOG"
echo "[test] expected duration: 15-40 min"
# Boot from ISO (cdrom). serial=stdio captures kernel + d-i progress.
timeout 2700 "${QEMU_BASE[@]}" \
-drive file="$ISO",format=raw,if=virtio,readonly=on,media=cdrom \
-boot d \
-serial file:"$INSTALL_LOG" \
-monitor null \
|| { echo "[test] FAIL: install phase exited non-zero (timeout or error). Last 30 lines of $INSTALL_LOG:" >&2; tail -30 "$INSTALL_LOG" >&2; exit 1; }
echo "[test] install phase exited (kernel reboot or shutdown)"
# Sanity: did the install actually finish? Look for late_command output.
if grep -q 'late_command' "$INSTALL_LOG" || grep -qi 'finishing the installation' "$INSTALL_LOG"; then
echo "[test] late_command observed — proceeding to phase 2"
else
echo "[test] WARN: no late_command marker in install log — install may have aborted mid-way"
tail -50 "$INSTALL_LOG" >&2
fi
echo
echo "[test] === Phase 2: first boot from installed system ==="
# Fresh OVMF_VARS to avoid stale boot order
cp /usr/share/OVMF/OVMF_VARS.fd "$VARS"
# Boot from disk; give 6 min for first boot + DKMS module build + tailscale unit
timeout 360 "${QEMU_BASE[@]}" \
-boot c \
-serial file:"$BOOT_LOG" \
-monitor null \
&
QEMU_PID=$!
# Wait for SSH to come up (max 5 min from now)
echo "[test] waiting for SSH on 127.0.0.1:2222 ..."
SSH_UP=0
for i in $(seq 1 60); do
if nc -z -w2 127.0.0.1 2222 2>/dev/null; then
SSH_UP=1
echo "[test] SSH responding after ${i}*5s"
break
fi
sleep 5
done
if [[ "$SSH_UP" -ne 1 ]]; then
echo "[test] FAIL: SSH never came up. firstboot log tail:" >&2
tail -50 "$BOOT_LOG" >&2 || true
kill -9 "$QEMU_PID" 2>/dev/null || true
wait "$QEMU_PID" 2>/dev/null || true
exit 2
fi
# Probe SSH banner (no auth needed for banner)
BANNER=$(timeout 5 nc 127.0.0.1 2222 < /dev/null | head -1 || true)
echo "[test] SSH banner: $BANNER"
[[ "$BANNER" == SSH-* ]] || { echo "[test] FAIL: bad SSH banner"; kill -9 "$QEMU_PID" 2>/dev/null; exit 3; }
# Power off cleanly
echo "[test] PASS: SSH up. Killing VM."
kill -9 "$QEMU_PID" 2>/dev/null || true
wait "$QEMU_PID" 2>/dev/null || true
echo
echo "[test] === Verification ==="
# Check post-install log inside the boot log if it surfaced
if grep -qE 's8n.*post-install (start|done)' "$BOOT_LOG"; then
echo "[test] OK: post-install run.sh signal found in boot log"
else
echo "[test] INFO: no s8n post-install marker in firstboot log (may be OK if runs only at install time)"
fi
# Did luks-rekey signal?
if grep -qE 'luks.*rotation complete' "$INSTALL_LOG" || grep -qE 'luks.*rotation complete' "$BOOT_LOG"; then
echo "[test] OK: LUKS rekey ran"
else
echo "[test] WARN: no LUKS rekey marker"
fi
echo
echo "[test] SUMMARY"
echo " ISO : $ISO"
echo " Variant : $VARIANT"
echo " Install log: $INSTALL_LOG ($(wc -l <"$INSTALL_LOG") lines)"
echo " Boot log : $BOOT_LOG ($(wc -l <"$BOOT_LOG") lines)"
echo
echo "[test] PASS: VM install+first-boot+SSH succeeded for $VARIANT"

View file

@ -0,0 +1,82 @@
# grub-overlay.cfg.tpl — replaces /EFI/debian/grub.cfg in the built ISO.
# Self-contained, single-icon Apple boot, kernel cmdline baked in.
# @GRUB_PARAMS@ replaced at build time. @VARIANT@ for menu label.
set timeout=8
set default=0
# Find the iso9660 partition. Apple firmware enumerates the USB at an
# unpredictable hd number (USB might be hd0, hd1, or hd2 depending on
# whether internal SSD was detected). Try in order:
# 1. ${cmdpath} = where GRUB was loaded from, e.g. (hd1,msdos2)/efi/debian.
# Extract the hdN, set root=(hdN,msdos1) — same disk, partition 1.
# 2. search --label (works if iso9660 module + partition not type 0x00)
# 3. Probe (hd0,msdos1) ... (hd3,msdos1)
# 4. Drop to GRUB shell so operator can debug with `ls`
insmod part_msdos
insmod part_gpt
insmod iso9660
insmod fat
insmod regexp
echo "GRUB loaded. cmdpath=${cmdpath}"
# Method 1: derive hd from cmdpath
regexp -s 1:disk '\((hd[0-9]+)' "$cmdpath"
if [ -n "$disk" ]; then
set root="($disk,msdos1)"
echo "trying root=$root (from cmdpath)"
fi
if [ -e /install.amd/vmlinuz ]; then echo "found kernel at $root"; else
# Method 2: filesystem label search
search --label --no-floppy --set=root @VOLID@
if [ -e /install.amd/vmlinuz ]; then echo "found kernel via label"; else
# Method 3: brute-force hd0..hd3
for d in 0 1 2 3 ; do
set root="(hd${d},msdos1)"
if [ -e /install.amd/vmlinuz ]; then echo "found kernel at hd${d}"; break; fi
done
if ! [ -e /install.amd/vmlinuz ]; then
echo "ERROR: cannot find /install.amd/vmlinuz on any disk."
echo "Drop to GRUB shell with 'c'. Run 'ls' to see devices, then"
echo "set root=(hdX,msdosY) where X,Y point at the iso9660 partition."
sleep 30
fi
fi
fi
if loadfont unicode ; then
insmod efi_gop
insmod efi_uga
insmod gfxterm
set gfxmode=auto
set gfxpayload=keep
terminal_output gfxterm
fi
set BAKED="@GRUB_PARAMS@"
menuentry --hotkey=i 'Auto-install [@VARIANT@] (preseed)' {
set background_color=black
linux /install.amd/vmlinuz auto=true priority=@PRIORITY@ file=/cdrom/preseed.cfg $BAKED vga=788 --- quiet
initrd /install.amd/initrd.gz
}
menuentry --hotkey=g 'Auto-install [@VARIANT@] (graphical)' {
set background_color=black
linux /install.amd/vmlinuz auto=true priority=@PRIORITY@ file=/cdrom/preseed.cfg $BAKED vga=788 --- quiet
initrd /install.amd/gtk/initrd.gz
}
menuentry --hotkey=m 'Manual install (no preseed) [@VARIANT@ kernel params]' {
set background_color=black
linux /install.amd/vmlinuz $BAKED vga=788 --- quiet
initrd /install.amd/initrd.gz
}
menuentry --hotkey=r 'Rescue mode [@VARIANT@ kernel params]' {
set background_color=black
linux /install.amd/vmlinuz $BAKED vga=788 rescue/enable=true --- quiet
initrd /install.amd/initrd.gz
}
menuentry --hotkey=s 'Drop to installer shell' {
set background_color=black
linux /install.amd/vmlinuz $BAKED vga=788 rescue/enable=true --- quiet
initrd /install.amd/initrd.gz
}

View file

@ -0,0 +1,15 @@
# server.list — extra packages installed via apt after base install.
# Server variant: hardened headless, SSH-only.
ufw
fail2ban
auditd
apparmor
apparmor-utils
apparmor-profiles
apparmor-profiles-extra
libpam-pwquality
dropbear-initramfs
cryptsetup-initramfs
htop
vim
tmux

60
shared/post-install/00-base.sh Executable file
View file

@ -0,0 +1,60 @@
#!/bin/sh
# 00-base.sh — install variant extra packages, baseline sysctl + ufw.
# Runs in-target (already inside installed system's chroot, /proc /sys /dev
# bind-mounted by d-i, /etc/resolv.conf working, apt sources configured).
set -eu
LIST=/root/s8n-postinstall/extra.list
if [ -s "$LIST" ]; then
echo "[00] installing extra packages from $LIST"
apt-get update
PKGS=$(grep -vE '^\s*(#|$)' "$LIST" | tr '\n' ' ')
if [ -n "$PKGS" ]; then
# `apt-get install -- $PKGS` lets a malicious extra.list still inject `--`
# tokens but only repo-controlled file is the source. The `--` separator
# is hygiene against accidental flag-like names.
# Failure here is a hard fail per `set -e` from caller — DKMS / wifi
# depending on these packages is critical for laptop variants.
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends -- $PKGS
fi
fi
echo "[00] applying sysctl hardening"
cat > /etc/sysctl.d/90-s8n.conf <<'SYSCTL'
# Personal sysctl baseline.
kernel.kptr_restrict=2
kernel.dmesg_restrict=1
kernel.unprivileged_bpf_disabled=1
net.core.bpf_jit_harden=2
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.default.rp_filter=1
net.ipv4.tcp_syncookies=1
net.ipv4.conf.all.accept_redirects=0
net.ipv6.conf.all.accept_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.all.accept_source_route=0
net.ipv6.conf.all.accept_source_route=0
fs.protected_hardlinks=1
fs.protected_symlinks=1
fs.protected_fifos=2
fs.protected_regular=2
SYSCTL
echo "[00] enabling ufw (if installed)"
if command -v ufw >/dev/null; then
# Idempotent: don't reset if already active (preserves user rules on rerun).
if ufw status 2>/dev/null | grep -q '^Status: active'; then
echo "[00] ufw already active, skipping reset"
else
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw --force enable
fi
fi
echo "[00] enabling unattended-upgrades"
if [ -f /etc/apt/apt.conf.d/50unattended-upgrades ]; then
systemctl enable unattended-upgrades || true
fi

28
shared/post-install/20-ssh.sh Executable file
View file

@ -0,0 +1,28 @@
#!/bin/sh
# 20-ssh.sh — harden sshd: pubkey only, no root login, no password auth.
# authorized_keys was already placed by preseed late_command.
set -eu
if [ ! -f /etc/ssh/sshd_config ]; then
echo "[20] sshd not installed, skipping"
exit 0
fi
cat > /etc/ssh/sshd_config.d/00-s8n.conf <<'SSHD'
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
X11Forwarding no
PermitEmptyPasswords no
LoginGraceTime 30
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
SSHD
# Fail2ban gets enabled by 00-base.sh, but the default jail covers sshd.
echo "[20] sshd hardened. authorized_keys placed by preseed."
systemctl enable ssh || true

View file

@ -0,0 +1,46 @@
#!/bin/sh
# 30-tailscale.sh — install Tailscale; auto-join tailnet if --ts-auth-key
# was passed at build time (key file at /root/s8n-postinstall/ts-auth-key).
# Without auth-key: install client only, manual `tailscale up` post-boot.
set -eu
if ! command -v tailscale >/dev/null; then
echo "[30] adding tailscale repo + installing"
curl -fsSL https://pkgs.tailscale.com/stable/debian/trixie.noarmor.gpg \
-o /usr/share/keyrings/tailscale-archive-keyring.gpg
curl -fsSL https://pkgs.tailscale.com/stable/debian/trixie.tailscale-keyring.list \
-o /etc/apt/sources.list.d/tailscale.list
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends tailscale
else
echo "[30] tailscale already installed"
fi
systemctl enable tailscaled || true
# Auto-join if auth-key file present. tailscaled isn't running yet (we're in
# chroot during install), so write a oneshot unit that joins on first boot.
KEY_FILE=/root/s8n-postinstall/ts-auth-key
if [ -s "$KEY_FILE" ]; then
echo "[30] auth-key found, deploying first-boot join unit"
install -m 600 "$KEY_FILE" /etc/tailscale-authkey
cat > /etc/systemd/system/s8n-tailscale-join.service <<'UNIT'
[Unit]
Description=s8n Tailscale first-boot join
After=tailscaled.service network-online.target
Wants=network-online.target tailscaled.service
ConditionPathExists=/etc/tailscale-authkey
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'tailscale up --login-server=https://hs.s8n.ru --auth-key=$(cat /etc/tailscale-authkey) && shred -u /etc/tailscale-authkey && systemctl disable s8n-tailscale-join.service'
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
UNIT
systemctl enable s8n-tailscale-join.service || true
else
echo "[30] no auth-key; install tailscale only. Login post-boot:"
echo " sudo tailscale up --login-server=https://hs.s8n.ru"
fi

174
shared/preseed.tpl Normal file
View file

@ -0,0 +1,174 @@
# debian-s8ns-prefs-iso preseed (template)
# @PLACEHOLDERS@ replaced at build time by build.sh
# Variant: @VARIANT@
# === Locale + keyboard ===
d-i debian-installer/locale string en_GB.UTF-8
d-i keyboard-configuration/xkb-keymap select gb
# === Network ===
d-i netcfg/choose_interface select auto
d-i netcfg/get_hostname string @HOSTNAME@
d-i netcfg/get_domain string
d-i netcfg/hostname string @HOSTNAME@
# === Mirror — OFFLINE install only (DVD-1 has all packages including GNOME) ===
# Disable choose-mirror entirely. DVD-1 base supplies tasksel + apt sources.
# Wifi driver (broadcom-sta-dkms) builds via DKMS post-install once user plugs
# in or runs `tailscale up`; not needed during d-i.
d-i mirror/cdrom_only boolean true
d-i mirror/country string manual
d-i mirror/protocol string http
d-i mirror/http/hostname string deb.debian.org
d-i mirror/http/directory string /debian
d-i mirror/http/proxy string
d-i apt-setup/use_mirror boolean false
d-i apt-setup/services-select multiselect
d-i apt-setup/security_host string
d-i apt-setup/cdrom/set-first boolean true
d-i apt-setup/cdrom/set-next boolean false
d-i apt-setup/cdrom/set-failed boolean false
d-i apt-setup/no_mirror boolean true
d-i netcfg/enable boolean false
d-i netcfg/disable_autoconfig boolean true
d-i netcfg/get_nameservers string
d-i netcfg/get_ipaddress string
d-i netcfg/get_netmask string
d-i netcfg/get_gateway string
# === Clock ===
d-i clock-setup/utc boolean true
d-i time/zone string Europe/London
d-i clock-setup/ntp boolean true
# === Users ===
# Root locked, primary user is sudoer.
# Password is yescrypt-crypted at build time via mkpasswd; chage -d 0 in
# late_command forces rotate on first SSH/console login.
d-i passwd/root-login boolean false
d-i passwd/make-user boolean true
d-i passwd/user-fullname string @USERNAME@
d-i passwd/username string @USERNAME@
d-i passwd/user-password-crypted password @USER_PW_CRYPTED@
d-i user-setup/encrypt-home boolean false
# === Partitioning: LUKS LVM, full disk ===
# LUKS install passphrase is generated per-build (random, NOT plaintext
# changeme-luks). late_command rotates it to a fresh random passphrase via
# luksAddKey + luksKillSlot 0 before reboot, so the install-time passphrase
# never persists on disk past the install.
d-i partman-auto/method string crypto
d-i partman-auto-lvm/guided_size string max
d-i partman-auto/disk string @DISK@
d-i partman-auto/choose_recipe select atomic
d-i partman-auto-crypto/erase_disk boolean true
d-i partman-crypto/passphrase password @LUKS_INSTALL_PW@
d-i partman-crypto/passphrase-again password @LUKS_INSTALL_PW@
d-i partman-crypto/weak_passphrase boolean true
d-i partman/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
d-i partman-md/confirm boolean true
d-i partman-md/confirm_nooverwrite boolean true
d-i partman-crypto/confirm boolean true
d-i partman-crypto/confirm_nooverwrite boolean true
d-i partman-lvm/device_remove_lvm boolean true
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm_nooverwrite boolean true
d-i partman-basicfilesystems/no_swap boolean false
# === Apt ===
d-i apt-setup/non-free-firmware boolean true
d-i apt-setup/non-free boolean true
d-i apt-setup/contrib boolean true
d-i apt-setup/services-select multiselect security, updates
d-i apt-setup/use_mirror boolean true
d-i apt-setup/cdrom/set-first boolean false
# === Tasksel ===
tasksel tasksel/first multiselect @TASKSEL_TASKS@
# === Extra packages (common to all variants) ===
# Firmware blobs so wifi/eth/CPU microcode work on first boot.
# broadcom-sta-dkms (BCM4360 wl driver) is in laptop.list because it requires
# DKMS build + linux-headers — handled in late_command via in-target apt.
d-i pkgsel/include string sudo curl wget rsync git ca-certificates unattended-upgrades apt-listchanges chrony python3 python3-apt firmware-iwlwifi firmware-realtek firmware-atheros firmware-misc-nonfree firmware-brcm80211 firmware-bnx2 firmware-bnx2x firmware-libertas firmware-zd1211 firmware-ti-connectivity intel-microcode amd64-microcode
d-i pkgsel/upgrade select full-upgrade
d-i pkgsel/update-policy select unattended-upgrades
d-i pkgsel/install-language-support boolean false
popularity-contest popularity-contest/participate boolean false
# === GRUB ===
d-i grub-installer/only_debian boolean true
d-i grub-installer/with_other_os boolean true
d-i grub-installer/bootdev string @DISK@
# === Reboot when done ===
d-i finish-install/reboot_in_progress note
d-i debian-installer/exit/reboot boolean true
# === Early command — mount S8N_LOGS partition (3rd MBR entry on USB) ===
# flash.sh creates a vfat partition labeled S8N_LOGS for collecting install
# logs. Mount it at /target/var/log-usb (we'll persist there) and at
# /tmp/s8n-logs (writable during install). Find by label across sd? / nvme?.
d-i preseed/early_command string \
set +e ; \
mkdir -p /tmp/s8n-logs ; \
DEV=$(blkid -L S8N_LOGS 2>/dev/null) ; \
if [ -n "$DEV" ] ; then \
mount -t vfat "$DEV" /tmp/s8n-logs && \
echo "[s8n] mounted log partition $DEV at /tmp/s8n-logs" >> /tmp/s8n-logs/early.log && \
date -u +%FT%TZ >> /tmp/s8n-logs/early.log ; \
fi ; \
set -e
# === Late command — wrapped in sh -c 'set -e' so partial failures abort install ===
# Steps:
# 1. Bind-mount /cdrom inside target so files stay reachable after pivot
# 2. Copy postinstall payload into installed system at /root/s8n-postinstall
# 3. Place SSH authorized_keys atomically (.tmp then mv)
# 4. Force user-pw rotation on first login (chage -d 0)
# 5. Rotate LUKS keyslot — kill the install-time pw, fresh random pw written
# to /target/root/luks-pw.txt mode 0600 (operator reads, transcribes, deletes)
# 6. Run in-target /root/s8n-postinstall/run.sh — DKMS, ufw, dark theme, etc.
# 7. Copy d-i logs + post-install log to S8N_LOGS partition (USB) for offline
# diagnostics. trap-style: even if earlier steps fail, the log copy still
# runs via a separate sh -c. Mounted at /tmp/s8n-logs by early_command.
# 8. Unmount /cdrom
d-i preseed/late_command string sh -c '\
{ \
set -e ; \
mkdir -p /target/cdrom ; \
mount --bind /cdrom /target/cdrom ; \
cp -r /cdrom/postinstall /target/root/s8n-postinstall ; \
chmod +x /target/root/s8n-postinstall/run.sh /target/root/s8n-postinstall/scripts/*.sh /target/root/s8n-postinstall/luks-rekey.sh ; \
install -d -m 700 -o @USERNAME@ -g @USERNAME@ /target/home/@USERNAME@/.ssh ; \
printf "%s\n" "@SSH_PUBKEY@" > /target/home/@USERNAME@/.ssh/authorized_keys.tmp ; \
chmod 600 /target/home/@USERNAME@/.ssh/authorized_keys.tmp ; \
chown @USERNAME@:@USERNAME@ /target/home/@USERNAME@/.ssh/authorized_keys.tmp ; \
mv /target/home/@USERNAME@/.ssh/authorized_keys.tmp /target/home/@USERNAME@/.ssh/authorized_keys ; \
in-target chage -d 0 @USERNAME@ ; \
sh /target/root/s8n-postinstall/luks-rekey.sh "@LUKS_INSTALL_PW@" ; \
in-target sh -e /root/s8n-postinstall/run.sh ; \
umount /target/cdrom ; \
rmdir /target/cdrom ; \
} ; STATUS=$? ; \
if mountpoint -q /tmp/s8n-logs ; then \
RUN_DIR=/tmp/s8n-logs/run-$(date -u +%Y%m%dT%H%M%SZ) ; \
mkdir -p "$RUN_DIR" ; \
cp -r /var/log/syslog /var/log/installer "$RUN_DIR/" 2>/dev/null || true ; \
cp /var/log/s8n-luks-rekey.log "$RUN_DIR/" 2>/dev/null || true ; \
cp /target/var/log/s8n-post-install.log "$RUN_DIR/" 2>/dev/null || true ; \
lsblk > "$RUN_DIR/lsblk.txt" 2>&1 ; \
lspci -nn > "$RUN_DIR/lspci.txt" 2>&1 ; \
dmesg > "$RUN_DIR/dmesg.txt" 2>&1 ; \
mount > "$RUN_DIR/mount.txt" 2>&1 ; \
df -h > "$RUN_DIR/df.txt" 2>&1 ; \
echo "$STATUS" > "$RUN_DIR/exit-status.txt" ; \
echo "@HOSTNAME@ @VARIANT@ $(date -u +%FT%TZ)" > "$RUN_DIR/build-info.txt" ; \
sync ; \
umount /tmp/s8n-logs || true ; \
fi ; \
exit $STATUS \
'

34
variants/server.cfg Normal file
View file

@ -0,0 +1,34 @@
# server.cfg — sourced by build.sh
# Variant: headless, hardened, SSH-only. Drop-in replacement for veilor-server with
# forky kernel + my prefs.
#
# Server is "deploy at console" — admin sets LUKS passphrase + hostname in person.
# - INTERACTIVE_LUKS=1: omit partman-crypto/passphrase from preseed → d-i prompts
# - INTERACTIVE_HOSTNAME=1: omit netcfg/get_hostname → d-i prompts (overridable
# via kernel cmdline `hostname=NAME` at boot)
# - USER_PW_PLAIN_DEFAULT=123: weak baked password + chage -d 0 forces rotate on
# first SSH login. Yescrypt-random would block console-only first login.
# - PRESEED_PRIORITY=high: missing-preseed questions surface as prompts instead
# of falling back to (potentially wrong) defaults.
VARIANT_NAME="server"
VARIANT_VOLID="S8N_SERVER"
GRUB_PARAMS="quiet"
PRESEED_PRIORITY="high"
INTERACTIVE_LUKS=1
INTERACTIVE_HOSTNAME=1
USER_PW_PLAIN_DEFAULT="123"
DEFAULT_DISK="/dev/nvme0n1"
TASKSEL_TASKS="standard, ssh-server"
PACKAGES_LIST="server.list"
POST_INSTALL_SCRIPTS=(
"00-base.sh"
"20-ssh.sh"
"30-tailscale.sh"
)