fix(install): swap=0 and unattended LUKS fail-closed
All checks were successful
Check / eval (push) Successful in 3m4s

Pass bare swapSize "0" when the user wants no swap (was "0G", which
still created @swap). Accept "0"/"0G" in disko-config. Unattended
installs require NOMARCHY_LUKS_PASSPHRASE or explicit NOMARCHY_NO_LUKS=1
instead of silently installing cleartext.

Verified: bash -n; nix-instantiate subvolume sets for 0/0G/16G.
VISION § v1.0 install golden path (workflow test iteration).
This commit is contained in:
Bernardo Magri
2026-07-09 09:40:07 +01:00
parent 02d7baeb7c
commit 208b8d4444
4 changed files with 35 additions and 26 deletions

View File

@@ -157,16 +157,6 @@ first-boot tips · human rebuild errors · neon-glass quarantine.
### Bugs / broken behavior (codebase exam 2026-07-09) ### Bugs / broken behavior (codebase exam 2026-07-09)
- **Installer: swap "none" still creates a 0G swap subvolume**
`nomarchy-install.sh` always passes `--argstr swapSize "${SWAP_GB}G"`.
When the user chooses 0 (no swap), disko gets `"0G"`, but
`disko-config.nix` only treats the exact string `"0"` as no-swap —
so it still creates `@swap` with size `0G`. Resume wiring correctly
skips (`SWAP_GB != "0"`), so layout and config disagree. Fix: pass
bare `"0"` (or teach disko to accept `"0G"`). Cheap pure check /
unit test for the swapSize contract would lock this. Cost: small
installer + disko-config change; high install-path impact.
- **Waybar doctor indicator never shows an icon** - **Waybar doctor indicator never shows an icon**
`modules/home/waybar.nix` `nomarchy-doctor-status` prints `modules/home/waybar.nix` `nomarchy-doctor-status` prints
`"text":""` on failure. Waybar custom modules with empty `text` `"text":""` on failure. Waybar custom modules with empty `text`
@@ -219,14 +209,6 @@ first-boot tips · human rebuild errors · neon-glass quarantine.
battery-notify. Cost: small multi-file; `[blocked:hw]` to confirm battery-notify. Cost: small multi-file; `[blocked:hw]` to confirm
on non-BAT* hardware. on non-BAT* hardware.
- **Unattended install silently disables LUKS if passphrase unset**
Interactive path defaults encrypt-on; unattended without
`NOMARCHY_LUKS_PASSPHRASE` installs **unencrypted** with only a log
line — no fail-closed. `test-install.sh` always sets a passphrase,
so the regression path is untested. Options: require the env var
when unattended, or an explicit `NOMARCHY_NO_LUKS=1`. Cost: small
policy change in install script + test matrix note.
### Docs / option-surface drift (codebase exam 2026-07-09) ### Docs / option-surface drift (codebase exam 2026-07-09)
- **`option-docs` would fail: undocumented `nomarchy.hardware.i2c.*`** - **`option-docs` would fail: undocumented `nomarchy.hardware.i2c.*`**
@@ -655,10 +637,9 @@ installer questionnaire.*
#### Bugs (stability contracts — fix, dont optionalize) #### Bugs (stability contracts — fix, dont optionalize)
- **Installer swap=0 / unattended LUKS fail-open** — already under - **Installer swap=0 + unattended LUKS fail-closed** — ✓ shipped
*Bugs / broken behavior* above (`"0G"` vs `"0"`; missing (workflow test run 2026-07-09): pass bare `"0"`; disko accepts
`NOMARCHY_LUKS_PASSPHRASE` → cleartext). Same audit reconfirmed both `"0"`/`"0G"`; unattended needs passphrase or `NOMARCHY_NO_LUKS=1`.
as install-path P0.
- **Offline mode must not silently fall back to network lock** - **Offline mode must not silently fall back to network lock**
When the network probe fails (or install is offline), When the network probe fails (or install is offline),

