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.
This commit is contained in:
2026-08-19 23:03:14 +00:00
parent c982d82dde
commit 069dd4b23f
3 changed files with 91 additions and 2 deletions
+14 -1
View File
@@ -1,6 +1,6 @@
import type { UserRole } from 'officerdb';
import { updateUser } from 'officerdb';
import { ensureOsUser, hardenOwnerHome, osUserHome } from '@@/os-user';
import { ensureOsUser, hardenOwnerHome, osUserHome, reassertHomeMask } from '@@/os-user';
import { provisionSshAccess } from '@@/os-user-ssh';
import { seedShellConfig } from '@@/os-user-shell';
import { provisionClaudeCli } from '@@/os-user-claude';
@@ -127,6 +127,19 @@ export async function provisionOsAccount(params: {
})
: null;
// LAST, and it has to be last.
//
// Every step above that creates a directory and then closes it — `.ssh` at 700, the claude installer's
// `.config` and `.local` — recomputes that directory's ACL mask from its new group bits. For 700 that is
// `mask::---`, which clamps the platform's own entry to nothing: the grant survives as
// `user:pastilhas:rwx #effective:---`, correct-looking and inert.
//
// Found on a fresh server where an Admin granted files, chat and terminal reached none of them, and the
// ACL looked right everywhere anyone checked. Non-fatal — but it is the difference between an account
// that works and one that only appears to.
const mask = await reassertHomeMask(osUserHome(params.email));
if (!mask.ok) console.warn(`[users] ${params.email}: ${mask.error}`);
// The Linux account is recorded either way: it exists, it is confined, and a member's terminal can run as
// it. Only the keys are missing, and that is what the error says.
const sshPublicKey = ssh.ok ? ssh.publicKey : null;