rootless docker per member — provisioning works, running a container does not yet
Not finished. Committed because the diagnosis is worth more than the code. WHY ROOTLESS AND NOT THE DOCKER GROUP. `usermod -aG docker <user>` is the one-line version and it is root: `docker run -v /:/host -it alpine chroot /host` is a root shell, which reads .env, every other member's home and the wallet seed. Every boundary from today, bypassed by one documented command. Rootless gives what was actually asked for — a daemon per account, containers in that account's user namespace, images in their own home. VERIFIED on this host: provisioning succeeds, the server reports 29.5.0, the daemon runs as the member, `docker pull` puts 403 MB under their own home, and `docker ps -a` shows nothing while the owner has four containers. That last line is the isolation, measured. NOT VERIFIED: actually running a container. It failed, and the cause is an interaction between two things built today: failed to copy xattrs: failed to set xattr "system.posix_acl_default" on …/volumes/…/_data Creating a volume copies xattrs, and the DEFAULT ACLs on a member's home — added so the file browser could read their files — are inherited by Docker's storage, where a mapped id inside a user namespace is not a valid id to set. Both features correct alone. The fix here strips default ACLs from ~/.local/share/docker only, leaving the access ACLs the file browser needs. That fix is UNPROVEN. The re-test failed for a different, environmental reason: probe users recycle uid 1001, and a stale lingering systemd user manager from a previous probe answered `systemctl --user`, so the unit appeared not to exist. Cleaned with `loginctl terminate-user`. Retest on a machine that has not had a uid-1001 user, or on a fresh uid. Also worth knowing before this ships: uid reuse after deleting a member is a real hazard, not just a test artefact — the next member gets the previous member's uid, and anything left lingering belongs to them. setup.sh gains uidmap and dbus-user-session as core packages; the shell template exports DOCKER_HOST from $XDG_RUNTIME_DIR when the socket exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { updateUser } from 'officerdb';
|
||||
import { OS_USERS_ENABLED, ensureOsUser } from '@@/os-user';
|
||||
import { OS_USERS_ENABLED, ensureOsUser, osUserHome } from '@@/os-user';
|
||||
import { provisionSshAccess } from '@@/os-user-ssh';
|
||||
import { seedShellConfig } from '@@/os-user-shell';
|
||||
import { provisionRootlessDocker } from '@@/os-user-docker';
|
||||
import { provisionUserDirs } from '@@/data-path';
|
||||
|
||||
// Giving an account its Linux side: the directory skeleton, the Linux user, the confinement, the keys.
|
||||
@@ -68,16 +69,33 @@ export async function provisionOsAccount(params: {
|
||||
authorizedKey: params.inboundKey,
|
||||
});
|
||||
|
||||
// The shell configuration. Last because it is the only step whose failure leaves nothing broken — the
|
||||
// account works, the keys work, the terminal opens; it just opens with zsh's bare defaults.
|
||||
// The shell configuration. Late because its failure leaves nothing broken — the account works, the keys
|
||||
// work, the terminal opens; it just opens with zsh's bare defaults.
|
||||
const shell = await seedShellConfig({ email: params.email, uid: account.uid, gid: account.gid });
|
||||
|
||||
// Their own rootless Docker daemon. Last, and the most tolerant of failure: a host without the uidmap
|
||||
// package or a kernel that will not do rootless still gets a perfectly good account, minus containers.
|
||||
//
|
||||
// Provisioned for every member rather than behind a toggle, because "can I run a database to develop
|
||||
// against" should not be an administrative request. The cost — one daemon and one image cache per member —
|
||||
// is real and is written down in os-user-docker.ts.
|
||||
const docker = await provisionRootlessDocker({
|
||||
osUser: account.osUser,
|
||||
uid: account.uid,
|
||||
home: osUserHome(params.email),
|
||||
});
|
||||
|
||||
// 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;
|
||||
await updateUser(params.userId, { osUser: account.osUser, osSshPublicKey: sshPublicKey });
|
||||
|
||||
// SSH first if both failed: no keys is the more consequential of the two.
|
||||
const error = !ssh.ok ? ssh.error : !shell.ok ? shell.error : null;
|
||||
// Reported in order of consequence, not in order of execution: no keys matters more than a plain prompt,
|
||||
// which matters more than no containers. Only one is surfaced because the UI shows one line — the rest are
|
||||
// in the log.
|
||||
for (const step of [shell, docker] as const) {
|
||||
if (!step.ok) console.warn(`[users] ${params.email}: ${step.error}`);
|
||||
}
|
||||
const error = !ssh.ok ? ssh.error : !shell.ok ? shell.error : !docker.ok ? docker.error : null;
|
||||
return { osUser: account.osUser, sshPublicKey, error };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user