feat(bootstrap): staging role — the host archetype for box-minted staging VMs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-07-17 15:51:36 +00:00
parent 1b68d3ca49
commit c4d64fb037
3 changed files with 41 additions and 8 deletions

View file

@ -8,11 +8,12 @@ usage() {
usage: rig <command> [args] usage: rig <command> [args]
commands: commands:
bootstrap <control-plane|workload|runner> [--hostname <name>] [--ts-tag <tag>] bootstrap <control-plane|workload|runner|staging> [--hostname <name>]
OS plumbing on a pristine Debian box: hardening, unattended-upgrades, OS plumbing on a pristine Debian box: hardening, unattended-upgrades,
tailscale join. Prompts for a single-use tailnet pre-auth key tailscale join. Prompts for a single-use TAGGED tailnet pre-auth key
(TS_AUTHKEY env overrides the prompt). Run as root. Role runner (TS_AUTHKEY env overrides the prompt); the key's tags are the tailnet
defaults to tag:ci and refuses tag:server. tag, verified after join. Run as root. Roles runner and staging
refuse tag:server.
coolify install --version <pin> coolify install --version <pin>
Pinned Coolify install (AUTOUPDATE=false). Control-plane box only. Pinned Coolify install (AUTOUPDATE=false). Control-plane box only.
coolify backup install [options] coolify backup install [options]

View file

@ -13,10 +13,15 @@ die() { printf 'rig-bootstrap: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
usage() { usage() {
cat <<'EOF' cat <<'EOF'
usage: rig bootstrap <control-plane|workload|runner> [--hostname <name>] usage: rig bootstrap <control-plane|workload|runner|staging> [--hostname <name>]
--hostname system + tailnet hostname (default: the role name) --hostname system + tailnet hostname (default: the role name)
Role staging is the host for box-minted staging VMs (Incus guests converged
from inside with `rig bootstrap workload`). Mint its key with tag:local: the
host is never managed by the control plane — its guest VMs are — so a staging
host may not carry tag:server.
The tailnet tag is NOT a rig argument. A pre-auth key is minted WITH its tags, The tailnet tag is NOT a rig argument. A pre-auth key is minted WITH its tags,
so the key is the single source of truth: rig no longer requests a tag it might so the key is the single source of truth: rig no longer requests a tag it might
disagree with. After the box joins, rig reads the tag control actually GRANTED disagree with. After the box joins, rig reads the tag control actually GRANTED
@ -31,10 +36,10 @@ EOF
# --- args (validated before the root check, so errors are testable) --------- # --- args (validated before the root check, so errors are testable) ---------
ROLE="${1:-}" ROLE="${1:-}"
case "$ROLE" in case "$ROLE" in
control-plane|workload|runner) shift ;; control-plane|workload|runner|staging) shift ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
"") usage >&2; die "role required (control-plane|workload|runner)" 2 ;; "") usage >&2; die "role required (control-plane|workload|runner|staging)" 2 ;;
*) die "unknown role: $ROLE (want control-plane|workload|runner)" 2 ;; *) die "unknown role: $ROLE (want control-plane|workload|runner|staging)" 2 ;;
esac esac
TS_HOSTNAME="$ROLE" TS_HOSTNAME="$ROLE"
@ -71,6 +76,12 @@ if [ -r /etc/os-release ]; then
else else
warn "cannot read /etc/os-release; proceeding anyway" warn "cannot read /etc/os-release; proceeding anyway"
fi fi
# A staging host exists to run VMs, so no /dev/kvm deserves a loud note — but
# only a note: the role is rehearsed in containers, where /dev/kvm is
# legitimately absent, and rig cannot tell a rehearsal from a misconfigured box.
if [ "$ROLE" = "staging" ] && [ ! -e /dev/kvm ]; then
warn "/dev/kvm is absent — a staging host is expected to run VMs. Harmless in a container rehearsal; on real hardware, enable virtualization (VT-x/AMD-V) in firmware."
fi
# The pre-auth key is acquired LATER, in the tailscale block — and only if the # The pre-auth key is acquired LATER, in the tailscale block — and only if the
# box has not already joined. rig is convergent by contract, so re-running it to # box has not already joined. rig is convergent by contract, so re-running it to
@ -216,6 +227,15 @@ verify_effective_tag() {
die "role runner joined with tag:server (effective tags: $(printf '%s' "$tags" | tr '\n' ' ')). The key you used grants tag:server to repo-controlled code; that must never happen. Re-run bootstrap with a key minted for a CI tag (e.g. tag:ci)." die "role runner joined with tag:server (effective tags: $(printf '%s' "$tags" | tr '\n' ' ')). The key you used grants tag:server to repo-controlled code; that must never happen. Re-run bootstrap with a key minted for a CI tag (e.g. tag:ci)."
fi fi
# Same policy, staging flavor: a staging HOST is never managed by the control
# plane — its guest VMs are, each registered there as its own server. The
# fleet has already been bitten by a host wrongly carrying tag:server, which
# extends every server grant to a box the control plane does not even know.
# Refused, never warned; rig can DETECT this but not FIX it, so name the repair.
if [ "$ROLE" = "staging" ] && printf '%s\n' "$tags" | grep -qx 'tag:server'; then
die "role staging joined with tag:server (effective tags: $(printf '%s' "$tags" | tr '\n' ' ')). A staging host is never managed by the control plane — its guest VMs are. Re-run bootstrap with a key minted for tag:local."
fi
log "verified effective tailnet tag(s): $(printf '%s' "$tags" | tr '\n' ' ')" log "verified effective tailnet tag(s): $(printf '%s' "$tags" | tr '\n' ' ')"
} }
@ -272,4 +292,6 @@ if [ "$ROLE" = "control-plane" ]; then
log "next: rig coolify install --version <pin>" log "next: rig coolify install --version <pin>"
elif [ "$ROLE" = "runner" ]; then elif [ "$ROLE" = "runner" ]; then
log "next: rig runner install --repo <owner/repo> --version <pin>" log "next: rig runner install --repo <owner/repo> --version <pin>"
elif [ "$ROLE" = "staging" ]; then
log "next: install the box CLI and run 'box setup-host' to prepare Incus, then mint staging boxes with 'box new --template staging'"
fi fi

