feat(secrets): decrypt the login password from sops (task 0010) #3

Merged
alexion merged 1 commits from task-0010-sops-skeleton-and-password into main 2026-07-19 23:44:17 -04:00
Owner

Implements .claude/tasks/0010-sops-skeleton-and-password.md.

Wires sops-nix into the shared base config as unconditional plumbing, with the two-tier age identity model the
spec calls for: an admin identity held only in the operator's password manager, and a neogaia identity genera
ted on the machine onto its LUKS-encrypted root. Both sshKeyPaths defaults are cleared, since sops-nix otherwi
se falls back to the SSH host keys — the exact coupling the spec forbids, and what keeps those keys free to beco
me secrets in 0011.

The primary user's password hash now lives in secrets/shared.yaml, encrypted to admin plus neogaia, consumed
through hashedPasswordFile and marked neededForUsers so it decrypts before accounts are created.

Deviation

users.mutableUsers = false was needed and is not in the plan. NixOS applies a declared hash to an account that
already exists in /etc/shadow only when that flag is false, so at the default the hand-set password would hav
e been kept and this change would have been silently inert — the manual acceptance criterion would have passed w
hile proving nothing.

Consequences: passwd no longer works (rotation is re-mkpasswd, re-encrypt, rebuild), and root is now locked
(!), which blocks direct root login and the sulogin emergency prompt. sudo from wheel is unaffected, and g
eneration rollback or init=/bin/sh remains available. Worth folding back into the parent spec before the serve
rs exist.

Verification

  • nix flake check builds the neogaia toplevel.
  • Negative cases exercised rather than assumed: a mistyped secret name fails with the key 'alexion-passwrd' can not be found; a missing secrets file fails with Path 'secrets/absent.yaml' does not exist in Git repository.
  • nixos-rebuild switch succeeded, /run/secrets-for-users/alexion-password materialized, /etc/shadow carrie
    s the decrypted hash, and console login against it was confirmed by the operator.

Review

Overall: HIGH

  • Blast radius: High — system/default.nix is the shared base, so every future host inherits mutableUsers = fa lse and a file-backed password; one without an identity gets an unusable account.
  • Reversibility: Medium — the Nix side rolls back by generation, but live state already mutated (root locked; re
    verting the option does not unlock it).
  • Test coverage: High — nix flake check only proves evaluation; decryption and login are manual by nature.
  • Sensitive domain: High — login credentials, key material, sudo reachability, and a committed ciphertext whose
    recipient list is the access-control model.
  • Size & complexity: Low — ~25 added lines, no branching.
  • Runtime criticality: High — authentication at boot; a decryption failure yields a machine no one can log into.

All four Standards findings were fixed in the diff (over-long .sops.yaml header; a comment justifying against
an alternative and describing future state; an inaccurate mutableUsers comment; the secret name constructed at
two sites, now a single passwordSecret binding). The fixes were comment-only plus a let binding — the rebui
lt derivation hash was identical, confirming no behavioural change.

Unaddressed:

  • Spec — users.mutableUsers = false is undeclared scope. Kept deliberately: it is load-bearing, and revert
    ing it makes the feature inert. Recording it in the spec is follow-up work, not a code change.
  • Risk — no SSH fallback. services.oogaia with no declared authorizedKeys,
    so SSH cannot rescue a console lockout. O 0011, which touches the sshd configurat
    ion anyway.
