diff --git a/src/servers/os-user-deprovision.ts b/src/servers/os-user-deprovision.ts index b4f61ca5..e9bda318 100644 --- a/src/servers/os-user-deprovision.ts +++ b/src/servers/os-user-deprovision.ts @@ -345,6 +345,30 @@ async function severMemberTree(params: { const chowned = await sudo(['chown', '-h', '-R', `${who}:${who}`, params.tree]); if (!chowned.ok) return { ok: false, error: `could not reassign ${params.tree} to ${who}: ${chowned.out}` }; + // ── Ownership is not the only link to the uid ── + // + // `confineUserTree` grants the member a NAMED ACL entry on their whole tree — `u::rwx` plus a + // `default:` copy inherited by everything either party creates afterwards. `chown` does not remove those: + // they are xattrs rather than ownership, and they record the uid NUMERICALLY. + // + // Measured on this host: after `chown -h -R` to the service user, `user::rwx` was still present on the + // directory, on its children, and in their defaults. So the tree reads as the platform's while still + // granting the freed uid read and write on every byte of it — and the next account allocated that number + // inherits the previous member's home, keys, credential and container storage. Severing ownership without + // severing this just moves the hazard somewhere ownership checks cannot see it, including `find -uid`. + // + // `-b` removes every ACL rather than the member's entries alone. The service user owns all of it now, so a + // named entry granting themselves what ownership already grants is redundant, and "no ACLs" is a much + // cheaper thing to verify than "no ACL naming one particular id". + // + // `-P` is the physical walk. It is already the default for `setfacl -R` — verified here, a symlink out of + // the tree was not followed — but it is stated for the same reason `chown` above carries `-h`: a member + // chooses what their symlinks point at, and this argv should not depend on a traversal default holding. + const stripped = await sudo(['setfacl', '-R', '-P', '-b', params.tree]); + if (!stripped.ok) { + return { ok: false, error: `reassigned ${params.tree} but could not clear its ACLs: ${stripped.out}` }; + } + if (params.policy === 'preserve') return { ok: true }; try {