diff --git a/README.md b/README.md index 0e0f029..e358237 100644 --- a/README.md +++ b/README.md @@ -237,11 +237,33 @@ the only shapes it manages — every other role refuses an effective `tag:server` after join, one rule instead of per-role exceptions. After the tag verification passes, bootstrap writes `/etc/rig/role` — one -line, `role=… root-door=… host=… join=…` — recording the **effective** traits, -overrides and all, so an overridden role never lies to the commands that read -the marker later (`rig users` keys root policy off `root-door=`). Written -post-join and cmp-guarded, so a marker never describes a box that failed to -become what it claims. +line, `role=… root-door=… host=… join=… join-by=…` — recording the +**effective** traits, overrides and all, plus whether this run performed the +tailnet join. `join-by=rig` means bootstrap called `tailscale up`; +`join-by=preexisting` means it found the node already joined. Old markers name +neither and are treated as unknown, never as permission to remove a join. +Written post-join and cmp-guarded, so a marker never describes a box that +failed to become what it claims. + +### `rig bootstrap --undo` + +```sh +sudo rig bootstrap --undo +``` + +Leaves the tailnet and then removes `/etc/rig/role`, but only when the marker +says `join-by=rig`. A pre-existing join, an old marker with no provenance, or +no marker at all is refused without calling `tailscale logout`; the refusal +names the manual repair. Re-running bootstrap writes the current marker shape. + +Undo also refuses while a GitHub runner is installed and points at +`rig runner remove`, because restoring the local machine while leaving an +off-box runner registration would create a ghost in the repository. If +`tailscale logout` fails, the marker stays in place so the command is retryable. + +This is intentionally not a general rollback. It does not uninstall packages, +reverse sshd hardening, remove Docker, Node, agent CLIs, or users. Those changes +are convergent rather than transactional and cannot be safely inferred away. Immediately after it, bootstrap stamps `/etc/rig/manifest` — **provenance**: which rig converged this box and when (see [`rig diff --git a/commands/bootstrap-undo.sh b/commands/bootstrap-undo.sh index 3f28cb9..71e7339 100755 --- a/commands/bootstrap-undo.sh +++ b/commands/bootstrap-undo.sh @@ -6,12 +6,21 @@ log() { printf 'rig-bootstrap: %s\n' "$*"; } die() { printf 'rig-bootstrap: ERROR: %s\n' "$*" >&2; exit 1; } MARKER="${RIG_ROLE_MARKER:-/etc/rig/role}" -RUNNER_DIR="${RIG_RUNNER_DIR:-/home/github-runner/actions-runner}" [ "$(id -u)" -eq 0 ] || die "must run as root" [ -e "$MARKER" ] || die "no /etc/rig/role marker — refusing to touch the tailnet" -if [ -e "$RUNNER_DIR/.runner" ]; then +runner_installed=0 +if [ -n "${RIG_RUNNER_DIR:-}" ]; then + [ -e "$RIG_RUNNER_DIR/.runner" ] && runner_installed=1 +else + for runner_config in /home/*/actions-runner/.runner /root/actions-runner/.runner; do + [ -e "$runner_config" ] && runner_installed=1 + done + compgen -G '/etc/systemd/system/actions.runner.*.service' >/dev/null \ + && runner_installed=1 +fi +if [ "$runner_installed" -eq 1 ]; then die "a GitHub runner is installed — run 'rig runner remove' first so undo does not leave a ghost runner in the repository" fi diff --git a/test/cli.sh b/test/cli.sh index f51a921..14cd921 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -143,6 +143,69 @@ check "bootstrap: login verify fails closed on a stalled backend" 0 "" \ # The marker is the traits' ground truth for rig users; assert the write exists. check "bootstrap: role marker write is present" 0 "" \ grep -q "/etc/rig/role" "$ROOT/commands/bootstrap.sh" +check "bootstrap: role marker records join provenance" 0 "join-by=%s" \ + grep -F "join-by=%s" "$ROOT/commands/bootstrap.sh" +# shellcheck disable=SC2016 +check "bootstrap: both first-join paths record join-by=rig" 0 "2" \ + bash -c 'test "$(grep -c "^[[:space:]]*JOIN_BY=rig$" "$1")" -eq 2; echo 2' _ \ + "$ROOT/commands/bootstrap.sh" +check "bootstrap: already-joined path defaults to join-by=preexisting" 0 "JOIN_BY=preexisting" \ + grep -F "JOIN_BY=preexisting" "$ROOT/commands/bootstrap.sh" + +# Drive the narrow inverse end to end. Every refusal also asserts the tailscale +# shim was NOT called: exit status alone would miss the destructive regression. +UNDO_FIX="$(mktemp -d)" +UNDO_BIN="$UNDO_FIX/bin" +UNDO_MARKER="$UNDO_FIX/role" +UNDO_RUNNER="$UNDO_FIX/runner" +UNDO_CALLS="$UNDO_FIX/tailscale.calls" +mkdir -p "$UNDO_BIN" "$UNDO_RUNNER" +cat > "$UNDO_BIN/tailscale" <<'SH' +#!/usr/bin/env bash +printf '%s\n' "$*" >> "$UNDO_CALLS" +if [ "${TAILSCALE_LOGOUT_FAIL:-0}" = 1 ]; then exit 1; fi +SH +cat > "$UNDO_BIN/id" <<'SH' +#!/usr/bin/env bash +if [ "${1:-}" = -u ]; then printf '0\n'; else exec /usr/bin/id "$@"; fi +SH +chmod +x "$UNDO_BIN/tailscale" "$UNDO_BIN/id" +undo() { + env PATH="$UNDO_BIN:$PATH" UNDO_CALLS="$UNDO_CALLS" \ + RIG_ROLE_MARKER="$UNDO_MARKER" RIG_RUNNER_DIR="$UNDO_RUNNER" \ + "$ROOT/bin/rig" bootstrap --undo +} +undo_untouched() { + : > "$UNDO_CALLS" + if undo >/tmp/rig-undo.out 2>&1; then return 1; fi + [ ! -s "$UNDO_CALLS" ] +} +rm -f "$UNDO_MARKER" +check "bootstrap --undo: no marker refuses without touching tailnet" 0 "" undo_untouched +printf '%s\n' 'role=workload-server root-door=open host=no join=authkey' > "$UNDO_MARKER" +check "bootstrap --undo: old marker names missing provenance" \ + 1 "marker predates join-by provenance" undo +check "bootstrap --undo: old marker leaves tailnet untouched" 0 "" undo_untouched +printf '%s\n' 'role=workload-server root-door=open host=no join=authkey join-by=preexisting' > "$UNDO_MARKER" +check "bootstrap --undo: pre-existing join refuses by name" 1 "join-by=preexisting" undo +check "bootstrap --undo: pre-existing join leaves tailnet untouched" 0 "" undo_untouched +printf '%s\n' 'role=runner-server root-door=open host=no join=authkey join-by=rig' > "$UNDO_MARKER" +printf '%s\n' '{}' > "$UNDO_RUNNER/.runner" +check "bootstrap --undo: installed runner points at its removal verb" \ + 1 "rig runner remove" undo +check "bootstrap --undo: installed runner leaves tailnet untouched" 0 "" undo_untouched +rm -f "$UNDO_RUNNER/.runner" +check "bootstrap --undo: failed logout is loud" \ + 1 "role marker kept" env TAILSCALE_LOGOUT_FAIL=1 PATH="$UNDO_BIN:$PATH" \ + UNDO_CALLS="$UNDO_CALLS" RIG_ROLE_MARKER="$UNDO_MARKER" \ + RIG_RUNNER_DIR="$UNDO_RUNNER" "$ROOT/bin/rig" bootstrap --undo +check "bootstrap --undo: failed logout preserves the marker" 0 "" test -e "$UNDO_MARKER" +: > "$UNDO_CALLS" +check "bootstrap --undo: proven rig join succeeds" 0 "tailnet join removed" undo +check "bootstrap --undo: successful logout was called" 0 "logout" cat "$UNDO_CALLS" +check "bootstrap --undo: success removes the marker" 1 "" test -e "$UNDO_MARKER" +check "bootstrap --undo: second run refuses cleanly" 1 "no /etc/rig/role marker" undo +rm -rf "$UNDO_FIX" /tmp/rig-undo.out # ...and that it is written in the CURRENT vocabulary (#77). New markers say # root-door=; the retired class= spelling is something rig READS forever and # WRITES never, so a marker line that reintroduces it must not ship green.