Implements `.claude/tasks/0010-sops-skeleton-and-password.md`. Wires `sops-nix` into the shared base config as unconditional plumbing, with the two-tier age identity model the spec calls for: an admin identity held only in the operator's password manager, and a `neogaia` identity genera ted on the machine onto its LUKS-encrypted root. Both `sshKeyPaths` defaults are cleared, since sops-nix otherwi se falls back to the SSH host keys — the exact coupling the spec forbids, and what keeps those keys free to beco me secrets in 0011. The primary user's password hash now lives in `secrets/shared.yaml`, encrypted to admin plus `neogaia`, consumed through `hashedPasswordFile` and marked `neededForUsers` so it decrypts before accounts are created. ## Deviation `users.mutableUsers = false` was needed and is not in the plan. NixOS applies a declared hash to an account that already exists in `/etc/shadow` only when that flag is false, so at the default the hand-set password would hav e been kept and this change would have been silently inert — the manual acceptance criterion would have passed w hile proving nothing. Consequences: `passwd` no longer works (rotation is re-`mkpasswd`, re-encrypt, rebuild), and root is now locked (`!`), which blocks direct root login and the `sulogin` emergency prompt. `sudo` from wheel is unaffected, and g eneration rollback or `init=/bin/sh` remains available. Worth folding back into the parent spec before the serve rs exist. ## Verification - `nix flake check` builds the `neogaia` toplevel. - Negative cases exercised rather than assumed: a mistyped secret name fails with `the key 'alexion-passwrd' can not be found`; a missing secrets file fails with `Path 'secrets/absent.yaml' does not exist in Git repository`. - `nixos-rebuild switch` succeeded, `/run/secrets-for-users/alexion-password` materialized, `/etc/shadow` carrie s the decrypted hash, and console login against it was confirmed by the operator. ## Review **Overall: HIGH** - Blast radius: High — `system/default.nix` is the shared base, so every future host inherits `mutableUsers = fa lse` and a file-backed password; one without an identity gets an unusable account. - Reversibility: Medium — the Nix side rolls back by generation, but live state already mutated (root locked; re verting the option does not unlock it). - Test coverage: High — `nix flake check` only proves evaluation; decryption and login are manual by nature. - Sensitive domain: High — login credentials, key material, sudo reachability, and a committed ciphertext whose recipient list is the access-control model. - Size & complexity: Low — ~25 added lines, no branching. - Runtime criticality: High — authentication at boot; a decryption failure yields a machine no one can log into. All four Standards findings were fixed in the diff (over-long `.sops.yaml` header; a comment justifying against an alternative and describing future state; an inaccurate `mutableUsers` comment; the secret name constructed at two sites, now a single `passwordSecret` binding). The fixes were comment-only plus a `let` binding — the rebui lt derivation hash was identical, confirming no behavioural change. Unaddressed: - **Spec — `users.mutableUsers = false` is undeclared scope.** Kept deliberately: it is load-bearing, and revert ing it makes the feature inert. Recording it in the spec is follow-up work, not a code change. - **Risk — no SSH fallback.** `services.oogaia` with no declared `authorizedKeys`, so SSH cannot rescue a console lockout. O 0011, which touches the `sshd` configurat ion anyway.
alexion added 1 commit 2026-07-19 23:43:29 -04:00
Wire sops-nix into the shared base config as unconditional plumbing, with
a two-tier age identity model: an admin identity held outside the repo, and
a per-host identity generated on the machine and kept on its encrypted root.
Both `sshKeyPaths` defaults are cleared so the SSH host keys stay out of the
decryption path and remain free to become secrets in their own right.

The primary user's password hash moves into a shared secrets file encrypted
to admin plus neogaia, consumed through `hashedPasswordFile` and decrypted
before accounts are created.

This needs `users.mutableUsers = false`: NixOS applies a declared hash to an
already-existing account only when that flag is false, so at the default the
hand-set password would have been kept and the change would have been inert.
Root consequently has no password and is locked; sudo from wheel is the way
in, and generation rollback remains the recovery path.
alexion force-pushed task-0010-sops-skeleton-and-password from df5bfbbd3d to ce103a7353 2026-07-19 23:43:29 -04:00 Compare
alexion merged commit ce103a7353 into main 2026-07-19 23:44:17 -04:00
alexion deleted branch task-0010-sops-skeleton-and-password 2026-07-19 23:44:17 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: alexion/dotfiles#3