forked from heavy-duty/rig
docs(users): own the runner-row divergence from #17 — server class keeps root, deliberately, runner included
#17's table said runner 'can close root once an admin user is proven'; the class model (#26) superseded the per-role call, and close-root refuses on class=server — runner's class. The gate does not change: the refusal message now explains itself (server-class machines are automation identities whose management plane IS root SSH; a CI box meant to be administered like a human machine is --class human at bootstrap, not an exception), and the README's identity-model section records the divergence in one paragraph. README also documents the @root seed token and close-root's reachability proofs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d1b6fec5f8
commit
ceceb71e34
3 changed files with 46 additions and 4 deletions
35
README.md
35
README.md
|
|
@ -209,6 +209,15 @@ On `class=server` it stays open — key-only, as bootstrap left it — because
|
||||||
root there is the **automation** identity the control plane (Coolify) SSHes
|
root there is the **automation** identity the control plane (Coolify) SSHes
|
||||||
in as. It is a machine door, never a human one.
|
in as. It is a machine door, never a human one.
|
||||||
|
|
||||||
|
**Where this diverges from #17's original table:** that table let the
|
||||||
|
`runner` role close root ("no Coolify involved"). The class model supersedes
|
||||||
|
the per-role call: runner is `class=server` — an automation identity, not a
|
||||||
|
person's box — and on every server-class machine root SSH is the management
|
||||||
|
plane rig itself converges through, so `close-root` refuses there
|
||||||
|
deliberately, runner included. A CI box you mean to administer like a human
|
||||||
|
machine is `--class human` at bootstrap, not an exception carved out of the
|
||||||
|
gate.
|
||||||
|
|
||||||
**The detection side benefit:** once humans never use root, any root login
|
**The detection side benefit:** once humans never use root, any root login
|
||||||
that is not the control plane is anomalous *by definition* — a cheap,
|
that is not the control plane is anomalous *by definition* — a cheap,
|
||||||
high-signal alert that a shared root identity makes impossible to write.
|
high-signal alert that a shared root identity makes impossible to write.
|
||||||
|
|
@ -520,6 +529,20 @@ refused as a username: this file names operators; root's fate is class policy.
|
||||||
`--file -` reads stdin. A bad file exits 2 with **every** error listed at
|
`--file -` reads stdin. A bad file exits 2 with **every** error listed at
|
||||||
once, before anything changes — one fix cycle, not one round-trip per line.
|
once, before anything changes — one fix cycle, not one round-trip per line.
|
||||||
|
|
||||||
|
**`@root` — seed keys from the door you came in through (#17).** A key field
|
||||||
|
of exactly `@root` means "this user's `authorized_keys` becomes root's
|
||||||
|
CURRENT `/root/.ssh/authorized_keys`". The point is lockout-avoidance: you
|
||||||
|
provably hold a root private key — you SSHed in with it to run apply at all —
|
||||||
|
so the seeded key is the one key rig can *know* opens for you; any pasted
|
||||||
|
literal can be a key you do not hold. `@root` mixes with literal lines
|
||||||
|
(seeded keys land first, literals append after), re-runs re-seed from root's
|
||||||
|
then-current file — convergent to it, so a seeded key you hand-remove from
|
||||||
|
the admin returns until you switch the line to literal keys — and apply dies
|
||||||
|
if root has no `authorized_keys` to seed. Root's key lines are copied
|
||||||
|
verbatim, options included: a `from=`/`command=` restriction follows the key,
|
||||||
|
and on a Coolify-managed box root's file also carries *Coolify's* key — on
|
||||||
|
`class=server`, prefer literal keys.
|
||||||
|
|
||||||
**Public tool, private state, here too.** The users file lives in *your*
|
**Public tool, private state, here too.** The users file lives in *your*
|
||||||
private infra repo and is passed per invocation — rig never persists it. It
|
private infra repo and is passed per invocation — rig never persists it. It
|
||||||
holds nothing secret anyway: usernames, roles, and *public* keys.
|
holds nothing secret anyway: usernames, roles, and *public* keys.
|
||||||
|
|
@ -602,9 +625,15 @@ automation identity and closing it severs fleet management. Then at least one
|
||||||
non-empty `authorized_keys` alone proves a file, not a door: the gate checks
|
non-empty `authorized_keys` alone proves a file, not a door: the gate checks
|
||||||
the `StrictModes` shape (home, `.ssh`, and `authorized_keys` owned by the
|
the `StrictModes` shape (home, `.ssh`, and `authorized_keys` owned by the
|
||||||
user and not group/world-writable), a real login shell, and an unexpired
|
user and not group/world-writable), a real login shell, and an unexpired
|
||||||
account, and its refusal names which check failed, per candidate. It proves
|
account — and then two **reachability** proofs (#17): `sudo -n true` under
|
||||||
the door *should* open, not that it does — which is why the separate-session
|
`runuser` must answer, so NOPASSWD sudo is effective rather than merely
|
||||||
verification below stays load-bearing. Never close the only door.
|
written, and `sshd -T -C user=<admin>` must resolve a per-user effective
|
||||||
|
config that accepts the login (`pubkeyauthentication yes`, no `DenyUsers`
|
||||||
|
hit, `AllowUsers` — if set — names them), so a `Match` block elsewhere cannot
|
||||||
|
quietly exclude the admin while every file looks right. The refusal names
|
||||||
|
which check failed, per candidate. What no local check can prove is that you
|
||||||
|
*hold* the private key — which is why the separate-session verification below
|
||||||
|
stays load-bearing. Never close the only door.
|
||||||
|
|
||||||
Before running it, prove the admin door in a **separate** session — `ssh
|
Before running it, prove the admin door in a **separate** session — `ssh
|
||||||
<admin>@<box>` while this one stays open. Root SSH is being welded shut; the
|
<admin>@<box>` while this one stays open. Root SSH is being welded shut; the
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,14 @@ assert_marker_human() {
|
||||||
*class=server*)
|
*class=server*)
|
||||||
# Root SSH on a server IS the control plane's (Coolify's) automation
|
# Root SSH on a server IS the control plane's (Coolify's) automation
|
||||||
# identity — closing it severs fleet management. No --force exists.
|
# identity — closing it severs fleet management. No --force exists.
|
||||||
printf '%s\n' "class=server: root here is the control plane's automation identity — closing it severs fleet management"
|
# Deliberately per-CLASS, not per-role: #17's original table let the
|
||||||
|
# runner role close root ("no Coolify involved"), but the class model
|
||||||
|
# (#26) supersedes that — every server-class box, runner included, is
|
||||||
|
# an automation identity whose management plane is root SSH, and rig
|
||||||
|
# itself converges through that door. A CI box someone administers
|
||||||
|
# like a human machine is class=human at bootstrap, not an exception
|
||||||
|
# carved out here.
|
||||||
|
printf '%s\n' "class=server: root here is the control plane's automation identity — closing it severs fleet management. Every server-class box (runner included) keeps root deliberately: it is an automation identity, and root SSH is its management plane; a box meant to be administered like a human machine is --class human at bootstrap, not an exception here"
|
||||||
return 1 ;;
|
return 1 ;;
|
||||||
*)
|
*)
|
||||||
printf '%s\n' "marker names no class (${marker}): re-run rig bootstrap; refusing to shut the root door blind"
|
printf '%s\n' "marker names no class (${marker}): re-run rig bootstrap; refusing to shut the root door blind"
|
||||||
|
|
|
||||||
|
|
@ -584,6 +584,12 @@ check "users close-root: absent marker refuses, names bootstrap as the repair" \
|
||||||
1 "no /etc/rig/role marker" marker_gate "$MARKER_DIR/absent"
|
1 "no /etc/rig/role marker" marker_gate "$MARKER_DIR/absent"
|
||||||
check "users close-root: class=server refuses, names the control plane" \
|
check "users close-root: class=server refuses, names the control plane" \
|
||||||
1 "control plane" marker_gate "$MARKER_DIR/server"
|
1 "control plane" marker_gate "$MARKER_DIR/server"
|
||||||
|
# #17's original table let the runner ROLE close root; the class model
|
||||||
|
# supersedes it — runner is class=server, an automation identity, and the
|
||||||
|
# refusal must SAY so or the divergence reads as a bug to anyone holding the
|
||||||
|
# old table.
|
||||||
|
check "users close-root: the server refusal owns the runner row (#17)" \
|
||||||
|
1 "runner included" marker_gate "$MARKER_DIR/server"
|
||||||
check "users close-root: class=human passes the gate" \
|
check "users close-root: class=human passes the gate" \
|
||||||
0 "" marker_gate "$MARKER_DIR/human"
|
0 "" marker_gate "$MARKER_DIR/human"
|
||||||
rm -rf "$MARKER_DIR"
|
rm -rf "$MARKER_DIR"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue