feat: runner follow-ups — bootstrap role + latest-version resolution #4

Merged
dan-claude-bot merged 2 commits from feat/runner-bootstrap-role into main 2026-07-11 18:46:57 +00:00
5 changed files with 55 additions and 16 deletions
Showing only changes of commit e395d6754a - Show all commits

View file

@ -20,7 +20,7 @@ PATH (`/usr/local/bin` when root). Re-run any time to upgrade.
## 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
second run changes nothing.
@ -28,10 +28,13 @@ second run changes nothing.
```sh
rig bootstrap control-plane --hostname my-coolify-box
rig bootstrap workload --hostname my-prod-box
rig bootstrap runner --hostname my-ci-box
```
- `--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
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
lives in process memory only — rig never writes a credential to disk.
The two roles are identical today except the default hostname; they exist
because control-plane and workload boxes diverge over time, and because the
next command applies to exactly one of them.
`control-plane` and `workload` are identical today except the default
hostname; they exist because the boxes diverge over time, and because each
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>`
@ -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>`
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
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
```

View file

@ -8,10 +8,11 @@ usage() {
usage: rig <command> [args]
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,
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>
Pinned Coolify install (AUTOUPDATE=false). Control-plane box only.
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() {
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)
--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
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) ---------
ROLE="${1:-}"
case "$ROLE" in
control-plane|workload) shift ;;
control-plane|workload|runner) shift ;;
-h|--help) usage; exit 0 ;;
"") usage >&2; die "role required (control-plane|workload)" 2 ;;
*) die "unknown role: $ROLE (want control-plane|workload)" 2 ;;
"") usage >&2; die "role required (control-plane|workload|runner)" 2 ;;
*) die "unknown role: $ROLE (want control-plane|workload|runner)" 2 ;;
esac
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
case "$1" in
--hostname)
@ -42,6 +49,12 @@ while [ $# -gt 0 ]; do
esac
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 ------------------------------------------------------------------
[ "$(id -u)" -eq 0 ] || die "must run as root"
if [ -r /etc/os-release ]; then
@ -109,4 +122,6 @@ fi
log "done — role ${ROLE}, hostname ${TS_HOSTNAME}"
if [ "$ROLE" = "control-plane" ]; then
log "next: rig coolify install --version <pin>"
elif [ "$ROLE" = "runner" ]; then
log "next: rig runner install --repo <owner/repo> --version <pin>"
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)
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 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: 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
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
echo "skip: bootstrap non-root refusal (running as root)"
echo "skip: bootstrap non-root refusals (running as root)"
fi
check "coolify: version required, exit 2" 2 "--version" "$ROOT/commands/coolify-install.sh"