diff --git a/ct/runner.sh b/ct/runner.sh new file mode 100644 index 0000000..7323256 --- /dev/null +++ b/ct/runner.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# Allgemeiner Gitea-Actions-Runner — instanzweit, OHNE Deploy-Rechte +# +# Motivation (nexus K-114-Blocker): Der nexus-Runner läuft auf dem +# Produktions-LXC und besitzt sudoers-Deploy-Rechte — er darf deshalb NICHT +# instanzweit registriert werden (jedes Repo könnte sonst Workflows auf der +# Produktionsmaschine ausführen). Dieser LXC ist die saubere Trennung: +# - instanzweite Registrierung (Site Administration → Actions → Runners) +# - Label "homelab:host" (Deploy-Jobs bleiben auf "nexus") +# - KEINE sudoers-Regeln, kein Zugriff auf Produktions-Verzeichnisse +# - Docker via Nesting für Wegwerf-Test-Container (z. B. pgvector in CI) +# +# Sicherheitsmodell: siehe Kopfkommentar in install/runner-install.sh — +# instanzweit + docker-Gruppe heißt: jedes Repo der Instanz kann diesen LXC +# kontrollieren. Akzeptiert, WEIL er nichts besitzt. Nicht auf privilegierte +# LXCs übertragen. +# +# Run on a Proxmox VE host: +# bash -c "$(curl -fsSL https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/ct/runner.sh)" + +set -euo pipefail + +APP="runner" +APP_DESCRIPTION="Allgemeiner Gitea-Actions-Runner (instanzweit, Label homelab, Docker, ohne Deploy-Rechte)" + +LIB_URL="${LIB_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/lib}" +INSTALL_SCRIPT_URL="${INSTALL_SCRIPT_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/install/runner-install.sh}" + +source <(curl -fsSL "$LIB_URL/build.func") + +DEFAULT_HOSTNAME="runner" +DEFAULT_DISK="30" +DEFAULT_CORES="4" +DEFAULT_RAM="6144" + +# ── Validierung (Review-Finding 80): Werte wandern in die Deploy-Env und in +# systemd/argv — strikte Zeichenklassen, Re-Prompt statt Abbruch. +# Bewusst LOKALE Validatoren: die Libs werden zur Laufzeit von main geladen, +# dieses Script muss aber unabhängig vom Merge-Stand der K-114-Helfer +# (prompt_validated/require_valid) funktionieren. Semantik ist identisch; +# Konsolidierung auf die build.func-Helfer ist als Follow-up notiert. ────── +_valid_url() { [[ "$1" =~ ^https?://[A-Za-z0-9.-]+(:[0-9]{1,5})?$ ]]; } +_valid_token() { [[ "$1" =~ ^[A-Za-z0-9_-]{16,128}$ ]]; } +_valid_word() { [[ "$1" =~ ^[A-Za-z0-9._:,-]+$ ]]; } + +_prompt_until_valid() { # var prompt default validator secret(0|1) + local __var="$1" __prompt="$2" __default="$3" __validator="$4" __secret="${5:-0}" __val + while true; do + if [[ "$__secret" == "1" ]]; then + read -rsp "$__prompt" __val || { echo; msg_err "Eingabe abgebrochen (EOF)"; exit 1; } + echo + else + read -rp "$__prompt" __val || { echo; msg_err "Eingabe abgebrochen (EOF)"; exit 1; } + fi + __val="${__val:-$__default}" + if [[ -n "$__val" ]] && "$__validator" "$__val"; then + printf -v "$__var" '%s' "$__val" + return 0 + fi + msg_warn "Ungültige Eingabe — bitte erneut (kein Paste mit Sonderzeichen)." + done +} + +prompt_app_config() { + echo + echo "── Runner configuration ─────────────────────────────────────" + # env-präsetzte Werte werden validiert (Abbruch bei ungültig — K-114-Konvention), + # interaktive Eingaben re-prompten bis gültig. + if [[ -n "${GITEA_INSTANCE_URL:-}" ]]; then + _valid_url "$GITEA_INSTANCE_URL" || { msg_err "GITEA_INSTANCE_URL (env) ungültig"; exit 1; } + else + _prompt_until_valid GITEA_INSTANCE_URL \ + "Gitea instance URL [https://gitea.luki-net.org]: " \ + "https://gitea.luki-net.org" _valid_url 0 + fi + # WICHTIG: den INSTANZWEITEN Token verwenden + # (Site Administration → Actions → Runners → Create new runner), + # NICHT den Repo-Token — sonst wiederholt sich der K-114-Scope-Blocker. + if [[ -n "${RUNNER_TOKEN:-}" ]]; then + _valid_token "$RUNNER_TOKEN" || { msg_err "RUNNER_TOKEN (env) ungültig (16–128 Zeichen [A-Za-z0-9_-])"; exit 1; } + else + _prompt_until_valid RUNNER_TOKEN \ + "INSTANZWEITER Runner-Registration-Token: " \ + "" _valid_token 1 + fi + RUNNER_NAME="${RUNNER_NAME:-$CT_HOSTNAME}" + RUNNER_LABELS="${RUNNER_LABELS:-homelab:host}" + RUNNER_VERSION="${RUNNER_VERSION:-0.2.13}" + NODE_MAJOR="${NODE_MAJOR:-22}" + _valid_word "$RUNNER_NAME" || { msg_err "RUNNER_NAME ungültig"; exit 1; } + _valid_word "$RUNNER_LABELS" || { msg_err "RUNNER_LABELS ungültig"; exit 1; } + [[ "$RUNNER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { msg_err "RUNNER_VERSION ungültig"; exit 1; } + [[ "$NODE_MAJOR" =~ ^[0-9]+$ ]] || { msg_err "NODE_MAJOR ungültig"; exit 1; } + echo " → instance: $GITEA_INSTANCE_URL" + echo " → runner: $RUNNER_NAME labels: $RUNNER_LABELS (act_runner $RUNNER_VERSION, host mode, scope: INSTANZ)" +} + +push_app_config() { + msg_info "Pushing runner config into container..." + local tmpf; tmpf=$(mktemp) + # Werte sind oben strikt validiert (keine Quotes/Whitespace möglich) — + # damit ist die env-Datei frei von Quoting-/Injection-Fallen (vgl. Finding 76). + cat >"$tmpf" </dev/null 2>&1 && break + sleep 2 + done + msg_ok "Container restarted with nesting enabled" +} + +print_app_summary() { + local runner_state + runner_state=$(pct exec "$CTID" -- systemctl is-active act-runner.service 2>/dev/null | tr -d '\r\n') + cat </dev/null + apt-get install -y -qq nodejs >/dev/null + msg_ok "Node $(node -v) installed" +else + msg_warn "Node $(node -v) already present, skipping" +fi + +if ! command -v docker >/dev/null 2>&1; then + msg_info "Installing Docker (get.docker.com)..." + curl -fsSL https://get.docker.com | sh >/dev/null + msg_ok "Docker $(docker --version | awk '{print $3}' | tr -d ',')" +else + msg_warn "Docker already present, skipping" +fi + +# ── act_runner binary ───────────────────────────────────────────────────────── +if [[ ! -x /usr/local/bin/act_runner ]]; then + ARCH="$(dpkg --print-architecture)" + case "$ARCH" in amd64|arm64) ;; *) msg_err "unsupported arch: $ARCH"; exit 1 ;; esac + msg_info "Downloading act_runner $RUNNER_VERSION ($ARCH)..." + curl -fsSL "https://dl.gitea.com/act_runner/${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-linux-${ARCH}" \ + -o /usr/local/bin/act_runner + chmod +x /usr/local/bin/act_runner + msg_ok "act_runner $(/usr/local/bin/act_runner --version 2>/dev/null | head -n1)" +else + msg_warn "act_runner already present, skipping download" +fi + +# ── User (docker-Gruppe VOR dem Daemon-Start — Lesson aus nexus K-103: +# Gruppenmitgliedschaften werden beim Prozessstart eingefroren) ───────────── +create_system_user "$APP_USER" "$APP_HOME" +usermod -aG docker "$APP_USER" +mkdir -p "$RUNNER_DIR" +chown -R "$APP_USER:$APP_USER" "$APP_HOME" + +# ── Registrierung (idempotent; braucht CONF-Werte nur beim Erstlauf) ────────── +if [[ ! -f "$RUNNER_DIR/.runner" ]]; then + : "${GITEA_INSTANCE_URL:?missing GITEA_INSTANCE_URL (Erstlauf braucht /root/runner.deploy.env)}" + : "${RUNNER_TOKEN:?missing RUNNER_TOKEN (Erstlauf braucht /root/runner.deploy.env)}" + msg_info "Registering runner '$RUNNER_NAME' [$RUNNER_LABELS] with $GITEA_INSTANCE_URL (instance scope)..." + # Werte sind oben strikt validiert (keine Quotes/Whitespace möglich) — + # Übergabe als argv an runuser, keine erneute Shell-Interpolation (vgl. Finding 76). + # CWD = RUNNER_DIR via run_user (Finding 85). + run_user /usr/local/bin/act_runner register \ + --no-interactive \ + --config /dev/null \ + --instance "$GITEA_INSTANCE_URL" \ + --token "$RUNNER_TOKEN" \ + --name "$RUNNER_NAME" \ + --labels "$RUNNER_LABELS" \ + 2>&1 | sed "s#$RUNNER_TOKEN##g" || { msg_err "Registrierung fehlgeschlagen"; exit 1; } + # Belt-and-suspenders: falls eine künftige act_runner-Version doch ins HOME schreibt + if [[ -f "$APP_HOME/.runner" && ! -f "$RUNNER_DIR/.runner" ]]; then + mv "$APP_HOME/.runner" "$RUNNER_DIR/.runner" + fi + [[ -f "$RUNNER_DIR/.runner" ]] || { msg_err ".runner nach Registrierung nicht gefunden"; exit 1; } + chown -R "$APP_USER:$APP_USER" "$RUNNER_DIR" + msg_ok "Runner registered" +else + msg_warn "Runner already registered (.runner exists), skipping" +fi + +# ── SSH-Root-Login-Policy anwenden, falls vom Host-Script übergeben +# (Funktion existiert in install.func ab K-114/PR #5 — guarded Aufruf, +# damit dieser Branch vor und nach dem Merge funktioniert) ──────────────── +if declare -F configure_ssh_root_login >/dev/null 2>&1; then + configure_ssh_root_login +fi + +# ── systemd-Unit (bewusst KEINE sudoers-Datei; Findings 82 + 70) ────────────── +cat >/etc/systemd/system/act-runner.service <"$CRED_FILE" <} +Name/Label: $RUNNER_NAME [$RUNNER_LABELS] +User: $APP_USER (docker-Gruppe, KEIN sudo) +Workdir: $RUNNER_DIR +Logs: journalctl -u act-runner -f + +Repos nutzen ihn mit: runs-on: ${RUNNER_LABELS%%:*} + +SICHERHEITSMODELL: docker-Gruppe == de-facto root in DIESEM LXC; instanzweit +heißt: jedes Repo der Instanz kann den Runner-LXC kontrollieren. Akzeptiert, +weil Single-Admin-Instanz und dieser LXC nichts besitzt (keine sudoers, keine +Produktions-Mounts). Deploy-Jobs gehören NICHT hierher — die bleiben auf dem +repo-scoped nexus-Runner. Trennung beibehalten. +EOF +chmod 600 "$CRED_FILE" + +if [[ -f "$CONF" ]]; then + shred -u "$CONF" 2>/dev/null || rm -f "$CONF" +fi +apt_cleanup +msg_ok "runner installation finished"