Der Pull Request sah aus wie ein routinemäßiges Dependency-Update. Versteckt im Diff steckte eine kleine Änderung an der CI-Konfiguration, die jedem Build einen zusätzlichen Schritt hinzugefügt hätte. Der Maintainer, der diese Geschichte auf Medium erzählt hat, ließ seine Jobs auf einem selbst gehosteten Runner laufen, einem günstigen VPS, den er sich hielt, weil gehostete Minuten ins Geld gehen, und diese Kiste nahm Jobs aus Forks an, während in einer Config-Datei Cloud-Zugangsdaten lagen. Er hat das Diff gründlich gelesen, den PR geschlossen, und nichts ist passiert. Mein Argument heute: Wir anderen sollten uns so verhalten, als läge genau dieser PR schon in unserer Review-Queue.

Wenn du ein öffentliches PHP-Package pflegst, überleg mal, was deine Pipeline tut, wenn ein Fremder einen PR aufmacht. composer install kann Plugins und Skripte ausführen, bevor dein erster Test überhaupt startet. Deine Matrix deckt vermutlich mehrere PHP-Versionen auf einem Runner ab, der Dependencies zwischen Jobs cached. CI aus einem Fork zu triggern heißt, Code auszuführen, den du nie gelesen hast, auf Hardware, für die du verantwortlich bist. Das ist ein feiner Service für Contributor. Gefährlich wird es in dem Moment, in dem die Maschine irgendetwas erreichen kann, das dir wichtig ist, und eine Credentials-Datei, die dein früheres Ich dort liegen gelassen hat, zählt definitiv dazu.

Der Reflex ist, den Runner in einen Container zu packen, und ich behaupte nicht, dass das nichts bringt. Aber ein Container leiht sich den Kernel des Hosts, die meisten CI-Setups mounten den Docker-Socket hinein, damit Builds Images bauen können, und das Cache-Verzeichnis, das deine Pipeline schnell macht, ist für jeden beschreibbar, der den vorherigen Job ausgeführt hat. Ein bösartiger Build kann dem nächsten ein Geschenk hinterlassen, und ein Neustart des Containers dazwischen räumt das nicht weg. Isolation, die Zustand zwischen Jobs teilt, ist Isolation mit Sternchen.

Die Antwort des Autors war eine microVM pro Job: Jeder Build bekommt eine frische Firecracker-VM, dieselbe Isolationsschicht, auf der AWS Lambda gebaut hat, sie bootet in unter einer Sekunde, wird zerstört, sobald der Build fertig ist, und lebt in einem privaten Netz ohne offene Ports. Er betreibt das über einen Dienst namens Krova Cloud, und ja, der Post liest sich stellenweise wie Werbung dafür. Zieh das Branding ab, und die Eigenschaft hält trotzdem: Wenn die gesamte Lebensdauer einer Maschine ein einzelner Job ist, hört Persistenz auf, etwas zu sein, das ein Angreifer erreichen kann. Ein manipulierter Cache stirbt mit der Kiste, genau wie alles, was ein Build auf die Platte geschrieben hat in der Hoffnung, dass ein späterer Job darüber stolpert.

Bevor du aber losziehst und microVM-Hosting einkaufst, verdient das langweilige Gegenargument seinen Auftritt. GitHubs gehostete Runner sind bereits ephemer, und für die durchschnittliche PHP-Library decken die freien Minuten die ganze Test-Matrix ab. Du kannst außerdem verlangen, dass Workflows von externen Contributorn erst nach manueller Freigabe laufen. Wenn du selbst hostest, weil Integrationstests eine dicke Maschine brauchen, in Ordnung. Wenn du nur selbst hostest, um bei einem öffentlichen Repo Minuten zu sparen, ist die sicherste Änderung eine Einstellungsseite und dauert fünf Minuten. Ephemeralität, die du nicht selbst bauen musst, ist eine Menge wert.

Egal worauf du deine Jobs laufen lässt, der Teil, den ich ins Team-Wiki pinnen würde, betrifft Credentials, und hier ist der Autor erfrischend ehrlich, was die Grenzen seines eigenen Setups angeht: Eine makellose VM hilft dir nichts bei einem langlebigen Cloud-Key, den du in jeden Build injizierst. Die Exfiltration passiert dann einfach von schönerer Hardware aus. Erzeuge Tokens pro Job über OIDC-Federation, beschränke sie auf den einen Bucket oder die eine Registry, die der Job tatsächlich anfasst, und lass sie nach Minuten ablaufen. Wenn dein Test-Job trotzdem jede Ressource im Account auflisten kann, ist die schicke Runner-Architektur nur Dekoration.

Was mir bleibt, ist, wie gewöhnlich dieser Angriff war. Kein Zero-Day, kein Kernel-Escape. Ein Hunk in einer Workflow-Datei und ein Maintainer, der spät an einem Dienstagabend reviewt, das war der ganze Plan, und er hätte fast funktioniert. Die Gegenmaßnahme ist genauso gewöhnlich: Reviewe jede Änderung unter .github/workflows mit demselben Misstrauen, das du einer Änderung an deiner Auth-Middleware entgegenbringen würdest, und behandle die Maschine, die Fork-PRs ausführt, standardmäßig als feindliches Gelände. Also erzähl mir, wie du das in der Praxis handhabst. Hostest du Runner für ein öffentliches Repo selbst, und wenn ja, was steht konkret zwischen einem Fork-Job und deiner Umgebung? Ich würde gern von den Setups hören, die den Kontakt mit dem PR eines Fremden überlebt haben, und genauso gern von denen, bei denen es schiefging.