Shuffle/docs/adr/0003-ci-runner-sicherheit.md
cschlaefke ffa264f827
Some checks failed
CI / flutter (pull_request) Successful in 1m23s
CI / web-build (pull_request) Successful in 1m16s
CI / supabase (pull_request) Successful in 9s
CI / supabase-tests (pull_request) Successful in 33s
CI / e2e (pull_request) Failing after 5m50s
docs(adr): accept no-approval rule for fork PR runs (ADR-0003)
Refs #43

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 01:06:55 +01:00

108 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-0003: CI-Sicherheit – Fork-PRs und der Runner `docker-host`
- **Status:** Teilweise angenommen (Chris, 2026-11-04): Betriebsregel unten gilt; Maßnahmen A und B sowie die Frage „öffentlich oder privat“ sind noch offen, Issue #43
- **Datum:** 2026-11-03
## Kontext
Das Repo ist öffentlich. Der Runner `shuffle-mac-runner` (Label `docker-host`) läuft auf Chris' Mac mit Docker-in-Docker;
die Jobs `supabase-tests` und `e2e` sprechen den dind-Daemon per TCP an (`DOCKER_HOST=tcp://<Gateway>:2375`, ohne TLS).
Wer dort Code ausführt, hat Root-Äquivalent im dind (Container mit Host-Mounts, Zugriff auf alles, was der dind sieht).
Bei `on: pull_request` wird die Workflow-Datei aus dem PR-Head gelesen. Ein Fork-PR kann Bedingungen wie
`head.repo.full_name == github.repository` entfernen oder ein beliebiges `runs-on:` setzen. **Ein `if:` in `ci.yml` schützt
daher nicht** und kein Inhalt des Repos kann einen Fork-PR daran hindern, `runs-on: docker-host` anzufordern.
Stand der Prüfung (Forgejo 8.0.3+gitea-1.22.0, Runner v5.0.4): Die Freigabe-Logik wurde im Quelltext von Forgejo v8.0.3
gelesen (`services/actions/notifier_helper.go`, Funktion `ifNeedApproval`). Es gab **keinen Live-Test** mit einem Fork-PR;
das Verhalten des Runners und die Aussagen zu Runner-Konfiguration stammen aus Doku-Kenntnis. Prüfschritte stehen unten.
## Was Forgejo 8.0.3 bietet
| Mechanismus | Verfügbarkeit | Bewertung |
|---|---|---|
| Freigabe für Fork-PR-Workflows (`need approval`) | Eingebaut, **keine Repo-Einstellung zum Ein-/Ausschalten**. Entscheidet über den ganzen Lauf. Laut Quelltext v8.0.3 braucht ein Fork-PR (Event `pull_request`) Freigabe, außer der Nutzer hat Schreibrecht oder es gibt im Repo schon einen freigegebenen Lauf dieses Nutzers. | Hauptschutz mit Lücke, siehe Absatz unter der Tabelle. |
| Nutzer `restricted` (Administration -> Benutzerkonten; `app.ini` `[service] DEFAULT_USER_IS_RESTRICTED` für neue Konten, **unverifiziert**) | Eingebaut | Erzwingt die Freigabe bei jedem Lauf (Quelltext). Nebenwirkung vermutlich: keine Sicht auf öffentliche Repos ohne Zuordnung (**unverifiziert**), siehe C.2. |
| `pull_request_target` | Läuft mit Workflow aus dem Base-Branch | Nicht verwenden, um PR-Code auf `docker-host` zu bauen (klassische Pwn-Request-Lücke). |
| Runner-Scoping (Instanz/Organisation/Nutzer/Repo) | Eingebaut | Begrenzt, *welche Repos* den Runner nutzen, nicht *welche Events*. Hilft hier nicht gegen Fork-PRs im selben Repo. |
| Runner-Labels | Eingebaut | Jeder Workflow darf jedes Label anfordern. Kein Schutz. |
| Event-/Repo-Filter im Runner (`runner.yaml`) | Nicht vorhanden | -- |
| Repo privat stellen | Eingebaut | Beseitigt das Problem (Fremde können keine PRs öffnen), kostet Öffentlichkeit. |
| Forks instanzweit verbieten (`app.ini`, Forgejo-Doku prüfen: `[repository] DISABLE_FORKS`/ähnlich) | In 8.0.3 nicht verifiziert | Nur als Prüfpunkt. |
**Vertrauenslücke der Freigabe:** Eine einzige Freigabe vertraut dem Nutzer dauerhaft. Für alle künftigen PRs dieses Nutzers im
Repo (nicht nur für diesen PR) laufen Fork-PR-Workflows ohne weitere Freigabe, auch nach einem Push mit manipuliertem Workflow.
Eine Freigabe ist also faktisch die Entscheidung „diesem Nutzer vertraue ich den Runner an“.
## Entscheidung (Vorschlag)
Verteidigung in drei Schichten; nur die erste lässt sich im Repo ausdrücken, der Rest braucht Chris:
1. **Repo (umgesetzt):** Der `if:`-Guard an `supabase-tests` und `e2e` hält nur einen unveränderten, bereits freigegebenen
Fork-PR von `docker-host` fern. Er verhindert weder das Warten auf Freigabe (die gilt für den ganzen Lauf, das Job-`if`
wird erst danach ausgewertet) noch einen manipulierten Lauf. Die Kommentare in `ci.yml` sagen das.
2. **Forgejo (Chris):** Fork-PR-Läufe auf diesem Runner **nicht freigeben**, solange er nicht isoliert ist (3.) oder das Repo
nicht privat ist. Grund: Eine Freigabe gilt für alle künftigen PRs des Nutzers im Repo. Fremde Beiträge lokal prüfen oder als
Branch ins Haupt-Repo übernehmen.
3. **Runner/Mac (Chris):** Blast Radius des dind begrenzen (siehe Anleitung).
Solange 2. und 3. nicht erledigt sind, gilt `docker-host` als **nicht für externe Beiträge geeignet**. Empfehlung: vor dem
ersten externen Beitrag, spätestens vor Release, entweder 3. umsetzen oder das Repo privat stellen.
## Betriebsregel (angenommen, Chris, 2026-11-04)
**Fork-PR-Läufe werden nie freigegeben. Fremde Beiträge werden lokal geprüft und als Branch ins Haupt-Repo übernommen**
(dort läuft die CI ohne Freigabe, weil Schreibrecht besteht). Das ist die einfachste wirksame Regel und kostet nichts; sie ist
eine Disziplin-Regel, kein technischer Schutz: Eine einzige Freigabe vertraut dem Nutzer den Runner dauerhaft an (siehe
Vertrauenslücke).
Weiterhin offen und vor dem ersten externen Beitrag, spätestens vor dem Release zu entscheiden: Prüfung A (Verhalten live prüfen),
Maßnahme B (Runner isolieren) oder das Repo privat stellen. Issue #43 bleibt bis dahin offen.
## Anleitung für Chris (nicht automatisiert; nichts davon wurde ausgeführt)
### A. Verhalten prüfen (10 Minuten, empfohlen als Erstes)
1. Zweites Forgejo-Konto ohne Schreibrecht anlegen (oder Test-Nutzer), Repo forken, im Fork `ci.yml` ändern
(`if:`-Zeilen entfernen, harmlosen Schritt `echo hallo` ergänzen), PR gegen `main` öffnen.
2. Erwartung: Lauf steht auf „Wartet auf Freigabe“ und startet erst nach Klick auf **Freigeben** (PR-Seite bzw. Actions-Lauf).
Falls er ohne Freigabe startet (Quelltext sagt anderes): Das Repo sofort privat stellen (Einstellungen -> Repository -> Sichtbarkeit) und
Runner pausieren (B.3), dann Forgejo-Version/Doku prüfen.
3. Zweiten Push im selben PR machen: Läuft er jetzt ohne erneute Freigabe? Dann ist „einmal freigegeben, immer freigegeben“ bestätigt.
4. Aufräumen: Testkonto und Fork **löschen** (dauerhaft vertrautes Konto!). Falls das nicht geht: mindestens `restricted` setzen und
Anmeldung verbieten (Administration -> Benutzerkonten).
Hinweis: Der Test mit `restricted` (jeder Lauf einzeln freigegeben) ist eventuell nicht durchführbar, weil eingeschränkte Nutzer
vermutlich keine öffentlichen Repos ohne Zuordnung sehen (**unverifiziert**, siehe C.2). Dann entfällt er; die Quelltext-Aussage bleibt.
### B. Auf dem Mac (Runner/dind)
1. Labels sind keine Grenze: Ein Fork-PR kann `runs-on: docker-host` selbst setzen. Eine Trennung `docker` / `docker-host` im
Workflow schützt deshalb nichts, und `container.network`/`valid_volumes` der Runner-Konfiguration gelten für den ganzen
Runner, nicht je Label. Schutz bieten nur Freigabe, ein Daemon/VM ohne Wertvolles (Punkt 2) oder ein privates Repo. Sollen
`docker` und `docker-host` unterschiedlich exponiert sein, braucht es getrennte Runner-Registrierungen mit eigener
Konfiguration, idealerweise auf getrennten Hosts. Prüfen kannst du, ob ein `docker`-Job den dind erreicht
(`curl -s -m 3 http://<Default-Gateway>:2375/version`, Gateway wie in `ci.yml` aus `/proc/net/route`); antwortet er,
gilt die Warnung für alle Jobs des Runners.
2. Wirksam: Runner in eine Wegwerf-VM (z. B. Lima/Colima/UTM) oder auf einen separaten Rechner/Hetzner-Server verlagern, ohne
Host-Mounts, ohne Zugriff aufs LAN, ohne Zugangsdaten und ohne persönliche Daten auf dem Mac. Ziel: Ein Ausbruch aus dem dind
trifft nichts Wertvolles. Eigenes Registrierungs-Token verwenden; `.runner` und Token nicht ins Repo und nicht in Logs.
3. Notbremse: Runner-Prozess auf dem Mac beenden (z. B. `launchctl unload` des Dienstes oder Prozess/Container stoppen; der genaue Weg hängt von der Einrichtung ab, **unverifiziert**). In Forgejo (Einstellungen -> Actions -> Runner) gibt es in 8.0.3 evtl. nur Bearbeiten/Löschen statt Deaktivieren (**unverifiziert**); Löschen macht den Runner unbrauchbar, bis er neu registriert wird.
4. Runner nur für dieses Repo registrieren (Repo-Scope statt Instanz-/Nutzer-Scope), damit andere Repos ihn nicht ansprechen können.
### C. In Forgejo
1. Fork-PR-Läufe nicht freigeben (siehe Entscheidung 2.).
2. Fremde Konten als `restricted` zu führen ist mit Vorsicht zu genießen: Eingeschränkte Nutzer sehen vermutlich keine öffentlichen
Repos ohne explizite Zuordnung (**unverifiziert**). Dann könnten sie keinen Fork/PR anlegen, d. h. `restricted` sperrt externe
Beiträge faktisch aus. Das ist eine Alternative zum Repo-privat-Stellen, kein Mittel für „jeder Lauf einzeln freigeben“.
Erst mit dem Testkonto aus A prüfen.
3. Optional: Branch-Schutz für `main` mit geschützten Dateimustern (UI-Bezeichnung in 8.0.3 **unverifiziert**, bei
Gitea „Protected file patterns“) `.forgejo/**;e2e/**;supabase/config.toml`. Achtung, vermutlich blockiert das auch das
Mergen von PRs, die solche Dateien ändern, also auch deine eigenen CI-PRs (**unverifiziert**); nur nutzen, wenn du dafür eine
Ausnahme (Admin-Merge/Whitelist) hast, sonst weglassen. Es schützt nur `main`, nicht die PR-Läufe.
4. Repo privat stellen, wenn externe Beiträge nicht gewollt sind (löst das Problem vollständig).
5. Runner-Token rotieren (Registrierungs-Token ist kein Dauer-Geheimnis, das `.runner` auf dem Mac enthält das Laufzeit-Token):
Runner in Forgejo (Einstellungen -> Actions -> Runner) löschen, im selben Dialog ein neues Registrierungs-Token erzeugen, den
Runner auf dem Mac mit `forgejo-runner register` neu registrieren und die alte `.runner` löschen. Vorschlag: alle 3 Monate
sowie sofort bei Verdacht. Token nie in Chats, Logs oder ins Repo kopieren.
## Konsequenzen
- Fork-PRs bekommen ohne Freigabe kein CI (der ganze Lauf wartet, mit allen Jobs). Eine Freigabe vertraut dem Nutzer den ganzen
Runner dauerhaft an. Fremde Beiträge daher als Branch ins Haupt-Repo übernehmen (dort läuft CI ohne Freigabe, weil Schreibrecht).
- Externe Beiträge laufen nicht auf `docker-host`, bis der Runner isoliert ist oder das Repo privat ist.
- Der `if:`-Guard ändert daran nichts (siehe Entscheidung 1).
- Offen: Ergebnis der Prüfung A und Umsetzung B. Bis dahin Issue #43 nicht schließen.