From 89848a2fa13769db8435d0ed57968de765c1ac36 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 10 Nov 2026 13:54:23 -0600 Subject: [PATCH] docs: clarify host trust boundary --- docs/plans/2026-11-10_accounts-design.md | 78 ++++++++++++++++-------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/docs/plans/2026-11-10_accounts-design.md b/docs/plans/2026-11-10_accounts-design.md index 69f775dc8..1e3a79600 100644 --- a/docs/plans/2026-11-10_accounts-design.md +++ b/docs/plans/2026-11-10_accounts-design.md @@ -891,28 +891,43 @@ they can read the token file, the bus database and every key, or change the files directly. A token could help a remote admin CLI that has no host access. That is a possible later feature, not part of Phase M. -Phase M reduces who holds the instance uid and makes local account actions -visible. A dedicated instance uid is used by no agent seat or human login. -In M2, the accounts loader refuses an accounts file with the wrong owner or -group/other write permission; `mosaic accounts` refuses when its uid does -not own the config directory and writes the OS uid into its receipt. In M3, -stamped builds and `.deployed` refuse dev flags. Case 2 runs agent processes -in containers, limiting their access to the host. +Phase M narrows who is assigned the instance uid. In the current managed +host setup, both Case 1 and Case 2 use `mosaic`: the host run sheet creates +it as a system account with a non-login shell and no supplementary groups +or sudo rule (`docs/guides/host-run-sheet.md:19-26`). Case 1 acceptance +checks that the Console and bus run as the configured `--user` +(`scripts/case1-accept.sh:102-116,457-469`). Case 2's unit renderer uses +its `--user` for both services (`scripts/case2-unit.sh:136-145,514`); the +host setup runs that path as `mosaic` +(`docs/plans/2026-10-20_case2-rows.md:1197-1200`). These are today's +configured identities, not an installer guarantee: `case2-unit.sh` accepts +any existing non-root user with a matching home. The run sheet, rather than +the renderer, selects the dedicated account. -The install paths do not yet establish the dedicated-uid rule. Case 1's -acceptance script takes `--user` and `--home` from its arguments -(`scripts/case1-accept.sh:102-116`) and checks that the Console and bus units -run as that user (`scripts/case1-accept.sh:457-469`). Case 2's install runs -as a supplied non-root user (`scripts/case2-install.sh:2-3,95-103`); its -unit renderer accepts an existing non-root `--user` without checking that -it is dedicated -(`scripts/case2-unit.sh:136-145`) and writes `User=$USER_NAME` -(`scripts/case2-unit.sh:514`). Neither path checks that this account is -non-login or unused by agent seats. In particular, Case 2 can use a human's -own uid today. This is a deployment gap, not a property of the accounts -checks. M3 follow-up 1 closes it by making both install paths require a -dedicated non-login uid, verify config and data-root ownership, and reject -an uid assigned to an agent seat. +There is also a separate Console path. `webui-service.sh` installs a +systemd user unit, then asks the operator to start it with +`systemctl --user` (`scripts/webui-service.sh:1-7,19,413-418`). That Console +runs as the account whose user manager starts it, not as the managed +`mosaic` service account. It is a single-user development path, not a safe +accounts-mode deployment path. M3 follow-up 1 is to refuse a deployed or +accounts-mode Console service through this user-unit path. + +The stack's own managed agent sessions run as the instance uid inside a +bubblewrap sandbox, not in containers (`scripts/case1-accept.sh:480-496`, +`docs/plans/2026-10-20_case2-rows.md:1031-1034,1197-1199`). The sandbox +limits their filesystem view. A sandbox escape has the same authority as +holding the instance uid. This is different from a development agent seat: +Phase M requires the instance uid to differ from every human login and +development seat uid. A non-login shell prevents direct login, but root or a +human with sudo or equivalent authority can still act as `mosaic`. + +M2's accounts loader refuses a file with the wrong uid or group or other +write permission. M2 does not currently specify the config-directory owner +check for `mosaic accounts` or an OS-uid receipt. M3 pins both as integration +expectations. The recorded uid identifies the process, not +necessarily the person who invoked it. The receipt is writable by the +instance uid and is not tamper-evident against that uid. Stamped builds and +`.deployed` also refuse dev flags in M3. The residual risk is accepted: host root, or the instance uid, owns the instance. Accounts protect users from one another through the Console and @@ -1332,7 +1347,7 @@ Rollback keeps OIDC. Nothing in it removes `--sso` or turns sign-in off. | Identity confusion across providers | Identity is `(provider, sub)` with the issuer from config. Email matches invites only. A subject belongs to one account. | | Local CLI acting outside the check | In accounts mode `mosaic org` and `decide --org` refuse. `mosaic accounts` runs as the instance uid, which can already read every bus file, and has no rights verbs. | | Tampering with accounts.json on disk | The file must belong to the instance uid and not be group or other writable. Anyone who can write it already runs as the instance user and can read every bus. That is outside this design, as it is for access.json. | -| Host root or the instance uid bypassing accounts | Accepted residual risk: either can read and change the instance config, data root, bus files and credentials. Phase M limits and records who holds the instance uid; it does not make that uid untrusted. | +| Host root or the instance uid bypassing accounts | Accepted residual risk: either can read and change the instance config, data root, bus files and credentials. An administrator who can become the instance uid has the same authority. An OS-uid receipt identifies the process, not the human, and the instance uid can change the receipt. | Not covered: a compromised identity provider, a stolen session cookie before its 1 hour idle limit, and a compromised Console process, which holds @@ -1435,10 +1450,21 @@ Acceptance tests, against a fake OIDC provider on port 0: - `--dev-local-owner` from a checkout refuses with accounts.json present, with `console.sso`, and with `.deployed`. - `webui-service.sh render` and `case2-unit.sh` refuse a dev flag. -- Case 1 and Case 2 install tests reject a login-capable uid or an agent-seat - uid as the instance uid, require it to own the config directory and data - root, and verify that the Console and bus units run as that same dedicated - uid. A designated non-login instance uid passes. +- `case2-unit.sh` accepts the host run sheet's `mosaic` account fixture only + when its shell is non-login, its primary group has no other members, and + it owns the config directory and data root. Fixtures for a human login and + a development seat with interactive shells are refused. Both rendered + system units use that same uid. +- `case1-accept.sh` F1 continues to verify that the running Console and bus + processes have the configured instance uid. +- M3 integration tests verify that `mosaic accounts` refuses when its + effective uid does not own the config directory and that its receipt + records the effective OS uid. The test documents that this is process + identity, not human attribution or tamper evidence. +- **M3 follow-up 1:** accounts mode refuses the `webui-service.sh` systemd + user-unit path. Managed accounts instances start through the system units + rendered by `case2-unit.sh`. Tests show the user-unit path cannot start a + deployed accounts instance while development mode remains available. - The existing sso and dev-owner suites stay green. Grant needed: `stack-copy.sh` writes a new file into every copy, and the