Commit Graph
2 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 35715546e2 a checker for the deprovision spec, and the trap that makes it pass for free
scripts/assert-uid-free.sh is the verification half of docs/deprovision-os-account.md, written
outside the implementation on purpose: a checker the function calls is a restatement of its own
beliefs rather than an audit. Nine checks — passwd entry, uid reuse, both subid files, linger,
runtime dir, live processes, files owned by the uid, and files owned anywhere in the freed
subuid range.

Two modes, because the range has to be captured BEFORE deletion. userdel removes the
/etc/subuid entry along with the account, and after that there is no way to ask what range it
held — so a checker that only runs afterwards silently drops the half most likely to be wrong.

Exercised against green while fully provisioned: eight of nine checks fail, exit 1. A checker
that has never been seen to fail is not evidence.

And the trap worth knowing before anyone trusts a green result: the subuid check passes
vacuously on most accounts. Files get a mapped owner only when a process inside a container runs
as a NON-root user; an image whose files are root-owned maps to the member's own uid and leaves
the range empty. Measured on green after a night of real use — claude installed, an image
pulled, transcripts written — the range check found zero files and passed without testing
anything. The spec now says how to build a specimen that actually exercises it, and to watch the
check fail on that tree before trusting it to pass on a cleaned one.

Docs and a script only; no behaviour change. On a branch, for whoever merges it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 04:05:48 +00:00
pastilhasandClaude Opus 5 4da82e7f91 spec deprovisionOsAccount, from a real teardown rather than from reading the code
Written in docs/ rather than COMMS because COMMS is deleted when per-user Claude lands and
this describes a project that starts after it — a spec that gets deleted before it is
implemented is not a spec.

Everything measured on this host on 2026-08-11. The evidence for why it exists: after deleting
a member through the UI, the row was gone and the Linux account, a working login shell, a
healthy postgres container, 454MB of home and Docker storage, lingering, the runtime directory
and the subuid ranges were all still there.

Three things the spec carries that reading the code would not have produced.

terminate-user is not a barrier. A member's /bin/zsh -i survived it by three hours, and userdel
refuses while a process owned by the account is alive, so an implementation that trusts it
works on a quiet account and fails on a member who left a shell open.

The subuid half. Rootless Docker storage is owned by MAPPED ids, not the member's uid —
postgres's data directory belonged to 231141, not 1002. userdel releases the range and a later
account can be allocated it, so a check for "nothing owned by the freed uid" passes while
hundreds of megabytes are still owned by the freed range. Verification has to scan the range.

And a correction to the order I actually used: sever the data from the uid BEFORE releasing
it. The teardown ran userdel first and removed data after, which leaves a window where the uid
is free while files still carry it. The irreversible step goes last.

Also specified: never userdel -r, preserve-by-chown as the default with destroy opt-in, refuse
to release the uid if the sever failed, and do not run anything as the member after
terminating — creating a session recreates the runtime directory the step just removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:26:23 +00:00