9.1 KiB
ADR-0003: CI-Sicherheit – Fork-PRs und der Runner docker-host
- Status: Vorgeschlagen (Chris entscheidet über die Maßnahmen unten), 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:
- Repo (umgesetzt): Der
if:-Guard ansupabase-testsunde2ehält nur einen unveränderten, bereits freigegebenen Fork-PR vondocker-hostfern. Er verhindert weder das Warten auf Freigabe (die gilt für den ganzen Lauf, das Job-ifwird erst danach ausgewertet) noch einen manipulierten Lauf. Die Kommentare inci.ymlsagen das. - 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.
- 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.
Anleitung für Chris (nicht automatisiert; nichts davon wurde ausgeführt)
A. Verhalten prüfen (10 Minuten, empfohlen als Erstes)
- Zweites Forgejo-Konto ohne Schreibrecht anlegen (oder Test-Nutzer), Repo forken, im Fork
ci.ymländern (if:-Zeilen entfernen, harmlosen Schrittecho halloergänzen), PR gegenmainöffnen. - 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.
- Zweiten Push im selben PR machen: Läuft er jetzt ohne erneute Freigabe? Dann ist „einmal freigegeben, immer freigegeben“ bestätigt.
- Aufräumen: Testkonto und Fork löschen (dauerhaft vertrautes Konto!). Falls das nicht geht: mindestens
restrictedsetzen 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)
- Labels sind keine Grenze: Ein Fork-PR kann
runs-on: docker-hostselbst setzen. Eine Trennungdocker/docker-hostim Workflow schützt deshalb nichts, undcontainer.network/valid_volumesder 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. Sollendockerunddocker-hostunterschiedlich exponiert sein, braucht es getrennte Runner-Registrierungen mit eigener Konfiguration, idealerweise auf getrennten Hosts. Prüfen kannst du, ob eindocker-Job den dind erreicht (curl -s -m 3 http://<Default-Gateway>:2375/version, Gateway wie inci.ymlaus/proc/net/route); antwortet er, gilt die Warnung für alle Jobs des Runners. - 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;
.runnerund Token nicht ins Repo und nicht in Logs. - Notbremse: Runner-Prozess auf dem Mac beenden (z. B.
launchctl unloaddes 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. - Runner nur für dieses Repo registrieren (Repo-Scope statt Instanz-/Nutzer-Scope), damit andere Repos ihn nicht ansprechen können.
C. In Forgejo
- Fork-PR-Läufe nicht freigeben (siehe Entscheidung 2.).
- Fremde Konten als
restrictedzu 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.restrictedsperrt 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. - Optional: Branch-Schutz für
mainmit 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 nurmain, nicht die PR-Läufe. - Repo privat stellen, wenn externe Beiträge nicht gewollt sind (löst das Problem vollständig).
- Runner-Token rotieren (Registrierungs-Token ist kein Dauer-Geheimnis, das
.runnerauf 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 mitforgejo-runner registerneu registrieren und die alte.runnerlö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.