Restricted incus tier: per-user projects converged onto hardened boxnet (#74) #75

Merged
dan-claude-bot merged 10 commits from feat/restricted-tier into main 2026-07-18 13:21:16 +00:00
3 changed files with 6 additions and 4 deletions
Showing only changes of commit 565e7693e8 - Show all commits

View file

@ -22,8 +22,8 @@ which records not just what changed but what each drill run proved.
call, `setup-host` and `doctor` answer at the caller's tier. Rehearsed
end-to-end by `drill/multiuser.sh` (criteria al: confinement, lifecycle,
cross-user visibility, name collisions, the in-box isolation contract,
escape hatches, re-sync survival, revoke) — 41/41 on the design host, in
both container and VM mode.
escape hatches, re-sync survival, revoke incl. the live-session case) —
42/42 on the design host, in both container and VM mode.
- **CI runs the multi-user rehearsal on a real Incus** — a second `rehearsal`
job stands up the full stack on the runner (setup-host, doctor, then
`multiuser.sh --container`), so every PR proves the tier's semantics

View file

@ -1,8 +1,9 @@
# Restricted incus tier — design and measured results (#74)
claude-bot-andresmgsl commented 2026-07-18 01:29:43 +00:00 (Migrated from github.com)
Review

On a shared boxnet this contract is now doing tenant isolation, not just defense-in-depth. Note that of these four, only security.port_isolation (L2) and the nft bridge drop actually stop box→box — the ACL is L3/L4 and, per profiles/box-net.yaml's own comment, was silently bypassed at L2. So the multi-tenant boundary rests on those two mechanisms. Worth stating that explicitly here.

On a shared `boxnet` this contract is now doing *tenant* isolation, not just defense-in-depth. Note that of these four, only `security.port_isolation` (L2) and the nft bridge drop actually stop box→box — the ACL is L3/L4 and, per `profiles/box-net.yaml`'s own comment, was silently bypassed at L2. So the multi-tenant boundary rests on those two mechanisms. Worth stating that explicitly here.
claude-bot-andresmgsl commented 2026-07-18 01:29:43 +00:00 (Migrated from github.com)
Review

This widens access to include boxnet but (per #74) incusbr-<uid> stays in the access list too. Does the convergence also remove/repoint the auto default profile so a restricted user can't incus launch onto their unhardened incusbr-<uid>, skipping the box-net contract entirely? If not, the isolation stack is opt-in for anyone with raw incus in their project. Please spell out how the per-user bridge is neutralized — and whether the widened restricted.networks.access survives an incus-user re-sync.

This widens access to include `boxnet` but (per #74) `incusbr-<uid>` stays in the access list too. Does the convergence also remove/repoint the auto default profile so a restricted user can't `incus launch` onto their unhardened `incusbr-<uid>`, skipping the box-net contract entirely? If not, the isolation stack is opt-in for anyone with raw `incus` in their project. Please spell out how the per-user bridge is neutralized — and whether the widened `restricted.networks.access` survives an incus-user re-sync.
claude-bot-andresmgsl commented 2026-07-18 01:29:43 +00:00 (Migrated from github.com)
Review

For criteria (a)–(f), please include an active cross-user probe, not just "user A can't see user B's instances": from restricted user A's box, ARP/SYN/broadcast at user B's box on the same boxnet subnet, and assert silence. The single-owner drill already trusts port_isolation; multi-tenant is exactly where an L2 gap would bite, and #74's own reachability claim is the thing under test.

For criteria (a)–(f), please include an *active cross-user* probe, not just "user A can't see user B's instances": from restricted user A's box, ARP/SYN/broadcast at user B's box on the same `boxnet` subnet, and assert silence. The single-owner drill already trusts `port_isolation`; multi-tenant is exactly where an L2 gap would bite, and #74's own reachability claim is the thing under test.
claude-bot-andresmgsl commented 2026-07-18 01:29:43 +00:00 (Migrated from github.com)
Review

CI (.github/workflows/ci.yml) explicitly does not run the drill or the multiuser rehearsal — ubuntu-latest can't reliably nest Incus + /dev/kvm. So "Test suite expansion + CI wiring" needs a concrete story: a self-hosted runner, or gated/manual drill jobs with shellcheck-only on GH-hosted. Otherwise "wired into CI" overstates what actually gates a merge.

CI (`.github/workflows/ci.yml`) explicitly does *not* run the drill or the multiuser rehearsal — `ubuntu-latest` can't reliably nest Incus + `/dev/kvm`. So "Test suite expansion + CI wiring" needs a concrete story: a self-hosted runner, or gated/manual drill jobs with shellcheck-only on GH-hosted. Otherwise "wired into CI" overstates what actually gates a merge.
**Status: implemented and rehearsed.** 41/41 rehearsal criteria green on the
**Status: implemented and rehearsed.** 42/42 rehearsal criteria green on the
design host (Debian 13 trixie, Incus 6.0.4, nested KVM), in container mode
and VM mode. This doc records the design, what was measured, and why each
and VM mode — and green in CI on ubuntu-latest / incus 6.0.0 (whose one
version-drift false FAIL is MU-4 in `drill/RUNS.md`). This doc records the design, what was measured, and why each
decision fell where it did. It supersedes the vetoed #72 design
(`docs/plans/2026-07-17-multiuser-hosts.md` on `feat/restricted-tier-wip`).

View file

@ -195,6 +195,7 @@ No listener is needed, and none should be started: see trap 3.
| Run | Result | What it cost |
| --- | --- | --- |
| MU-4 | 41/1 on CI (ubuntu-latest, incus 6.0.0) → 42/0 | the CI job earned its keep on its second-ever run: the attach-refusal probe matched 6.0.4's error wording and 6.0.0 words it differently (and refuses at a different stage) — a version-drift false FAIL the design host could never show. The probe now asserts the OUTCOME (nothing running on the private bridge) and prints the refusal as evidence. Also this round, from adversarial review: revoke survives the live-session case (groups are read at login; --purge now terminates sessions first, else a stale-group process could recreate the project UNHARDENED after the purge), grant backs out its own group-add on failure, and the session-dies-with-the-tier check is criterion 42 |
| **MU-3** | **41/0 (containers) + 41/0 (VMs) — the tier holds** | `multiuser.sh` (#74) from zero on Debian 13 / Incus 6.0.4: grant, confinement, lifecycle, the in-box contract with the cross-user sibling drop, closed escape hatches, re-sync survival, scoped revoke. The VM pass needed right-sized boxes (1GiB): two 2GiB nested VMs on a 7.7GiB host measured swap, not isolation |
| MU-2 | 40/41 (VMs) | the egress probe cried broken over a TLS handshake that outlived its 8s budget under nested-KVM memory pressure — every isolation probe (which PASSES on silence) was immune. Probe budgets raised, reachable-probes retry once, and the mint keeps its log for the post-mortem |
| MU-1 | 39/41 (containers) | both FAILs were the instrument: a `box new` rc swallowed with its narration (the drill's oldest sin, relearned), and the enumeration probe resolving the box's OWN name — /etc/hosts answered, not the gateway. Probe a SIBLING's name or you measure cloud-init's hostname hygiene. Found for real along the way: `box restore` dispatched `incus restore`, which does not exist in Incus 6 — broken for every tier since the rename |