From 0f5bbf004a75b03f8082bf8bc9cda1101ca6a690 Mon Sep 17 00:00:00 2001 From: obsidian-ai Date: Fri, 8 May 2026 13:53:38 +0100 Subject: [PATCH] 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. --- .gitignore | 8 + CHANGELOG.md | 157 ++++++++++ README.md | 83 ++++++ build.sh | 427 ++++++++++++++++++++++++++++ flash.sh | 143 ++++++++++ install.sh | 119 ++++++++ scripts/read-usb-logs.sh | 78 +++++ scripts/test-vm.sh | 150 ++++++++++ shared/grub-overlay.cfg.tpl | 82 ++++++ shared/packages/server.list | 15 + shared/post-install/00-base.sh | 60 ++++ shared/post-install/20-ssh.sh | 28 ++ shared/post-install/30-tailscale.sh | 46 +++ shared/preseed.tpl | 174 ++++++++++++ variants/server.cfg | 34 +++ 15 files changed, 1604 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100755 build.sh create mode 100755 flash.sh create mode 100755 install.sh create mode 100755 scripts/read-usb-logs.sh create mode 100755 scripts/test-vm.sh create mode 100644 shared/grub-overlay.cfg.tpl create mode 100644 shared/packages/server.list create mode 100755 shared/post-install/00-base.sh create mode 100755 shared/post-install/20-ssh.sh create mode 100755 shared/post-install/30-tailscale.sh create mode 100644 shared/preseed.tpl create mode 100644 variants/server.cfg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..384cb5e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +work/ +out/ +*.iso +*.iso.sha256 +*.deb +boot-configs/ +.secrets +*.swp diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4b297e4 --- /dev/null +++ b/CHANGELOG.md @@ -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-/`. +- **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 `.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 -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6348d27 --- /dev/null +++ b/README.md @@ -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 --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@ +# 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. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..b619ddd --- /dev/null +++ b/build.sh @@ -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--.iso +# out/debian-s8ns--.iso.sha256 +# out/debian-s8ns--.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 <-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' /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" < "$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" diff --git a/flash.sh b/flash.sh new file mode 100755 index 0000000..52f57e1 --- /dev/null +++ b/flash.sh @@ -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 </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 <&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VARIANT="" +HOSTNAME_OVERRIDE="" +SKIP_PKGS=0 + +usage() { + cat <&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." diff --git a/scripts/read-usb-logs.sh b/scripts/read-usb-logs.sh new file mode 100755 index 0000000..1932091 --- /dev/null +++ b/scripts/read-usb-logs.sh @@ -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-/ +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 diff --git a/scripts/test-vm.sh b/scripts/test-vm.sh new file mode 100755 index 0000000..b054847 --- /dev/null +++ b/scripts/test-vm.sh @@ -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" diff --git a/shared/grub-overlay.cfg.tpl b/shared/grub-overlay.cfg.tpl new file mode 100644 index 0000000..c551128 --- /dev/null +++ b/shared/grub-overlay.cfg.tpl @@ -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 +} diff --git a/shared/packages/server.list b/shared/packages/server.list new file mode 100644 index 0000000..7618eb8 --- /dev/null +++ b/shared/packages/server.list @@ -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 diff --git a/shared/post-install/00-base.sh b/shared/post-install/00-base.sh new file mode 100755 index 0000000..2ab5bd5 --- /dev/null +++ b/shared/post-install/00-base.sh @@ -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 diff --git a/shared/post-install/20-ssh.sh b/shared/post-install/20-ssh.sh new file mode 100755 index 0000000..631670a --- /dev/null +++ b/shared/post-install/20-ssh.sh @@ -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 diff --git a/shared/post-install/30-tailscale.sh b/shared/post-install/30-tailscale.sh new file mode 100755 index 0000000..c3cb148 --- /dev/null +++ b/shared/post-install/30-tailscale.sh @@ -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 diff --git a/shared/preseed.tpl b/shared/preseed.tpl new file mode 100644 index 0000000..7ecd447 --- /dev/null +++ b/shared/preseed.tpl @@ -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 \ +' diff --git a/variants/server.cfg b/variants/server.cfg new file mode 100644 index 0000000..226aa28 --- /dev/null +++ b/variants/server.cfg @@ -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" +)