Giteas act_runner rootless ausführen
In diesem Artikel zeige ich, wie ich den act_runner meiner Gitea-Instanz von gitea/act_runner
auf gitea/act_runner:latest-dind-rootless
umstelle.
Warum dieser Aufwand? Weil act_runner
in Docker läuft und Zugriff auf den Daemon per /var/run/docker.sock
als sogenanntes Volume (gemeint im Sinne von Datenträger) bzw. Bind-Mount benötigt, um zu funktionieren. Der Besitzer dieses Sockets ist root
, wodurch der Container praktisch Vollzugriff auf das Hostsystem erhält.
“RULE #1 - Do not expose the Docker daemon socket (even to the containers)” - OWASP / Docker Security Cheat Sheet
Dies ist hoch riskant, denn es liegt im Prinzip des Runners, von Dritten erzeugten Job-Code auszuführen. Ein offenes Einfallstor. In Verbindung mit direktem Durchgriff auf das Hostsystem kann das im Falle eines erfolgreichen Angriffes einen Totalausfall bzw. die “feindliche Übernahme” meiner Infrastruktur bedeuten.
Gefahr durch docker.sock
Ich richte mich in dieser Anleitung nach der Empfehlung von OWASP zum Thema Docker-Sicherheit und wende deren Regel(n) in der Praxis an. OWASP steht für “Open Web Application Security Project”. Es ist eine Organisation, die sich für höhere Sicherheit von Webanwendungen einsetzt und dabei Anwender wie mich mit kostenfreien Artikeln, Dokumentation und Technologie unterstützt.
Die unter Regel 1 beschriebenen Maßnahmen lauten:
Dockers tcp Socket abgeschaltet lassen
Sollte der Zugang zum Docker Daemon per tcp eingeschaltet sein, kann sich mit diesem - sofern keine weiteren Vorkehrungen getroffen wurden - über eine ungesicherte Verbindung und ohne Authentisierung verbunden werden. Der Daemon ist dann durch praktisch jeden Internetnutzer zugänglich und damit angreifbar.
Wie stelle ich also sicher, dass der tcp Socket abgeschaltet ist? Die Anleitung hierzu findet sich in den Docker Docs und muss quasi verkehrt herum angewandt werden. Auch dort wird ausdrücklich vor ungeschützter Verfügbarmachung des tcp-Sockets gewarnt.
Nach Ausführen aller erforderlichen Schritte lasse ich netstat
einmal durchlaufen. Es listet mir offene Sockets, Netzwerkschnittstellen und Routing-Tabellen auf:
# check if Docker Daemon's "dockerd" tcp socket is exposed
schallbert@machine:~# netstat -lntp | grep dockerd
# test is a pass if this command does not return anything.
Bei mir findet sich kein Eintrag mit dockerd
. Mein Server ist an dieser Stelle schon einmal nicht verwundbar.
Den Docker Socket /var/run/docker.sock
nicht in andere Container einbinden
Hier wird es schon etwas schwieriger: act_runner benötigt den Socket, um Job-Container zu erzeugen, zu verwalten und schließlich wieder zu entsorgen. Ohne Zugriff auf den Docker Daemon über den Socket funktioniert die Build-Pipeline schlicht und einfach nicht - es sei denn, man möchte komplett auf Docker verzichten und sowohl den Runner als auch die Build-Jobs direkt auf der Host-Maschine laufen lassen. Damit gingen aber viele Nachteile einher: Verlust der Kapselung, keine Portabilität, schlechte Skalierbarkeit, geringere Sicherheit…
Aber auch für die Option mit Docker gibt es eine Lösung: act_runner Docker-in-Docker aufsetzen. Mit diesem Setup erhält act_runner einen eigenen Docker Daemon, jedoch mit eingeschränkten Rechten und ohne Zugriff auf den des Host-Systems. Dieser übernimmt dann den Lebenszyklus von Job-Containern, sodass sie komplett unabhängig vom Host-System laufen.
Docker in Docker
Folgendes Schaubild verdeutlicht den Unterschied:
Act_runner “rootless” starten
Also folge ich der Anleitung und kopiere mir eine passende docker-compose.yml
zusammen. Wichtig hier:
- Es muss
privileged: true
gesetzt werden. Ansonsten kann der Docker Daemon im act_runner-Container nicht richtig starten, da ihm Kernel-Funktionen fehlen. Somit schmiert der ganze Container wieder und wieder ab, ohne dabei hilfreiche Fehlermeldungen zu erzeugen. - Die Umgebungsvariable
DOCKER_HOST=unix:///var/run/user/1000/docker.sock
muss gesetzt sein. Hier wird der Docker Socket über einen nicht-privilegierten User gesteuert und steht dem Runner für die Verwaltung von Job Containern zur Verfügung. Der Daemon läuft gekapselt im Container und steht nicht in Verbindung mit der Host-Maschine.
Stand Jul-2025: Problem mit 0.2.12-dind-rootless
Hier hatte ich direkt mein erstes Problem. Der Runner stürzt kurz nach dem Start mit folgender Fehlermeldung ab:
schallbert@machine:~# docker logs gitea-runner
# [...]
[rootlesskit:parent] error: failed to start the child: fork/exec /proc/self/exe: operation not permitted
Hilfe kam aus der Gitea-Community. Mit der Vorgängerversion 0.2.11 läuft act_runner sauber hoch. Trotzdem wird er aus mir unbekannten Gründen auf Gitea als 0.2.12 angezeigt.
Volume-Verwirrung
Prima! Da der Runner nun läuft, lasse ich ihn direkt mal auf einen Job los. Leider schlägt das Bauen bereits nach Sekundenbruchteilen mit dieser Meldung fehl:
# Runner step: Set up job
failed to start container: Error response from daemon: error while creating mount source path '<volumeSourceFullPath>': mkdir <volumeSourcePath>: permission denied
Hier habe ich stundenlang recherchieren müssen und war lange der falschen Überzeugung, dass es an fehlenden Berechtigungen bei Ordnern auf der Host-Maschine liegt. Später erst verstand ich wirklich, dass Docker-in-Docker genau so gemeint ist, wie es heißt: Nicht nur werden Container durch Container erzeugt, sondern innerhalb des Containers läuft ein separater Docker Daemon!
Damit funktioniert nämlich der klassische Weg nicht mehr, Volumes in Job Containern direkt vom Host-System per -v /a/b:/x/y
verfügbar zu machen.
Stattdessen müssen Volumes nun durchgereicht werden. Beispiel:
host directory
/opt/server/www/blog-artifacts
-> act_runner volume/tmp/blog-artifacts:z
–> job container volume/tmp/blog-artifacts
Das :z
im Volume vom act_runner wird jetzt wichtig. Es zeigt Docker gegenüber an, dass dieses Volume zwischen Containern geteilt wird. Diese Volumes müssen nicht nur in der docker-compose.yml
von act_runner angegeben werden, sondern auch die Job-Skripte im Ordner .gitea/workflows/
sind jetzt entsprechend anzupassen. Hier wird das :z
auf der “rechten Seite” hingegen nicht benötigt.
“is not a valid volume”
Allem Aufwand zum Trotz laufen meine Jobs noch immer nicht durch. Dieses Mal wegen einer Fehlermeldung, die mir bereits bekannt vorkam:
# Runner step: Set up job
[/tmp/blog-artifacts] is not a valid volume, will be ignored
Also rein in die config.yml
von act_runner und die Volume-Namen hinzufügen:
# /gitea/runner/config.yml
# [...]
valid_volumes: ["/tmp/blog-artifacts", "/tmp/lectures-artifacts"]
# [...]
In dieser Datei kann ich privileged: false
stehen lassen, denn der Job Container selbst benötigt anders als act_runner keine Kernel-Features.
Berechtigungen korrekt setzen
Nun erhalte ich erneut Permission Denied
Fehlermeldungen beim Ausführen meiner Jobs, wenn auch nicht direkt im ersten Schritt der Aktionen. Da ich die Volume-Pfade jetzt bis ins Kleinste geprüft habe, kann es eigentlich nur noch an den Ordnerberechtigungen auf der Host-Maschine liegen.
Damit die vom Job-Container erzeugten Artefakte durch die Volumes auf meinem Host abgelegt werden können, muss ich das zu schreibende Verzeichnis und alle Unterordner -R
an den vorher definierten, nicht-privilegierten User ID=1000
übergeben:
schallbert@machine:~# chown -R 1000:1000 /target/path/to/artifact/
Endlich funkioniert alles reibungslos und ich habe der (unwahrscheinlichen, aber möglichen) Übernahme meines Host-Systems durch bösartige Job-Container einen Riegel vorgeschoben.