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>
124 lines
5 KiB
Bash
124 lines
5 KiB
Bash
#!/usr/bin/env bash
|
|
# box revoke <user> [--purge] — take the restricted tier back (#74).
|
|
#
|
|
# Two strengths, deliberately:
|
|
# · bare revoke removes the user from the 'incus' group. That closes the
|
|
# socket — the only path their certificate can travel — so access ends at
|
|
# their next login, while their project and boxes stay intact (and their
|
|
# boxes stay RUNNING: revoking a person does not kill their workloads).
|
|
# 'box grant' restores everything untouched.
|
|
# · --purge also deletes what the tier created: their boxes, their images,
|
|
# their project, the private bridge, the trust-store certificate, the
|
|
# incus-user state. Irreversible, so it asks first.
|
|
set -euo pipefail
|
|
|
|
usage() { echo "usage: box revoke <user> [--purge]" >&2; exit 2; }
|
|
|
|
user=""; purge=0
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
--purge) purge=1 ;;
|
|
-*) usage ;;
|
|
*) [ -z "$user" ] || usage; user="$a" ;;
|
|
esac
|
|
done
|
|
[ -n "$user" ] || usage
|
|
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
SUDO=""
|
|
elif command -v sudo >/dev/null 2>&1; then
|
|
SUDO="sudo"
|
|
else
|
|
echo "ERROR: box revoke needs root and 'sudo' was not found." >&2
|
|
exit 1
|
|
fi
|
|
|
|
getent passwd "$user" >/dev/null || { echo "box revoke: no such user: $user" >&2; exit 1; }
|
|
uid="$(id -u "$user")"
|
|
project="user-$uid"
|
|
# incus-user's own naming rule, mirrored exactly: the bridge is incusbr-<uid>
|
|
# unless that would not fit in an interface name (15 chars), then user-<uid>.
|
|
bridge="incusbr-$uid"
|
|
[ "${#bridge}" -gt 15 ] && bridge="user-$uid"
|
|
|
|
if [ "$purge" -eq 1 ]; then
|
|
# Destructive and irreversible: a TTY to ask on, or BOX_YES=1, or refuse —
|
|
# the same non-interactive contract as install.sh.
|
|
if [ -z "${BOX_YES:-}" ]; then
|
|
if [ -t 0 ]; then
|
|
printf 'box revoke: delete ALL of %s'\''s boxes, images and their project %s? this cannot be undone. [y/N] ' "$user" "$project"
|
|
read -r reply
|
|
case "$reply" in y|Y|yes|YES|Yes) : ;; *) echo "box revoke: aborted." >&2; exit 1 ;; esac
|
|
else
|
|
echo "box revoke: refusing to --purge without a terminal to confirm on. BOX_YES=1 means yes." >&2
|
|
exit 2
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# The group, first — access ends even if a purge step below trips.
|
|
if id -nG "$user" | tr ' ' '\n' | grep -qx incus; then
|
|
$SUDO gpasswd -d "$user" incus >/dev/null
|
|
echo "group: removed $user from 'incus' — the socket closes with their next login"
|
|
else
|
|
echo "group: $user was not in 'incus'"
|
|
fi
|
|
|
|
if [ "$purge" -eq 0 ]; then
|
|
if incus project show "$project" >/dev/null 2>&1 </dev/null; then
|
|
echo "kept: project $project and its boxes (still running — revoking a person does not kill their workloads)"
|
|
echo " 'box revoke $user --purge' deletes them; 'box grant $user' restores access"
|
|
fi
|
|
echo "revoked: $user no longer has the restricted tier."
|
|
exit 0
|
|
fi
|
|
|
|
# --purge: unmake what the tier made. Instances one at a time — a wildcard
|
|
# delete that half-fails leaves a state nobody can name; a loop that fails
|
|
# names the box it failed on (the wipe.sh discipline).
|
|
if incus project show "$project" >/dev/null 2>&1 </dev/null; then
|
|
while IFS=, read -r inst _; do
|
|
[ -n "$inst" ] || continue
|
|
echo "purge: deleting instance $inst"
|
|
incus --project "$project" delete -f "$inst" </dev/null
|
|
done < <(incus --project "$project" list --format csv --columns n 2>/dev/null)
|
|
|
|
while IFS=, read -r fp _; do
|
|
[ -n "$fp" ] || continue
|
|
incus --project "$project" image delete "$fp" </dev/null
|
|
done < <(incus --project "$project" image list --format csv --columns f 2>/dev/null)
|
|
|
|
incus --project "$project" profile delete box-net >/dev/null 2>&1 </dev/null || true
|
|
incus project delete "$project" </dev/null \
|
|
|| { echo "box revoke: could not delete $project — something is still in it (incus --project $project list / image list / storage volume list)" >&2; exit 1; }
|
|
echo "purge: project $project removed"
|
|
fi
|
|
|
|
if incus network delete "$bridge" >/dev/null 2>&1 </dev/null; then
|
|
echo "purge: private bridge $bridge removed"
|
|
fi
|
|
|
|
# The trust-store certificate incus-user minted for them. Named, not guessed:
|
|
# incus-user calls it incus-user-<uid>.
|
|
while IFS=, read -r name fp _; do
|
|
[ "$name" = "incus-user-$uid" ] || continue
|
|
incus config trust remove "$fp" </dev/null && echo "purge: trust-store certificate $name removed"
|
|
done < <(incus config trust list --format csv --columns nf 2>/dev/null)
|
|
|
|
# incus-user's per-user client state (their key pair). Removed so a future
|
|
# re-grant starts clean instead of trusting a key the purge revoked.
|
|
if [ -d "/var/lib/incus/users/$uid" ]; then
|
|
$SUDO rm -rf "/var/lib/incus/users/$uid"
|
|
echo "purge: incus-user state for uid $uid removed"
|
|
fi
|
|
|
|
# Assert absence rather than trusting exit codes — the wipe.sh discipline.
|
|
leftover=""
|
|
incus project show "$project" >/dev/null 2>&1 </dev/null && leftover="$leftover $project"
|
|
incus network show "$bridge" >/dev/null 2>&1 </dev/null && leftover="$leftover $bridge"
|
|
if [ -n "$leftover" ]; then
|
|
echo "box revoke: purge INCOMPLETE — still present:$leftover" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "revoked: $user is out, and everything the tier created is gone."
|