feat(installer): allow user/LUKS passwords to be set via the answerfile

Previously the user password (and the LUKS passphrase for encrypted installs)
were always prompted interactively, so an answerfile install could never be
fully hands-free. Add optional "password" and "luks_password" answerfile fields:

- arch-autoinstall.sh: read both via af_get; when present use them (chpasswd /
  cryptsetup --key-file=- with --batch-mode and stdin-piped luksAddKey auth),
  otherwise fall back to the interactive prompt. Empty/null/absent => prompt.
- generate-answerfile.sh: replace the "passwords are never stored" notice with
  an optional confirmed-entry password prompt (and a LUKS one when encryption is
  enabled); emit both as JSON (null when declined).

Secrets stored this way are plain text in the file (and world-readable once
embedded in an ISO) — documented in the header; decline to keep prompting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5kHioUMK3mtf2eiLEozCM
main
Amir Alexander Abdelbaki 2026-06-27 01:43:17 +02:00
parent 587b95cada
commit e7f251dde3
2 changed files with 69 additions and 15 deletions

View File

@ -5,7 +5,12 @@
# all prompts are answered from it. Missing fields fall back to interactive prompts.
#
# Answerfile fields: drive, kernel, keymap, hostname, username, encrypt, fido2_root,
# fido2_user, run_tui (password always prompted interactively)
# fido2_user, run_tui, password, luks_password
# The user "password" (and "luks_password" for encrypted installs) may be supplied
# in the answerfile for fully unattended deployment; when omitted they are prompted
# interactively. Storing secrets in the answerfile is a convenience/secret-handling
# tradeoff — keep the file access-restricted (it is embedded world-readable in the
# ISO by build.sh, so only bake passwords into images you control).
# -E: propagate ERR trap into functions and subshells; -e: abort on any error;
# -u: treat unset variables as errors; -o pipefail: catch failures inside pipes.
@ -254,8 +259,16 @@ else
read -rp "Enable FIDO2 authentication for user login? (YES/NO): " FIDO_USER
fi
# Password always prompted — never stored in answerfile
# User password: use the answerfile's "password" field when present (enables a
# fully unattended install); otherwise prompt interactively. Read via af_get so a
# missing/empty field cleanly falls through to the prompt.
USERPASS=""
if $AF_MODE; then USERPASS="$(af_get '.password')"; fi
if [[ -n "$USERPASS" ]]; then
echo "Password for $USERNAME: taken from answerfile."
else
read -rp "Enter password for $USERNAME: " USERPASS
fi
[[ -z "$USERPASS" ]] && { echo "Error: password cannot be empty."; exit 1; }
# In interactive mode, decide now whether to run the dotfiles TUI inside chroot.
@ -328,11 +341,23 @@ LUKS_BACKUP_KEY="" # path to key file, set only when encryption is active
if [[ "$ENCRYPT_DISK" == "YES" ]]; then
echo "Encrypting root partition..."
# LUKS passphrase: from the answerfile's "luks_password" when present (so an
# encrypted install can run unattended), otherwise prompt interactively.
LUKS_PASS=""
if $AF_MODE; then LUKS_PASS="$(af_get '.luks_password')"; fi
# --type luks2 is required for systemd-cryptenroll (FIDO2 token enrollment).
if [[ -n "$LUKS_PASS" ]]; then
echo "LUKS passphrase: taken from answerfile."
# --batch-mode skips the interactive "Are you sure" + passphrase prompts;
# --key-file=- reads the passphrase from stdin (the piped value).
printf '%s' "$LUKS_PASS" | cryptsetup -v luksFormat "$ROOT_PART" --type luks2 --batch-mode --key-file=-
printf '%s' "$LUKS_PASS" | cryptsetup open "$ROOT_PART" cryptroot --key-file=-
else
# -v (verbose) shows progress; prompts for the primary passphrase interactively.
cryptsetup -v luksFormat "$ROOT_PART" --type luks2
# Open the container and expose it as /dev/mapper/cryptroot for formatting.
cryptsetup open "$ROOT_PART" cryptroot
fi
# ── Auto-generate backup LUKS key ──────────────────────────────────────────
# A random key is enrolled as a second LUKS slot so recovery is possible
@ -343,9 +368,14 @@ if [[ "$ENCRYPT_DISK" == "YES" ]]; then
# wrapping) to produce a printable key with high entropy.
dd if=/dev/urandom bs=64 count=1 2>/dev/null | base64 -w0 > "$LUKS_BACKUP_KEY"
echo "Enrolling auto-generated backup LUKS key..."
# luksAddKey reads the existing passphrase interactively to authorise adding
# the new key file into a free LUKS slot.
# luksAddKey needs the existing passphrase to authorise adding the new key file
# into a free LUKS slot. With an answerfile passphrase we pipe it in on stdin;
# otherwise cryptsetup prompts for it interactively.
if [[ -n "$LUKS_PASS" ]]; then
printf '%s' "$LUKS_PASS" | cryptsetup luksAddKey "$ROOT_PART" "$LUKS_BACKUP_KEY"
else
cryptsetup luksAddKey "$ROOT_PART" "$LUKS_BACKUP_KEY"
fi
# ── Optional FIDO2 enrollment ─────────────────────────────────────────────
if [[ "$FIDO_ROOT" == "YES" ]]; then

