users apply cannot tell 'remove everyone' from 'I truncated the file' #65

Closed
opened 2026-07-19 18:58:29 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-19 18:58:29 +00:00 (Migrated from github.com)

rig users apply cannot tell "remove everyone" from "I truncated the file"

The situation

rig users apply <file> converges a box to the users file exactly: users in the
file are created and converged, users not in it are revoked — account
expired, authorized_keys renamed to authorized_keys.revoked-by-rig, home and
uid kept for attribution.

That is correct and deliberate. Removing a name from the file is how you
off-board someone.

But it means an empty file is a valid instruction to revoke every operator on
the box
— and it is indistinguishable from an empty file produced by accident.
A stray > in a shell, a failed generation step in a private infra repo, a
truncated copy: all produce a file that says, in rig's language, "nobody should
have access to this machine."

Why the current safeguards do not cover it

Per-user warnings arrive after the decision and scale wrong. apply does warn
as it revokes each user. But twenty operators means twenty lines of scrollback,
which reads as noise rather than as "you are about to lock every human out of
this box." The signal is loudest exactly when it is least likely to be read.

#57/#59 fixed the adjacent case, deliberately not this one. rig bootstrap
now refuses an empty users file, because bootstrap asserts who lives on a box
— an empty answer there is a self-contradiction, and a flat refusal costs
nothing. rig users apply is a convergence verb where "converge to zero" is a
complete, legitimate instruction, so the same flat refusal would break real
de-provisioning. The two commands genuinely need different answers.

The distinction worth drawing

rig already keeps a ledger at /etc/rig/users of the users it manages. So it can
tell these apart:

  • file empty, ledger empty → nothing to do, no ambiguity
  • file empty, ledger non-empty → this run revokes every managed operator

Only the second is dangerous, and rig has everything it needs to recognize it.

Proposed shape

A ledger-gated confirmation, not a refusal: when the file names zero users
and the ledger is non-empty, say plainly how many operators are about to be
revoked and require explicit confirmation.

The part that needs its own design round

The non-interactive case is the whole difficulty. apply runs unattended —
from rig bootstrap, from CI, from a config-management run with no TTY. A
confirmation that cannot be answered must not silently proceed and must not
silently hang. That means an explicit flag (--yes / RIG_YES=1, matching box's
BOX_YES convention) with a documented non-interactive contract, on the model
of box revoke --purge, which already solves exactly this problem: destructive,
confirmed on a TTY, refuses without one unless the env var says yes.

Worth designing against that precedent rather than inventing a new shape.

Open question

Should this apply to any mass revocation, not just the empty-file case? A file
that drops nineteen of twenty operators is nearly as alarming and currently just
as quiet. A threshold ("this run revokes N of M managed users") may be the more
honest gate — but it introduces a number someone has to justify, where "the file
is empty" is a bright line that needs none.

Context

Raised by the agent implementing #57 and recorded there rather than actioned,
since it changes a different command's contract than the one that PR touches.

# `rig users apply` cannot tell "remove everyone" from "I truncated the file" ## The situation `rig users apply <file>` converges a box to the users file exactly: users in the file are created and converged, users **not** in it are revoked — account expired, `authorized_keys` renamed to `authorized_keys.revoked-by-rig`, home and uid kept for attribution. That is correct and deliberate. Removing a name from the file is how you off-board someone. But it means **an empty file is a valid instruction to revoke every operator on the box** — and it is indistinguishable from an empty file produced by accident. A stray `>` in a shell, a failed generation step in a private infra repo, a truncated copy: all produce a file that says, in rig's language, "nobody should have access to this machine." ## Why the current safeguards do not cover it **Per-user warnings arrive after the decision and scale wrong.** apply does warn as it revokes each user. But twenty operators means twenty lines of scrollback, which reads as noise rather than as "you are about to lock every human out of this box." The signal is loudest exactly when it is least likely to be read. **#57/#59 fixed the adjacent case, deliberately not this one.** `rig bootstrap` now refuses an empty users file, because bootstrap *asserts* who lives on a box — an empty answer there is a self-contradiction, and a flat refusal costs nothing. `rig users apply` is a *convergence* verb where "converge to zero" is a complete, legitimate instruction, so the same flat refusal would break real de-provisioning. The two commands genuinely need different answers. ## The distinction worth drawing rig already keeps a ledger at `/etc/rig/users` of the users it manages. So it can tell these apart: - file empty, ledger empty → nothing to do, no ambiguity - file empty, **ledger non-empty** → this run revokes every managed operator Only the second is dangerous, and rig has everything it needs to recognize it. ## Proposed shape A **ledger-gated confirmation, not a refusal**: when the file names zero users and the ledger is non-empty, say plainly how many operators are about to be revoked and require explicit confirmation. ## The part that needs its own design round **The non-interactive case is the whole difficulty.** apply runs unattended — from `rig bootstrap`, from CI, from a config-management run with no TTY. A confirmation that cannot be answered must not silently proceed *and* must not silently hang. That means an explicit flag (`--yes` / `RIG_YES=1`, matching box's `BOX_YES` convention) with a documented non-interactive contract, on the model of `box revoke --purge`, which already solves exactly this problem: destructive, confirmed on a TTY, refuses without one unless the env var says yes. Worth designing against that precedent rather than inventing a new shape. ## Open question Should this apply to *any* mass revocation, not just the empty-file case? A file that drops nineteen of twenty operators is nearly as alarming and currently just as quiet. A threshold ("this run revokes N of M managed users") may be the more honest gate — but it introduces a number someone has to justify, where "the file is empty" is a bright line that needs none. ## Context Raised by the agent implementing #57 and recorded there rather than actioned, since it changes a different command's contract than the one that PR touches.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/rig#65
No description provided.