feat: runner bootstrap role — defaults tag:ci, refuses tag:server

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
claude-hdb 2026-07-11 18:25:47 +00:00
parent d9bb83a915
commit e395d6754a
5 changed files with 55 additions and 16 deletions

View file

@ -20,7 +20,7 @@ PATH (`/usr/local/bin` when root). Re-run any time to upgrade.
## Commands ## Commands
### `rig bootstrap <control-plane|workload>` ### `rig bootstrap <control-plane|workload|runner>`
Run as root on the fresh box (over SSH). Convergent — safe to re-run; a Run as root on the fresh box (over SSH). Convergent — safe to re-run; a
second run changes nothing. second run changes nothing.
@ -28,10 +28,13 @@ second run changes nothing.
```sh ```sh
rig bootstrap control-plane --hostname my-coolify-box rig bootstrap control-plane --hostname my-coolify-box
rig bootstrap workload --hostname my-prod-box rig bootstrap workload --hostname my-prod-box
rig bootstrap runner --hostname my-ci-box
``` ```
- `--hostname <name>` — tailnet hostname (default: the role name) - `--hostname <name>` — tailnet hostname (default: the role name)
- `--ts-tag <tag>` — tailnet tag to advertise (default: `tag:server`) - `--ts-tag <tag>` — tailnet tag to advertise (default: `tag:server`;
the `runner` role defaults to `tag:ci` instead, and **refuses**
`tag:server` outright — see below)
What it does: installs `curl ca-certificates unattended-upgrades` (and What it does: installs `curl ca-certificates unattended-upgrades` (and
enables periodic unattended upgrades); writes an sshd hardening drop-in enables periodic unattended upgrades); writes an sshd hardening drop-in
@ -42,9 +45,14 @@ tailscale and joins your tailnet.
the interactive prompt. Use a **single-use, tagged, short-expiry** key. It the interactive prompt. Use a **single-use, tagged, short-expiry** key. It
lives in process memory only — rig never writes a credential to disk. lives in process memory only — rig never writes a credential to disk.
The two roles are identical today except the default hostname; they exist `control-plane` and `workload` are identical today except the default
because control-plane and workload boxes diverge over time, and because the hostname; they exist because the boxes diverge over time, and because each
next command applies to exactly one of them. follow-up command applies to exactly one role. `runner` is the box a CI
agent will live on, and it differs behaviorally: it defaults `--ts-tag` to
`tag:ci` and **refuses `tag:server`** — a runner executes repo-controlled
code, and advertising your server tag would extend every grant your servers
hold (SSH between them, say) to that code. The refusal turns the worst
misconfiguration from a documentation warning into a hard error.
### `rig coolify install --version <pin>` ### `rig coolify install --version <pin>`
@ -55,10 +63,11 @@ explicit re-run with a new pin. The pin is required; there is no default.
### `rig runner install --repo <owner/repo> --version <pin>` ### `rig runner install --repo <owner/repo> --version <pin>`
Workload box only, run after `rig bootstrap workload`: Runner box only, run after `rig bootstrap runner` (the same two-step rhythm
as `bootstrap control-plane``coolify install`):
```sh ```sh
rig bootstrap workload --hostname my-ci-box --ts-tag tag:ci rig bootstrap runner --hostname my-ci-box
rig runner install --repo acme/widgets --version 2.335.1 rig runner install --repo acme/widgets --version 2.335.1
``` ```

View file