View File

@@ -17,6 +17,13 @@ Template:
--- ---
## 2026-07-09 — Install contracts: swap=0 + unattended LUKS (workflow test run)
- **Task:** VISION § v1.0 install golden path — PROPOSED install P0s (NEXT head blocked: item 14 wait-cron, 20 `[human]`, 33 V3 visual, 41 hw). Test of VISION→BACKLOG→verify→commit loop.
- **Did:** (1) `nomarchy-install` passes `swapSize=0` when swap disabled (was always `"${SWAP_GB}G"``"0G"` still created `@swap`). (2) `disko-config.nix` treats `"0"` and `"0G"` as no-swap. (3) Unattended LUKS fail-closed: require `NOMARCHY_LUKS_PASSPHRASE` or `NOMARCHY_NO_LUKS=1`.
- **Verified:** V0 — `bash -n`; pure `nix-instantiate` of disko subvols for `0`/`0G`/`16G` (no `@swap` only on zero). Not V2 install matrix this run.
- **Pending:** optional pure `checks.*` for swapSize; unattended no-swap/no-LUKS in `test-install.sh` when convenient.
- **Next suggestion:** promote another VISION § A slice (firmware menu / doctor) or boreal default to NEXT.
## 2026-07-08 — Theme UI Review: Typography Polish & Icon Call (iteration #87) ## 2026-07-08 — Theme UI Review: Typography Polish & Icon Call (iteration #87)
- **Task:** NEXT item 28 (Theme UI review). - **Task:** NEXT item 28 (Theme UI review).
- **Did:** - **Did:**

View File

