„
#1) Respect the privacy of others.
#2) Think before you type.
#3) With great power comes great responsibility.
”
Jedem der schon einmal mit Linux auf der Kommandozeile gearbeitet hat kommen diese Zeilen sicher bekannt vor. Bei vielen Linux-Distributionen werden sie beim Login mit dem User „root“ angezeigt und sollen den Nutzer daran erinnern, dass seine Aktionen ab nun ernste Konsequenzen haben können. Bei Linux – wie auch in der Welt der Container – gilt daher der Grundsatz, dass möglichst nicht als User „root“ gearbeitet werden soll.
Warum eigentlich nicht?
Aber seien wir ehrlich. Als Entwickler, Hobby-Admin oder Einsteiger im Thema erwischt man sich immer wieder dabei, seine Container mit erweiterten Rechten oder als root zu betreiben. Zu groß ist die Verlockung und vielfältig die vorgeschobenen Gründe:
- „Einfacher“
- „Schneller umzusetzen“
- “Technisch nicht machbar”
- “Zu kompliziert”
- “Schon immer so gemacht”
- “Läuft doch auch so”
- “Ist doch nur ein POC/Provisorium/Test…”
- “Ich weiß nicht, wie es geht”
Einige dieser Punkte treffen spätestens in einer produktiven Umgebung mit Kubernetes oder Openshift nicht mehr zu oder funktionieren nicht mehr (“Schon immer so gemacht”, “Läuft doch auch so”), da dort Container mit privilegierten Rechten in der Regel nicht ausgeführt werden dürfen. Oder sie sind mit der Nutzung von aktuellen Versionen von Docker beziehungsweise alternativen Tools (z.B. podman) obsolet geworden (“Technisch nicht machbar”), denn früher gab es noch keinen Support für rootless-Betrieb bei Docker. Manche Punkte hängen schlicht und ergreifend von der persönlichen Einstellung ab (“Ist doch nur ein POC/Provisorium/Test…”) und lassen sich entsprechend unwahrscheinlich mit einem Blogbeitrag ändern.
An den Punkten “Einfacher”, “Schneller umzusetzen” oder “Ich weiß nicht, wie es geht” können wir aber sehr wohl etwas tun und genau hier setzen wir an, um die Grundlagen von rootless Containern darzulegen.
Kleine Begriffskunde
Zuerst einmal wollen wir klären, was die Begriffe rootless und rootfull im Kontext von Containern bedeuten. Wichtig ist, dass es zwischen zwei Bereichen zu trennen gilt: Der Ausführung und dem Start der Container-Runtime selbst (Docker, Podman etc.) und dem Kontext innerhalb des Containers (unserer containerisierten Anwendung).
Die Container-Runtime kann dabei als normale Linux-Anwendung betrachtet werden, die entsprechend den Berechtigungen des Nutzers, unter dem sie läuft beziehungsweise gestartet wurde, auch entsprechende Berechtigungen auf dem Hostsystem erhält. Starte ich beispielsweise Docker als User root, erhält auch die Docker-Runtime root-Berechtigungen auf dem Hostsystem. Starte ich Docker als normaler User (nicht privilegierter Nutzer), erhält die Runtime auch nur entsprechende Berechtigungen auf dem Hostsystem, wie der unprivilegierte Nutzer.
So darf ein unprivilegierter Nutzer zum Beispiel keine sogenannten „lower ports“ (1-1024) auf dem System für seine Anwendungen nutzen, was dazu führt, dass ein Webserver beispielsweise nicht auf dem Standard-Port für Webtraffic (Port 80 und 443) lauschen darf, sondern Ports über 1024 nutzen muss. Um dies zu erreichen, muss entweder die Container-Runtime als root-Nutzer gestartet werden (aus Sicherheitsgründen nicht zu empfehlen) oder aber dem unprivilegierten Nutzer das Recht gewährt werden, auch auf den lower ports zu lauschen.
Der Betrieb und die Konfiguration der Container-Runtime ist für uns relevant, wenn wir die Hoheit über die Container-Runtime beziehungsweise den zugrundeliegenden Host haben, zum Beispiel auf unserem Entwicklernotebook oder unserem Heimserver.
Bei Containerplattformen wie Kubernetes oder Openshift werden der Betrieb und die Konfiguration in der Regel von gesonderten Betriebsteams übernommen, so dass wir als Entwickler keinen Einfluss nehmen können. Aus Sicherheitsgründen ist es in der Regel nicht gestattet, Container als root-User zu starten.
Im Gegensatz dazu steht der Anwendungskontext innerhalb unseres Containers. Dieser ist von der Container-Runtime und dem sie startenden Nutzer losgelöst. Hier dürfen wir als Entwickler entscheiden, welche Rechte wir benötigen und mit welchem Nutzer wir unsere Applikation im Container ausführen wollen. Ein Beispiel: die Container-Runtime wurde als nicht privilegierter Nutzer gestartet. Doch um in meinem Container Programme installieren zu können, benötige ich root-Rechte. Ist das ein Problem? Nein, denn in unserem Container sind wir losgelöst von den darüberliegenden Berechtigungen der Runtime. Wir können ohne weiteres in unserem Container zum User root werden, Software installieren und Änderungen vornehmen. Und das, obwohl die Runtime im unprivilegierten Modus läuft. Wie ist das möglich?
Technisch stecken dahinter die Konzepte von Namespaces sowie cgroups, welche fester Bestandteil des Linux-Kernels und moderner Distributionen sind. Sie ermöglichen es, jedem Prozess eigene Bereiche und Ressourcen zur Verfügung zu stellen und diese von den Bereichen und Ressourcen anderer Prozesse zu isolieren. (siehe auch What Are Namespaces and cgroups, and How Do They Work? – NGINX)
Ein paar Beispiele:
Wir starten auf unserer rootless-Dockerumgebung einen Alpine-Container im interaktiven Modus und mounten das root-Verzeichnis (“/”) unseres Hosts in das Verzeichnis “/host” in unserem Container.
$ docker run -ti -v /:/host alpine
Wir erhalten so ein interaktives Terminal, welches es uns ermöglicht, in unserem Container zu arbeiten. Ein schneller Test zeigt uns außerdem, dass wir als Nutzer root arbeiten:
/ $ whoami
root
Wir sind nun in der Lage, beliebige Operationen in unserem Container durchzuführen, ganz so, wie man es mit Root-Rechten erwarten würde. Zum Beispiel den Editor Nano nachinstallieren:
/ $ apk add nano
Anschließend versuchen wir eine Datei auf unserem Host zu löschen, indem wir in unserem Container im Ordner “/host/” versuchen, darauf zuzugreifen.
/ $ rm /host/usr/bin/sh
rm: can't remove '/host/usr/bin/sh': Permission denied
Wir erhalten eine Fehlermeldung, welche uns sagt, dass wir nicht die benötigten Berechtigungen haben, um die Aktion durchzuführen. Aber wir sind doch der User root, wie ist das möglich?
Die Antwort ist einfach, wir sind nur im Context des Containers als root und mit entsprechenden Rechten unterwegs. Außerhalb des Containers (die Datei, auf die wir hier zugreifen wollen, liegt außerhalb) arbeiten wir mit eingeschränkten Rechten. Genauer: mit den Rechten des Users, unter welchem die Container-Runtime läuft. Wäre die Runtime als User root gestartet worden, wäre unser Löschbefehl ohne weiteres ausgeführt worden.
Daher ist es im Allgemeinen eine schlechte Idee, die Container-Runtime als User root zu starten. Denn gelingt es einem Angreifer aus seinem Container auszubrechen, kann er unter Umständen auf die erweiterten Rechte der Runtime zurückgreifen und so den Host übernehmen.
Was gilt es zu beachten, wenn man rootless Docker einsetzen will?
Zunächst einmal muss die korrekte Version von Docker verwendet werden. Im Allgemeinen ist rootless ab Docker Version 20.10 problemlos unterstützt und stabil. Dennoch gibt es einige Besonderheiten und Fallstricke. Hier eine kurze Auflistung:
- Nutzen der niedrigen beziehungsweise privilegierten Netzwerk-Ports unter 1024 – Dies kann mit dem Gewähren entsprechender Berechtigungen (sysctl /proc/sys/net/ipv4/ip_unprivileged_port_start oder durch Setzen von CAP_NET_BIND SERVICE) umgangen werden
- Einschränken der Ressourcennutzung durch den Container wie etwa CPU und RAM – Dies erfordert cGroupsV2, welches in manchen Distributionen erst aktiviert werden muss, dann aber andere Probleme und Inkompatibilitäten nach sich ziehen kann
- Nicht alle Storagetreiber sind verfügbar
- Overlay Networks – Verwalten von (virtuellen) Netzen ist nur mit root-Berechtigungen möglich
- AppArmor – Aktuell nicht unterstützt. AppArmor-Profile können von Docker ohne root-Berechtigung nicht zur Laufzeit geladen werden
- Host- Modus für das Netzwerk (docker run –net=host) – Für diesen Modus werden root-Berechtigungen benötigt, ähnlich wie für die Nutzung der lower ports
Doch auch wenn man im rootless-Modus arbeitet, ist es nicht ratsam innerhalb des Containers als User root zu arbeiten, außer wenn es absolut nötig ist, etwa um beispielsweise Pakete nachträglich zu installieren. Tatsächlich verhindern die meisten Containerplattformen wie etwa Kubernetes und Openshift per Default die Ausführung eines Containers bei Nutzung des internen Users root.
Unterstützt werden soll dadurch die – nicht nur in der Containerwelt vorhandene – Best Practice, nach der eine Anwendung, ein Prozess oder ein Nutzer mit so wenig Rechten wie nötig laufen soll (vergleiche: Processes In Containers Should Not Run As Root | by Marc Campbell | Medium).
Die Umsetzung während des Build-Prozesses eines Containers ist tatsächlich auch nicht schwer und beschränkt sich auf zwei zusätzliche Zeilen im Dockerfile.
Mit folgender Direktive legt man zuerst einen entsprechenden User mitsamt Gruppe im Container an:
RUN groupadd -r <user> && useradd --no-log-init -r -g <user> <user>
Anschließend kann man mit dem Befehl USER <user>[:<group>] zu eben diesem User werden, wodurch alle folgenden Anweisungen im Kontext dieses Benutzers ausgeführt werden (siehe auch Dockerfile reference | Docker Documentation).
Zusammenfassung
Es kann festgehalten werden, dass auch mit Containern nach Möglichkeit immer rootless gearbeitet werden sollte. Bei Docker ist dafür initial ein gesondertes Installationsverfahren notwendig, bei der Alternative Podman funktioniert dies schon out-of-the-box.
Dies vereinfacht auch den späteren Betrieb oder die Überführung der Container auf Plattformen wie Kubernetes oder Openshift. Der Aufwand hält sich mit dem Anlegen des Nutzerkontos nebst Gruppe und dem Wechsel zu diesem User innerhalb des Dockerfiles auch in Grenzen.