per-user linux accounts, stage 1: the account and the privilege drop
A member gets a real Linux account whose home is the directory the platform already
provisions for them. Nothing uses it yet — this is the mechanism plus the account,
deliberately with no behaviour change, so the file browser and terminal can be moved
onto something already proven.
Bun.spawn silently ignores uid/gid. Verified on 1.3.10: from uid 1000,
Bun.spawn(['id','-u'], {uid: 65534}) exits 0 and prints 1000. No throw, no warning.
Bun's types don't declare the option so typed code can't reach it by accident, but the
runtime accepts it, and a silently absent isolation boundary is the worst outcome this
feature could have. So privilege drops go through sudo -n setpriv, and a test pins Bun's
behaviour — if it's ever implemented, that test tells us we may simplify.
sudo is required for the drop and not because of the uid: --init-groups fails with
"Operation not permitted" for an unprivileged caller even when reuid'ing to its own
account, because setgroups(2) is root-only. --reset-env is what stops the platform's
environment crossing; verified POSTGRES_URL is unset on the far side and HOME arrives
from the target's passwd entry.
Three bugs that only a real run with a real useradd could find:
- chmod after chown fails forever, because chmod needs ownership. Both orderings fail
unprivileged. Both operations now go through sudo, which is what makes it re-runnable.
- a member could read ANOTHER member's home: provisionUserDirs created at the default
umask (755) and only the account being created got confined. An unlistable parent is
no protection when the child is world-readable and emails are guessable. The skeleton
is now created closed, 711 on the account dir and 700 inside.
- platform/.env was 664 and a member's shell printed JWT_SECRET, which is enough to mint
an owner token and bypass every capability check. Now a boot check that refuses to
start with OFFICER_OS_USERS on while any .env in the project root is group- or
world-readable.
Design, the measured results and the staging plan: docs/per-user-linux-accounts.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,18 @@ export const users = pgTable(
|
||||
role: text('role', { enum: USER_ROLES }).notNull().default('Member'),
|
||||
name: text('name'),
|
||||
username: text('username').unique(),
|
||||
/**
|
||||
* The Linux account this platform account runs as, when per-user OS accounts are enabled.
|
||||
*
|
||||
* Stored rather than re-derived from `username`. `useradd` can adjust or refuse a name, and a derived
|
||||
* value would let the platform's idea of who someone is drift from what is actually in `/etc/passwd`
|
||||
* — which, for a field that decides whose uid executes a shell, is not a drift to discover later.
|
||||
*
|
||||
* NULL means no OS account: every account created before the feature, every account on a host where
|
||||
* it is switched off, and the owner (who runs as the service user itself).
|
||||
* See docs/per-user-linux-accounts.md.
|
||||
*/
|
||||
osUser: text('os_user').unique(),
|
||||
avatar: text('avatar'),
|
||||
passwordChangedAt: timestamp('password_changed_at', { withTimezone: true }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
Reference in New Issue
Block a user