Skip to content

fix(B-0835 Bug 3b): password activation-script — fixes operationally-ignored custom password (timing/path-mismatch root cause)#5351

Merged
AceHack merged 1 commit into
mainfrom
otto/b-0835-bug-3b-password-activation-script-fix-timing-mismatch-2026-05-26
May 26, 2026
Merged

fix(B-0835 Bug 3b): password activation-script — fixes operationally-ignored custom password (timing/path-mismatch root cause)#5351
AceHack merged 1 commit into
mainfrom
otto/b-0835-bug-3b-password-activation-script-fix-timing-mismatch-2026-05-26

Conversation

@AceHack
Copy link
Copy Markdown
Member

@AceHack AceHack commented May 26, 2026

Summary

Fixes B-0835 Bug 3b — the custom password the operator set during install was operationally ignored because of a build-time-eval vs install-time-write path mismatch.

Root cause

Prior implementation used `builtins.readFile` at NixOS evaluation time:

Step Where Path Result
zeta-install.sh writes hash Live ISO → install target /mnt/etc/zeta/initial-hashedpassword File written ✓
nixos-install evaluates flake Live ISO build-time eval Reads /etc/zeta/initial-hashedpassword File absent + pure-mode refuses
Module falls back to default initial-password.nix fallbackHash Default applied
Installed system boots Real hardware File at /etc/zeta/initial-hashedpassword Present but user config built with default

Fix

Replace `builtins.readFile` with `system.activationScripts.zetaInitialPassword` that reads at activation time (runtime on installed system):

```nix
system.activationScripts.zetaInitialPassword = {
deps = [ "users" ];
text = ''
if [ -f "${hashFile}" ]; then
hash=$(cat "${hashFile}" | tr -d '\n')
if [ -n "$hash" ] && [ "${hash:0:3}" = '$6$' ]; then
usermod -p "$hash" zeta
fi
fi
'';
};
```

Works for 3 scenarios

Scenario Behavior
Fresh install from live ISO Activation runs post-pivot; file present at /etc/zeta/; operator hash applied
Subsequent nixos-rebuilds File persists; activation re-applies
CI eval File absent; activation skips; default-hash stays

Security properties preserved

  • NO secret material in module source (only public default-fallback)
  • NO secret printed in activation log (only "applied" or "skipped" status)
  • Hash file at /etc/zeta/initial-hashedpassword chmod 0600 root:root (per zeta-install.sh Step 6.55)
  • usermod -p directly writes /etc/shadow (root-only readable)

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".

Test plan

  • Nix syntax valid (`nix-instantiate --parse`)
  • No secrets in module source
  • Activation script idempotent (re-applies same hash on each rebuild)
  • Skip-with-message when file absent (graceful CI eval)

🤖 Generated with Claude Code

…tivation-script runtime read — fixes operationally-ignored custom password

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 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 26, 2026 23:16
@AceHack AceHack enabled auto-merge (squash) May 26, 2026 23:16
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes B-0835 Bug 3b in the NixOS install flow where an operator-provided password hash was ignored due to evaluation-time file reads pointing at the wrong root (live ISO vs install target) and/or being blocked in pure evaluation.

Changes:

  • Removes evaluation-time builtins.readFile/builtins.pathExists password-hash injection logic.
  • Sets a build-time fallback hash for users.users.zeta.hashedPassword and adds an activation-time script that applies /etc/zeta/initial-hashedpassword (when present) via usermod -p.
  • Updates module commentary to document the root cause and the activation-time fix behavior across install/rebuild/CI scenarios.

@AceHack AceHack merged commit e00d322 into main May 26, 2026
30 checks passed
@AceHack AceHack deleted the otto/b-0835-bug-3b-password-activation-script-fix-timing-mismatch-2026-05-26 branch May 26, 2026 23:19
AceHack pushed a commit that referenced this pull request May 26, 2026
…d files (cluster-node-id + operator-authorized-keys) + log/comment accuracy

4 legitimate Copilot findings on prior #5354 commit, all real:

1. **Trap-based cleanup**: prior cleanup only fired on success path. If
   nixos-install fails OR Ctrl-C, /etc/zeta/cluster-node-id symlink
   would persist + dangle when /mnt is unmounted. FIX: trap EXIT
   handler runs cleanup on ALL exit paths (success/failure/signal).
   Defense-in-depth via explicit cleanup at end too.

2. **Misleading log message**: prior "symlinking $X → /etc/zeta/..."
   printed even when no symlink was actually created. FIX: move log
   inside the maybe_symlink helper so it only prints on actual creation.

3. **Comment vs code mismatch**: prior comment said "Symlinking
   /mnt/etc/zeta → /etc/zeta" (directory-level) but code only handled
   the single cluster-node-id file. FIX: rewrote comment to match
   per-file approach + named all affected modules.

