diff --git a/commands/coolify-backup-install.sh b/commands/coolify-backup-install.sh index 4f9a64b..f7c0570 100755 --- a/commands/coolify-backup-install.sh +++ b/commands/coolify-backup-install.sh @@ -7,6 +7,10 @@ # already-filled bindings file is left untouched. set -euo pipefail +HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" +# shellcheck source=SCRIPTDIR/lib/users-config.sh +. "$HERE/lib/users-config.sh" # read_role_marker — the traits line bootstrap wrote + log() { printf 'rig-coolify-backup: %s\n' "$*"; } warn() { printf 'rig-coolify-backup: WARNING: %s\n' "$*" >&2; } die() { printf 'rig-coolify-backup: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } @@ -67,6 +71,18 @@ done [ -n "$PG_USER" ] || die "--pg-user must not be empty" 2 [ -n "$PG_DB" ] || die "--pg-db must not be empty" 2 +# --- role-marker sanity (issue #25) ------------------------------------------ +# Same advisory check as `rig coolify install`, same reasoning: this command +# dumps the CONTROL PLANE's database, so a marker naming any other role almost +# certainly means the wrong SSH session — but the marker is advisory and may be +# absent, so WARN, never die, and warn before the root check so the harness can +# prove it non-root (RIG_ROLE_MARKER points it at fixtures, repo precedent). +MARKER_LINE="$(read_role_marker "${RIG_ROLE_MARKER:-/etc/rig/role}")" +case "$MARKER_LINE" in + ""|"role=control-plane "*) ;; + *) warn "this box's role marker says '${MARKER_LINE}' — not a control-plane box. The nightly dump targets Coolify's own database, which lives on role control-plane; if this is the wrong box, stop here and re-check your SSH session." ;; +esac + # --- guards ---------------------------------------------------------------- [ "$(id -u)" -eq 0 ] || die "must run as root" if [ -r /etc/os-release ]; then diff --git a/commands/coolify-install.sh b/commands/coolify-install.sh index 9c18cdd..aee7868 100755 --- a/commands/coolify-install.sh +++ b/commands/coolify-install.sh @@ -4,8 +4,13 @@ # explicit act. set -euo pipefail -log() { printf 'rig-coolify: %s\n' "$*"; } -die() { printf 'rig-coolify: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } +HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" +# shellcheck source=SCRIPTDIR/lib/users-config.sh +. "$HERE/lib/users-config.sh" # read_role_marker — the traits line bootstrap wrote + +log() { printf 'rig-coolify: %s\n' "$*"; } +warn() { printf 'rig-coolify: WARNING: %s\n' "$*" >&2; } +die() { printf 'rig-coolify: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } usage() { cat <<'EOF' @@ -32,6 +37,26 @@ if [ -z "$VERSION" ]; then die "--version is required" 2 fi +# --- role-marker sanity (issue #25) ------------------------------------------ +# Coolify belongs on the control-plane box and nowhere else — but the marker is +# ADVISORY, never a gate. It may legitimately be absent (a box bootstrapped +# before rig wrote markers, or a hand-built one), and rig refuses to guess from +# silence. When the marker EXISTS and names another role, the likeliest story +# is an operator in the wrong SSH session about to put a control plane on a +# workload box — so say it loudly. But WARN, never die: the operator may also +# be deliberately repurposing the box, and an advisory file must never outrank +# the human running the command (contrast close-root, where the marker IS the +# gate — shutting the root door blind is irreversible in a way an extra +# Coolify is not). Placed BEFORE the root check for the same reason arg errors +# are: the harness proves it non-root, and reading a 0644 file needs no +# privilege. RIG_ROLE_MARKER overrides the path so tests point it at fixtures +# (repo precedent: users-apply, users-close-root). +MARKER_LINE="$(read_role_marker "${RIG_ROLE_MARKER:-/etc/rig/role}")" +case "$MARKER_LINE" in + ""|"role=control-plane "*) ;; + *) warn "this box's role marker says '${MARKER_LINE}' — not a control-plane box. Coolify belongs on role control-plane; if this is the wrong box, stop here and re-check your SSH session. Repurposing it on purpose? Re-run 'rig bootstrap control-plane' first so the marker tells the truth." ;; +esac + [ "$(id -u)" -eq 0 ] || die "must run as root" export AUTOUPDATE=false diff --git a/test/cli.sh b/test/cli.sh index 5449708..4429c55 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -200,6 +200,52 @@ else echo "skip: coolify backup non-root refusal (running as root)" fi +# --- role-marker sanity: coolify verbs off the control plane (#25) ----------- +# Both coolify commands read /etc/rig/role and WARN — never die — when the +# marker names a non-control-plane role: the likeliest story is the wrong SSH +# session, but the marker is advisory and must not outrank the operator. The +# warning fires BEFORE the root check (same testability rule as arg errors), +# so a non-root run prints it and then hits the root refusal — provable here +# with RIG_ROLE_MARKER pointed at fixtures (repo precedent: the close-root +# marker gate). Counting fires proves silence too: a control-plane marker, an +# absent marker, and a marker-less box must all stay quiet, because warning on +# absence would nag every pre-marker box on every legitimate run. +marker_warns() { # marker_warns — how many warnings fired + local marker="$1"; shift + env RIG_ROLE_MARKER="$marker" "$@" 2>&1 | grep -c "not a control-plane box" || true +} +MARKER_FIX="$(mktemp -d)" +printf 'role=workload class=server host=no join=authkey\n' > "$MARKER_FIX/workload" +printf 'role=control-plane class=server host=no join=authkey\n' > "$MARKER_FIX/control-plane" +if [ "$(id -u)" -ne 0 ]; then + check "coolify: warns on a non-control-plane marker" 0 "1" \ + marker_warns "$MARKER_FIX/workload" "$ROOT/commands/coolify-install.sh" --version 4.1.2 + check "coolify: control-plane marker stays silent" 0 "0" \ + marker_warns "$MARKER_FIX/control-plane" "$ROOT/commands/coolify-install.sh" --version 4.1.2 + check "coolify: absent marker stays silent (advisory, not a gate)" 0 "0" \ + marker_warns "$MARKER_FIX/absent" "$ROOT/commands/coolify-install.sh" --version 4.1.2 + # The warning must stay a warning: the run proceeds past it and stops at the + # root check (exit 1), never turned into a marker refusal. + check "coolify: the marker warns but never refuses" 1 "must run as root" \ + env RIG_ROLE_MARKER="$MARKER_FIX/workload" "$ROOT/commands/coolify-install.sh" --version 4.1.2 + check "coolify backup: warns on a non-control-plane marker" 0 "1" \ + marker_warns "$MARKER_FIX/workload" "$ROOT/commands/coolify-backup-install.sh" + check "coolify backup: control-plane marker stays silent" 0 "0" \ + marker_warns "$MARKER_FIX/control-plane" "$ROOT/commands/coolify-backup-install.sh" + check "coolify backup: the marker warns but never refuses" 1 "must run as root" \ + env RIG_ROLE_MARKER="$MARKER_FIX/workload" "$ROOT/commands/coolify-backup-install.sh" +else + echo "skip: coolify role-marker warning checks (running as root)" +fi +rm -rf "$MARKER_FIX" +# Root runs skip the live checks above, so also pin the warning's presence in +# both shipped scripts — a deleted advisory cannot ship green (repo precedent: +# the staging/runner tag greps). +check "coolify: marker warning present in the shipped script" 0 "" \ + grep -q "not a control-plane box" "$ROOT/commands/coolify-install.sh" +check "coolify backup: marker warning present in the shipped script" 0 "" \ + grep -q "not a control-plane box" "$ROOT/commands/coolify-backup-install.sh" + # --- rig db (ad-hoc dump/restore) ------------------------------------------- check "bare db shows usage, exit 2" 2 "usage:" "$ROOT/bin/rig" db check "db --help exits 0" 0 "usage:" "$ROOT/bin/rig" db --help