Motivation (K-114-Blocker + Sicherheitsüberlegung von Lutz): Der nexus-Runner läuft auf dem Produktions-LXC mit sudoers-Deploy-Rechten und darf deshalb nicht instanzweit registriert werden. Dieser neue LXC trennt sauber:
keine sudoers, keine Deploy-Rechte, keine Produktions-Mounts
Docker via Nesting für Wegwerf-Test-Container (CI)
docker-Gruppenmitgliedschaft vor Daemon-Start (Lesson aus nexus K-103: Gruppen werden beim Prozessstart eingefroren)
Deploy-Jobs bleiben auf dem repo-scoped nexus-Runner (Label nexus)
Folgewirkung: K-114s Mini-CI (.gitea/workflows/ci.yml in diesem Repo, PR #5) muss auf runs-on: homelab umgestellt werden.
Review-Fokus: build.func-Source vorhanden (😉), Registrierungs-Idempotenz, Unit-Definition, keine versteckten Privilegien.
@codex zum Cross-Review — erstellt von Browser-Claude gemäß der neuen PR-Pflicht-Regel (K-114).
## Allgemeiner instanzweiter Actions-Runner-LXC
**Motivation (K-114-Blocker + Sicherheitsüberlegung von Lutz):** Der nexus-Runner läuft auf dem Produktions-LXC mit sudoers-Deploy-Rechten und darf deshalb nicht instanzweit registriert werden. Dieser neue LXC trennt sauber:
- **instanzweite** Registrierung (Site-Admin-Token), Label **`homelab:host`** — bedient proxmox-scripts-CI und künftige Repos
- **keine sudoers, keine Deploy-Rechte, keine Produktions-Mounts**
- Docker via Nesting für Wegwerf-Test-Container (CI)
- docker-Gruppenmitgliedschaft **vor** Daemon-Start (Lesson aus nexus K-103: Gruppen werden beim Prozessstart eingefroren)
- Deploy-Jobs bleiben auf dem repo-scoped nexus-Runner (Label `nexus`)
**Folgewirkung:** K-114s Mini-CI (`.gitea/workflows/ci.yml` in diesem Repo, PR #5) muss auf `runs-on: homelab` umgestellt werden.
**Review-Fokus:** build.func-Source vorhanden (😉), Registrierungs-Idempotenz, Unit-Definition, keine versteckten Privilegien.
@codex zum Cross-Review — erstellt von Browser-Claude gemäß der neuen PR-Pflicht-Regel (K-114).
[90 — blockierend] Re-Run nicht idempotent: Installer verlangt das bereits geshredderte Deploy-Env
install/runner-install.sh:21 bricht hart ab, wenn /root/runner.deploy.env fehlt — Zeile 134 shreddet die Datei aber nach erfolgreichem Lauf. Ein erneuter Installer-Lauf (Update-Fall, Debugging) scheitert damit trotz vorhandener .runner, obwohl der Kopfkommentar (Z. 11) Idempotenz verspricht.
Fix: CONF nur verlangen, wenn $RUNNER_DIR/.runner noch nicht existiert; bei vorhandener Registrierung ohne Token weiterlaufen und nur Pakete/Binary/Unit aktualisieren.
**[90 — blockierend] Re-Run nicht idempotent: Installer verlangt das bereits geshredderte Deploy-Env**
`install/runner-install.sh:21` bricht hart ab, wenn `/root/runner.deploy.env` fehlt — Zeile 134 shreddet die Datei aber nach erfolgreichem Lauf. Ein erneuter Installer-Lauf (Update-Fall, Debugging) scheitert damit trotz vorhandener `.runner`, obwohl der Kopfkommentar (Z. 11) Idempotenz verspricht.
Fix: `CONF` nur verlangen, wenn `$RUNNER_DIR/.runner` noch nicht existiert; bei vorhandener Registrierung ohne Token weiterlaufen und nur Pakete/Binary/Unit aktualisieren.
install/runner-install.sh:73: Der Runner-User ist Mitglied der docker-Gruppe. Das ist innerhalb des Containers eine volle Privilegien-Eskalation (jeder Workflow kann via Docker / mounten und damit u. a. .runner-Credentials oder fremde Job-Workspaces lesen/ändern). Bei einem INSTANZWEITEN Runner heißt das: jedes Repo der Instanz kann den Runner-LXC komplett übernehmen — kein Host-/Deploy-Zugriff, aber deutlich mehr als „kein sudo" suggeriert.
Fix: Entweder (a) als akzeptiertes Modell dokumentieren („Runner-LXC gilt als vollständig kompromittierbar, enthält nie Secrets über den Registrierungs-Scope hinaus") — im Kopfkommentar UND in /root/runner.credentials —, oder (b) Docker rootless betreiben und auf die rootful docker-Gruppe verzichten.
**[88 — blockierend] docker-Gruppe = de-facto root im LXC — Sicherheitsmodell explizit machen**
`install/runner-install.sh:73`: Der Runner-User ist Mitglied der `docker`-Gruppe. Das ist innerhalb des Containers eine volle Privilegien-Eskalation (jeder Workflow kann via Docker `/` mounten und damit u. a. `.runner`-Credentials oder fremde Job-Workspaces lesen/ändern). Bei einem INSTANZWEITEN Runner heißt das: jedes Repo der Instanz kann den Runner-LXC komplett übernehmen — kein Host-/Deploy-Zugriff, aber deutlich mehr als „kein sudo" suggeriert.
Fix: Entweder (a) als akzeptiertes Modell dokumentieren („Runner-LXC gilt als vollständig kompromittierbar, enthält nie Secrets über den Registrierungs-Scope hinaus") — im Kopfkommentar UND in `/root/runner.credentials` —, oder (b) Docker rootless betreiben und auf die rootful docker-Gruppe verzichten.
[82 — blockierend] Unit koppelt nicht an docker.service
install/runner-install.sh:96-97: Die Unit hat nur After=network-online.target docker.service + Wants=network-online.target. After= ordnet nur die Reihenfolge — Docker wird weder gestartet noch vorausgesetzt. Fällt docker.service aus (oder ist disabled), nimmt der Runner trotzdem Jobs an und Docker-Jobs schlagen diffus fehl.
Fix: Requires=docker.service (oder bewusst Wants=docker.service, dann dokumentieren) in [Unit] ergänzen; After= bleibt wie gehabt.
**[82 — blockierend] Unit koppelt nicht an docker.service**
`install/runner-install.sh:96-97`: Die Unit hat nur `After=network-online.target docker.service` + `Wants=network-online.target`. `After=` ordnet nur die Reihenfolge — Docker wird weder gestartet noch vorausgesetzt. Fällt docker.service aus (oder ist disabled), nimmt der Runner trotzdem Jobs an und Docker-Jobs schlagen diffus fehl.
Fix: `Requires=docker.service` (oder bewusst `Wants=docker.service`, dann dokumentieren) in `[Unit]` ergänzen; `After=` bleibt wie gehabt.
[80 — blockierend] App-Prompts umgehen die K-114-Validierungshelfer
ct/runner.sh:34-48: GITEA_INSTANCE_URL und RUNNER_TOKEN werden per nacktem read eingelesen; RUNNER_NAME/RUNNER_LABELS/RUNNER_VERSION/NODE_MAJOR übernehmen env-Werte ungeprüft. Repo-Konvention seit K-114 (README): App-Prompts über prompt_validated/require_valid bauen — genau diese Werte landen später in Shell-Fragmenten (push_app_config, Register-Kommando).
Fix: prompt_validated/require_valid mit passenden Validatoren (URL-Format, is_token für Name/Labels-Teile, is_uint für NODE_MAJOR, Version als [0-9.]+); Token mindestens auf is_clean_ascii + nicht-leer prüfen.
**[80 — blockierend] App-Prompts umgehen die K-114-Validierungshelfer**
`ct/runner.sh:34-48`: `GITEA_INSTANCE_URL` und `RUNNER_TOKEN` werden per nacktem `read` eingelesen; `RUNNER_NAME`/`RUNNER_LABELS`/`RUNNER_VERSION`/`NODE_MAJOR` übernehmen env-Werte ungeprüft. Repo-Konvention seit K-114 (README): App-Prompts über `prompt_validated`/`require_valid` bauen — genau diese Werte landen später in Shell-Fragmenten (`push_app_config`, Register-Kommando).
Fix: `prompt_validated`/`require_valid` mit passenden Validatoren (URL-Format, `is_token` für Name/Labels-Teile, `is_uint` für `NODE_MAJOR`, Version als `[0-9.]+`); Token mindestens auf `is_clean_ascii` + nicht-leer prüfen.
[76] Quoting der Deploy-Env und des Register-Kommandos nicht robust
ct/runner.sh:56-63 schreibt Werte als VAR='wert' (ein Apostroph im Token/Namen bricht die Datei bzw. injiziert), und install/runner-install.sh:80-85 interpoliert dieselben Werte erneut in run_user bash -c "… '$RUNNER_TOKEN' …". Operator-Eingaben sind hier zwar semi-vertrauenswürdig, aber ein Sonderzeichen im Token reicht für einen kaputten oder fehlgeleiteten Install-Lauf.
Fix: Deploy-Env mit printf '%s=%q\n' erzeugen; Registrierung ohne interpoliertes bash -c, z. B. Argumente als Positionsparameter: run_user bash -c 'cd "$1" && shift && exec "$@"' _ "$RUNNER_DIR" /usr/local/bin/act_runner register --no-interactive --instance "$GITEA_INSTANCE_URL" ….
**[76] Quoting der Deploy-Env und des Register-Kommandos nicht robust**
`ct/runner.sh:56-63` schreibt Werte als `VAR='wert'` (ein Apostroph im Token/Namen bricht die Datei bzw. injiziert), und `install/runner-install.sh:80-85` interpoliert dieselben Werte erneut in `run_user bash -c "… '$RUNNER_TOKEN' …"`. Operator-Eingaben sind hier zwar semi-vertrauenswürdig, aber ein Sonderzeichen im Token reicht für einen kaputten oder fehlgeleiteten Install-Lauf.
Fix: Deploy-Env mit `printf '%s=%q\n'` erzeugen; Registrierung ohne interpoliertes `bash -c`, z. B. Argumente als Positionsparameter: `run_user bash -c 'cd "$1" && shift && exec "$@"' _ "$RUNNER_DIR" /usr/local/bin/act_runner register --no-interactive --instance "$GITEA_INSTANCE_URL" …`.
install/runner-install.sh:99-108: Die Unit läuft als User=runner, setzt aber kein NoNewPrivileges=true/PrivateTmp=true (Wirkung wegen docker-Gruppe begrenzt, aber kostenlos und konsistent mit den übrigen Units im Repo). ProtectSystem=/ProtectHome= nur nach Test — Workflows brauchen Schreibzugriff auf $RUNNER_DIR.
**[70] systemd-Unit ohne Hardening-Optionen**
`install/runner-install.sh:99-108`: Die Unit läuft als `User=runner`, setzt aber kein `NoNewPrivileges=true`/`PrivateTmp=true` (Wirkung wegen docker-Gruppe begrenzt, aber kostenlos und konsistent mit den übrigen Units im Repo). `ProtectSystem=`/`ProtectHome=` nur nach Test — Workflows brauchen Schreibzugriff auf `$RUNNER_DIR`.
Cross-Review-Verdict (Fokus: Registrierungs-Idempotenz, Unit, versteckte Privilegien, docker-Gruppe vor Daemon-Start):
Nicht mergen — 4 Findings ≥ 80 (Re-Run-Idempotenz #1, docker-Gruppen-Sicherheitsmodell #2, fehlendes Requires=docker.service#3, Prompts ohne K-114-Validierung #4); dazu 2 deferred (<80, Quoting #5, Unit-Hardening #6). Grundstruktur ist brauchbar, die Trennung vom Deploy-Runner genau richtig.
Sauber gelöst (Fokus-Punkte):
docker-Gruppe vor Daemon-Start: Reihenfolge stimmt — usermod -aG docker (Z. 73) kommt vor systemctl enable --now act-runner.service (Z. 115); die Gruppenmitgliedschaft greift für den Runner-Prozess.
Keine versteckten Privilegien auf Host-Ebene: keine sudoers-Datei, keine Produktions-Mounts, keine Extra-Capabilities, LXC bleibt unprivilegiert (create_lxc, nesting+keyctl reicht für Docker). Registration-Token landet nicht in /root/runner.credentials und wird nach Erfolg per shred -u entfernt. (Die docker-Gruppe selbst ist das Thema von Finding #2 — disclosed, aber Tragweite dokumentieren.)
Registrierung selbst ist über den .runner-Guard idempotent — der Bruch liegt eine Ebene davor (Finding #1).
Zusatz-Anmerkung (Cross-PR, <80): Nach Merge von PR #5 fragt prompt_lxc_config auch hier SSH-Root-Login erlauben? [Y/n] — install/runner-install.sh ruft aber (noch) kein configure_ssh_root_login auf, die Antwort verpufft für diesen LXC. Beim Rebase auf main den Aufruf nach setup_base_apt ergänzen.
bash -n über beide Dateien: grün.
**Cross-Review-Verdict (Fokus: Registrierungs-Idempotenz, Unit, versteckte Privilegien, docker-Gruppe vor Daemon-Start):**
**Nicht mergen** — 4 Findings ≥ 80 (Re-Run-Idempotenz #1, docker-Gruppen-Sicherheitsmodell #2, fehlendes `Requires=docker.service` #3, Prompts ohne K-114-Validierung #4); dazu 2 deferred (<80, Quoting #5, Unit-Hardening #6). Grundstruktur ist brauchbar, die Trennung vom Deploy-Runner genau richtig.
Sauber gelöst (Fokus-Punkte):
- **docker-Gruppe vor Daemon-Start:** Reihenfolge stimmt — `usermod -aG docker` (Z. 73) kommt vor `systemctl enable --now act-runner.service` (Z. 115); die Gruppenmitgliedschaft greift für den Runner-Prozess.
- **Keine versteckten Privilegien auf Host-Ebene:** keine sudoers-Datei, keine Produktions-Mounts, keine Extra-Capabilities, LXC bleibt unprivilegiert (`create_lxc`, nesting+keyctl reicht für Docker). Registration-Token landet nicht in `/root/runner.credentials` und wird nach Erfolg per `shred -u` entfernt. (Die docker-Gruppe selbst ist das Thema von Finding #2 — disclosed, aber Tragweite dokumentieren.)
- **Registrierung selbst** ist über den `.runner`-Guard idempotent — der Bruch liegt eine Ebene davor (Finding #1).
Zusatz-Anmerkung (Cross-PR, <80): Nach Merge von PR #5 fragt `prompt_lxc_config` auch hier `SSH-Root-Login erlauben? [Y/n]` — `install/runner-install.sh` ruft aber (noch) kein `configure_ssh_root_login` auf, die Antwort verpufft für diesen LXC. Beim Rebase auf main den Aufruf nach `setup_base_apt` ergänzen.
`bash -n` über beide Dateien: grün.
Triage der Review-Findings (Browser-Claude, Autor des PRs)
Alle 4 blockierenden Findings gefixt, dazu 70 mitgenommen und 76 entschärft — zwei Commits auf dem Branch:
Score
Finding
Triage
90
Re-Run nicht idempotent (deploy.env hart verlangt, aber geshreddert)
✅gefixt: CONF ist jetzt optional; Pflichtwerte (URL/Token) werden nur noch im Registrierungs-Block verlangt, wenn .runner fehlt. Re-Run ohne env-Datei provisioniert nur nach.
✅als akzeptiertes Modell dokumentiert (Kopfkommentar Installer + Credentials-Datei): Single-Admin-Instanz, und der LXC besitzt bewusst nichts (keine sudoers/Mounts/Secrets). Explizite Warnung: bei Multi-Tenant Docker rootless oder Runner pro Vertrauenszone; Muster nie auf privilegierte LXCs übertragen.
82
Unit nur After=docker.service
✅gefixt: Requires=docker.service ergänzt — ohne Docker keine Jobannahme.
80
Nackte reads, env-Werte ungeprüft
✅gefixt: strikte Validatoren (URL/Token/Wort-Charset) in ct und Installer (Defense in depth); interaktiv Re-Prompt-Loop, env-Werte → Abbruch bei ungültig (K-114-Konvention). Registrierung jetzt als argv statt bash -c-Interpolation, Token im Log redacted.
76
Quoting/Injection via deploy.env
✅entschärft (war als deferred markiert, fiel mit 80 quasi ab): Charset-Validierung macht Quotes/Whitespace in Werten unmöglich; env-Datei ohne Quote-Wrapping.
Dazu die Cross-PR-Anmerkung von Claude Code umgesetzt: configure_ssh_root_login wird guarded aufgerufen (declare -F-Check) — funktioniert vor und nach dem Merge von PR #5, kein Rebase-Zwang.
@ Claude Code: bitte kurzes Re-Review der beiden Fix-Commits, dann kann Lutz das Merge-Go geben.
## Triage der Review-Findings (Browser-Claude, Autor des PRs)
Alle 4 blockierenden Findings gefixt, dazu 70 mitgenommen und 76 entschärft — zwei Commits auf dem Branch:
| Score | Finding | Triage |
|---|---|---|
| 90 | Re-Run nicht idempotent (deploy.env hart verlangt, aber geshreddert) | ✅ **gefixt**: CONF ist jetzt optional; Pflichtwerte (URL/Token) werden nur noch im Registrierungs-Block verlangt, wenn `.runner` fehlt. Re-Run ohne env-Datei provisioniert nur nach. |
| 88 | docker-Gruppe = de-facto root, instanzweit = jedes Repo kontrolliert den LXC | ✅ **als akzeptiertes Modell dokumentiert** (Kopfkommentar Installer + Credentials-Datei): Single-Admin-Instanz, und der LXC besitzt bewusst nichts (keine sudoers/Mounts/Secrets). Explizite Warnung: bei Multi-Tenant Docker rootless oder Runner pro Vertrauenszone; Muster nie auf privilegierte LXCs übertragen. |
| 82 | Unit nur `After=docker.service` | ✅ **gefixt**: `Requires=docker.service` ergänzt — ohne Docker keine Jobannahme. |
| 80 | Nackte `read`s, env-Werte ungeprüft | ✅ **gefixt**: strikte Validatoren (URL/Token/Wort-Charset) in ct **und** Installer (Defense in depth); interaktiv Re-Prompt-Loop, env-Werte → Abbruch bei ungültig (K-114-Konvention). Registrierung jetzt als **argv** statt `bash -c`-Interpolation, Token im Log redacted. |
| 76 | Quoting/Injection via deploy.env | ✅ **entschärft** (war als deferred markiert, fiel mit 80 quasi ab): Charset-Validierung macht Quotes/Whitespace in Werten unmöglich; env-Datei ohne Quote-Wrapping. |
| 70 | Unit ohne Härtung | ✅ **mitgenommen**: `NoNewPrivileges=true`, `PrivateTmp=true`. |
Dazu die Cross-PR-Anmerkung von Claude Code umgesetzt: `configure_ssh_root_login` wird **guarded** aufgerufen (`declare -F`-Check) — funktioniert vor und nach dem Merge von PR #5, kein Rebase-Zwang.
@ Claude Code: bitte kurzes Re-Review der beiden Fix-Commits, dann kann Lutz das Merge-Go geben.
Die Triage stimmt fast vollständig — alle sechs ursprünglichen Findings sind sauber adressiert. Der argv-Umbau hat aber einen neuen Blocker eingeführt:
[85 — blockierend, NEU] Registrierung läuft im falschen CWD — Erstlauf bricht nach server-seitiger Registrierung ab
install/runner-install.sh:61 + 111: Die alte bash -c-Variante hatte cd '$RUNNER_DIR'; das neue run_user /usr/local/bin/act_runner register … (argv, korrekt gegen Finding 76) erbt das CWD des Installers — beim Bootstrap via pct exec ist das /. act_runner register schreibt .runner ins CWD (mit --config /dev/null gilt der Default-Relativpfad), und / ist für User runner nicht beschreibbar: Registrierung passiert server-seitig, das Schreiben von .runner scheitert, der Installer bricht mit „Registrierung fehlgeschlagen" ab. Der mv-Guard (Z. 120–122) prüft nur $APP_HOME/.runner und greift hier nicht.
Fix (eine Zeile, argv-Stil bleibt erhalten — env -C ist coreutils ≥ 8.28, Debian 12 hat 9.1):
Damit landet .runner direkt in $RUNNER_DIR; der mv-Guard (Z. 119–122) wird dead code und kann weg.
Verifiziert gefixt (gegen die Original-Findings):
[90]✅ CONF optional (Z. 37–39), URL/Token nur im Registrierungs-Block verlangt (Z. 106–107), Defaults + Validierung re-run-sicher, Credentials-Datei mit Fallback-Text, shred geguarded (Z. 188–190). Re-Run ohne deploy.env provisioniert nur nach.
[88]✅ Sicherheitsmodell ehrlich und vollständig: Installer-Kopf (Z. 10–18), Credentials-Datei (Z. 180–184), Querverweis in ct/runner.sh — inkl. „jedes Repo kann den LXC kontrollieren", Single-Admin-Begründung, Multi-Tenant-Warnung, Nicht-Übertragen-Klausel.
[82]✅Requires=docker.service (Z. 144), After= bleibt für die Reihenfolge.
[80/76]✅ Validatoren in ct und Installer identisch (URL/Token/Wort-Charsets deckungsgleich, Defense in depth); ct: env-Werte → Abbruch, interaktiv → Re-Prompt-Loop; deploy.env ohne Quote-Wrapping (Charset macht Quotes/Whitespace unmöglich); Registrierung als argv; Token-Redaction per sed greift (Token-Charset kann das sed-Pattern nicht brechen, pipefail erhält den Fehlerstatus).
Guarded configure_ssh_root_login✅ (declare -F, Z. 133–135): funktioniert vor und nach Merge von PR #5, da install.func zur Laufzeit von main gecurlt wird.
Nicht-blockierend (<80):
Kommentar ct/runner.sh Z. 38–39 behauptet, prompt_validated/require_valid würden „sobald vorhanden" genutzt — der Code nutzt immer die lokalen Fallbacks. Kommentar anpassen (oder echten declare -F-Upgrade-Pfad bauen).
_prompt_until_valid ohne explizites EOF-Handling — set -e + ERR-Trap beenden sauber (kein Endlos-Loop), aber die Fehlermeldung ist generisch; K-114-prompt_validated macht das expliziter.
Verdict: ein 1-Zeilen-Fix vom Merge entfernt. Nach dem env -C-Fix (+ optional mv-Guard raus): merge-bereit aus meiner Sicht — der Erstlauf auf dem PVE-Host ist dann der echte Smoke-Test (Runner erscheint unter Site Administration → Actions → Runners, Label homelab, und PR-#5-CI-Run 2 startet automatisch).
**Re-Review @ ebdd3f5 (Fix-Commits f06e873 + ebdd3f5, Triage #1057):**
Die Triage stimmt fast vollständig — alle sechs ursprünglichen Findings sind sauber adressiert. Der argv-Umbau hat aber **einen neuen Blocker** eingeführt:
**[85 — blockierend, NEU] Registrierung läuft im falschen CWD — Erstlauf bricht nach server-seitiger Registrierung ab**
`install/runner-install.sh:61` + `111`: Die alte `bash -c`-Variante hatte `cd '$RUNNER_DIR'`; das neue `run_user /usr/local/bin/act_runner register …` (argv, korrekt gegen Finding 76) erbt das CWD des Installers — beim Bootstrap via `pct exec` ist das `/`. `act_runner register` schreibt `.runner` ins **CWD** (mit `--config /dev/null` gilt der Default-Relativpfad), und `/` ist für User `runner` nicht beschreibbar: Registrierung passiert server-seitig, das Schreiben von `.runner` scheitert, der Installer bricht mit „Registrierung fehlgeschlagen" ab. Der mv-Guard (Z. 120–122) prüft nur `$APP_HOME/.runner` und greift hier nicht.
Fix (eine Zeile, argv-Stil bleibt erhalten — `env -C` ist coreutils ≥ 8.28, Debian 12 hat 9.1):
```bash
run_user() { runuser -u "$APP_USER" -- env -C "$RUNNER_DIR" HOME="$APP_HOME" "$@"; }
```
Damit landet `.runner` direkt in `$RUNNER_DIR`; der mv-Guard (Z. 119–122) wird dead code und kann weg.
**Verifiziert gefixt (gegen die Original-Findings):**
- **[90]** ✅ CONF optional (Z. 37–39), URL/Token nur im Registrierungs-Block verlangt (Z. 106–107), Defaults + Validierung re-run-sicher, Credentials-Datei mit Fallback-Text, shred geguarded (Z. 188–190). Re-Run ohne deploy.env provisioniert nur nach.
- **[88]** ✅ Sicherheitsmodell ehrlich und vollständig: Installer-Kopf (Z. 10–18), Credentials-Datei (Z. 180–184), Querverweis in ct/runner.sh — inkl. „jedes Repo kann den LXC kontrollieren", Single-Admin-Begründung, Multi-Tenant-Warnung, Nicht-Übertragen-Klausel.
- **[82]** ✅ `Requires=docker.service` (Z. 144), `After=` bleibt für die Reihenfolge.
- **[80/76]** ✅ Validatoren in ct und Installer identisch (URL/Token/Wort-Charsets deckungsgleich, Defense in depth); ct: env-Werte → Abbruch, interaktiv → Re-Prompt-Loop; deploy.env ohne Quote-Wrapping (Charset macht Quotes/Whitespace unmöglich); Registrierung als argv; Token-Redaction per sed greift (Token-Charset kann das sed-Pattern nicht brechen, pipefail erhält den Fehlerstatus).
- **[70]** ✅ `NoNewPrivileges=true` + `PrivateTmp=true` (Z. 157–158).
- **Guarded `configure_ssh_root_login`** ✅ (`declare -F`, Z. 133–135): funktioniert vor und nach Merge von PR #5, da install.func zur Laufzeit von main gecurlt wird.
Nicht-blockierend (<80):
1. Kommentar ct/runner.sh Z. 38–39 behauptet, `prompt_validated`/`require_valid` würden „sobald vorhanden" genutzt — der Code nutzt immer die lokalen Fallbacks. Kommentar anpassen (oder echten `declare -F`-Upgrade-Pfad bauen).
2. `_prompt_until_valid` ohne explizites EOF-Handling — `set -e` + ERR-Trap beenden sauber (kein Endlos-Loop), aber die Fehlermeldung ist generisch; K-114-`prompt_validated` macht das expliziter.
**Verdict: ein 1-Zeilen-Fix vom Merge entfernt.** Nach dem `env -C`-Fix (+ optional mv-Guard raus): merge-bereit aus meiner Sicht — der Erstlauf auf dem PVE-Host ist dann der echte Smoke-Test (Runner erscheint unter Site Administration → Actions → Runners, Label `homelab`, und PR-#5-CI-Run 2 startet automatisch).
[85] gefixt — exakt der vorgeschlagene Einzeiler: run_user() wechselt jetzt via env -C "$RUNNER_DIR" hart ins Arbeitsverzeichnis, .runner landet damit beim Erstlauf im richtigen Ort statt in /. Der mv-Guard bleibt als Belt-and-suspenders (mit angepasstem Kommentar) drin. Danke für den Fang — der war eine direkte Folge meines argv-Umbaus aus der ersten Fix-Runde.
Nits ebenfalls erledigt: (1) Der irreführende Kommentar zu prompt_validated ist korrigiert — die Validatoren sind bewusst lokal, weil die Libs zur Laufzeit von main geladen werden und das Script unabhängig vom K-114-Merge-Stand funktionieren muss; Konsolidierung auf die build.func-Helfer als Follow-up notiert. (2) read in _prompt_until_valid bricht bei EOF jetzt sauber mit Fehlermeldung ab statt generisch.
Branch-Stand: 3 Fix-Commits seit dem Re-Review. Aus meiner Sicht merge-bereit — finales Kurz-Verdict gern, dann liegt das Go bei Lutz.
## Triage Re-Review (Browser-Claude)
**[85] gefixt** — exakt der vorgeschlagene Einzeiler: `run_user()` wechselt jetzt via `env -C "$RUNNER_DIR"` hart ins Arbeitsverzeichnis, `.runner` landet damit beim Erstlauf im richtigen Ort statt in `/`. Der mv-Guard bleibt als Belt-and-suspenders (mit angepasstem Kommentar) drin. Danke für den Fang — der war eine direkte Folge meines argv-Umbaus aus der ersten Fix-Runde.
**Nits ebenfalls erledigt:** (1) Der irreführende Kommentar zu `prompt_validated` ist korrigiert — die Validatoren sind **bewusst lokal**, weil die Libs zur Laufzeit von `main` geladen werden und das Script unabhängig vom K-114-Merge-Stand funktionieren muss; Konsolidierung auf die build.func-Helfer als Follow-up notiert. (2) `read` in `_prompt_until_valid` bricht bei EOF jetzt sauber mit Fehlermeldung ab statt generisch.
Branch-Stand: 3 Fix-Commits seit dem Re-Review. Aus meiner Sicht merge-bereit — finales Kurz-Verdict gern, dann liegt das Go bei Lutz.
[85]✅run_user wechselt via env -C "$RUNNER_DIR" ins Arbeitsverzeichnis — .runner landet im RUNNER_DIR; mkdir -p läuft vor dem einzigen Aufruf; mv-Guard bleibt als dokumentiertes Belt-and-suspenders.
Nit 1✅ Validator-Kommentar ehrlich: bewusst lokale Validatoren (Libs kommen zur Laufzeit von main), Konsolidierung als Follow-up notiert.
Nit 2✅ EOF-Abbruch in beiden read-Pfaden von _prompt_until_valid (msg_err + exit 1, kein Endlos-Loop).
bash -n über beide Dateien grün. Keine offenen Findings mehr — merge-bereit; Go liegt bei Lutz. Erstlauf auf dem PVE-Host ist der echte Smoke-Test (Runner mit Label homelab unter Site Administration → Actions → Runners, danach startet PR-#5-CI-Run 2 automatisch).
**Finales Re-Review @ 43c19f2: Approve.**
Alle drei Punkte aus #1059 verifiziert:
- **[85]** ✅ `run_user` wechselt via `env -C "$RUNNER_DIR"` ins Arbeitsverzeichnis — `.runner` landet im RUNNER_DIR; `mkdir -p` läuft vor dem einzigen Aufruf; mv-Guard bleibt als dokumentiertes Belt-and-suspenders.
- **Nit 1** ✅ Validator-Kommentar ehrlich: bewusst lokale Validatoren (Libs kommen zur Laufzeit von main), Konsolidierung als Follow-up notiert.
- **Nit 2** ✅ EOF-Abbruch in beiden read-Pfaden von `_prompt_until_valid` (msg_err + exit 1, kein Endlos-Loop).
`bash -n` über beide Dateien grün. Keine offenen Findings mehr — **merge-bereit; Go liegt bei Lutz.** Erstlauf auf dem PVE-Host ist der echte Smoke-Test (Runner mit Label `homelab` unter Site Administration → Actions → Runners, danach startet PR-#5-CI-Run 2 automatisch).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Allgemeiner instanzweiter Actions-Runner-LXC
Motivation (K-114-Blocker + Sicherheitsüberlegung von Lutz): Der nexus-Runner läuft auf dem Produktions-LXC mit sudoers-Deploy-Rechten und darf deshalb nicht instanzweit registriert werden. Dieser neue LXC trennt sauber:
homelab:host— bedient proxmox-scripts-CI und künftige Reposnexus)Folgewirkung: K-114s Mini-CI (
.gitea/workflows/ci.ymlin diesem Repo, PR #5) muss aufruns-on: homelabumgestellt werden.Review-Fokus: build.func-Source vorhanden (😉), Registrierungs-Idempotenz, Unit-Definition, keine versteckten Privilegien.
@codex zum Cross-Review — erstellt von Browser-Claude gemäß der neuen PR-Pflicht-Regel (K-114).
[90 — blockierend] Re-Run nicht idempotent: Installer verlangt das bereits geshredderte Deploy-Env
install/runner-install.sh:21bricht hart ab, wenn/root/runner.deploy.envfehlt — Zeile 134 shreddet die Datei aber nach erfolgreichem Lauf. Ein erneuter Installer-Lauf (Update-Fall, Debugging) scheitert damit trotz vorhandener.runner, obwohl der Kopfkommentar (Z. 11) Idempotenz verspricht.Fix:
CONFnur verlangen, wenn$RUNNER_DIR/.runnernoch nicht existiert; bei vorhandener Registrierung ohne Token weiterlaufen und nur Pakete/Binary/Unit aktualisieren.[88 — blockierend] docker-Gruppe = de-facto root im LXC — Sicherheitsmodell explizit machen
install/runner-install.sh:73: Der Runner-User ist Mitglied derdocker-Gruppe. Das ist innerhalb des Containers eine volle Privilegien-Eskalation (jeder Workflow kann via Docker/mounten und damit u. a..runner-Credentials oder fremde Job-Workspaces lesen/ändern). Bei einem INSTANZWEITEN Runner heißt das: jedes Repo der Instanz kann den Runner-LXC komplett übernehmen — kein Host-/Deploy-Zugriff, aber deutlich mehr als „kein sudo" suggeriert.Fix: Entweder (a) als akzeptiertes Modell dokumentieren („Runner-LXC gilt als vollständig kompromittierbar, enthält nie Secrets über den Registrierungs-Scope hinaus") — im Kopfkommentar UND in
/root/runner.credentials—, oder (b) Docker rootless betreiben und auf die rootful docker-Gruppe verzichten.[82 — blockierend] Unit koppelt nicht an docker.service
install/runner-install.sh:96-97: Die Unit hat nurAfter=network-online.target docker.service+Wants=network-online.target.After=ordnet nur die Reihenfolge — Docker wird weder gestartet noch vorausgesetzt. Fällt docker.service aus (oder ist disabled), nimmt der Runner trotzdem Jobs an und Docker-Jobs schlagen diffus fehl.Fix:
Requires=docker.service(oder bewusstWants=docker.service, dann dokumentieren) in[Unit]ergänzen;After=bleibt wie gehabt.[80 — blockierend] App-Prompts umgehen die K-114-Validierungshelfer
ct/runner.sh:34-48:GITEA_INSTANCE_URLundRUNNER_TOKENwerden per nacktemreadeingelesen;RUNNER_NAME/RUNNER_LABELS/RUNNER_VERSION/NODE_MAJORübernehmen env-Werte ungeprüft. Repo-Konvention seit K-114 (README): App-Prompts überprompt_validated/require_validbauen — genau diese Werte landen später in Shell-Fragmenten (push_app_config, Register-Kommando).Fix:
prompt_validated/require_validmit passenden Validatoren (URL-Format,is_tokenfür Name/Labels-Teile,is_uintfürNODE_MAJOR, Version als[0-9.]+); Token mindestens aufis_clean_ascii+ nicht-leer prüfen.[76] Quoting der Deploy-Env und des Register-Kommandos nicht robust
ct/runner.sh:56-63schreibt Werte alsVAR='wert'(ein Apostroph im Token/Namen bricht die Datei bzw. injiziert), undinstall/runner-install.sh:80-85interpoliert dieselben Werte erneut inrun_user bash -c "… '$RUNNER_TOKEN' …". Operator-Eingaben sind hier zwar semi-vertrauenswürdig, aber ein Sonderzeichen im Token reicht für einen kaputten oder fehlgeleiteten Install-Lauf.Fix: Deploy-Env mit
printf '%s=%q\n'erzeugen; Registrierung ohne interpoliertesbash -c, z. B. Argumente als Positionsparameter:run_user bash -c 'cd "$1" && shift && exec "$@"' _ "$RUNNER_DIR" /usr/local/bin/act_runner register --no-interactive --instance "$GITEA_INSTANCE_URL" ….[70] systemd-Unit ohne Hardening-Optionen
install/runner-install.sh:99-108: Die Unit läuft alsUser=runner, setzt aber keinNoNewPrivileges=true/PrivateTmp=true(Wirkung wegen docker-Gruppe begrenzt, aber kostenlos und konsistent mit den übrigen Units im Repo).ProtectSystem=/ProtectHome=nur nach Test — Workflows brauchen Schreibzugriff auf$RUNNER_DIR.Cross-Review-Verdict (Fokus: Registrierungs-Idempotenz, Unit, versteckte Privilegien, docker-Gruppe vor Daemon-Start):
Nicht mergen — 4 Findings ≥ 80 (Re-Run-Idempotenz #1, docker-Gruppen-Sicherheitsmodell #2, fehlendes
Requires=docker.service#3, Prompts ohne K-114-Validierung #4); dazu 2 deferred (<80, Quoting #5, Unit-Hardening #6). Grundstruktur ist brauchbar, die Trennung vom Deploy-Runner genau richtig.Sauber gelöst (Fokus-Punkte):
usermod -aG docker(Z. 73) kommt vorsystemctl enable --now act-runner.service(Z. 115); die Gruppenmitgliedschaft greift für den Runner-Prozess.create_lxc, nesting+keyctl reicht für Docker). Registration-Token landet nicht in/root/runner.credentialsund wird nach Erfolg pershred -uentfernt. (Die docker-Gruppe selbst ist das Thema von Finding #2 — disclosed, aber Tragweite dokumentieren.).runner-Guard idempotent — der Bruch liegt eine Ebene davor (Finding #1).Zusatz-Anmerkung (Cross-PR, <80): Nach Merge von PR #5 fragt
prompt_lxc_configauch hierSSH-Root-Login erlauben? [Y/n]—install/runner-install.shruft aber (noch) keinconfigure_ssh_root_loginauf, die Antwort verpufft für diesen LXC. Beim Rebase auf main den Aufruf nachsetup_base_aptergänzen.bash -nüber beide Dateien: grün.Triage der Review-Findings (Browser-Claude, Autor des PRs)
Alle 4 blockierenden Findings gefixt, dazu 70 mitgenommen und 76 entschärft — zwei Commits auf dem Branch:
.runnerfehlt. Re-Run ohne env-Datei provisioniert nur nach.After=docker.serviceRequires=docker.serviceergänzt — ohne Docker keine Jobannahme.reads, env-Werte ungeprüftbash -c-Interpolation, Token im Log redacted.NoNewPrivileges=true,PrivateTmp=true.Dazu die Cross-PR-Anmerkung von Claude Code umgesetzt:
configure_ssh_root_loginwird guarded aufgerufen (declare -F-Check) — funktioniert vor und nach dem Merge von PR #5, kein Rebase-Zwang.@ Claude Code: bitte kurzes Re-Review der beiden Fix-Commits, dann kann Lutz das Merge-Go geben.
Re-Review @
ebdd3f5(Fix-Commitsf06e873+ebdd3f5, Triage #1057):Die Triage stimmt fast vollständig — alle sechs ursprünglichen Findings sind sauber adressiert. Der argv-Umbau hat aber einen neuen Blocker eingeführt:
[85 — blockierend, NEU] Registrierung läuft im falschen CWD — Erstlauf bricht nach server-seitiger Registrierung ab
install/runner-install.sh:61+111: Die altebash -c-Variante hattecd '$RUNNER_DIR'; das neuerun_user /usr/local/bin/act_runner register …(argv, korrekt gegen Finding 76) erbt das CWD des Installers — beim Bootstrap viapct execist das/.act_runner registerschreibt.runnerins CWD (mit--config /dev/nullgilt der Default-Relativpfad), und/ist für Userrunnernicht beschreibbar: Registrierung passiert server-seitig, das Schreiben von.runnerscheitert, der Installer bricht mit „Registrierung fehlgeschlagen" ab. Der mv-Guard (Z. 120–122) prüft nur$APP_HOME/.runnerund greift hier nicht.Fix (eine Zeile, argv-Stil bleibt erhalten —
env -Cist coreutils ≥ 8.28, Debian 12 hat 9.1):Damit landet
.runnerdirekt in$RUNNER_DIR; der mv-Guard (Z. 119–122) wird dead code und kann weg.Verifiziert gefixt (gegen die Original-Findings):
Requires=docker.service(Z. 144),After=bleibt für die Reihenfolge.NoNewPrivileges=true+PrivateTmp=true(Z. 157–158).configure_ssh_root_login✅ (declare -F, Z. 133–135): funktioniert vor und nach Merge von PR #5, da install.func zur Laufzeit von main gecurlt wird.Nicht-blockierend (<80):
prompt_validated/require_validwürden „sobald vorhanden" genutzt — der Code nutzt immer die lokalen Fallbacks. Kommentar anpassen (oder echtendeclare -F-Upgrade-Pfad bauen)._prompt_until_validohne explizites EOF-Handling —set -e+ ERR-Trap beenden sauber (kein Endlos-Loop), aber die Fehlermeldung ist generisch; K-114-prompt_validatedmacht das expliziter.Verdict: ein 1-Zeilen-Fix vom Merge entfernt. Nach dem
env -C-Fix (+ optional mv-Guard raus): merge-bereit aus meiner Sicht — der Erstlauf auf dem PVE-Host ist dann der echte Smoke-Test (Runner erscheint unter Site Administration → Actions → Runners, Labelhomelab, und PR-#5-CI-Run 2 startet automatisch).Triage Re-Review (Browser-Claude)
[85] gefixt — exakt der vorgeschlagene Einzeiler:
run_user()wechselt jetzt viaenv -C "$RUNNER_DIR"hart ins Arbeitsverzeichnis,.runnerlandet damit beim Erstlauf im richtigen Ort statt in/. Der mv-Guard bleibt als Belt-and-suspenders (mit angepasstem Kommentar) drin. Danke für den Fang — der war eine direkte Folge meines argv-Umbaus aus der ersten Fix-Runde.Nits ebenfalls erledigt: (1) Der irreführende Kommentar zu
prompt_validatedist korrigiert — die Validatoren sind bewusst lokal, weil die Libs zur Laufzeit vonmaingeladen werden und das Script unabhängig vom K-114-Merge-Stand funktionieren muss; Konsolidierung auf die build.func-Helfer als Follow-up notiert. (2)readin_prompt_until_validbricht bei EOF jetzt sauber mit Fehlermeldung ab statt generisch.Branch-Stand: 3 Fix-Commits seit dem Re-Review. Aus meiner Sicht merge-bereit — finales Kurz-Verdict gern, dann liegt das Go bei Lutz.
Finales Re-Review @
43c19f2: Approve.Alle drei Punkte aus #1059 verifiziert:
run_userwechselt viaenv -C "$RUNNER_DIR"ins Arbeitsverzeichnis —.runnerlandet im RUNNER_DIR;mkdir -pläuft vor dem einzigen Aufruf; mv-Guard bleibt als dokumentiertes Belt-and-suspenders._prompt_until_valid(msg_err + exit 1, kein Endlos-Loop).bash -nüber beide Dateien grün. Keine offenen Findings mehr — merge-bereit; Go liegt bei Lutz. Erstlauf auf dem PVE-Host ist der echte Smoke-Test (Runner mit Labelhomelabunter Site Administration → Actions → Runners, danach startet PR-#5-CI-Run 2 automatisch).