Giteas act_runner rootless: Startprobleme
Das Fehlerbild
Mein docker-in-docker (DinD) act_runner stürzt kurz nach dem Start mit folgender Fehlermeldung ab:
![Image: console window with docker logs text output: "[rootlesskit:parent] error: failed to start the child: fork/exec /proc/self/exe: operation not permitted"](/assets/images/posts/2026-03-28-act_runner-dind-failed-to-start-child-problem.avif)
Issue-Ticket bei Gitea
In der Gitea-Community (issue #721) habe ich ein Ticket für das Problem angelegt. Im Folgenden gehe ich auf eigene Erkenntnisse ein und fasse die Diskussion etwas zusammen.
Problem mit Kernel-Berechtigungen?
Der act_runner muss bei der Docker-in-Docker Konfiguration selbst einen eigenen Docker Daemon betreiben. Nur so kann der Runner für die Actions eigene Container erstellen. Dies ist aus Sicherheitsgründen erst einmal nicht erlaubt.
Warum Container keine Container starten dürfen
Im Kontext des act_runner führt eine Action Code aus, der selbst Teil des Repository ist. Wird unbemerkt Schadcode in die Action eingeschleust, z.B. von einem “Collaborator”, so kann dieser bei einer Docker-Installation ohne DinD über den unter Umständen mit root-Rechten ausgestatteten Docker-Daemon dockerd direkt auf das Hostsystem durchgreifen.
Auswirkungen, falls trotz DinD etwas schief geht
Bei Anwendung des DinD-Konzeptes kann dies bei einem Angriff lediglich zum Zugriff auf den dockerd innerhalb des act_runner Containers führen - und das nur, wenn die Action nicht sauber gekapselt ist. Immer noch nicht gut, aber vertretbar: Bei Neustart des Containers ist der Status quo ante wiederhergestellt. Durchgriff auf das Hostsystem besteht allenfalls indirekt dadurch, dass der Container selbst mit sämtlichen Kernel-Features ausgestattet ist.
Einfache Lösung: DinD-Container privileged starten
Um dem DinD-Container jetzt die Möglichkeit zu geben eigene Actions aufzusetzen, kann in der Konfiguration privileged: true gesetzt werden. Damit werden dem Container alle Kernel-Fähigkeiten eingeräumt. Dies ist laut (dem hervorragend gemachten) OWASP Security Cheat Sheet möglichst zu vermeiden. Sollte act_runner nun selbst angegriffen werden oder kritische Sicherheitslücken offenbaren, hätte der Eindringling auf diese Weise bereits alle Möglichkeiten, der Kapselung vor dem Host-System zu entgehen.
Will man den einfachen Weg gehen, sieht die entsprechende docker-compose.yml wie folgt aus.
# docker-compose.yml
# section ACT_RUNNER
runner:
image: gitea/act_runner:latest-dind-rootless
container_name: gitea-runner
privileged: true
# [...]
“And remember: Do not run containers with the –privileged flag!!!” - OWASP’s Docker Security Cheat Sheet, 2026 License
Sackgasse: Nur unbedingt benötigte Rechte vergeben
Nach kurzer Recherche finde ich einen Artikel auf CodeStudy.net1, welcher sich mit dem Thema DinD beschäftigt. Hieraus bastele ich Änderungen in meiner docker-compose.yml zusammen.
Meine Vorgehensweise:
- Änderungen in
docker-compose.ymlvornehmen - Service neu starten
docker compose restart <service> - Logs anschauen
docker logs --tail 100 <container-name> - Falls gestartet: Programm laufen lassen
web-app->repo->jobs->rerun_all-jobs - Bei Fehlschlag: Wiederholen
Ich beginne mit wenigen, aber sehr mächtigen Rechten. Nach ein paar Iterationen erhalte ich:
# docker-compose.yml
# section act_runner DinD-rootless
runner:
image: gitea/act_runner:latest-dind-rootless
container_name: gitea-runner
privileged: false
cap_add:
- SYS_ADMIN
security_opt:
- no-new-privileges:true
- apparmor:unconfined
- systempaths=unconfined
- writable-cgroups=true
# [...]
Aber noch immer fehlen mir Rechte, die außerhalb von Kernel-Privilegien und Sicherheitszonen liegen: Der Docker-Daemon möchte auf sysfs und proc, also Systemdateiverzeichnisse und -Prozesse zugreifen können.
level=warning msg="[rootlesskit:child ] failed to mount sysfs, falling back to read-only mount: operation not permitted"
Das kann ich selbst mit cap_add: ALL nicht ausgleichen. Zudem wird die Quellenlage im Internet auf dieser technischen Ebene echt dünn (und ich bin kein Docker-Spezialist). Ein Re-Mount der Dateisysteme könnte hier noch helfen; das erscheint mir aber zu experimentell und fehleranfällig.
Nach weiteren Stunden Recherche und Trial-and-Error auf meinem Server muss ich @tianon in einer Diskussion auf Github zustimmen: Der Docker Daemon benötigt so viele Rechte und Fähigkeiten, dass man gleich bei privileged: true bleiben kann und sich nicht mit einer Armada an CAP_ADD und System-bind-mounts herumschlagen muss.
Zukünftige Lösung: Virtualisierung oder Daemonless
Doch es gibt möglicherweise bessere Lösungen: Eigene Container-Laufzeitumgebungen wie Sysbox. Die Box hat selbst auf dem Hostsystem keine besonderen Rechte, ähnlich wie in einer virtuellen Maschine scheint sie in ihr laufenden Applikationen aber alle Rechte und Fähigkeiten zur Verfügung stellen zu können.
Installation und Inbetriebnahme bis hin zu funktionierenden Job-Containern führt für diesen Post zu weit. Daher muss ich vorerst auf andere Quellen verweisen.
Auch das Umschwenken von Docker auf Podman ist eine mögliche Lösung: Podman ist Daemonless, rootless und bietet ähnlichen Funktionsumfang wie Docker. Ein Umzug nach Podman wäre für mich ein eigenes Projekt und passt nicht in den Kontext dieses Posts.
Linux Security Modules (seccomp, AppArmor, SELinux)
Begriffsklärung: LSM ermöglichen verschiedene Sicherheitsprüfungen und -Sperren auf Kernelebene. Durch “Mandatory Access Control” können Sicherheitserweiterungen wie AppArmor Kernel-Fähigkeiten für einzelne Anwendungen steuern und Zugriffe falls nötig sperren.
Apparmor
Ich betreibe meinen Server (VPS) mit Ubuntu. Dieses Betriebssystem hat in den aktuelleren Versionen “user namespace creation restrictions” in AppArmor integriert, was das Betreiben von appimages, WebApps und eben auch Containers mit höheren Rechten verhindert.
“Unprivileged user namespaces are a feature in the Linux kernel […]; it enables unprivileged users to gain administrator (root) permissions within a confined environment […]” - mbelair, Ubuntu Discourse, as of May-2026, Ubuntu Discourse Website
Kurzgesagt wurde dieses Werkzeug als security-hardening-Maßnahme entwickelt, um Angriffsfläche auf dem Host-System bei Ausführung von Programmen zu reduzieren, die höhere Rechte erfordern.
Einfache Lösung: Komplett abstellen
Die Dampfhammermethode stellt das Feature für Benutzernamensräume komplett kalt. Wir teilen AppAmor mit, dass Drittprogramme wie in alten Betriebssystemversionen auch ohne Einschränkungen Kernel-Features oder höhere Rechte nutzen dürfen. Den zugehörigen Befehl habe ich aus dem askubuntu-Forum entliehen.
Er schaltet die Einschränkung von Nicht-Admin-Benutzernamensräumen (die Übertragung ins Deutsche tut schon fast weh) für diese Sitzung ab, indem er die Kernelparameter zur Laufzeit umschreibt (-w).
schallbertTestsThis@machine:~# sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
Ich halte den Befehl zur Fehlersuche für sehr hilfreich. Möchte man herausfinden, ob das gewünschte Programm wegen AppArmor nicht startet:
- Obiges Kommando eingeben
- das Drittprogramm, den Container etc. testen
- durchbooten. Oder den Befehl mit
=1eingeben. Danach ist der Ursprungszustand wiederhergestellt.
Möchte man diese Verwundbarkeit permanent erhalten, so gibt man ein:
makeVulnerability@machine:~# echo 'kernel.apparmor_restrict_unprivileged_userns = 0' | sudo tee /etc/sysctl.d/20-apparmor-donotrestrict.conf
makeVulnerability@machine:~# sudo shutdown -r now
Dies alleine stellt noch keine Sicherheitslücke dar. Es ist jetzt nur prinzipiell einfacher geworden, eventuelle Schwächen im Kernel auszunutzen und der “Sandbox” des Containers zu entrinnen.
Richtige Lösung: Für jede Anwendung justieren
Es gibt Anwendungen (wie meine DinD-Version von act_runner), die benötigen zwingend erweiterte Rechte um ordnungsgemäß zu funktionieren. Und nur diese sollten die Möglichkeit erhalten, Nicht-Admin-Benutzernamensräume zu erhalten.
Docker selbst hat AppAmor einen Abschnitt in der Dokumentation gewidmet. Um zur Lösung für den DinD-Runner zu gelangen, benötigt es eine Transferleistung (danke, @thespad). Zur Wiederholung nochmal die Fehlermeldung von oben:
gitea-runner | [rootlesskit:parent] error: failed to start the child: fork/exec /proc/self/exe: operation not permitted
gitea-runner | s6-svwait: fatal: some services reported permanent failure or their supervisor died
Das Logfile sagt aus, dass der parent (Docker-Daemon im Container) sein child (Runner-Container) nicht starten kann, weil er für andere Teilnehmer (fork) selbst keine Prozesse (/proc/self) erstellen kann. Wer ist der User dieses Daemons? rootlesskit.
Genau hier hakt unsere Lösung ein: Wir müssen in der docker-compose.yml das AppArmor-Profil für rootlesskit im DinD-Container des act_runner freigeben.
# docker-compose.yml
# section act_runner DinD-rootless
runner:
image: gitea/act_runner:latest-dind-rootless
container_name: gitea-runner
privileged: true
security_opt:
- apparmor=rootlesskit
# [...]
Entsprechend gut sieht jetzt auch das Logfile aus. Fertig!

-
Ich habe das Gefühl, dass jener Post mit “KI” geschrieben wurde. Wenn es konkret wird und es um “critical volume mounts” geht, stehen dort nämlich keine echten System-Pfade mehr und der Artikel wird insgesamt so vage, dass ich den Ratschlägen folgend gar nicht bis zum Ziel komme. ↩