diff --git a/full-ai-cluster/nixos/modules/initial-password.nix b/full-ai-cluster/nixos/modules/initial-password.nix index f7d4bf5050..407f871b42 100644 --- a/full-ai-cluster/nixos/modules/initial-password.nix +++ b/full-ai-cluster/nixos/modules/initial-password.nix @@ -1,35 +1,60 @@ # full-ai-cluster/nixos/modules/initial-password.nix # # Initial password substrate for the `zeta` user on fresh installs. -# Per `.claude/rules/human-audit-and-legal-risk-acceptance-pattern-in-settings.md` -# Shape A: hashedPassword baked into per-host Nix module + operator -# rotates on first login. Composes with the Touch ID + biometric -# substrate (full-ai-cluster/tools/zflash-setup.ts) for the operator's -# Mac side; this is the cluster-node side. # -# THE INITIAL PASSWORD IS: zeta-change-me +# iter-5.3 (B-0792 follow-on; the maintainer 2026-05-26 "also on +# startup can it ask for me to type a password instead of having a +# default"): the operator-chosen password set at install-time via +# zeta-install.sh's prompt-password step (read -s + mkpasswd → hash +# → /mnt/etc/zeta/initial-hashedpassword). This module reads that +# file via builtins.readFile at NixOS evaluation time + uses it +# for users.users.zeta.hashedPassword. # -# zeta-install.sh prints this in big letters to the console + writes -# zeta-initial-credentials.txt back to the boot USB before the 10s -# auto-reboot so the operator can read it after pulling the USB. +# Operator UX (one TYPED prompt at install time; can't avoid for +# password since secrets shouldn't transit non-operator surfaces): # -# Operator MUST rotate on first login: +# zeta-install.sh: +# [iter-5.3] Set initial password for the `zeta` user: +# (will be required for console login; not for SSH +# if iter-4.2 pubkey was injected) +# Password: ******** +# Confirm: ******** +# [iter-5.3] wrote hash to /mnt/etc/zeta/initial-hashedpassword # -# passwd zeta +# Operator can still rotate later via `passwd zeta` if they want +# to change it again. # -# Hash format: sha512crypt ($6$...). Generated via: -# openssl passwd -6 'zeta-change-me' +# BACKWARD-COMPAT FALLBACK: if /etc/zeta/initial-hashedpassword +# does NOT exist (e.g., during nixos-rebuild on an already-installed +# system where the file was never written, OR during `nix flake +# check` in CI where the file path is meaningless), fall back to +# the documented iter-4.x default hash of `zeta-change-me` so the +# module still evaluates + the system still has a known-default +# credential. Operator should rotate immediately in that case. # -# Per simplest-first (per B-0786 memory): sha512crypt is the -# universally-portable shape; promote to yescrypt or agenix/sops-nix -# when the simple shape demonstrably can't meet a real requirement. -# Iter-4 v1 ships sha512crypt; iter-5+ may promote to a stronger -# secret-management substrate when (a) repo goes public OR -# (b) multi-operator key isolation becomes load-bearing. +# Hash format: sha512crypt ($6$...). zeta-install.sh generates via +# mkpasswd from the nixpkgs `mkpasswd` package. { config, pkgs, lib, ... }: +let + hashFile = "/etc/zeta/initial-hashedpassword"; + injectedHash = + if builtins.pathExists hashFile + then + let + raw = builtins.readFile hashFile; + trimmed = lib.removeSuffix "\n" raw; + in + if lib.hasPrefix "$6$" trimmed then trimmed else null + else null; + # iter-4 v1 backward-compat fallback hash (= sha512crypt of + # "zeta-change-me"). Used when the operator-chosen hash isn't + # present (e.g., CI eval, nixos-rebuild without prior install). + fallbackHash = + "$6$wMTsqITU4II043Y8$DBR58Hhh.d975YkA40kwYNxQAunevJ9Cu9rYYigi9YjBYVEjlNrs.rk4hu.332sh6GkQuCb7yyLYr7lPTxySD1"; +in { users.users.zeta.hashedPassword = - "$6$wMTsqITU4II043Y8$DBR58Hhh.d975YkA40kwYNxQAunevJ9Cu9rYYigi9YjBYVEjlNrs.rk4hu.332sh6GkQuCb7yyLYr7lPTxySD1"; + if injectedHash != null then injectedHash else fallbackHash; } diff --git a/full-ai-cluster/usb-nixos-installer/nixos/installer/configuration.nix b/full-ai-cluster/usb-nixos-installer/nixos/installer/configuration.nix index 4b0b5bd39b..c834303dbc 100644 --- a/full-ai-cluster/usb-nixos-installer/nixos/installer/configuration.nix +++ b/full-ai-cluster/usb-nixos-installer/nixos/installer/configuration.nix @@ -153,6 +153,12 @@ nixos-install-tools nix-output-monitor nvd + + # iter-5.3 prompt-password substrate (zeta-install.sh Step 6.55): + # mkpasswd is provided by the `mkpasswd` nixpkgs package; needed + # at install-time to hash the operator-typed password before + # writing to /mnt/etc/zeta/initial-hashedpassword. + mkpasswd nh # Declarative disk partitioning — used by the cookie-cutter # disko-shapes/ modules. Pre-staged on the ISO so installs diff --git a/full-ai-cluster/usb-nixos-installer/zeta-install.sh b/full-ai-cluster/usb-nixos-installer/zeta-install.sh index 7f8b134d1a..43e025ab5e 100755 --- a/full-ai-cluster/usb-nixos-installer/zeta-install.sh +++ b/full-ai-cluster/usb-nixos-installer/zeta-install.sh @@ -348,6 +348,74 @@ else echo "==============================" fi +# ── Step 6.55: iter-5.3 prompt-for-initial-password (B-0792) ──── +# +# Per the maintainer 2026-05-26: "also on startup can it ask for +# me to type a password instead of having a default" — replaces +# the iter-4.x hardcoded `zeta-change-me` default with an +# operator-chosen password set at install time. +# +# Operator types password ONCE on cluster console (read -s; hidden); +# script hashes via mkpasswd ($6$ = sha512crypt); writes hash to +# /mnt/etc/zeta/initial-hashedpassword. The +# nixos/modules/initial-password.nix module reads that file via +# builtins.readFile at NixOS evaluation time + sets +# users.users.zeta.hashedPassword. +# +# Fallback: if operator presses Enter to skip (no password typed), +# the module's BACKWARD-COMPAT fallback hash (= sha512crypt of +# "zeta-change-me") stays in effect so the system still boots +# with a known credential. +# +# Why type-on-console (one exception to typing-avoidance discipline): +# secrets shouldn't transit non-operator surfaces (USB ESP, Aaron's +# Mac keychain, etc.); operator-typed at install time is the +# safest path. This composes with the wifi nmtui exception in +# zeta-first-boot.sh — both are operator-typed-once-on-cluster. +echo +echo "[iter-5.3] ── prompt for initial password (instead of default) ──" +echo "[iter-5.3] Set initial password for the 'zeta' user (used for" +echo "[iter-5.3] console login; SSH uses the iter-4.2-injected pubkey)." +echo "[iter-5.3] Operator can rotate later via 'passwd zeta' on the" +echo "[iter-5.3] installed system. Press Enter to skip + keep the" +echo "[iter-5.3] iter-4.x default ('zeta-change-me')." +echo +INJECTED_PW="" +INJECTED_PW_CONFIRM="" +# -s = silent (hidden); -p = inline prompt +read -r -s -p "[iter-5.3] Password (or Enter to skip): " INJECTED_PW +echo +if [ -n "$INJECTED_PW" ]; then + read -r -s -p "[iter-5.3] Confirm: " INJECTED_PW_CONFIRM + echo + if [ "$INJECTED_PW" != "$INJECTED_PW_CONFIRM" ]; then + echo "[iter-5.3] WARN: passwords don't match; skipping (keeps default)" + INJECTED_PW="" + fi +fi +if [ -n "$INJECTED_PW" ]; then + # mkpasswd from nixpkgs `mkpasswd` package. -m sha-512 selects + # sha512crypt; -s reads password from stdin (avoids exposing it + # in argv via ps). + INJECTED_HASH=$(echo "$INJECTED_PW" | mkpasswd -m sha-512 -s 2>/dev/null || echo "") + unset INJECTED_PW + unset INJECTED_PW_CONFIRM + if [ -n "$INJECTED_HASH" ] && echo "$INJECTED_HASH" | grep -Eq '^\$6\$'; then + sudo mkdir -p /mnt/etc/zeta + echo "$INJECTED_HASH" | sudo tee /mnt/etc/zeta/initial-hashedpassword >/dev/null + sudo chmod 0600 /mnt/etc/zeta/initial-hashedpassword + sudo chown root:root /mnt/etc/zeta/initial-hashedpassword + echo "[iter-5.3] operator-chosen password hash written + chmod 0600" + unset INJECTED_HASH + else + echo "[iter-5.3] WARN: mkpasswd produced invalid hash; falling back to default" + fi +else + echo "[iter-5.3] no password entered; iter-4.x default 'zeta-change-me' stays" + echo "[iter-5.3] in effect (rotate via 'passwd zeta' after first SSH login)" +fi +echo + # ── Step 6.6: iter-5.2 hostname injection (B-0792) ────────────── # # Per the maintainer 2026-05-26: "since our different roles are