@@ -15,12 +15,14 @@
# proved fragile twice before. # proved fragile twice before.
{ mainDrive { mainDrive
, withLuks ? true , withLuks ? true
, swapSize ? "0" # "0" = no swap; otherwise e.g. "16G" (sized for hibernation) , swapSize ? "0" # "0" or "0G" = no swap; otherwise e.g. "16G" (hibernation-sized)
, ... , ...
}: }:
let let
btrfsMountOptions = [ "compress=zstd" "noatime" ]; btrfsMountOptions = [ "compress=zstd" "noatime" ];
# Installer historically always appended "G"; accept both "0" and "0G".
noSwap = swapSize == "0" || swapSize == "0G";
rootBtrfs = { rootBtrfs = {
type = "btrfs"; type = "btrfs";
@@ -32,7 +34,7 @@ let
"@log" = { mountpoint = "/var/log"; mountOptions = btrfsMountOptions; }; "@log" = { mountpoint = "/var/log"; mountOptions = btrfsMountOptions; };
# snapper timeline snapshots (nomarchy.system.snapper.enable) # snapper timeline snapshots (nomarchy.system.snapper.enable)
"@snapshots" = { mountpoint = "/.snapshots"; mountOptions = btrfsMountOptions; }; "@snapshots" = { mountpoint = "/.snapshots"; mountOptions = btrfsMountOptions; };
} // (if swapSize == "0" then { } else { } // (if noSwap then { } else {
# Hibernation-ready swapfile on its own subvolume; disko's # Hibernation-ready swapfile on its own subvolume; disko's
# mkswapfile handles the BTRFS NOCOW requirements. # mkswapfile handles the BTRFS NOCOW requirements.
"@swap" = { "@swap" = {

View File

@@ -18,8 +18,11 @@
# [NOMARCHY_TIMEZONE=UTC] [NOMARCHY_LUKS_PASSPHRASE=...] \ # [NOMARCHY_TIMEZONE=UTC] [NOMARCHY_LUKS_PASSPHRASE=...] \
# [NOMARCHY_LOCALE=en_US.UTF-8] [NOMARCHY_KB_LAYOUT=us] [NOMARCHY_KB_VARIANT=] \ # [NOMARCHY_LOCALE=en_US.UTF-8] [NOMARCHY_KB_LAYOUT=us] [NOMARCHY_KB_VARIANT=] \
# [NOMARCHY_SWAP_GB=N (default: RAM size; 0 = none)] \ # [NOMARCHY_SWAP_GB=N (default: RAM size; 0 = none)] \
# [NOMARCHY_LUKS_PASSPHRASE=... | NOMARCHY_NO_LUKS=1] \
# [NOMARCHY_HW="auto"|"none"|"mod1 mod2"] [NOMARCHY_FINISH=none|reboot|poweroff] # [NOMARCHY_HW="auto"|"none"|"mod1 mod2"] [NOMARCHY_FINISH=none|reboot|poweroff]
# nomarchy-install # nomarchy-install
# Unattended encryption is fail-closed: set a passphrase, or explicit
# NOMARCHY_NO_LUKS=1 — never silently install cleartext.
set -euo pipefail set -euo pipefail
@@ -126,7 +129,15 @@ section "Disk encryption"
# logs you straight into the desktop (the passphrase already gates access). # logs you straight into the desktop (the passphrase already gates access).
LUKS_PASSPHRASE="" LUKS_PASSPHRASE=""
if [[ "$UNATTENDED" == "1" ]]; then if [[ "$UNATTENDED" == "1" ]]; then
LUKS_PASSPHRASE="${NOMARCHY_LUKS_PASSPHRASE:-}" # Fail-closed: unattended without a passphrase used to install
# cleartext (easy CI footgun). Require an explicit opt-out.
if [[ "${NOMARCHY_NO_LUKS:-}" == "1" ]]; then
LUKS_PASSPHRASE=""
elif [[ -n "${NOMARCHY_LUKS_PASSPHRASE:-}" ]]; then
LUKS_PASSPHRASE="$NOMARCHY_LUKS_PASSPHRASE"
else
fail "Unattended install needs NOMARCHY_LUKS_PASSPHRASE or NOMARCHY_NO_LUKS=1"
fi
elif gum confirm --default=yes "Encrypt the disk with LUKS? (default — also enables passwordless desktop login)"; then elif gum confirm --default=yes "Encrypt the disk with LUKS? (default — also enables passwordless desktop login)"; then
while true; do while true; do
p1=$(gum input --password --placeholder "LUKS passphrase (min 8 chars)") p1=$(gum input --password --placeholder "LUKS passphrase (min 8 chars)")
@@ -317,11 +328,19 @@ if [[ $WITH_LUKS == true ]]; then
unset LUKS_PASSPHRASE unset LUKS_PASSPHRASE
fi fi
# disko-config treats exact "0" as no-swap; "${SWAP_GB}G" would pass "0G"
# and still create a useless @swap subvolume (layout vs resume disagreed).
if [[ "$SWAP_GB" == "0" ]]; then
DISKO_SWAP_SIZE="0"
else
DISKO_SWAP_SIZE="${SWAP_GB}G"
fi
disko_log=$(mktemp --suffix=.disko.log) disko_log=$(mktemp --suffix=.disko.log)
if ! disko --mode destroy,format,mount --yes-wipe-all-disks \ if ! disko --mode destroy,format,mount --yes-wipe-all-disks \
--argstr mainDrive "$TARGET_DISK" \ --argstr mainDrive "$TARGET_DISK" \
--arg withLuks "$WITH_LUKS" \ --arg withLuks "$WITH_LUKS" \
--argstr swapSize "${SWAP_GB}G" \ --argstr swapSize "$DISKO_SWAP_SIZE" \
"$SHARE/disko-config.nix" >"$disko_log" 2>&1; then "$SHARE/disko-config.nix" >"$disko_log" 2>&1; then
tail -n 30 "$disko_log" tail -n 30 "$disko_log"
fail "disko failed — full log: $disko_log" fail "disko failed — full log: $disko_log"