f119bbdf93a73f5028ebb525cdef3745ca2bbbf0
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
069dd4b23f |
provisioning: re-assert the acl mask, last
on a fresh server an Admin granted files, chat and terminal reached none of them. the grants were right, the linux account existed, the named acl entries were on the home — and the file browser still raised EACCES. a posix acl mask is not independent of the mode bits. chmod recomputes it from the GROUP bits, so a directory closed to 700 gets `mask::---`, which clamps every named entry to nothing. the platform's grant survived as `user:pastilhas:rwx #effective:---`: present, correct-looking, and inert. it reads as a working acl in every place anyone thought to check. ensureOsUser already knows this and orders itself correctly — chmod, then setfacl, with a comment saying why. verified that order is right by running both ways on the affected host: chmod-then-setfacl gives mask::rwx, setfacl-then-chmod gives mask::---. what it cannot account for is the steps AFTER it, each of which creates a directory and then closes it: .ssh at 700 from the key setup, .config and .local from the claude installer. each inherits the default acl at creation and clamps its own mask a moment later. the correlation on the live host was exact — every 770 directory had mask::rwx and every 700 one had mask::---. so the mask is re-asserted at the very end of provisionOsAccount, recursively, and nothing else is touched: the named entries were never wrong, and re-granting them here would hide it if they ever were. non-fatal if it fails, like every other side effect in that function. but it is the difference between an account that works and one that only appears to. repaired on the affected server by hand first, and confirmed: home readable by the service user, zero `effective:---` entries left under it. |
||
|
|
56ddb6bebf |
close the anthropic proxy secret, and re-close it on every boot
data/sidecar/claude-state.json holds proxySecret, and that string IS the owner's Claude Max subscription: officer-anthropic-proxy accepts it as x-api-key, swaps it for the owner's OAuth bearer and forwards to api.anthropic.com. it listens on 127.0.0.1, which every local account can reach. it was 664 in a 775 directory. nothing had ever set a mode - mkdirSync and Bun.write at ubuntu's default umask of 002. confirmed by reading the live secret as 'nobody', an account with no relationship to the platform, and by confirming the proxy 401s without the key so the secret is the only control. this does NOT mean members shared the subscription. spawnClaudeAsMember runs their claude through setpriv --reset-env with a six-name allowlist, so ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY cannot cross into it and their turns go straight to anthropic on their own OAuth. that design is intact - it could just be walked around by anyone with a shell. fixed twice on purpose. state.ts sets 0600/0700 on every write, which is the authoritative fix, and on every write rather than at creation because writeFileSync leaves an existing file's mode alone. secureRuntimeSecrets() sweeps at boot because officer-anthropic-proxy is a separate pm2 process that can recreate the file hours after officer started, and because every existing install has the old mode until something repairs it. it repairs rather than refusing to boot, unlike assertSecretsClosed beside it. the line is who wrote the file: .env is the installer's and we must not guess at it; this is our own output at the machine's umask, so refusing to start would punish the user for our bug. the per-account copies are swept too. no live secret in them today, but they map session keys to transcript uuids for named accounts. tests verified to bite: removing the chmod while still reporting success fails four of them. a chmod does not undo exposure - the secret still needs rotating on the production host. TODO.md has the sequence, including that officer-claude-code caches it and must be restarted alongside the proxy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ea0d2396f7 |
linux accounts use the chosen username, and refuse to take one over
Two changes, and the second is what makes the first safe. The officer_ prefix is gone: a member's account is the username the owner typed, so whoami says who they are and a commit from their checkout is attributed to something recognisable. Measured first — useradd on this host accepts everything validateUsername permits, including dots, hyphens, underscores and uppercase. The prefix was also load-bearing, though, and not for looks. ensureOsUser REUSES an existing account, which is what makes it re-runnable, and that was safe by construction while only we created officer_* names. Unprefixed, adoption becomes the dangerous path: a platform account named root would have found root in passwd, and every runAs for that member would have been a root shell. So adoption now requires the existing account's passwd home to be exactly the home we are about to confine — that is what makes it ours — and any uid below 1000 is refused outright. Verified: root and daemon refused as system accounts, and the owner's own username refused by name with its real home quoted back. Also, the ancestor trap from the first real install. A member's home is under DATA_PATH, which is under the OWNER'S home, and /home/<owner> is 750 on Debian and Ubuntu — so every mode bit on the account tree was right, the directory existed, and the member still could not reach it for want of x four levels up. It surfaced as "ssh-keygen: Could not stat …/.ssh: Permission denied", which points at the wrong thing entirely. firstUntraversableAncestor now walks the chain as the member before anything uses the home, and the error names the directory and the chmod. The dev machine was already 751, and the probe used /tmp, so it never crossed the ancestor that mattered. Worth remembering as a shape of mistake. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5c7ceb2283 |
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>
|