diff --git a/README.md b/README.md new file mode 100644 index 0000000..cffceb2 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# Void Bootstrapp Wrapper + +Ein Bash-Wrapper, der die kritischen Vorarbeiten fuer eine verschluesselte Void-Linux-Installation erledigt und danach den offiziellen `void-installer` nutzt. + +## Ablauf + +1. Sanity Checks (Root, UEFI, Zielplatte) +2. Partitionierung (GPT, ESP + Root) +3. LUKS-Verschluesselung und Mapping +4. Dateisysteme (btrfs oder ext4) +5. Mounts fuer den Installer +6. Installer-Handoff (manuelle Mountpoints, kein Reformat) +7. Post-Install (crypttab, dracut, GRUB, optional Swapfile) + +## Usage + +```bash +sudo ./src/main.sh [--dry-run] [--skip-installer] +``` + +## Flags + +- `--dry-run`: Zeigt die Konfiguration und bricht ohne Aenderungen ab. +- `--skip-installer`: Laesst den Start von `void-installer` aus (manuell starten). + +## Installer Bedienung nach dem Script + +Im `void-installer` musst du die vorbereiteten Mounts uebernehmen und darfst nichts formatieren. + +1. Waehle die manuelle Partitionierung. +2. Setze nur die Mountpoints, ohne Formatierung. +3. Root: + - Device: `/dev/mapper/cryptroot` + - Label: `void-root` (btrfs und ext4) + - Mountpoint: `/` + - Format: aus +4. ESP: + - Device: erste Partition der Disk (meist `...1`) + - Label: `EFI` + - Mountpoint: `/boot/efi` + - Format: aus + +Wenn Labels nicht angezeigt werden, pruefe mit `lsblk -f`, welches Device `void-root` bzw. `EFI` traegt. +Bei btrfs sind Subvolumes bereits angelegt und gemountet: `@`, `@home`, `@var`, `@log`, `@snapshots`, `@swap`. +Der Installer soll keine neuen Subvolumes anlegen oder formatieren, sondern die vorhandenen Mounts unveraendert lassen. +Es reicht, im Installer nur `/` und `/boot/efi` zu setzen; die Subvolumes werden im Post-Install in `/etc/fstab` eingetragen. +Mapping der Subvolumes (gesetzt durch den Wrapper/Post-Install): +- `@` -> `/` +- `@home` -> `/home` +- `@var` -> `/var` +- `@log` -> `/var/log` +- `@snapshots` -> `/.snapshots` +- `@swap` -> `/swap` + +## Hinweise + +- Der Wrapper ist destruktiv und loescht die Zielplatte nach expliziter Bestaetigung. +- UEFI ist Pflicht; BIOS-Installationen sind nicht unterstuetzt. +- Logs landen unter `/tmp/void-wrapper-YYYY-MM-DD-HHMMSS.log`. diff --git a/src/config.sh b/src/config.sh index 03c24b1..45bff24 100644 --- a/src/config.sh +++ b/src/config.sh @@ -1,13 +1,19 @@ +#!/usr/bin/env bash + # === Motivation === # Centralize user-provided inputs to avoid scattered magic values. + # === Problem Statement === # Disk targets, encryption mode, filesystem choice, and host metadata need a single source of truth. + # === Scope === # In scope: definition of configurable parameters and validation expectations. # Out of scope: parsing implementation and storage mechanism. + # === Concepts === # Preset: a named set of config values for repeatable installs. # Interactive input: prompting the user for values at runtime. + # === Decisions === # Keep defaults conservative and require explicit confirmation for destructive values. # Surface all critical values before any destructive phase begins. @@ -18,14 +24,167 @@ # Swap defaults to a file (works with both btrfs and ext4). # Mandatory values are DISK, HOSTNAME, and size inputs where no safe default exists. # Interactive prompts are the default input mechanism; no preset formats are defined yet. + # === Alternatives Considered === # Hard-coded values rejected because they do not scale across machines. + # === Constraints === # Config must be readable and editable without specialized tools. # Target boot mode is UEFI only; BIOS is out of scope. + # === Open Questions === # Should we support config file presets for batch installations, or keep it fully interactive? # How should we validate user-provided disk paths to prevent typos? # Should swap size have intelligent defaults based on RAM size, or always ask? + # === Success Criteria === # Users can clearly see and confirm target disk, encryption type, and filesystem choice. + +export DISK="" +export HOSTNAME="" +export FS_TYPE="btrfs" +export LUKS_VERSION="2" +export ESP_SIZE="1GiB" +export ROOT_END="100%" +export SWAP_SIZE="4GiB" +export CRYPT_NAME="cryptroot" +export MOUNT_ROOT="/mnt" +export ESP_MOUNT="/mnt/boot/efi" +export ROOT_LABEL="void-root" +export EFI_LABEL="EFI" + +prompt_with_default() { + local var_name="$1" + local prompt_text="$2" + local default_value="$3" + local input + + read -r -p "${prompt_text} [${default_value}]: " input + if [[ -z "$input" ]]; then + input="$default_value" + fi + printf -v "$var_name" '%s' "$input" +} + +prompt_required() { + local var_name="$1" + local prompt_text="$2" + local default_value="$3" + local input="" + + while [[ -z "$input" ]]; do + read -r -p "${prompt_text} [${default_value}]: " input + if [[ -z "$input" ]]; then + input="$default_value" + fi + if [[ -z "$input" ]]; then + log_warn "Value is required." + fi + done + + printf -v "$var_name" '%s' "$input" +} + +prompt_choice() { + local var_name="$1" + local prompt_text="$2" + local default_value="$3" + shift 3 + local options=("$@") + local input="" + local valid="" + + while [[ -z "$valid" ]]; do + read -r -p "${prompt_text} [${default_value}]: " input + if [[ -z "$input" ]]; then + input="$default_value" + fi + for option in "${options[@]}"; do + if [[ "$input" == "$option" ]]; then + valid="yes" + break + fi + done + if [[ -z "$valid" ]]; then + log_warn "Invalid choice. Allowed: ${options[*]}" + fi + done + + printf -v "$var_name" '%s' "$input" +} + +config_prompt_interactive() { + require_command lsblk + log_info "Available disks:" + lsblk -dpno NAME,SIZE,TYPE | awk '$3=="disk" {printf " %s (%s)\n", $1, $2}' + + prompt_required DISK "Target disk (e.g. /dev/sda)" "${DISK}" + prompt_required HOSTNAME "Hostname" "${HOSTNAME}" + prompt_choice FS_TYPE "Filesystem (btrfs/ext4)" "${FS_TYPE}" "btrfs" "ext4" + prompt_choice LUKS_VERSION "LUKS version (2/1)" "${LUKS_VERSION}" "2" "1" + prompt_with_default ESP_SIZE "ESP size (e.g. 1GiB)" "${ESP_SIZE}" + prompt_with_default ROOT_END "Root partition end (e.g. 100% or 200GiB)" "${ROOT_END}" + prompt_with_default SWAP_SIZE "Swap size (e.g. 4GiB, 0 to disable)" "${SWAP_SIZE}" +} + +config_validate() { + if [[ -z "$DISK" ]]; then + die "Target disk is required." + fi + if [[ -z "$HOSTNAME" ]]; then + die "Hostname is required." + fi + + case "$FS_TYPE" in + btrfs|ext4) ;; + *) die "Unsupported filesystem: $FS_TYPE" ;; + esac + + case "$LUKS_VERSION" in + 1|2) ;; + *) die "Unsupported LUKS version: $LUKS_VERSION" ;; + esac + + if [[ "$SWAP_SIZE" == "0" || "$SWAP_SIZE" == "0G" || "$SWAP_SIZE" == "0GiB" ]]; then + SWAP_SIZE="0" + fi +} + +config_print_summary() { + : "${DISK:?Target disk is required}" + : "${HOSTNAME:?Hostname is required}" + : "${FS_TYPE:?Filesystem type is required}" + : "${LUKS_VERSION:?LUKS version is required}" + : "${MOUNT_ROOT:?Mount root is required}" + : "${ESP_MOUNT:?ESP mount path is required}" + log_info "Configuration summary:" + log_info " Disk: $DISK" + log_info " Hostname: $HOSTNAME" + log_info " Filesystem: $FS_TYPE" + log_info " LUKS version: $LUKS_VERSION" + log_info " ESP size: $ESP_SIZE" + log_info " Root end: $ROOT_END" + log_info " Swap size: ${SWAP_SIZE}" + log_info " Mount root: $MOUNT_ROOT" + log_info " ESP mount: $ESP_MOUNT" +} + +config_confirm_destructive() { + : "${DISK:?Target disk is required}" + local confirmation + + log_warn "All data on $DISK will be destroyed." + read -r -p "Type the target disk to confirm: " confirmation + if [[ "$confirmation" != "$DISK" ]]; then + die "Disk confirmation failed." + fi + + read -r -p "Type YES to continue: " confirmation + if [[ "$confirmation" != "YES" ]]; then + die "Confirmation not received. Aborting." + fi +} + +swap_enabled() { + [[ "$SWAP_SIZE" != "0" ]] +} diff --git a/src/encryption.sh b/src/encryption.sh index 789ee6e..c46e85b 100644 --- a/src/encryption.sh +++ b/src/encryption.sh @@ -1,13 +1,19 @@ +#!/usr/bin/env bash + # === Motivation === # Protect user data with full-disk encryption while keeping boot reliable. + # === Problem Statement === # The wrapper must create and open an encrypted container for the root filesystem. + # === Scope === # In scope: encryption mode selection, key handling policy, and naming conventions. # Out of scope: specific encryption tool commands. + # === Concepts === # LUKS version: compatibility trade-off between bootloader support and features. # Mapping name: the identifier exposed for the opened container. + # === Decisions === # Default to LUKS2 to benefit from improved security features and tooling. # Never store passphrases in the script or config files. @@ -15,16 +21,37 @@ # Use default LUKS2 settings (PBKDF2 or Argon2i) for key derivation. # If GRUB fails to unlock LUKS2 after installation, provide clear guidance to recreate with LUKS1. # Do not auto-downgrade to LUKS1 silently; make the user aware of the compatibility issue. + # === Alternatives Considered === # Unencrypted root rejected because it defeats the goal. # Defaulting to LUKS1 rejected because LUKS2 improves maintainability and security tooling. # Auto-detecting GRUB LUKS2 support rejected due to complexity and version fragmentation. + # === Constraints === # Bootloader support limits available encryption features when /boot is inside encrypted root. # GRUB versions before 2.06 may have incomplete LUKS2 support depending on distribution patches. + # === Open Questions === # Should we allow users to customize LUKS2 PBKDF parameters (iterations, memory), or use defaults? # How do we handle GRUB boot failures with LUKS2 - provide recovery instructions or force LUKS1? # Should the wrapper test GRUB's ability to unlock LUKS2 before completing installation? + # === Success Criteria === # The encrypted container opens reliably and is ready for filesystem creation. + +encrypt_root() { + : "${ROOT_PART:?Root partition is required}" + : "${LUKS_VERSION:?LUKS version is required}" + : "${CRYPT_NAME:?Crypt mapping name is required}" + local luks_type="luks2" + + if [[ "$LUKS_VERSION" == "1" ]]; then + luks_type="luks1" + fi + + log_info "Creating LUKS container ($luks_type) on $ROOT_PART" + cryptsetup luksFormat --type "$luks_type" "$ROOT_PART" + + log_info "Opening LUKS container as $CRYPT_NAME" + cryptsetup open "$ROOT_PART" "$CRYPT_NAME" +} diff --git a/src/filesystems.sh b/src/filesystems.sh index 8315525..e280df9 100644 --- a/src/filesystems.sh +++ b/src/filesystems.sh @@ -1,20 +1,28 @@ +#!/usr/bin/env bash + # === Motivation === # Provide a stable filesystem base aligned with user intent. + # === Problem Statement === # The wrapper must format the root and boot partitions using the selected filesystem. + # === Scope === # In scope: filesystem options, defaults, and naming. # Out of scope: exact formatting command flags. + # === Concepts === # Root filesystem: primary data store inside the encrypted container. # Boot filesystem: unencrypted partition for firmware and bootloader data. + # === Decisions === # Default to btrfs with opt-in alternatives for users who want ext4. # Ensure filesystem choices are explicit and confirmed before formatting. # Use swap file instead of swap partition to keep layout simple and flexible. # Swap file lives inside encrypted root for automatic encryption protection. # Swap file size is chosen interactively based on system needs. -# For btrfs, swap file must reside on a No-COW subvolume to avoid corruption. +# For btrfs, swap file must reside on a No-COW directory to avoid corruption. +# For btrfs, create subvolumes for root, home, var, log, snapshots, and swap. + # === Alternatives Considered === # Automatic detection of best filesystem rejected to keep behavior predictable. # Swap partition rejected because it requires either: @@ -22,14 +30,51 @@ # - Unencrypted swap (security risk: sensitive data in swap) # - LVM setup (out of scope for this wrapper) # Swap partition also has fixed size, while swap file can be resized post-install. + # === Constraints === # Filesystem choice must be compatible with the initramfs and bootloader. # Hibernation with swap file requires kernel >= 5.0 and resume_offset parameter. -# For btrfs swap files, COW must be disabled on the swap subvolume. +# For btrfs swap files, COW must be disabled on the swap directory. + # === Open Questions === -# Should btrfs subvolumes be created for root, home, and swap, or keep it flat? # How should we handle swap file creation - in this phase or defer to post-install? # Should we support configurable filesystem options (compression, mount flags)? + # === Success Criteria === # Filesystems are prepared in the right places and match the chosen layout. # Swap file is ready for activation and resides within encrypted root. + +format_filesystems() { + : "${ESP_PART:?ESP partition is required}" + : "${CRYPT_NAME:?Crypt mapping name is required}" + : "${FS_TYPE:?Filesystem type is required}" + : "${ROOT_LABEL:?Root filesystem label is required}" + : "${EFI_LABEL:?EFI filesystem label is required}" + local root_device + root_device="/dev/mapper/${CRYPT_NAME}" + + log_info "Formatting ESP on $ESP_PART" + mkfs.vfat -F32 -n "$EFI_LABEL" "$ESP_PART" + + if [[ "$FS_TYPE" == "btrfs" ]]; then + log_info "Formatting root as btrfs on $root_device" + mkfs.btrfs -L "$ROOT_LABEL" "$root_device" + + log_info "Creating btrfs subvolumes" + local temp_mount + temp_mount="/tmp/void-wrapper-btrfs" + mkdir -p "$temp_mount" + mount "$root_device" "$temp_mount" + btrfs subvolume create "$temp_mount/@" + btrfs subvolume create "$temp_mount/@home" + btrfs subvolume create "$temp_mount/@var" + btrfs subvolume create "$temp_mount/@log" + btrfs subvolume create "$temp_mount/@snapshots" + btrfs subvolume create "$temp_mount/@swap" + umount "$temp_mount" + rmdir "$temp_mount" + else + log_info "Formatting root as ext4 on $root_device" + mkfs.ext4 -L "$ROOT_LABEL" "$root_device" + fi +} diff --git a/src/installer.sh b/src/installer.sh index b04fec9..614fea4 100644 --- a/src/installer.sh +++ b/src/installer.sh @@ -1,22 +1,61 @@ +#!/usr/bin/env bash + # === Motivation === # Keep the official installer as the configuration authority. + # === Problem Statement === # We need a clean handoff so the installer uses existing mounts without reformatting. + # === Scope === # In scope: instructions and guardrails for the user during the installer run. # Out of scope: automated installer configuration. + # === Concepts === # Handoff: a pause where the wrapper delegates to the installer. + # === Decisions === # Provide clear, minimal guidance to avoid overriding prepared filesystems. # Support only CLI installer flow to keep guidance consistent. + # === Alternatives Considered === # Fully scripted installation rejected for this phase. + # === Constraints === # The wrapper must not hide or alter installer behavior. + # === Open Questions === # Should we provide a checklist or step-by-step guide during the installer handoff? # How do we detect if the installer reformatted filesystems against our intent? # Should we monitor the installer process, or fully delegate control? + # === Success Criteria === # The installer completes using the prepared mounts without reformatting. + +run_installer() { + : "${MOUNT_ROOT:?Mount root is required}" + : "${ESP_MOUNT:?ESP mount path is required}" + if ! findmnt "$MOUNT_ROOT" >/dev/null 2>&1; then + die "Mount root not found: $MOUNT_ROOT" + fi + if ! findmnt "$ESP_MOUNT" >/dev/null 2>&1; then + die "ESP mount not found: $ESP_MOUNT" + fi + + log_info "Installer handoff: use the prepared mounts without formatting." + log_info "In the installer: choose manual partitioning and only set mountpoints." + + if [[ "${SKIP_INSTALLER:-0}" -eq 1 ]]; then + log_warn "Skipping installer per request." + return 0 + fi + + read -r -p "Press Enter to launch void-installer..." _ + + if command -v void-installer >/dev/null 2>&1; then + void-installer + else + log_warn "void-installer not found. Run it manually in another shell." + fi + + read -r -p "Press Enter once the installer is complete..." _ +} diff --git a/src/logging.sh b/src/logging.sh index dd8c4ef..0f6cc72 100644 --- a/src/logging.sh +++ b/src/logging.sh @@ -1,14 +1,20 @@ +#!/usr/bin/env bash + # === Motivation === # Provide transparency and auditability during destructive operations. + # === Problem Statement === # Users need to see what the wrapper is about to do and what it did. + # === Scope === # In scope: logging levels, user prompts, and summary output. # Out of scope: external logging services. + # === Concepts === # Plan summary: a preflight printout of intended actions. # Log levels: INFO (progress), WARN (recoverable issues), ERROR (fatal failures). # Secret masking: never log passphrases or cryptographic material. + # === Decisions === # Use clear, non-ambiguous wording for destructive steps. # Capture a local log file for later review and debugging. @@ -17,13 +23,67 @@ # Use plain text format for readability (not JSON). # Distinguish between stdout (user-facing messages) and log file (detailed trace). # Prefix log lines with timestamp and level: [2025-01-15 14:23:45] [INFO] message + # === Alternatives Considered === # Silent mode rejected because it hides risk. + # === Constraints === # Logging must not expose secrets such as passphrases. + # === Open Questions === # Should we support a verbose/debug mode for troubleshooting, beyond the standard levels? # Should the log file path be displayed to the user at the end for manual review? # How should we handle log rotation if the wrapper is run multiple times in the same session? + # === Success Criteria === # Users can review what happened before and after the installer handoff. + +LOG_FILE="" + +# Initialize the log file early so every phase can append to it. +logging_init() { + local timestamp + timestamp="$(date "+%Y-%m-%d-%H%M%S")" + LOG_FILE="/tmp/void-wrapper-${timestamp}.log" + touch "$LOG_FILE" +} + +log_line() { + local level="$1" + shift + local message="$*" + local timestamp + timestamp="$(date "+%Y-%m-%d %H:%M:%S")" + local line="[${timestamp}] [${level}] ${message}" + + if [[ -n "${LOG_FILE:-}" ]]; then + if [[ "$level" == "ERROR" ]]; then + echo "$line" | tee -a "$LOG_FILE" >&2 + else + echo "$line" | tee -a "$LOG_FILE" + fi + else + if [[ "$level" == "ERROR" ]]; then + echo "$line" >&2 + else + echo "$line" + fi + fi +} + +log_info() { + log_line "INFO" "$*" +} + +log_warn() { + log_line "WARN" "$*" +} + +log_error() { + log_line "ERROR" "$*" +} + +die() { + log_error "$*" + exit 1 +} diff --git a/src/main.sh b/src/main.sh index 523a1f3..49be21f 100644 --- a/src/main.sh +++ b/src/main.sh @@ -1,26 +1,33 @@ +#!/usr/bin/env bash + # === Motivation === # Provide a reproducible wrapper around the Void installer for encrypted setups. # Reduce manual, error-prone steps while keeping user control over installer choices. + # === Problem Statement === # The stock installer does not automate disk preparation for encrypted layouts. # We need a guided wrapper that prepares the target disk before the installer runs. + # === Scope === # In scope: orchestration of phases, user confirmations, mode selection. # Out of scope: replacing the official installer UI, package selection, BIOS support. + # === Concepts === # Wrapper: a script that executes pre-install tasks then hands off to the installer. # Phases: sanity, disk layout, encryption, filesystems, mounts, installer, post-install. + # === User Journey === # 1. User boots Void live medium and runs the wrapper script. # 2. Sanity checks validate environment (UEFI mode, root privileges, target disk exists). # 3. User provides configuration: target disk, encryption mode, filesystem choice, hostname. # 4. User reviews planned layout, encryption settings, and filesystem choices. # 5. User confirms destructive operation with explicit disk identifier confirmation. -# 6. Wrapper prepares disk: partition → encrypt → format → mount. +# 6. Wrapper prepares disk: partition -> encrypt -> format -> mount. # 7. Wrapper pauses and instructs user to run official installer using existing mounts. # 8. User completes installer, selecting packages and configuring system preferences. # 9. After installer exits, wrapper performs post-install configuration (crypttab, dracut, GRUB). # 10. User reboots into encrypted Void system, enters passphrase at boot. + # === Decisions === # Keep the official installer in the loop because it handles system configuration choices. # Prefer a phase-based flow to make recovery and retries understandable. @@ -28,16 +35,129 @@ # Default to a UEFI-first flow with an ESP. # Start with a fully interactive flow; presets are out of scope for now. # Provide a dry-run mode to review planned actions before execution. + # === Alternatives Considered === # Full automation without the official installer rejected due to maintenance burden. # Manual instructions only rejected because they remain error-prone. + # === Constraints === # Must be safe for destructive operations and require explicit confirmation. # Must support UEFI boot flow; BIOS is out of scope. + # === Open Questions === # Should the wrapper support resuming from a failed phase, or always start from scratch? # How should the wrapper handle pre-existing partitions on the target disk? # Should the dry-run mode generate a detailed execution plan, or just show configuration summary? + # === Success Criteria === # A user can run the wrapper, confirm disk intent, and reach the installer with correct mounts. # The narrative of each phase is clear enough to audit and adapt. + +set -euo pipefail +IFS=$'\n\t' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# shellcheck source=src/logging.sh +source "$SCRIPT_DIR/logging.sh" +# shellcheck source=src/config.sh +source "$SCRIPT_DIR/config.sh" +# shellcheck source=src/sanity.sh +source "$SCRIPT_DIR/sanity.sh" +# shellcheck source=src/partitioning.sh +source "$SCRIPT_DIR/partitioning.sh" +# shellcheck source=src/encryption.sh +source "$SCRIPT_DIR/encryption.sh" +# shellcheck source=src/filesystems.sh +source "$SCRIPT_DIR/filesystems.sh" +# shellcheck source=src/mounts.sh +source "$SCRIPT_DIR/mounts.sh" +# shellcheck source=src/installer.sh +source "$SCRIPT_DIR/installer.sh" +# shellcheck source=src/postinstall.sh +source "$SCRIPT_DIR/postinstall.sh" +# shellcheck source=src/rollback.sh +source "$SCRIPT_DIR/rollback.sh" + +DRY_RUN=0 +SKIP_INSTALLER=0 +CURRENT_PHASE="" + +usage() { + cat </dev/null 2>&1 && findmnt "$ESP_MOUNT" >/dev/null 2>&1; then + if [[ "$FS_TYPE" == "btrfs" ]]; then + for mount_point in "$MOUNT_ROOT/home" "$MOUNT_ROOT/var" "$MOUNT_ROOT/var/log" "$MOUNT_ROOT/.snapshots" "$MOUNT_ROOT/swap"; do + if ! findmnt "$mount_point" >/dev/null 2>&1; then + die "Mount verification failed for $mount_point." + fi + done + fi + log_info "Mounts verified." + else + die "Mount verification failed." + fi +} diff --git a/src/partitioning.sh b/src/partitioning.sh index e4e88b1..c1a70f9 100644 --- a/src/partitioning.sh +++ b/src/partitioning.sh @@ -1,14 +1,20 @@ +#!/usr/bin/env bash + # === Motivation === # Establish a predictable disk layout that supports encrypted root. + # === Problem Statement === # We need a partition scheme that works for UEFI and leaves encryption boundaries clear. + # === Scope === # In scope: layout choices, partition roles, and size policy. # Out of scope: exact partitioning commands and tooling. + # === Concepts === # EFI System Partition (ESP): unencrypted boot partition for UEFI. # Root partition: the encrypted container for the main system. # Separate boot partition: unencrypted /boot used only in alternative layouts. + # === Decisions === # Keep ESP unencrypted to meet firmware requirements. # Make root the only encrypted container by default to keep the flow simple. @@ -16,13 +22,54 @@ # BIOS support is out of scope for this wrapper. # Do not create a separate unencrypted /boot; /boot lives inside encrypted root. # Default ESP size is 1G; root size is chosen interactively. + # === Alternatives Considered === # Separate encrypted /home rejected for phase 1 due to complexity. + # === Constraints === # Layout must remain compatible with the chosen bootloader. + # === Open Questions === # Should we support GPT labels for partitions, or use default numeric identifiers? # What partition alignment should we use for optimal SSD performance? # Should ESP size be configurable, or keep the 1G default fixed? + # === Success Criteria === # The layout supports boot and allows the installer to mount targets cleanly. + +partition_path() { + local disk="$1" + local number="$2" + + if [[ "$disk" =~ [0-9]$ ]]; then + echo "${disk}p${number}" + else + echo "${disk}${number}" + fi +} + +partition_disk() { + : "${DISK:?Target disk is required}" + : "${ESP_SIZE:?ESP size is required}" + : "${ROOT_END:?Root partition end is required}" + log_info "Partitioning disk: $DISK" + + if command -v wipefs >/dev/null 2>&1; then + log_warn "Wiping existing signatures on $DISK" + wipefs -a "$DISK" + else + log_warn "wipefs not found; proceeding without signature wipe" + fi + + parted -s "$DISK" mklabel gpt + parted -s "$DISK" mkpart ESP fat32 1MiB "$ESP_SIZE" + parted -s "$DISK" set 1 esp on + parted -s "$DISK" mkpart ROOT "$ESP_SIZE" "$ROOT_END" + partprobe "$DISK" + + ESP_PART="$(partition_path "$DISK" 1)" + ROOT_PART="$(partition_path "$DISK" 2)" + + log_info "ESP partition: $ESP_PART" + log_info "Root partition: $ROOT_PART" +} diff --git a/src/postinstall.sh b/src/postinstall.sh index 655d6cd..ae7f7ce 100644 --- a/src/postinstall.sh +++ b/src/postinstall.sh @@ -1,10 +1,15 @@ +#!/usr/bin/env bash + # === Motivation === # Ensure the system can boot with encrypted root after installation. + # === Problem Statement === # Post-install steps must align initramfs, bootloader, and encryption metadata. + # === Scope === # In scope: required configuration updates inside the installed system. # Out of scope: package selection and user account management. + # === Concepts === # Initramfs: early boot image that unlocks encrypted storage. # Bootloader config: entries that point to the encrypted root. @@ -12,6 +17,7 @@ # Crypttab: /etc/crypttab maps LUKS UUIDs to device names for initramfs. # GRUB cryptodisk: GRUB_ENABLE_CRYPTODISK=y in /etc/default/grub enables LUKS unlock. # Kernel parameters: rd.luks.uuid= tells dracut which LUKS container to unlock. + # === Decisions === # Keep post-install steps explicit and minimal, focused on boot viability. # Default to GRUB because it is widely supported on Void without systemd dependencies. @@ -21,16 +27,153 @@ # Set GRUB_ENABLE_CRYPTODISK=y in /etc/default/grub to allow GRUB to unlock LUKS. # Add rd.luks.uuid= to GRUB_CMDLINE_LINUX for initramfs LUKS unlock. # Run grub-install to embed cryptodisk support, then grub-mkconfig to update menu entries. + # === Alternatives Considered === # Skipping post-install updates rejected because it risks unbootable systems. # systemd-boot and rEFInd rejected as defaults due to availability and scope constraints. # EFISTUB rejected as default because it increases manual UEFI entry management. + # === Constraints === # Steps must run in the target system context. + # === Open Questions === # Should we generate a rescue initramfs in addition to the default one? # Should we verify GRUB can unlock LUKS2 before rebooting, or trust the configuration? # How do we handle future kernel updates - should we document the dracut reconfiguration process? # Should swap file activation be configured in this phase, or deferred to first boot? + # === Success Criteria === # After reboot, the system prompts for decryption and boots successfully. + +postinstall_bind_mounts() { + mkdir -p "$MOUNT_ROOT/dev" "$MOUNT_ROOT/proc" "$MOUNT_ROOT/sys" "$MOUNT_ROOT/run" + mount --rbind /dev "$MOUNT_ROOT/dev" + mount --make-rslave "$MOUNT_ROOT/dev" + mount -t proc /proc "$MOUNT_ROOT/proc" + mount --rbind /sys "$MOUNT_ROOT/sys" + mount --make-rslave "$MOUNT_ROOT/sys" + mount --rbind /run "$MOUNT_ROOT/run" + mount --make-rslave "$MOUNT_ROOT/run" +} + +postinstall_unbind_mounts() { + umount -R "$MOUNT_ROOT/dev" 2>/dev/null || true + umount -R "$MOUNT_ROOT/proc" 2>/dev/null || true + umount -R "$MOUNT_ROOT/sys" 2>/dev/null || true + umount -R "$MOUNT_ROOT/run" 2>/dev/null || true +} + +postinstall_run() { + : "${ROOT_PART:?Root partition is required}" + : "${CRYPT_NAME:?Crypt mapping name is required}" + : "${MOUNT_ROOT:?Mount root is required}" + : "${FS_TYPE:?Filesystem type is required}" + : "${SWAP_SIZE:?Swap size is required}" + : "${HOSTNAME:?Hostname is required}" + local luks_uuid + local root_uuid + + luks_uuid="$(cryptsetup luksUUID "$ROOT_PART")" + root_uuid="$(blkid -s UUID -o value "/dev/mapper/${CRYPT_NAME}")" + + log_info "Post-install: configuring target system in chroot" + postinstall_bind_mounts + + LUKS_UUID="$luks_uuid" ROOT_UUID="$root_uuid" FS_TYPE="$FS_TYPE" SWAP_SIZE="$SWAP_SIZE" HOSTNAME="$HOSTNAME" \ + chroot "$MOUNT_ROOT" /bin/bash -s <<'EOS' +set -euo pipefail + +# Ensure required tools are available inside the target system. +for cmd in grub-install grub-mkconfig xbps-reconfigure fallocate mkswap; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Missing required command inside chroot: $cmd" >&2 + exit 1 + fi +done + +# Write the hostname explicitly to match wrapper configuration. +printf '%s\n' "$HOSTNAME" > /etc/hostname + +# Ensure crypttab exists with the correct UUID mapping. +mkdir -p /etc +printf 'cryptroot UUID=%s none luks\n' "$LUKS_UUID" > /etc/crypttab + +# Ensure dracut loads crypt and optional resume modules. +mkdir -p /etc/dracut.conf.d +if [[ "$SWAP_SIZE" != "0" ]]; then + printf 'add_dracutmodules+=" crypt resume "\n' > /etc/dracut.conf.d/10-crypt.conf +else + printf 'add_dracutmodules+=" crypt "\n' > /etc/dracut.conf.d/10-crypt.conf +fi + +# Prepare a swap file inside the encrypted root if enabled. +if [[ "$SWAP_SIZE" != "0" ]]; then + mkdir -p /swap + if [[ "$FS_TYPE" == "btrfs" ]]; then + if command -v chattr >/dev/null 2>&1; then + chattr +C /swap + fi + if command -v btrfs >/dev/null 2>&1; then + btrfs property set /swap compression none || true + fi + fi + fallocate -l "$SWAP_SIZE" /swap/swapfile + chmod 600 /swap/swapfile + mkswap /swap/swapfile + if ! grep -q '^/swap/swapfile' /etc/fstab; then + printf '/swap/swapfile none swap defaults 0 0\n' >> /etc/fstab + fi +fi + +# Ensure btrfs subvolumes are reflected in /etc/fstab. +if [[ "$FS_TYPE" == "btrfs" ]]; then + touch /etc/fstab + + update_fstab_entry() { + local mount_point="$1" + local options="$2" + local line="UUID=$ROOT_UUID $mount_point btrfs $options 0 0" + + if awk -v mp="$mount_point" '$2==mp {found=1} END {exit found?0:1}' /etc/fstab; then + awk -v mp="$mount_point" -v line="$line" 'BEGIN{OFS=" "} $2==mp {$0=line} {print}' /etc/fstab > /etc/fstab.tmp + mv /etc/fstab.tmp /etc/fstab + else + printf '%s\n' "$line" >> /etc/fstab + fi + } + + update_fstab_entry / "defaults,subvol=@" + update_fstab_entry /home "defaults,subvol=@home" + update_fstab_entry /var "defaults,subvol=@var" + update_fstab_entry /var/log "defaults,subvol=@log" + update_fstab_entry /.snapshots "defaults,subvol=@snapshots" + update_fstab_entry /swap "defaults,subvol=@swap" +fi + +# Ensure GRUB unlocks the encrypted root and passes the LUKS UUID to initramfs. +mkdir -p /etc/default +touch /etc/default/grub +if grep -q '^GRUB_ENABLE_CRYPTODISK=' /etc/default/grub; then + sed -i 's/^GRUB_ENABLE_CRYPTODISK=.*/GRUB_ENABLE_CRYPTODISK=y/' /etc/default/grub +else + printf 'GRUB_ENABLE_CRYPTODISK=y\n' >> /etc/default/grub +fi + +if grep -q '^GRUB_CMDLINE_LINUX=' /etc/default/grub; then + if ! grep -q "rd.luks.uuid=$LUKS_UUID" /etc/default/grub; then + sed -i "s/^GRUB_CMDLINE_LINUX=\"/GRUB_CMDLINE_LINUX=\"rd.luks.uuid=$LUKS_UUID /" /etc/default/grub + fi +else + printf 'GRUB_CMDLINE_LINUX="rd.luks.uuid=%s"\n' "$LUKS_UUID" >> /etc/default/grub +fi + +# Regenerate initramfs and GRUB configuration. +xbps-reconfigure -fa + +grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Void --recheck +grub-mkconfig -o /boot/grub/grub.cfg +EOS + + postinstall_unbind_mounts + log_info "Post-install complete." +} diff --git a/src/rollback.sh b/src/rollback.sh index a3648cc..3f6109c 100644 --- a/src/rollback.sh +++ b/src/rollback.sh @@ -1,13 +1,19 @@ +#!/usr/bin/env bash + # === Motivation === # Reduce fallout if a phase fails mid-way. + # === Problem Statement === # The wrapper should leave the system in a predictable state after errors. + # === Scope === # In scope: cleanup expectations and guidance for manual recovery. # Out of scope: full automatic rollback of disk changes. + # === Concepts === # Cleanup: unmounting and closing encryption mappings. # Best-effort rollback: undo reversible operations, leave destructive changes as-is. + # === Decisions === # Rollback is limited to reversible operations only: # - Unmount all filesystems under /mnt @@ -16,13 +22,49 @@ # Only perform cleanup after explicit user confirmation. # Provide clear manual recovery instructions if automatic cleanup fails. # Track which phase failed to offer targeted recovery advice. + # === Alternatives Considered === # Full rollback rejected due to complexity and risk. + # === Constraints === # Cleanup must avoid touching unrelated devices. + # === Open Questions === # Should rollback be automatic on failure, or require explicit user confirmation? # What state should be left after rollback - empty disk, partial setup, or unchanged? # Should we provide manual recovery commands if automatic rollback fails? + # === Success Criteria === # After a failure, mounts and encryption mappings are closed when safe to do so. + +rollback_offer() { + : "${MOUNT_ROOT:?Mount root is required}" + : "${CRYPT_NAME:?Crypt mapping name is required}" + local response + + if ! findmnt "$MOUNT_ROOT" >/dev/null 2>&1 && [[ ! -e "/dev/mapper/$CRYPT_NAME" ]]; then + return 0 + fi + + read -r -p "Cleanup mounts and close LUKS mapping? (yes/no): " response + if [[ "$response" != "yes" ]]; then + log_warn "Skipping cleanup. Manual recovery may be required." + return 0 + fi + + rollback_cleanup +} + +rollback_cleanup() { + : "${MOUNT_ROOT:?Mount root is required}" + : "${CRYPT_NAME:?Crypt mapping name is required}" + if findmnt "$MOUNT_ROOT" >/dev/null 2>&1; then + log_info "Unmounting $MOUNT_ROOT" + umount -R "$MOUNT_ROOT" || log_warn "Failed to unmount $MOUNT_ROOT" + fi + + if [[ -e "/dev/mapper/$CRYPT_NAME" ]]; then + log_info "Closing LUKS mapping $CRYPT_NAME" + cryptsetup close "$CRYPT_NAME" || log_warn "Failed to close LUKS mapping" + fi +} diff --git a/src/sanity.sh b/src/sanity.sh index 93fc65f..7d45a2b 100644 --- a/src/sanity.sh +++ b/src/sanity.sh @@ -1,25 +1,96 @@ +#!/usr/bin/env bash + # === Motivation === # Prevent catastrophic mistakes before any disk operations. + # === Problem Statement === # The wrapper must ensure it is running in a safe context with the intended target. + # === Scope === # In scope: checks for privileges, environment, and target disk presence. # Out of scope: detailed hardware inventory. + # === Concepts === # Preflight: a set of checks that must pass before continuing. + # === Decisions === # Fail fast on missing privileges or ambiguous disk selection. # Require a typed confirmation of the target disk identifier. # Reject unsupported architectures; x86_64 is the only supported target. # Reject non-UEFI boot modes; BIOS systems are out of scope. # Provide a dry-run mode to review planned actions before execution. + # === Alternatives Considered === # Proceeding with warnings only rejected due to data loss risk. + # === Constraints === # Checks must run without relying on network access. + # === Open Questions === # Should we check for minimum disk size requirements before proceeding? # How do we detect if the system was booted in UEFI mode reliably? # Should we warn if the target disk contains existing partitions, or just require explicit confirmation? + # === Success Criteria === # The wrapper refuses to proceed if the target disk is unclear or unsafe. + +require_command() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + die "Missing required command: $cmd" + fi +} + +sanity_check_base() { + if [[ "$(id -u)" -ne 0 ]]; then + die "Run this script as root." + fi + + if [[ "$(uname -m)" != "x86_64" ]]; then + die "Unsupported architecture: $(uname -m)." + fi + + if [[ ! -d /sys/firmware/efi ]]; then + die "UEFI mode required. BIOS systems are out of scope." + fi +} + +sanity_check_commands() { + require_command lsblk + require_command partprobe + require_command parted + require_command cryptsetup + require_command mkfs.vfat + require_command blkid + require_command mount + require_command umount + require_command findmnt + require_command chroot + if [[ "$FS_TYPE" == "btrfs" ]]; then + require_command mkfs.btrfs + require_command btrfs + else + require_command mkfs.ext4 + fi +} + +sanity_check_disk() { + : "${DISK:?Target disk is required}" + if [[ ! -b "$DISK" ]]; then + die "Target disk is not a block device: $DISK" + fi + + local disk_type + disk_type="$(lsblk -dn -o TYPE "$DISK")" + if [[ "$disk_type" != "disk" ]]; then + die "Target is not a disk device: $DISK" + fi + + if lsblk -n -o MOUNTPOINT "$DISK" | grep -q '/'; then + log_warn "Target disk has mounted partitions. Proceed with caution." + fi + + if lsblk -n -o NAME,TYPE "$DISK" | grep -q 'part'; then + log_warn "Target disk has existing partitions. They will be erased." + fi +}