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

10 commits

Author SHA1 Message Date
dan-claude-bot
f1b038774b rehearsal: measure both worlds of the raw-attach version fork (MU-6)
incus 6.0.4 lets a restricted cert reference an allowed network with a raw
--network flag; 6.0.0 (ubuntu-latest) refuses it at the permission layer.
The first is the world criterion (m)'s measurements exist for; the second
is PREVENTION — the strongest of the review's three resolutions — and the
probe now recognizes it as such (with the refusal quoted) instead of dying
unmeasured. The (n) blocker is now staged with 'incus config device
override' — their own instance, their own stock profile NIC lifted
instance-local, permitted on both versions, and the exact shape that
actually blocks narrowing. Both staging failures now print their reasons;
swallowed narration is how this file's false FAILs happened.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 07:05:49 +00:00
dan-claude-bot
8e6f3a4bb8 grant/rehearsal: the codex round — verified rollback, loud partial states, and the raw-attach guarantee measured (#75)
Review 4727756972 (A2): the backout no longer trusts gpasswd — it re-reads
the live group database after removal; verified-absent gets the safe
message, anything else screams ROLLBACK INCOMPLETE, exits nonzero, and
names the exact remediation. The concurrent-login window (a session begun
between usermod and backout keeps the group) is CLOSED to the extent the
database can't reach: the backout detects live processes and names
loginctl terminate-user, and the success wording claims only what was
verified.

Review 4727641752 (A1): a failed grant for a user whose membership predates
the run (the hand-added-user scenario) now fails LOUDLY — they retain
socket access on part-converged policy, and the message says so with both
remediations (box revoke now, or fix and re-run). Their membership is not
stripped: breaking a working user over a failed re-grant is its own hazard.
The default-profile eth0 removal is deliberately not restored on failure —
that mutation only reduces capability, and restoring it would move the
failure state AWAY from fail-closed. Injected-failure coverage is criterion
(n), both flavors: fresh-user backout (fault at the LAST mutation, so the
rollback runs after every earlier one) with the group's absence verified
and a converging re-run; blocked narrowing staged for real with an
instance-local NIC parked on the private bridge.

Review A3, resolution 3 with the measurement demanded: criterion (m)
launches exactly 'incus launch --network boxnet' as the restricted user and
probes the raw NIC from inside — egress works, RFC1918 dropped (the ACL is
the network's), sibling probes dropped BOTH directions (the nft drop is the
host's), name enumeration blocked. The scoped guarantee is now stated in
box-design.md and measured on every run: box-minted instances carry per-NIC
port_isolation; raw attachments keep every network- and host-owned control,
losing only that redundant L2 layer. Instrument lesson kept as MU-5: the
probe's first cut minted the non-cloud image — no DHCP client, no lease,
and a dead NIC passes every negative probe vacuously; it now requires the
lease before believing its own answers.

Rehearsal: 54/54 (containers). test/cli.sh: 82 checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 06:41:05 +00:00
dan-claude-bot
565e7693e8 docs: log MU-4 (the CI job's version-drift catch) and refresh the counts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 05:38:04 +00:00
dan-claude-bot
47aa1e09fe rehearsal: assert the attach-refusal by OUTCOME, not by error wording (MU-4)
incus 6.0.4 refuses the private-bridge attach before 'Launching', 6.0.0
(ubuntu-latest) after it — with different words. Chasing message strings
across releases is a losing game: the probe now demands the fact (rc!=0 AND
nothing running on the bridge) and prints the refusal line as evidence in
every run log. First caught by the CI rehearsal doing exactly the job it
was added for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 05:34:37 +00:00
dan-claude-bot
ec5e898094 revoke/grant: survive the live-session case — the review's one real hole (#74)
Supplementary groups are read at LOGIN, so 'gpasswd -d' does nothing to a
session the user already holds — and after --purge, a stale-group process
could touch incus-user and lazily RECREATE the project with stock defaults:
the unhardened NAT bridge, un-narrowed, strictly worse than the granted
state. Adversarial review caught it; verified live, then closed:

- revoke --purge terminates the user's sessions first (loginctl, then
  pkill), and refuses to purge under processes it cannot kill
- bare revoke says out loud that held sessions keep the socket until they
  end, and names the loginctl command — instead of claiming a lockout it
  did not deliver (help/README/design doc reworded to match)
- a failed grant backs out its own group-add on exit (trap, disarmed on
  success): no half-granted user holding an un-narrowed socket while the
  admin reads the error. Verified by injecting a bad profile YAML
- the rehearsal now holds a session open across the purge and demands it
  dies with the tier (criterion l, 42nd check)

Smaller review findings, same pass: the escape-hatch probes assert the
refusal's REASON instead of any nonzero exit (an image hiccup must not read
as 'the escape is closed'); probe_from maps an outer-timeout kill to
dropped, not reachable; the rehearsal cleanup keeps the account when a purge
fails so doctor can name the leftovers; the purge asserts the trust
certificate's absence; cmd_new distinguishes a dead daemon from a missing
stack before prescribing setup-host; grant's success message names the
user-<uid> bridge variant correctly on big-uid hosts.

Rehearsal after: 42/42 (containers). test/cli.sh: 76 checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 05:11:20 +00:00
dan-claude-bot
3f9b38ac96 doctor: name an interrupted rehearsal's leftover users instead of absorbing them
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 04:12:33 +00:00
dan-claude-bot
2097982ac2 docs: the restricted tier — README, design doc, plan doc with measured results (#74)
The plan doc records what was measured and why each decision fell where it
did: the private bridge is worse than unhardened (a live NAT bridge with
IPv6 on), incus-user blocks snapshots, no daemon-level template exists (read
in incus-user's source), widening survives re-sync (same source, then
measured live). box-design gains the access-tiers section — including why
narrowing to boxnet-only is the load-bearing decision and why the nft bridge
drop is the layer a restricted user cannot strip. RUNS.md logs MU-1..3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 04:09:48 +00:00
dan-claude-bot
56eed6250e test+ci: the multi-user rehearsal, and CI that runs it on a real incus (#74)
drill/multiuser.sh: two throwaway users, real grants, real boxes, and the
contract measured from INSIDE them — #74's criteria (a)-(f) plus what the
findings added: the cross-user sibling drop, the closed private-bridge
escape hatches, incus-user re-sync survival, scoped revoke --purge. Its own
first runs are in RUNS.md (MU-1..3): both false FAILs were the instrument,
and both lessons (keep the mint's narration; probe a SIBLING's name, never
your own — /etc/hosts answers for you) are now comments in the script.

test/cli.sh: box_tier() driven against a shim id for all five cases
(including both-groups → admin: membership wins at the socket), the two
copies diffed byte-identical, the tier scripts' usage contracts driven for
real, and grep-guards on every load-bearing line a daemon-free run cannot
exercise — the expose guard's position, the boxnet-and-only-boxnet
narrowing, the snapshot allow, 'incus:snapshot restore'.

ci.yml: a second job stands up the real stack on the runner (setup-host,
doctor, then the rehearsal in container mode) — every PR now proves the
tier's semantics against a live daemon. The tier's mechanics are
instance-type-independent (the drop, the ACL, dns.mode and port_isolation
bind to boxnet, not the instance); the VM trust boundary itself stays a
real-hardware ritual, like the full drill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 04:09:48 +00:00
dan-claude-bot
0429a11020 feat: the restricted tier — box grant/revoke converge users onto the hardened boxnet (#74)
incus-user confines an incus-group user to their own project, but its
defaults miss box's contract three measured ways (Debian 13 / Incus 6.0.4):
a private UNHARDENED NAT bridge per user (ipv6.nat=true, no ACL, no DNS
isolation), snapshots blocked, and the box-net profile invisible to their
project. So the tier is an admin-run idempotent convergence:

  box grant <user>   # incus group; touch incus-user (the project is lazy);
                     # drop the private-bridge eth0 from their default
                     # profile; restricted.networks.access=boxnet — and ONLY
                     # boxnet, or the unhardened bridge stays one --network
                     # flag away; restricted.snapshots=allow; install the
                     # shipped box-net profile into their project
  box revoke <user>  # group removal closes the socket, boxes keep running
         --purge     # ...or delete their world, and assert the absence

box_tier() (live credentials, argless id -nG; byte-identical copy in
setup-host.sh) drives the tier-aware surface: new pre-flights the profile
and names the right fix per tier, expose refuses before any daemon call
(without the guard the failure is a lie — restricted certs cannot read
boxnet's redacted config, so box_net_ip claims a running box has no
address), setup-host exits 0 with the honest note, doctor judges only what
the caller can see.

Also fixed while the rehearsal exercised the lifecycle: box restore
dispatched 'incus restore', which does not exist in Incus 6 (it is
'incus snapshot restore') — the verb had never worked. Fixed for every tier.

Convergence survives incus-user restarts by that tool's own design (it
configures a project only at creation) — read in its source, then measured.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 04:09:48 +00:00
dan-claude-bot
f7b93a91cd docs(plan): stake out restricted-tier PR for #74 (placeholder)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 01:06:34 +00:00