From 8e09ed62f8842cd58e7eaadfa1650606ffcb617c Mon Sep 17 00:00:00 2001 From: Lior Date: Tue, 26 May 2026 19:16:04 -0400 Subject: [PATCH] =?UTF-8?q?fix(B-0835=20Bug=203b):=20replace=20builtins.re?= =?UTF-8?q?adFile=20build-time=20eval=20with=20activation-script=20runtime?= =?UTF-8?q?=20read=20=E2=80=94=20fixes=20operationally-ignored=20custom=20?= =?UTF-8?q?password?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROOT CAUSE: prior implementation read /etc/zeta/initial-hashedpassword via builtins.readFile at NixOS evaluation time (build-time). During nixos-install from live ISO: - Build evaluation happens BEFORE chroot pivot to install target - Path /etc/zeta/initial-hashedpassword refers to LIVE ISO (file absent) NOT install target /mnt/etc/zeta/initial-hashedpassword (file present) - Flake pure-mode refuses builtins.readFile on non-store absolute paths - Build silently falls back to default-hash; operator's custom password is preserved on disk but NEVER applied to user config FIX: use NixOS system.activationScripts to read the file at ACTIVATION TIME (runtime on the installed system, when /etc/zeta/ IS the actual path with the operator's hash). Build-time config uses the default-hash; activation overrides via `usermod -p $hash zeta` if operator-chosen hash file exists. Works for: - Fresh installs from live ISO (activation runs post-pivot; /etc/zeta/initial-hashedpassword present) - Subsequent nixos-rebuilds on installed system (file persists; activation re-applies the operator's hash) - CI eval (file absent; activation skips; default-hash stays) Empirical anchor: operator 2026-05-26 physical hardware-support test: "the password i set it still says password: zeta-change-me" + "the password error is not just display issue it's operational bug the password i set earlier in install is ignored". Per `.claude/rules/methodology-hard-limits.md` + B-0794 homelab-mode: - NO secret material in module source (only the public default-fallback) - NO secret printed in activation log (only "applied" or "skipped" status) - Hash file at /etc/zeta/initial-hashedpassword is chmod 0600 root:root (written that way by zeta-install.sh Step 6.55) - usermod -p directly writes to /etc/shadow (root-only readable) Co-Authored-By: Claude Opus 4.7 --- .../nixos/modules/initial-password.nix | 102 +++++++++++------- 1 file changed, 62 insertions(+), 40 deletions(-) diff --git a/full-ai-cluster/nixos/modules/initial-password.nix b/full-ai-cluster/nixos/modules/initial-password.nix index 407f871b42..ae696a09c2 100644 --- a/full-ai-cluster/nixos/modules/initial-password.nix +++ b/full-ai-cluster/nixos/modules/initial-password.nix @@ -2,35 +2,44 @@ # # Initial password substrate for the `zeta` user on fresh installs. # -# iter-5.3 (B-0792 follow-on; the maintainer 2026-05-26 "also on +# iter-5.3 (B-0792 follow-on; the operator 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. -# -# Operator UX (one TYPED prompt at install time; can't avoid for -# password since secrets shouldn't transit non-operator surfaces): -# -# 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 -# -# Operator can still rotate later via `passwd zeta` if they want -# to change it again. -# -# 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. +# → /mnt/etc/zeta/initial-hashedpassword). +# +# B-0835 Bug 3b FIX (2026-05-26): the prior implementation read the +# file via `builtins.readFile` at NixOS EVALUATION TIME (build-time). +# That fails operationally because: +# +# 1. During `nixos-install` from live ISO: the build evaluation +# happens BEFORE the chroot pivot; the path `/etc/zeta/initial- +# hashedpassword` refers to the LIVE ISO (file not there), NOT +# the install target's `/mnt/etc/zeta/initial-hashedpassword` +# 2. Flake pure-mode (default for `nixos-install --flake`) refuses +# to read non-store absolute paths via `builtins.readFile` +# +# Result: build silently fell back to the default-hash, custom +# password was IGNORED on the installed system. Operator's empirical +# anchor 2026-05-26: "the password i set it still says password: +# zeta-change-me" + "the password error is not just display issue +# it's operational bug the password i set earlier in install is +# ignored". +# +# FIX: use a NixOS activation script that reads the file at +# ACTIVATION TIME (runtime on the installed system, when /etc/zeta/ +# IS the actual path with the operator's hash). The build-time +# config uses the default-hash; the activation script overrides it +# via `usermod -p $hash zeta` if the operator-chosen hash file +# exists. This works for: +# +# - Fresh installs from live ISO (activation runs on installed +# system after pivot; /etc/zeta/initial-hashedpassword present) +# - Subsequent nixos-rebuilds (file persists; activation re-applies) +# - CI eval (file absent; activation skips; default-hash stays) +# +# BACKWARD-COMPAT: same fallback hash applies when the file is +# absent. Operator can rotate via `passwd zeta` post-install. # # Hash format: sha512crypt ($6$...). zeta-install.sh generates via # mkpasswd from the nixpkgs `mkpasswd` package. @@ -38,23 +47,36 @@ { 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). + # "zeta-change-me"). Build-time default; activation overrides + # if /etc/zeta/initial-hashedpassword exists at activation time. fallbackHash = "$6$wMTsqITU4II043Y8$DBR58Hhh.d975YkA40kwYNxQAunevJ9Cu9rYYigi9YjBYVEjlNrs.rk4hu.332sh6GkQuCb7yyLYr7lPTxySD1"; + + hashFile = "/etc/zeta/initial-hashedpassword"; in { - users.users.zeta.hashedPassword = - if injectedHash != null then injectedHash else fallbackHash; + # Build-time default; will be overridden at activation if the + # operator-chosen hash file is present on the installed system. + users.users.zeta.hashedPassword = fallbackHash; + + # B-0835 Bug 3b fix — runtime password injection via activation + # script. Runs after users.users.* but before login services; the + # `usermod -p` call directly updates /etc/shadow. + system.activationScripts.zetaInitialPassword = { + deps = [ "users" ]; + text = '' + if [ -f "${hashFile}" ]; then + hash=$(${pkgs.coreutils}/bin/cat "${hashFile}" | ${pkgs.coreutils}/bin/tr -d '\n') + if [ -n "$hash" ] && [ "''${hash:0:3}" = '$6$' ]; then + ${pkgs.shadow}/bin/usermod -p "$hash" zeta + echo "[iter-5.3 / B-0835 Bug 3b fix] applied operator-chosen password hash from ${hashFile}" + else + echo "[iter-5.3 / B-0835 Bug 3b fix] WARN: ${hashFile} present but content is not a sha512crypt hash; default stays" + fi + else + echo "[iter-5.3 / B-0835 Bug 3b fix] ${hashFile} absent; default fallback hash stays in effect (rotate via 'passwd zeta')" + fi + ''; + }; }