View file

@ -47,9 +47,19 @@ check "bootstrap: --ts-tag is removed (with value), exit 2" 2 "comes from the pr
"$ROOT/commands/bootstrap.sh" runner --ts-tag tag:server "$ROOT/commands/bootstrap.sh" runner --ts-tag tag:server
check "bootstrap: --ts-tag is removed (no value), exit 2" 2 "comes from the pre-auth key" \ check "bootstrap: --ts-tag is removed (no value), exit 2" 2 "comes from the pre-auth key" \
"$ROOT/commands/bootstrap.sh" runner --ts-tag "$ROOT/commands/bootstrap.sh" runner --ts-tag
check "bootstrap: staging + removed --ts-tag exits 2" 2 "comes from the pre-auth key" \
"$ROOT/commands/bootstrap.sh" staging --ts-tag tag:server
# The staging tag:server refusal rides the EFFECTIVE tag, inside
# verify_effective_tag — a path that needs a real tailnet, so it belongs to the
# rehearsal. What the harness CAN prove is that the refusal exists in the
# shipped script: grep the die message, so a deleted guard cannot ship green
# (the same reason the runner-install repo guard is grepped below).
check "bootstrap: staging effective-tag refusal is present" 0 "" \
grep -q "role staging joined with tag:server" "$ROOT/commands/bootstrap.sh"
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
check "bootstrap: refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload check "bootstrap: refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload
check "bootstrap: runner role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" runner check "bootstrap: runner role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" runner
check "bootstrap: staging role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" staging
else else
echo "skip: bootstrap non-root refusals (running as root)" echo "skip: bootstrap non-root refusals (running as root)"
fi fi