aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-08-14 11:51:27 -0500
committerCraig Jennings <c@cjennings.net>2026-08-14 11:51:27 -0500
commite9f82d19c75ac8602f5840794d6bd62e30576c30 (patch)
treede8d6f412254e9d19adf83a39665265d0035d668
parent1a900e50433b0fcd3a192eed1c7989233a6928cf (diff)
downloadarchangel-e9f82d19c75ac8602f5840794d6bd62e30576c30.tar.gz
archangel-e9f82d19c75ac8602f5840794d6bd62e30576c30.zip
fix(install): refuse out-of-range passphrases before the disk is wiped
The unattended path only checked that a passphrase was non-empty, while zpool create enforces 8-512 characters, so a short passphrase failed after partitioning had already destroyed the old pool. The velox reinstall hit exactly that: its profile shipped a 7-char placeholder, and run 1 died post-wipe. validate_encryption_passphrase now takes min/max bounds. ZFS gets 8-512 pre-flight, and LUKS gets the same 8 minimum the interactive prompt enforces. Two adjacent gaps close with it: SWAP_SIZE now rejects zero sizes, which previously passed validation and died at sgdisk after the wipe. validate_config warns when the swap partition lands next to an encrypted root, since a hibernate image is a full RAM dump with keys included. The tracked example profiles' 7-char placeholders are now 8 characters.
-rwxr-xr-xinstaller/archangel10
-rw-r--r--installer/lib/config.sh33
-rw-r--r--installer/velox-btrfs.conf.example4
-rw-r--r--installer/velox-zfs.conf.example4
-rw-r--r--tests/unit/test_config.bats75
5 files changed, 113 insertions, 13 deletions
diff --git a/installer/archangel b/installer/archangel
index b9817e8..065772c 100755
--- a/installer/archangel
+++ b/installer/archangel
@@ -157,11 +157,15 @@ gather_input() {
# Required-field, disk, and timezone validation runs in main()
# via validate_config before this function is reached.
- # Filesystem-specific encryption-passphrase validation
+ # Filesystem-specific encryption-passphrase validation. ZFS gets
+ # the 8-512 bounds zpool create enforces, so an out-of-range
+ # passphrase refuses here instead of after partitioning has wiped
+ # the disk. LUKS gets the same 8 minimum the interactive prompt
+ # enforces, for parity between the two paths.
if [[ "$FILESYSTEM" == "zfs" ]]; then
- validate_encryption_passphrase ZFS_PASSPHRASE
+ validate_encryption_passphrase ZFS_PASSPHRASE 8 512
elif [[ "$FILESYSTEM" == "btrfs" ]]; then
- validate_encryption_passphrase LUKS_PASSPHRASE
+ validate_encryption_passphrase LUKS_PASSPHRASE 8
fi
# Determine RAID level if not specified
diff --git a/installer/lib/config.sh b/installer/lib/config.sh
index dea65a0..8dedc96 100644
--- a/installer/lib/config.sh
+++ b/installer/lib/config.sh
@@ -146,10 +146,12 @@ validate_config() {
((++errors))
fi
- # SWAP_SIZE: sgdisk size syntax (digits + K/M/G/T), single disk only —
- # a per-disk swap on a RAID layout has no defined resume device.
+ # SWAP_SIZE: sgdisk size syntax (nonzero digits + K/M/G/T), single
+ # disk only — a per-disk swap on a RAID layout has no defined resume
+ # device. A zero size would pass here and die at sgdisk, after the
+ # wipe, so the leading digit must be 1-9.
if [[ -n "$SWAP_SIZE" ]]; then
- if [[ ! "$SWAP_SIZE" =~ ^[0-9]+[KMGT]$ ]]; then
+ if [[ ! "$SWAP_SIZE" =~ ^[1-9][0-9]*[KMGT]$ ]]; then
warn "Invalid SWAP_SIZE '$SWAP_SIZE' (expected e.g. 100G)"
((++errors))
fi
@@ -157,6 +159,12 @@ validate_config() {
warn "SWAP_SIZE is only supported on single-disk installs"
((++errors))
fi
+ # Advisory only: the installer creates the partition but doesn't
+ # encrypt it, and a hibernate image is a full RAM dump — keys
+ # included. The velox recipe LUKS-encrypts it post-install.
+ if [[ "$NO_ENCRYPT" != "yes" ]]; then
+ warn "SWAP_SIZE partition is created unencrypted — encrypt it before hibernating (root is encrypted, swap won't be)"
+ fi
fi
if [[ $errors -gt 0 ]]; then
@@ -176,13 +184,26 @@ validate_filesystem() {
# Ensure an encryption passphrase variable is set when encryption is
# on. Takes the variable name (ZFS_PASSPHRASE or LUKS_PASSPHRASE) and
-# errors out if NO_ENCRYPT is not "yes" and the named variable is
-# empty. Indirect expansion (${!var_name}) lets one helper handle both
+# an optional minimum length, and errors out if NO_ENCRYPT is not "yes"
+# and the named variable is empty or shorter than the minimum. ZFS
+# enforces an 8-char minimum at pool creation, which lands AFTER
+# partitioning has destroyed the disk — checking here refuses before the
+# wipe. Indirect expansion (${!var_name}) lets one helper handle both
# ZFS and Btrfs passphrase fields without duplicating the conditional
# in gather_input's filesystem dispatch.
validate_encryption_passphrase() {
local var_name="$1"
- if [[ "$NO_ENCRYPT" != "yes" && -z "${!var_name}" ]]; then
+ local min_len="${2:-0}"
+ local max_len="${3:-0}"
+ local value="${!var_name}"
+ [[ "$NO_ENCRYPT" == "yes" ]] && return 0
+ if [[ -z "$value" ]]; then
error "Config missing required: ${var_name} (or set NO_ENCRYPT=yes)"
fi
+ if [[ ${#value} -lt $min_len ]]; then
+ error "${var_name} must be at least ${min_len} characters"
+ fi
+ if [[ $max_len -gt 0 && ${#value} -gt $max_len ]]; then
+ error "${var_name} must be at most ${max_len} characters"
+ fi
}
diff --git a/installer/velox-btrfs.conf.example b/installer/velox-btrfs.conf.example
index af7c0c0..08581f1 100644
--- a/installer/velox-btrfs.conf.example
+++ b/installer/velox-btrfs.conf.example
@@ -10,6 +10,6 @@ KEYMAP=us
DISKS=/dev/nvme0n1
RAID_LEVEL=
-LUKS_PASSPHRASE=welcome
-ROOT_PASSWORD=welcome
+LUKS_PASSPHRASE=changeme
+ROOT_PASSWORD=changeme
ENABLE_SSH=yes
diff --git a/installer/velox-zfs.conf.example b/installer/velox-zfs.conf.example
index a309843..05e7568 100644
--- a/installer/velox-zfs.conf.example
+++ b/installer/velox-zfs.conf.example
@@ -10,6 +10,6 @@ KEYMAP=us
DISKS=/dev/nvme0n1
RAID_LEVEL=
-ZFS_PASSPHRASE=welcome
-ROOT_PASSWORD=welcome
+ZFS_PASSPHRASE=changeme
+ROOT_PASSWORD=changeme
ENABLE_SSH=yes
diff --git a/tests/unit/test_config.bats b/tests/unit/test_config.bats
index 554c0c7..26d9e0a 100644
--- a/tests/unit/test_config.bats
+++ b/tests/unit/test_config.bats
@@ -303,6 +303,51 @@ EOF
! [[ "$output" == *"ZFS_PASSPHRASE"* ]]
}
+@test "validate_encryption_passphrase rejects a passphrase under the minimum length" {
+ NO_ENCRYPT=no
+ ZFS_PASSPHRASE="welcome"
+ run validate_encryption_passphrase ZFS_PASSPHRASE 8
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"at least 8"* ]]
+ [[ "$output" == *"ZFS_PASSPHRASE"* ]]
+}
+
+@test "validate_encryption_passphrase accepts a passphrase at exactly the minimum length" {
+ NO_ENCRYPT=no
+ ZFS_PASSPHRASE="welcome1"
+ run validate_encryption_passphrase ZFS_PASSPHRASE 8
+ [ "$status" -eq 0 ]
+}
+
+@test "validate_encryption_passphrase without a minimum keeps the empty-only check" {
+ NO_ENCRYPT=no
+ LUKS_PASSPHRASE="hunter2"
+ run validate_encryption_passphrase LUKS_PASSPHRASE
+ [ "$status" -eq 0 ]
+}
+
+@test "validate_encryption_passphrase skips the length check when NO_ENCRYPT=yes" {
+ NO_ENCRYPT=yes
+ ZFS_PASSPHRASE="short"
+ run validate_encryption_passphrase ZFS_PASSPHRASE 8
+ [ "$status" -eq 0 ]
+}
+
+@test "validate_encryption_passphrase rejects a passphrase over the maximum length" {
+ NO_ENCRYPT=no
+ ZFS_PASSPHRASE=$(printf 'a%.0s' {1..513})
+ run validate_encryption_passphrase ZFS_PASSPHRASE 8 512
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"at most 512"* ]]
+}
+
+@test "validate_encryption_passphrase accepts a passphrase at exactly the maximum length" {
+ NO_ENCRYPT=no
+ ZFS_PASSPHRASE=$(printf 'a%.0s' {1..512})
+ run validate_encryption_passphrase ZFS_PASSPHRASE 8 512
+ [ "$status" -eq 0 ]
+}
+
#############################
# SWAP_SIZE validation
#############################
@@ -338,6 +383,36 @@ EOF
[[ "$output" == *"Invalid SWAP_SIZE"* ]]
}
+@test "validate_config rejects a zero SWAP_SIZE" {
+ HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda); RAID_LEVEL=""; SWAP_SIZE=0G
+ run validate_config
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"Invalid SWAP_SIZE"* ]]
+}
+
+@test "validate_config rejects a leading-zero SWAP_SIZE" {
+ HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda); RAID_LEVEL=""; SWAP_SIZE=00G
+ run validate_config
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"Invalid SWAP_SIZE"* ]]
+}
+
+@test "validate_config warns that swap is unencrypted when encryption is on" {
+ HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda); RAID_LEVEL=""; SWAP_SIZE=100G; NO_ENCRYPT=no
+ run validate_config
+ [[ "$output" == *"unencrypted"* ]]
+}
+
+@test "validate_config does not warn about swap encryption when NO_ENCRYPT=yes" {
+ HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda); RAID_LEVEL=""; SWAP_SIZE=100G; NO_ENCRYPT=yes
+ run validate_config
+ [[ "$output" != *"unencrypted"* ]]
+}
+
@test "validate_config rejects SWAP_SIZE on a multi-disk layout" {
HOSTNAME=h; TIMEZONE=UTC; ROOT_PASSWORD=x
SELECTED_DISKS=(/dev/sda /dev/sdb); RAID_LEVEL=mirror; SWAP_SIZE=100G