Commit graph

5 commits

Author SHA1 Message Date
dan-claude-bot
a5b48d5b2c fix(close-root): DenyUsers judged fail-closed — patterns and USER@HOST flag
All three reviewers, same substance, and they were right that it was
lockout-adjacent: the literal grep passed a candidate whom a DenyUsers
PATTERN really denies ('DenyUsers dan*' vs admin 'dan'), and the door
closed on a false proof. The judgment now lives in the lib as a pure
deny_verdict: a literal hit flags, and so does ANY pattern or
host-qualified token — a token the check cannot prove irrelevant counts
as a hit, never as a pass. The asymmetry with AllowUsers is now the
same direction on both sides: every error closes toward repair, never
toward a welded-shut door.

Also (claude-bot): the -C probe resolves Match blocks against a
synthetic addr=127.0.0.1, so Match Address is out of the local proof's
scope — named in --help, the README, and the gate's comment, so the
separate-session advisory reads as load-bearing, not ceremony.

Regressions ride the sourced lib: wildcard (the review's dan* case),
'?', USER@HOST, literal hit, irrelevant-literals pass, plus a grep
guard that the shipped gate consults deny_verdict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:02:48 +00:00
dan-claude-bot
d1b6fec5f8 feat(users): close-root proves the door opens, not that it should — sudo -n and per-user sshd -T join the gate
The StrictModes-shaped gate reads files, and files can all look right while
the door stays shut: a sudoers drop-in that never landed, an AllowUsers or
Match block elsewhere in sshd's config. #17 names the two checks that
interrogate behavior instead, and they now run per candidate, additively,
before the drop-in installs: 'runuser -u <admin> -- sudo -n true' (NOPASSWD
sudo answers or it does not — -n never prompts; a missing runuser skips the
proof with a loud warning rather than blocking the door on a missing
prover), and 'sshd -T -C user=<admin>,host=...,addr=...' (the per-user
EFFECTIVE config — pubkeyauthentication yes, no literal DenyUsers hit,
AllowUsers if set must name them; Allow/Deny patterns match literally, fail
closed). The one thing no local check can prove remains possession of the
private key — the separate-session advisory stays load-bearing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 14:53:50 +00:00
Dan Claude Van Damme
c44a645670 fix(users): close-root no-op must prove the door, not the file
The clean-file fast path exited before the sshd -T assertion, so matching
bytes alone bought the 'root already closed' claim. Two ways that lies: an
earlier-sorting drop-in wins the first-wins fight while our file sits
pretty, and a prior run that died between install and restart leaves a
daemon that never read the file — sshd -T can't see that one either, since
it re-parses disk rather than interrogating the running daemon.

Now the no-op is taken only when the bytes match AND systemd says sshd
started strictly after the newest mtime across everything sshd reads (main
config, drop-in dir, drop-ins); anything less restarts behind the same
sshd -t gate, and the effective-config assertion runs on every path before
any success claim. Harness pins both: assert-before-claim ordering and the
daemon-start-vs-config-mtime proof.

Addresses PR #27 review (clean-file fast path convergence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:49:12 +00:00
Claude
3eeab687d0 fix(users): review findings — invoker gate, real SSH revocation, StrictModes-shaped close-root gate, trait-aware box role
Seven review findings on the users family, each with the harness check that
would have caught it:

- Invoker gate (apply + close-root): %rig's sudoers rule is binary-scoped but
  not argument-scoped, so `sudo rig users apply --file <me-as-admin>` made
  role rig silently root-equivalent through the very command that granted it.
  Identity management now refuses any sudo invoker outside rig-admin; direct
  root (bring-up, a root shell) proceeds.

- Offboarding revokes SSH, not just the password: a '!'-locked password is
  not a closed door under UsePAM — Debian sshd still honors the pubkey. A
  dropped user's account is now expired (usermod -L -e 1, the switch PAM
  actually enforces) and authorized_keys is renamed to
  authorized_keys.revoked-by-rig — access revoked, data kept, convergence
  never destroys. Present users get their expiry cleared idempotently, so a
  re-added user comes back to life.

- The ledger remembers: two-field lines ('name active' / 'name revoked',
  legacy bare names read as active), so dropped users no longer vanish from
  rig's memory on the next rewrite. status now reports the ledger state
  corroborated by the account's real expiry — passwd -S read L for everyone
  (apply locks all passwords always), so its locked/active was meaningless —
  and flags a mismatch loudly as drift.

- Perms are part of the converged state: ~/.ssh and authorized_keys ownership
  and mode converge on every run, not only when content changes — StrictModes
  treats them as load-bearing, so drifted perms were a broken login that
  "already converged" lied about. Only the content write stays cmp-guarded.

- close-root's admin-door gate checks the StrictModes shape per candidate —
  ownership, group/world-writability of home/.ssh/authorized_keys, a real
  login shell, an unexpired account — and names which check failed. It proves
  the door SHOULD open, not that it does; the separate-session advisory stays
  load-bearing.

- Usernames are validated in the parser's one-pass refusal matrix
  (^[a-z_][a-z0-9_-]{0,31}$): 'fo|o' corrupted the parser's own '|'-delimited
  stream, and a leading '-' read as a useradd flag mid-convergence.

- The box role is trait-aware: on a host=no box an absent incus group skips
  the role with a warning and converges everything else — one box-role user
  in a fleet-wide file must not abort apply everywhere VMs don't live.
  host=yes still dies pointing at box setup-host; a classless marker warns
  toward a bootstrap re-run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:01:19 +00:00
Claude
2dc396d557 feat(users): close-root — shut the human-class root door once an admin key works
class decides root SSH's fate, and this is human's: install
/etc/ssh/sshd_config.d/00-rig-users.conf (PermitRootLogin no), where the NAME
is the mechanism — sshd_config is first-wins, the Include glob expands
lexically, and '-' sorts before '.', so it is read before bootstrap's
00-rig.conf and wins. Gated three ways, no --force: a marker must exist
(never shut the root door blind), it must say class=human (on a server root
is the control plane's automation identity — closing it severs fleet
management), and some rig-admin member must already hold a non-empty
authorized_keys (never close the only door). The gate's policy lives in the
lib as assert_marker_human so the harness proves every refusal against
fixture markers as non-root; RIG_ROLE_MARKER keeps the command pointable at
the same fixtures. Apply is bootstrap's validate-then-apply shape verbatim —
cmp-guard, sshd -t on the merged config before the restart with rollback,
then the sshd -T effective assertion. Bootstrap's own permitrootlogin
assertion widens to accept 'no': the closed door is strictly harder, never
broken, and by first-wins bootstrap cannot reopen it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:28:25 +00:00