forked from heavy-duty/box
6 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8598cf96fe |
fix: grant the 'incus' membership to incus-admin members too (#101 review)
The previous revision skipped `usermod -aG incus` for an incus-admin member, reasoning that 'incus' is a strict subset of what incus-admin already opens. That is true of the daemon API and false of the filesystem. On Debian 13 / Incus 6.0.4 the two sockets are two files with two owning groups: /var/lib/incus/unix.socket group incus-admin 0660 /var/lib/incus/unix.socket.user group incus 0660 incus-admin opens the first and not the second, and only the second provisions a user-<uid> project. So for the incus-admin-ONLY user — the canonical #99 case — the pinned provisioning touch took EACCES, the `|| true` swallowed it, no project appeared, and the grant died blaming a healthy incus-user. Both reviewers converged on this independently and were right. The membership is now granted for everyone, with output carrying the concern the old no-op was built around (it is the key to a file, not a privilege; box_tier still reads them as admin). Everything downstream moves with it: - the backout rolls that membership back and verified, while refusing to call the rollback a lockout — incus-admin is untouched and still opens the host - revoke's bare path takes the membership back and reports `partial:` instead of "no-op, nothing was taken", still declining to call them "out" - grant's closing "gpasswd -d <user> incus-admin (no re-grant needed)" is now a true promise: they keep 'incus', so the drop lands them in their project - the socket existence probe goes through $SUDO, matching revoke's measured discipline about /var/lib/incus lying to a non-root admin Tests: the cli.sh assertions that encoded the old no-op design are flipped and the decision is pinned at the seam that broke; the sudo shim now runs `test` for real in both directions. Because the shims model neither INCUS_SOCKET nor permissions and so cannot reproduce the EACCES, drill/multiuser.sh gains criterion (o): an incus-admin-only member granted on real Incus in CI, with the membership, the project, a live connect() to unix.socket.user, and the post-drop landing all measured. Mutation-checked: 11 of the new/flipped assertions fail against the previous implementation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4a06c3ac00 |
fix: box grant provisions incus-admin members instead of refusing them
The refusal at host/grant-user.sh conflated permission with provisioning. The 'incus' group is a strict subset of what incus-admin opens — true, and the whole of what the refusal reasoned about. The user-<uid> project, the boxnet narrowing, the snapshot and backup allowances and the box-net profile installed into that project are not permissions, and an incus-admin member had none of them: box_tier() resolves them to admin, so they worked in the shared default project with no world of their own, and the one command that provisions one refused to run for them. box grant now converges them fully. The group step is a reported no-op — adding 'incus' would grant nothing and leave a group list implying a restriction that was never in force — and steps 2-5 run unchanged. The incus-user touch is pinned at incus-user's socket, which this turns out to require: the incus client picks its socket by writability, so for an incus-admin member an unpinned client sails past incus-user entirely and the project is never created. The user-side proof names their project for the same reason. On success it prints the caveat the hard exit was gesturing at: the restrictions are a default placement, not a confinement, and their own commands keep landing in the default project until incus-admin goes. The backout learns the third case (nothing added, nothing rolled back, still loud), and box revoke mirrors the whole thing rather than claiming a lockout it did not perform. Unblocks heavy-duty/rig#49. Closes #99 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1ef3ae739a |
feat: box export / import — state that survives the box and the host (#70)
'box rm' deletes a box and every snapshot it has; 'box new --from' clones, but the clone still lives on the same host. Nothing a box held could outlive a teardown — which made #66's upgrade refusal honest but lossy. This adds the way out and the way back: - box export <box> [<file>] [--instance-only]: wraps 'incus export' into one portable backup tarball (default <box>-<UTC stamp>.tar.gz), snapshots included by default. Requires the box stopped (require_stopped grew an honest reason parameter: export is down by OUR decision, not incus's). Credentials are SHOUTED, not scrubbed — the artifact carries the box's whole disk, and scrubbing a disk image is a promise tarball surgery cannot keep. - box import <file> [--name <box>]: reads the artifact's name from backup/index.yaml up front, refuses any name an existing instance holds (the resolve_box boundary from the other side), pre-flights the stack (require_stack, factored out of cmd_new), imports, then re-stamps the HOST's truth onto the artifact's: user.box=1 (legacy tag honored), the box-net placement (profile assign, the migrate-host move), fresh volatile MACs (imports restore volatile.* verbatim — a re-import beside its sibling collided at start with 'MAC address already defined on another NIC', measured live on Incus 6.0.4), and reset_identity, exactly like a clone. - restricted tier: box grant now converges restricted.backups allow — export rides the backup API, which incus-user's restricted projects block by default exactly like snapshots (incus 6.0 permissions.go, AllowBackupCreation). Import is plain instance creation and needs no key. - tests: driven usage errors + fail-closed grep/line-order guards for every daemon-gated invariant; CI's rehearsal job now runs a live round-trip (mint, write, snapshot, down, export, rm, import, assert the file, the snapshot, the tag, the agent, and the collision refusal). The whole flow was verified against a live Incus 6.0.4 daemon: running-box refusal, export, overwrite guard, rm, import with and without --name, re-home onto box-net, sibling re-import with distinct MACs and machine-ids, pre-export file and snapshot present in both. Closes #70 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|