@ -8,10 +8,11 @@ usage() {
usage: rig <command> [args] usage: rig <command> [args]
commands: commands:
bootstrap <control-plane|workload> [--hostname <name>] [--ts-tag <tag>] bootstrap <control-plane|workload|runner> [--hostname <name>] [--ts-tag <tag>]
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 tailnet pre-auth key
(TS_AUTHKEY env overrides the prompt). Run as root. (TS_AUTHKEY env overrides the prompt). Run as root. Role runner
defaults to tag:ci and refuses 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.
runner install --repo <owner/repo> --version <pin> [options] runner install --repo <owner/repo> --version <pin> [options]

View file

@ -9,10 +9,13 @@ die() { printf 'rig-bootstrap: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
usage() { usage() {
cat <<'EOF' cat <<'EOF'
usage: rig bootstrap <control-plane|workload> [--hostname <name>] [--ts-tag <tag>] usage: rig bootstrap <control-plane|workload|runner> [--hostname <name>] [--ts-tag <tag>]
--hostname tailnet hostname (default: the role name) --hostname tailnet hostname (default: the role name)
--ts-tag tailnet tag to advertise (default: tag:server) --ts-tag tailnet tag to advertise (default: tag:server;
role runner defaults to tag:ci and refuses tag:server —
a CI box executes repo-controlled code, and your server
tag's grants must never extend to it)
Provide the single-use tailscale pre-auth key via the TS_AUTHKEY env var, or Provide the single-use tailscale pre-auth key via the TS_AUTHKEY env var, or
enter it at the interactive prompt. It is used once and never written to disk. enter it at the interactive prompt. It is used once and never written to disk.
@ -22,14 +25,18 @@ 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) shift ;; control-plane|workload|runner) shift ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
"") usage >&2; die "role required (control-plane|workload)" 2 ;; "") usage >&2; die "role required (control-plane|workload|runner)" 2 ;;
*) die "unknown role: $ROLE (want control-plane|workload)" 2 ;; *) die "unknown role: $ROLE (want control-plane|workload|runner)" 2 ;;
esac esac
TS_HOSTNAME="$ROLE" TS_HOSTNAME="$ROLE"
TS_TAG="tag:server" if [ "$ROLE" = "runner" ]; then
TS_TAG="tag:ci"
else
TS_TAG="tag:server"
fi
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--hostname) --hostname)
@ -42,6 +49,12 @@ while [ $# -gt 0 ]; do
esac esac
done done
# A runner executes repo-controlled code; advertising the server tag would
# extend every grant your servers hold to that code. Refused, not warned.
if [ "$ROLE" = "runner" ] && [ "$TS_TAG" = "tag:server" ]; then
die "role runner must not advertise tag:server" 2
fi
# --- guards ------------------------------------------------------------------ # --- guards ------------------------------------------------------------------
[ "$(id -u)" -eq 0 ] || die "must run as root" [ "$(id -u)" -eq 0 ] || die "must run as root"
if [ -r /etc/os-release ]; then if [ -r /etc/os-release ]; then
@ -109,4 +122,6 @@ fi
log "done — role ${ROLE}, hostname ${TS_HOSTNAME}" log "done — role ${ROLE}, hostname ${TS_HOSTNAME}"
if [ "$ROLE" = "control-plane" ]; then if [ "$ROLE" = "control-plane" ]; then
log "next: rig coolify install --version <pin>" log "next: rig coolify install --version <pin>"
elif [ "$ROLE" = "runner" ]; then
log "next: rig runner install --repo <owner/repo> --version <pin>"
fi fi

View file

@ -274,6 +274,18 @@ git commit -m "docs: README section for runner install"
--- ---
## Addendum (2026-07-11, operator-requested, post final review)
A third bootstrap role, `runner`, joins `control-plane|workload` — requested
for CLI consistency (each follow-up command applies to exactly one role) and
because it closes a real footgun mechanically: the role defaults `--ts-tag`
to `tag:ci` and **refuses `tag:server`** (exit 2, validated before the root
check). Forgetting the tag flag previously joined the CI box with the default
server tag — the exact misconfiguration the runner posture exists to prevent.
Everything else about bootstrap is unchanged; `runner install`'s contract is
untouched. Tests: +2 (`runner refuses tag:server`, `runner role parses /
refuses non-root`) → 27 non-root.
## Integration (orchestrator, after final review — not an SDD task) ## Integration (orchestrator, after final review — not an SDD task)
1. Push the branch to the fork and open the PR **against upstream**: 1. Push the branch to the fork and open the PR **against upstream**:

View file

@ -36,10 +36,12 @@ check "bootstrap: --help exits 0" 0 "usage:" "$ROOT/commands/bo
check "bootstrap: unknown role exits 2" 2 "unknown role" "$ROOT/commands/bootstrap.sh" potato check "bootstrap: unknown role exits 2" 2 "unknown role" "$ROOT/commands/bootstrap.sh" potato
check "bootstrap: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap.sh" workload --nope check "bootstrap: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap.sh" workload --nope
check "bootstrap: hostname needs value" 2 "needs a value" "$ROOT/commands/bootstrap.sh" workload --hostname check "bootstrap: hostname needs value" 2 "needs a value" "$ROOT/commands/bootstrap.sh" workload --hostname
check "bootstrap: runner refuses tag:server" 2 "must not advertise tag:server" "$ROOT/commands/bootstrap.sh" runner --ts-tag tag:server
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
else else
echo "skip: bootstrap non-root refusal (running as root)" echo "skip: bootstrap non-root refusals (running as root)"
fi fi
check "coolify: version required, exit 2" 2 "--version" "$ROOT/commands/coolify-install.sh" check "coolify: version required, exit 2" 2 "--version" "$ROOT/commands/coolify-install.sh"