Self-hosted Runner sind praktisch. Eigene Hardware, volle Kontrolle über Tooling, keine Minuten-Abrechnung. Der bequeme Weg ist, eine VM einmal aufzusetzen und dann Job für Job darauf laufen zu lassen. Genau da fängt das Problem an. Ein Runner, der über hunderte Builds am Leben bleibt, ist keine saubere Umgebung mehr, sondern ein gemeinsam genutzter Rechner, auf dem jeder Job die Spuren des vorherigen erbt.
Was ein persistenter Runner mit sich schleppt
Zwischen zwei Jobs bleibt auf einer langlebigen Maschine mehr liegen, als man denkt. Ausgecheckte Repositories im Workspace, Layer im Docker-Cache, Anmelde-Token in ~/.docker/config.json, exportierte Umgebungsvariablen, npm- und pip-Caches, temporäre Dateien in /tmp. Ein bösartiger oder auch nur schlampiger Build kann etwas davon lesen oder manipulieren. Wenn ein Pull Request aus einem Fork auf demselben Runner läuft wie der Deploy-Job mit Produktions-Credentials, teilen sich beide dieselbe Festplatte und denselben Prozessraum.
Das ist keine theoretische Sorge. Der klassische Angriff ist ein Job, der eine Hintergrundprozess-Bombe hinterlässt oder eine Datei im Workspace platziert, die der nächste Build unbesehen ausführt. Solange der Runner steht, steht auch die Angriffsfläche.
Ephemer als Standard
Ein ephemerer Runner nimmt genau einen Job an und beendet sich danach. Die Maschine wird verworfen, eine neue, frische Instanz registriert sich für den nächsten Job. Jeder Build startet auf sauberem Boden. Bei den GitHub-Actions-Runnern reicht dafür ein Flag bei der Konfiguration.
./config.sh \ --url https://github.com/meine-org/mein-repo \ --token "$RUNNER_TOKEN" \ --ephemeral \ --name "runner-$(uuidgen)"./run.sh
Mit --ephemeral verarbeitet run.sh einen einzigen Job und kehrt dann zurück. Der Trick ist, den Prozess nicht in einer Schleife neu zu starten, sondern die ganze Maschine wegzuwerfen. Das heißt in der Praxis: der Runner läuft in einem Container oder einer VM, und die Orchestrierung startet für jeden Job ein neues Exemplar.
In Kubernetes mit dem Actions Runner Controller
Wer die Runner in einem Cluster betreibt, muss das Wegwerfen nicht selbst skripten. Der Actions Runner Controller fährt pro Job einen Pod hoch und räumt ihn danach ab. Die Isolation kommt vom Pod-Lebenszyklus, nicht von einem Cleanup-Schritt, den man vergessen kann.
apiVersion: actions.github.com/v1alpha1kind: AutoscalingRunnerSetmetadata: name: repo-runners namespace: arc-runnersspec: githubConfigUrl: https://github.com/meine-org/mein-repo githubConfigSecret: github-app minRunners: 0 maxRunners: 20 template: spec: containers: - name: runner image: ghcr.io/actions/actions-runner:latest
Runner aus dieser Runner-Scale-Set-Variante sind von Haus aus ephemer. minRunners: 0 bedeutet, dass ohne offene Jobs kein Pod läuft. Kommt ein Job, entsteht ein frischer Pod, danach ist er weg. Kein geteilter Workspace, kein übrig gebliebener Cache, kein Token aus dem letzten Build.
Was ephemer nicht löst
Ephemere Runner räumen den Zustand zwischen Jobs weg. Sie ersetzen keine Rechte-Hygiene innerhalb eines Jobs. Wenn der einzelne Build zu breite Token bekommt, hilft die frische Maschine nichts, weil der Schaden im laufenden Job passiert. Ephemer und Least Privilege gehören zusammen: der Runner ist sauber, und das, was der Job in die Hand bekommt, ist auf das Nötige beschränkt.
Der zweite Punkt ist das Image selbst. Ein ephemerer Pod, der aus einem veralteten oder ungeprüften Runner-Image startet, ist zwar frisch, aber nicht unbedingt vertrauenswürdig. Das Basis-Image gehört in denselben Scan- und Pinning-Prozess wie jedes andere Container-Image in der Lieferkette.
Für mich ist die Regel simpel: ein Job, ein Runner, danach Schluss. Alles, was zwischen zwei Builds überlebt, ist entweder Cache, den man bewusst und kontrolliert auslagert, oder ein Risiko. Wie sich das in eine durchgängige Pipeline-Absicherung einfügt, vom Runner über die Rechte bis zur Lieferkette, schreibe ich regelmäßig auf digital-business.blog.
Hinterlasse einen Kommentar