From c4d64fb0370ba806376738a7be63ffd1ea3ae316 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 15:51:36 +0000 Subject: [PATCH] =?UTF-8?q?feat(bootstrap):=20staging=20role=20=E2=80=94?= =?UTF-8?q?=20the=20host=20archetype=20for=20box-minted=20staging=20VMs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- bin/rig | 9 +++++---- commands/bootstrap.sh | 30 ++++++++++++++++++++++++++---- test/cli.sh | 10 ++++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/bin/rig b/bin/rig index 8301af6..767bfa3 100755 --- a/bin/rig +++ b/bin/rig @@ -8,11 +8,12 @@ usage() { usage: rig [args] commands: - bootstrap [--hostname ] [--ts-tag ] + bootstrap [--hostname ] OS plumbing on a pristine Debian box: hardening, unattended-upgrades, - tailscale join. Prompts for a single-use tailnet pre-auth key - (TS_AUTHKEY env overrides the prompt). Run as root. Role runner - defaults to tag:ci and refuses tag:server. + tailscale join. Prompts for a single-use TAGGED tailnet pre-auth key + (TS_AUTHKEY env overrides the prompt); the key's tags are the tailnet + tag, verified after join. Run as root. Roles runner and staging + refuse tag:server. coolify install --version Pinned Coolify install (AUTOUPDATE=false). Control-plane box only. coolify backup install [options] diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh index 45f8092..6b5c06a 100755 --- a/commands/bootstrap.sh +++ b/commands/bootstrap.sh @@ -13,10 +13,15 @@ die() { printf 'rig-bootstrap: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } usage() { cat <<'EOF' -usage: rig bootstrap [--hostname ] +usage: rig bootstrap [--hostname ] --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, 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 @@ -31,10 +36,10 @@ EOF # --- args (validated before the root check, so errors are testable) --------- ROLE="${1:-}" case "$ROLE" in - control-plane|workload|runner) shift ;; + control-plane|workload|runner|staging) shift ;; -h|--help) usage; exit 0 ;; - "") usage >&2; die "role required (control-plane|workload|runner)" 2 ;; - *) die "unknown role: $ROLE (want 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|staging)" 2 ;; esac TS_HOSTNAME="$ROLE" @@ -71,6 +76,12 @@ if [ -r /etc/os-release ]; then else warn "cannot read /etc/os-release; proceeding anyway" 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 # 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)." 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' ' ')" } @@ -272,4 +292,6 @@ if [ "$ROLE" = "control-plane" ]; then log "next: rig coolify install --version " elif [ "$ROLE" = "runner" ]; then log "next: rig runner install --repo --version " +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 diff --git a/test/cli.sh b/test/cli.sh index 6571de0..15aeae3 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -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 check "bootstrap: --ts-tag is removed (no value), exit 2" 2 "comes from the pre-auth key" \ "$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 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: staging role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" staging else echo "skip: bootstrap non-root refusals (running as root)" fi