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. # all prompts are answered from it. Missing fields fall back to interactive prompts.
# #
# Answerfile fields: drive, kernel, keymap, hostname, username, encrypt, fido2_root, # 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; # -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. # -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 read -rp "Enable FIDO2 authentication for user login? (YES/NO): " FIDO_USER
fi fi
# Password always prompted — never stored in answerfile # User password: use the answerfile's "password" field when present (enables a
read -rp "Enter password for $USERNAME: " USERPASS # 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; } [[ -z "$USERPASS" ]] && { echo "Error: password cannot be empty."; exit 1; }
# In interactive mode, decide now whether to run the dotfiles TUI inside chroot. # 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 if [[ "$ENCRYPT_DISK" == "YES" ]]; then
echo "Encrypting root partition..." 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). # --type luks2 is required for systemd-cryptenroll (FIDO2 token enrollment).
# -v (verbose) shows progress; prompts for the primary passphrase interactively. if [[ -n "$LUKS_PASS" ]]; then
cryptsetup -v luksFormat "$ROOT_PART" --type luks2 echo "LUKS passphrase: taken from answerfile."
# Open the container and expose it as /dev/mapper/cryptroot for formatting. # --batch-mode skips the interactive "Are you sure" + passphrase prompts;
cryptsetup open "$ROOT_PART" cryptroot # --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 ────────────────────────────────────────── # ── Auto-generate backup LUKS key ──────────────────────────────────────────
# A random key is enrolled as a second LUKS slot so recovery is possible # 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. # 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" dd if=/dev/urandom bs=64 count=1 2>/dev/null | base64 -w0 > "$LUKS_BACKUP_KEY"
echo "Enrolling auto-generated backup LUKS key..." echo "Enrolling auto-generated backup LUKS key..."
# luksAddKey reads the existing passphrase interactively to authorise adding # luksAddKey needs the existing passphrase to authorise adding the new key file
# the new key file into a free LUKS slot. # into a free LUKS slot. With an answerfile passphrase we pipe it in on stdin;
cryptsetup luksAddKey "$ROOT_PART" "$LUKS_BACKUP_KEY" # 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 ───────────────────────────────────────────── # ── Optional FIDO2 enrollment ─────────────────────────────────────────────
if [[ "$FIDO_ROOT" == "YES" ]]; then if [[ "$FIDO_ROOT" == "YES" ]]; then

View File

@ -140,11 +140,28 @@ AF_USERNAME=$(dialog --backtitle "$BACKTITLE" \
9 54 "" \ 9 54 "" \
3>&1 1>&2 2>&3) || AF_USERNAME="" 3>&1 1>&2 2>&3) || AF_USERNAME=""
# NOTE: passwords are intentionally NOT stored in the answerfile. # ── User password (optional) ──────────────────────────────────────────────────
dialog --backtitle "$BACKTITLE" \ # Storing the password makes the install fully unattended, but it is saved as
--title " Password " \ # plain text in the answerfile (and world-readable once embedded in an ISO). If
--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" \ # the user declines, the field is left empty and the installer prompts at runtime.
10 56 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 ─────────────────────────────────────────────────────────── # ── Disk encryption ───────────────────────────────────────────────────────────
dialog --backtitle "$BACKTITLE" \ dialog --backtitle "$BACKTITLE" \
@ -154,7 +171,10 @@ dialog --backtitle "$BACKTITLE" \
AF_FIDO2_ROOT="false" AF_FIDO2_ROOT="false"
AF_FIDO2_USER="false" AF_FIDO2_USER="false"
AF_LUKS_PASSWORD=""
if [[ "$AF_ENCRYPT" == "true" ]]; then 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" \ dialog --backtitle "$BACKTITLE" \
--title " FIDO2 Root Unlock " \ --title " FIDO2 Root Unlock " \
--yesno "\n Enable FIDO2 hardware key for LUKS root unlock?\n" \ --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 ' "keymap": %s,\n' "$(json_str "$AF_KEYMAP")"
printf ' "hostname": %s,\n' "$(json_str "$AF_HOSTNAME")" printf ' "hostname": %s,\n' "$(json_str "$AF_HOSTNAME")"
printf ' "username": %s,\n' "$(json_str "$AF_USERNAME")" 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 ' "encrypt": %s,\n' "$AF_ENCRYPT"
printf ' "fido2_root": %s,\n' "$AF_FIDO2_ROOT" printf ' "fido2_root": %s,\n' "$AF_FIDO2_ROOT"
printf ' "fido2_user": %s,\n' "$AF_FIDO2_USER" printf ' "fido2_user": %s,\n' "$AF_FIDO2_USER"