4. **Safety note wrong about operator-authorized-keys.nix**: prior
   note claimed "initial-password.nix doesn't use builtins.readFile"
   but didn't acknowledge that operator-authorized-keys.nix DOES use
   builtins.readFile on /etc/zeta/operator-authorized-keys. With
   --impure now active, that module ALSO needs the symlink-or-it-
   silently-loses-iter-5.4.0-pubkeys. FIX: extended the symlink
   approach to operator-authorized-keys too + updated safety note
   to correctly distinguish all 3 modules:
   - injected-hostname.nix     → symlinked (Bug 1 fix)
   - operator-authorized-keys.nix → symlinked (sibling-bug-class)
   - initial-password.nix       → activation-script (Bug 3b fix)

Helper function maybe_symlink() centralizes the "create only if file
exists AND destination doesn't" logic; trap handler removes only
files this script created.

Composes with PR #5351 (Bug 3b activation-script) — together the two
PRs fix all 3 instances of the build-time-eval-vs-install-time-write
path-mismatch bug class.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AceHack added a commit that referenced this pull request May 26, 2026
…val reads cluster-node-id (same bug class as Bug 3b) (#5354)

* fix(B-0835 Bug 1): hostname injection — symlink /mnt/etc/zeta → /etc/zeta + --impure so flake eval reads cluster-node-id

ROOT CAUSE: same bug class as Bug 3b (password). injected-hostname.nix
reads /etc/zeta/cluster-node-id via builtins.pathExists +
builtins.readFile at NixOS evaluation time (flake build-time).

During nixos-install from live ISO:
- zeta-install.sh Step 6.6 writes /mnt/etc/zeta/cluster-node-id ✓
- Flake eval reads /etc/zeta/cluster-node-id (LIVE ISO context; absent)
- Module falls through to flake's hardcoded networking.hostName
- Operator gets flake-default hostname (e.g., "control-plane") instead
  of unique node-<6hex> that iter-5.2.2 generated

FIX (different from Bug 3b's activation-script approach because
hostname CANNOT cleanly change at activation — many services bake
hostname at build time):

1. Symlink /mnt/etc/zeta/cluster-node-id → /etc/zeta/cluster-node-id
   BEFORE nixos-install runs. Makes the file visible at the path
   injected-hostname.nix expects during flake eval phase.
2. Add --impure flag to nixos-install so flake pure-mode allows
   builtins.pathExists + builtins.readFile on the non-store path.
3. Cleanup the symlink AFTER nixos-install (no dangling reference
   if /mnt is unmounted before reboot).

Subsequent rebuilds on the installed system work without the symlink
because /etc/zeta/cluster-node-id IS on the installed root filesystem
(written by the install).

Empirical anchor: operator 2026-05-26 physical hardware-support test
showed "control-plane login:" instead of unique node-<6hex>.

Safety:
- Only impure read is operator-chosen hostname (not a secret)
- Other modules (initial-password.nix per Bug 3b fix) use
  activation-scripts so they don't need --impure
- Symlink-then-cleanup is idempotent + reversible

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(PR-5354): Copilot 4 findings — trap-cleanup + symlink ALL affected files (cluster-node-id + operator-authorized-keys) + log/comment accuracy

4 legitimate Copilot findings on prior #5354 commit, all real:

1. **Trap-based cleanup**: prior cleanup only fired on success path. If
   nixos-install fails OR Ctrl-C, /etc/zeta/cluster-node-id symlink
   would persist + dangle when /mnt is unmounted. FIX: trap EXIT
   handler runs cleanup on ALL exit paths (success/failure/signal).
   Defense-in-depth via explicit cleanup at end too.

2. **Misleading log message**: prior "symlinking $X → /etc/zeta/..."
   printed even when no symlink was actually created. FIX: move log
   inside the maybe_symlink helper so it only prints on actual creation.

3. **Comment vs code mismatch**: prior comment said "Symlinking
   /mnt/etc/zeta → /etc/zeta" (directory-level) but code only handled
   the single cluster-node-id file. FIX: rewrote comment to match
   per-file approach + named all affected modules.

4. **Safety note wrong about operator-authorized-keys.nix**: prior
   note claimed "initial-password.nix doesn't use builtins.readFile"
   but didn't acknowledge that operator-authorized-keys.nix DOES use
   builtins.readFile on /etc/zeta/operator-authorized-keys. With
   --impure now active, that module ALSO needs the symlink-or-it-
   silently-loses-iter-5.4.0-pubkeys. FIX: extended the symlink
   approach to operator-authorized-keys too + updated safety note
   to correctly distinguish all 3 modules:
   - injected-hostname.nix     → symlinked (Bug 1 fix)
   - operator-authorized-keys.nix → symlinked (sibling-bug-class)
   - initial-password.nix       → activation-script (Bug 3b fix)

Helper function maybe_symlink() centralizes the "create only if file
exists AND destination doesn't" logic; trap handler removes only
files this script created.

Composes with PR #5351 (Bug 3b activation-script) — together the two
PRs fix all 3 instances of the build-time-eval-vs-install-time-write
path-mismatch bug class.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Lior <lior@zeta.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants