#!/usr/bin/env bash # Runner installer — runs inside the LXC, called by ct/runner.sh # # Allgemeiner, INSTANZWEITER Gitea-Actions-Runner ohne Deploy-Rechte: # - Node.js (für actions/checkout im Host-Mode) + git + rsync # - Docker (für Wegwerf-Test-Container in CI, z. B. pgvector) # - act_runner als unprivilegierter User "runner" in der docker-Gruppe # - systemd-Unit act-runner.service (Requires=docker.service) # # SICHERHEITSMODELL (Review-Finding 88, bewusst akzeptiert & dokumentiert): # docker-Gruppe == de-facto root IN DIESEM LXC. Da der Runner instanzweit # ist, kann jedes Repo der Gitea-Instanz via Workflow den Runner-Container # vollständig kontrollieren. Das ist hier akzeptiert, weil (a) alle Repos # der Instanz vom selben Admin (Lutz) stammen — kein Multi-Tenant — und # (b) dieser LXC GENAU DESHALB nichts besitzt: keine sudoers, keine # Produktions-Mounts, keine Secrets außer dem (geshredderten) Reg-Token. # Bei Öffnung der Instanz für Dritte: Docker rootless oder eigener Runner # pro Vertrauenszone. NICHT auf LXCs mit Deploy-Rechten übertragen. # # Idempotent: erneuter Lauf (auch OHNE /root/runner.deploy.env) überspringt # Vorhandenes und provisioniert nur nach — Registrierung braucht die env-Datei # nur beim Erstlauf (Finding 90). set -euo pipefail LIB_URL="${LIB_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/lib}" source <(curl -fsSL "$LIB_URL/install.func") [[ "$EUID" -eq 0 ]] || { msg_err "Must run as root"; exit 1; } APP_USER="runner" APP_HOME="/opt/runner" RUNNER_DIR="$APP_HOME/data" # ── Konfiguration laden (optional bei Re-Run, Finding 90) ───────────────────── CONF="/root/runner.deploy.env" if [[ -f "$CONF" ]]; then set -a; . "$CONF"; set +a fi RUNNER_NAME="${RUNNER_NAME:-$(hostname)}" RUNNER_LABELS="${RUNNER_LABELS:-homelab:host}" RUNNER_VERSION="${RUNNER_VERSION:-0.2.13}" NODE_MAJOR="${NODE_MAJOR:-22}" # ── Validierung (Finding 80): Werte landen in Shell-Fragmenten/systemd — # strikte Zeichenklassen statt Vertrauen. Re-Runs ohne CONF validieren # nur, was gesetzt ist; Registrierungs-Pflichtwerte prüft der Reg-Block. ── valid_url() { [[ "$1" =~ ^https?://[A-Za-z0-9.-]+(:[0-9]{1,5})?$ ]]; } valid_token() { [[ "$1" =~ ^[A-Za-z0-9_-]{16,128}$ ]]; } valid_token_word() { [[ "$1" =~ ^[A-Za-z0-9._:,-]+$ ]]; } [[ -z "${GITEA_INSTANCE_URL:-}" ]] || valid_url "$GITEA_INSTANCE_URL" \ || { msg_err "GITEA_INSTANCE_URL ungültig: nur http(s)://host[:port]"; exit 1; } [[ -z "${RUNNER_TOKEN:-}" ]] || valid_token "$RUNNER_TOKEN" \ || { msg_err "RUNNER_TOKEN ungültig (erwartet 16–128 Zeichen [A-Za-z0-9_-])"; exit 1; } valid_token_word "$RUNNER_NAME" || { msg_err "RUNNER_NAME enthält unzulässige Zeichen"; exit 1; } valid_token_word "$RUNNER_LABELS" || { msg_err "RUNNER_LABELS enthält unzulässige Zeichen"; 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; } # Finding 85 (Re-Review): act_runner schreibt .runner ins CWD — run_user wechselt # deshalb hart ins RUNNER_DIR (env -C), sonst landet die Registrierung in / und # der unprivilegierte User darf dort nicht schreiben. run_user() { runuser -u "$APP_USER" -- env -C "$RUNNER_DIR" HOME="$APP_HOME" "$@"; } # ── Pakete: git/rsync, Node (checkout-Action), Docker (Test-Container) ──────── setup_base_apt git rsync ca-certificates curl NODE_HAVE="$(command -v node >/dev/null 2>&1 && node -v | sed -E 's/^v([0-9]+).*/\1/' || echo 0)" if [[ "$NODE_HAVE" != "$NODE_MAJOR" ]]; then msg_info "Installing Node.js ${NODE_MAJOR}.x (NodeSource)..." curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - >/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"