fix(installer): harden multi-disk LUKS, password handling, revision pinning
Several installer reliability fixes that were left uncommitted:
- Impermanence + multi-disk LUKS: disko-config.nix names the main LUKS
mapping `crypted` for single-disk and `crypted_main` once extraDrives is
non-empty. The impermanence rollback hook used to hardcode `crypted`,
which made every multi-disk install fail to mount root in initrd. Added
a `nomarchy.system.impermanence.mainLuksName` option and wired the
installer to write the correct value into the generated system.nix
based on the drive count.
- Password no longer cleartext in /etc/nixos: installer now hashes the
user password with `mkpasswd -m sha-512` and emits
`initialHashedPassword` instead of `initialPassword`. Added mkpasswd to
the live ISO. Cleartext is unset immediately after hashing.
USER_PASSWORD_HASH is deliberately not persisted in --resume state —
configure_user re-prompts on resume.
- Revision pinning that actually works on the live ISO: `inputs.self`
strips .git in the Nix store copy, so `git rev-parse HEAD` would silently
return empty on a real install and the generated flake would track main.
Live ISO now writes `/etc/nomarchy-rev` from `inputs.self.rev` at build
time; install.sh reads it first, falls back to git, and aborts with a
loud confirmation prompt if both are empty (instead of silently
installing an unpinned system).
- Generated `/mnt/etc/nixos/state.json`: toggle scripts (nomarchy-tz-select,
nomarchy-setup-{fido2,fingerprint}, nomarchy-toggle-hybrid-gpu,
nomarchy-wifi-powersave) `jq` this file in place and fail hard if it
doesn't exist. Fresh installs now ship a schema-conformant file matching
lib/state-schema.nix.
- Unmount /mnt before exiting `finish()` regardless of reboot choice. Clean
unmount avoids dirty BTRFS on reboot; on "no", leaving /mnt mounted
blocked a second installer run on the same live ISO.
- Removed obsolete `installer/disko-btrfs-luks.nix` (superseded by
`disko-config.nix` per commit 3aadc36) and dropped its dangling
`docs/STRUCTURE.md` reference.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ TARGET_DRIVE=""
|
||||
USERNAME=""
|
||||
LUKS_PASSWORD=""
|
||||
USER_PASSWORD=""
|
||||
USER_PASSWORD_HASH=""
|
||||
TIMEZONE="UTC"
|
||||
KEYMAP_LAYOUT=""
|
||||
KEYMAP_VARIANT=""
|
||||
@@ -92,6 +93,14 @@ parse_args() {
|
||||
# Persist non-secret answers so an interrupted install can pick up where it
|
||||
# left off. Uses `declare -p` so each line is a self-contained `declare --`
|
||||
# statement that `source` re-establishes verbatim.
|
||||
#
|
||||
# USER_PASSWORD_HASH is intentionally NOT persisted, even though a SHA-512
|
||||
# crypt hash isn't reversible. The contract is: after --resume, the password
|
||||
# prompt re-runs. configure_user's early-return guard at the top of the
|
||||
# function checks `[[ -n "$USER_PASSWORD_HASH" ]]` for exactly this reason —
|
||||
# if you ever change that guard to skip on USERNAME+HOSTNAME alone, --resume
|
||||
# will silently install a system with an empty password hash and lock the
|
||||
# user out. Keep the guard checking the hash.
|
||||
save_state() {
|
||||
declare -p \
|
||||
TARGET_DRIVE USERNAME HOSTNAME TIMEZONE \
|
||||
@@ -128,7 +137,7 @@ clear_step_state() {
|
||||
case "$1" in
|
||||
select_disk) TARGET_DRIVE="" ;;
|
||||
get_luks_passphrase) LUKS_PASSWORD="" ;;
|
||||
configure_user) USERNAME=""; HOSTNAME=""; USER_PASSWORD="" ;;
|
||||
configure_user) USERNAME=""; HOSTNAME=""; USER_PASSWORD=""; USER_PASSWORD_HASH="" ;;
|
||||
select_keymap_locale) KEYMAP_LAYOUT=""; KEYMAP_VARIANT=""; LOCALE="" ;;
|
||||
select_timezone) TIMEZONE="" ;;
|
||||
select_hardware) HARDWARE_MODULES=""; NOMARCHY_HW_OPTS="" ;;
|
||||
@@ -206,13 +215,32 @@ check_environment() {
|
||||
# Capture the exact commit we're installing from. The generated flake
|
||||
# pins `nomarchy.url` to this revision so the installed system can't
|
||||
# silently drift onto a newer (possibly breaking) main.
|
||||
if command -v git >/dev/null 2>&1 && [[ -d "$NOMARCHY_REPO/.git" ]]; then
|
||||
#
|
||||
# Three sources, in priority order:
|
||||
# 1. /etc/nomarchy-rev — written at ISO build time from `inputs.self.rev`
|
||||
# (the only source that works on a normal live-ISO install, because
|
||||
# `inputs.self` strips .git from the Nix store copy at /etc/nomarchy).
|
||||
# 2. `git rev-parse HEAD` in the repo — works when running the installer
|
||||
# from a dev checkout instead of the live ISO.
|
||||
# 3. Empty → unpinned, user gets a loud confirmation prompt below.
|
||||
if [[ -z "$NOMARCHY_REV" ]] && [[ -f /etc/nomarchy-rev ]]; then
|
||||
NOMARCHY_REV=$(tr -d '[:space:]' < /etc/nomarchy-rev)
|
||||
fi
|
||||
if [[ -z "$NOMARCHY_REV" ]] && command -v git >/dev/null 2>&1 && [[ -d "$NOMARCHY_REPO/.git" ]]; then
|
||||
NOMARCHY_REV=$(git -C "$NOMARCHY_REPO" rev-parse HEAD 2>/dev/null || echo "")
|
||||
fi
|
||||
if [[ -n "$NOMARCHY_REV" ]]; then
|
||||
success "Pinning Nomarchy to $NOMARCHY_REV"
|
||||
else
|
||||
info "Could not determine Nomarchy revision; downstream flake will track main."
|
||||
error "Could not determine Nomarchy revision."
|
||||
info "The installed system would silently track upstream main, which"
|
||||
info "defeats the point of locking inputs at install time."
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
if ! nrun gum confirm --default=false \
|
||||
"Continue anyway with an unpinned (tracking main) configuration?"; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check internet
|
||||
@@ -452,8 +480,8 @@ configure_user() {
|
||||
section "User Configuration"
|
||||
|
||||
if [[ -n "$USERNAME" && -n "$HOSTNAME" ]]; then
|
||||
# Password check skipped in dry run or if already set
|
||||
if [[ "$DRY_RUN" == "true" ]] || [[ -n "$USER_PASSWORD" ]]; then
|
||||
# Password check skipped in dry run or if already hashed in this session.
|
||||
if [[ "$DRY_RUN" == "true" ]] || [[ -n "$USER_PASSWORD_HASH" ]]; then
|
||||
success "User $USERNAME @ $HOSTNAME configured"
|
||||
return 0
|
||||
fi
|
||||
@@ -493,6 +521,9 @@ configure_user() {
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "Dry run: skipping user password prompt."
|
||||
USER_PASSWORD="dryrun-not-used"
|
||||
# Stable placeholder hash so generated system.nix still parses as Nix.
|
||||
# Never lands on a real install — dry-run skips nixos-install.
|
||||
USER_PASSWORD_HASH='$6$dryrun$3xxK3aQ.0bGcv0fM2RhV4Q9oN3p1mYxz5kSjQ.bC8tZpZ7QnFv2cN0Yhd5lDqJ8X9mP2K1L0vR6BqWqzNk7Yo/'
|
||||
save_state
|
||||
return
|
||||
fi
|
||||
@@ -521,6 +552,20 @@ configure_user() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Hash now so we never have to embed the cleartext in /etc/nixos/system.nix
|
||||
# (where it would be world-readable and break Nix parsing on quotes/backslash).
|
||||
# mkpasswd is added to the live ISO via hosts/nomarchy-live.nix.
|
||||
if ! command -v mkpasswd >/dev/null 2>&1; then
|
||||
error "mkpasswd not found on the live ISO — cannot hash the user password."
|
||||
exit 1
|
||||
fi
|
||||
USER_PASSWORD_HASH=$(printf '%s' "$USER_PASSWORD" | mkpasswd -m sha-512 -s)
|
||||
if [[ -z "$USER_PASSWORD_HASH" ]]; then
|
||||
error "mkpasswd produced an empty hash."
|
||||
exit 1
|
||||
fi
|
||||
unset pass1 pass2 USER_PASSWORD
|
||||
|
||||
success "User password set"
|
||||
save_state
|
||||
}
|
||||
@@ -1346,9 +1391,17 @@ EOF
|
||||
# ============================================================================
|
||||
|
||||
generate_flake_config() {
|
||||
# Impermanence must mount the same /dev/mapper name that disko created.
|
||||
# disko-config.nix uses "crypted" for single-disk and "crypted_main" once
|
||||
# extraDrives is non-empty — keep these in sync.
|
||||
local impermanence_opt=""
|
||||
if [[ "$ENABLE_IMPERMANENCE" == "true" ]]; then
|
||||
impermanence_opt="nomarchy.system.impermanence.enable = true;"
|
||||
local _main_luks_name="crypted"
|
||||
local _drives=($TARGET_DRIVE)
|
||||
if (( ${#_drives[@]} > 1 )); then
|
||||
_main_luks_name="crypted_main"
|
||||
fi
|
||||
impermanence_opt=$'nomarchy.system.impermanence.enable = true;\n nomarchy.system.impermanence.mainLuksName = "'"$_main_luks_name"$'";'
|
||||
fi
|
||||
|
||||
local PROFILE_SYSTEM_OPTS=""
|
||||
@@ -1534,7 +1587,9 @@ $xkb_variant_line
|
||||
|
||||
users.users."$USERNAME" = {
|
||||
isNormalUser = true;
|
||||
initialPassword = "$USER_PASSWORD";
|
||||
# SHA-512 crypt hash generated by mkpasswd during install. Cleartext
|
||||
# never touches /etc/nixos. Change later with \`passwd\`.
|
||||
initialHashedPassword = "$USER_PASSWORD_HASH";
|
||||
extraGroups = [ "networkmanager" "wheel" "video" "audio" "render" ];
|
||||
};
|
||||
|
||||
@@ -1685,6 +1740,31 @@ EOF
|
||||
# Extra Home Manager modules go here (program configs, services, etc.).
|
||||
}
|
||||
EOF
|
||||
|
||||
# state.json — consumed by core/system/state.nix at every nixos-rebuild and
|
||||
# mutated by toggle scripts (nomarchy-tz-select, nomarchy-setup-fido2,
|
||||
# nomarchy-toggle-hybrid-gpu, nomarchy-wifi-powersave, ...). Those scripts
|
||||
# `jq` the file in place and fail hard if it doesn't exist or isn't valid
|
||||
# JSON, so a fresh install MUST ship one. Shape must match lib/state-schema.nix.
|
||||
# Quoted heredoc — no shell expansion except the explicit $TIMEZONE below.
|
||||
local _state_tz="${TIMEZONE:-UTC}"
|
||||
cat > /mnt/etc/nixos/state.json <<JSON_EOF
|
||||
{
|
||||
"theme": "nord",
|
||||
"timezone": "${_state_tz}",
|
||||
"dns": "DHCP",
|
||||
"customDns": [],
|
||||
"wifi": {
|
||||
"powersave": true
|
||||
},
|
||||
"features": {
|
||||
"fingerprint": false,
|
||||
"fido2": false,
|
||||
"hybridGPU": false,
|
||||
"makima": false
|
||||
}
|
||||
}
|
||||
JSON_EOF
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
@@ -1712,8 +1792,18 @@ finish() {
|
||||
echo " variant. Theme switches after that are instant (no rebuild)."
|
||||
echo ""
|
||||
|
||||
# Unmount /mnt before either reboot or returning to the live shell:
|
||||
# - Reboot: clean unmount avoids dirty BTRFS, which would otherwise
|
||||
# force a longer first-boot fsck/replay.
|
||||
# - Decline: leaving /mnt mounted blocks a second `install.sh` run on
|
||||
# the same live ISO (disko refuses to wipe a busy device).
|
||||
# `-R` recursively unmounts /mnt/boot, /mnt/home, /mnt/nix, etc.; the
|
||||
# `|| true` absorbs the case where /mnt was already torn down.
|
||||
if nrun gum confirm "Reboot now?"; then
|
||||
umount -R /mnt 2>/dev/null || true
|
||||
reboot
|
||||
else
|
||||
umount -R /mnt 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user