View File

@ -140,11 +140,28 @@ AF_USERNAME=$(dialog --backtitle "$BACKTITLE" \
9 54 "" \
3>&1 1>&2 2>&3) || AF_USERNAME=""
# NOTE: passwords are intentionally NOT stored in the answerfile.
dialog --backtitle "$BACKTITLE" \
--title " Password " \
--msgbox "\n Passwords are NOT stored in the answerfile.\n\n You will be prompted for the user password\n at install time even in automated mode.\n" \
10 56
# ── User password (optional) ──────────────────────────────────────────────────
# Storing the password makes the install fully unattended, but it is saved as
# plain text in the answerfile (and world-readable once embedded in an ISO). If
# the user declines, the field is left empty and the installer prompts at runtime.
AF_PASSWORD=""
# ask_pw <username> → echoes a confirmed password, or empty if cancelled/declined.
ask_pw() {
local who="$1" p1 p2
while true; do
p1=$(dialog --backtitle "$BACKTITLE" --title " Password " --insecure \
--passwordbox "\n Enter password for ${who}\n (leave blank to be prompted at install time):" 11 60 \
3>&1 1>&2 2>&3) || return 1
[[ -z "$p1" ]] && return 1 # blank = decline, prompt at install
p2=$(dialog --backtitle "$BACKTITLE" --title " Confirm Password " --insecure \
--passwordbox "\n Re-enter the password to confirm:" 10 60 \
3>&1 1>&2 2>&3) || return 1
if [[ "$p1" == "$p2" ]]; then printf '%s' "$p1"; return 0; fi
dialog --backtitle "$BACKTITLE" --title " Mismatch " \
--msgbox "\n Passwords did not match. Try again.\n" 8 50
done
}
AF_PASSWORD=$(ask_pw "the user account") || AF_PASSWORD=""
# ── Disk encryption ───────────────────────────────────────────────────────────
dialog --backtitle "$BACKTITLE" \
@ -154,7 +171,10 @@ dialog --backtitle "$BACKTITLE" \
AF_FIDO2_ROOT="false"
AF_FIDO2_USER="false"
AF_LUKS_PASSWORD=""
if [[ "$AF_ENCRYPT" == "true" ]]; then
# Optional LUKS passphrase so an encrypted install can also run unattended.
AF_LUKS_PASSWORD=$(ask_pw "LUKS disk encryption") || AF_LUKS_PASSWORD=""
dialog --backtitle "$BACKTITLE" \
--title " FIDO2 Root Unlock " \
--yesno "\n Enable FIDO2 hardware key for LUKS root unlock?\n" \
@ -403,6 +423,10 @@ mkdir -p "$(dirname "$OUTPUT")"
printf ' "keymap": %s,\n' "$(json_str "$AF_KEYMAP")"
printf ' "hostname": %s,\n' "$(json_str "$AF_HOSTNAME")"
printf ' "username": %s,\n' "$(json_str "$AF_USERNAME")"
# Passwords are emitted as JSON null when not supplied; the installer treats
# null/absent as "prompt interactively".
printf ' "password": %s,\n' "$(json_str "$AF_PASSWORD")"
printf ' "luks_password": %s,\n' "$(json_str "$AF_LUKS_PASSWORD")"
printf ' "encrypt": %s,\n' "$AF_ENCRYPT"
printf ' "fido2_root": %s,\n' "$AF_FIDO2_ROOT"
printf ' "fido2_user": %s,\n' "$AF_FIDO2_USER"