Skip to main content

Privilegierte Docker-Container – brauchen Sie sie wirklich?

Artikel von

5. November 2020

0 Min. Lesezeit

Diese Woche bin ich bei Tests mit Podman in eine rabbit hole geraten: Warum war zum Ausführen eines bestimmten Containers in einer rootless-Konfiguration das Flag --privileged erforderlich? Mein Kollege Eric Smalling fragte völlig zu Recht, warum dafür dieses Flag nötig sein sollte.

Letztlich ist --privileged eine Kurzform für „alles freigeben“. Auch wenn Sie vielleicht denken, dass das beim rootless Betrieb keine große Rolle spielt: Es widerspricht den Prinzipien der geringsten Berechtigungen und Zero Trust. Auch wenn Sie Container rootless ausführen, sollten Sie immer genau wissen, was Ihr Container benötigt, und ihm nur die dafür erforderlichen Mindestberechtigungen erteilen.

Dieses Flag gewährt Prozessen, die rootless laufen, zwar keine Berechtigungen, die über die des jeweiligen Benutzers hinausgehen. Trotzdem wollte ich wissen, welche Capabilities dieser Prozess tatsächlich benötigt und warum diese Konfiguration empfohlen wurde. Ganz im Geiste der Entdeckungsfreude schnürte ich meine Ausrüstung für die Erkundungstour und machte mich auf den Weg ins Herz der Dunkelheit.

Schauen wir uns das genauer an

Sehen wir uns zunächst an, was es bedeutet, Container rootless auszuführen. Dahinter steckt der Zauber der User-Namespaces im Linux-Kernel, mit denen nicht privilegierte Benutzer neue User-Namespaces erstellen können. Wenn ein Benutzer einen neuen User-Namespace erstellt und aufruft, wird er im Kontext dieses Namespace zu root und erhält die meisten Berechtigungen, die zum Starten eines funktionsfähigen Containers erforderlich sind. Root hat in User-Namespaces natürlich nicht dieselben Berechtigungen wie der System-Root. Das bedeutet aber, dass wir Container ohne erhöhte Berechtigungen ausführen können und so die potenzielle Angriffsfläche für Schwachstellen erheblich verkleinern.

Ich verwende eine virtuelle Maschine mit einer minimalen CentOS-8-Installation, die in VirtualBox auf meinem MacBook läuft. Zuerst habe ich Podman mit dnf installiert:

dnf install podman

Anschließend habe ich im Home-Verzeichnis meines Benutzers ein Verzeichnis erstellt und dort ein selbstsigniertes Zertifikat generiert:

mkdir certs
openssl req -newkey rsa:4096 -nodes -sha256 -keyout certs/domain.key -x509 -days 365 -out certs/domain.crt

Nun möchte ich dieses Zertifikat für eine lokale Docker-Registry zu Testzwecken verwenden – und hier beginnt unsere eigentliche Reise. In der gesamten Dokumentation, die ich dazu gefunden hatte, wurde das Flag --privileged empfohlen:

[matt@localhost ~]$ podman run -d --name registry -p 5000:5000 -v "$(pwd)"/certs:/certs  --restart=always -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key --privileged registry:2

Wie Sie der obigen Befehlszeile entnehmen können, führen wir das Registry-Image mit dem Tag 2 aus, binden das Zertifikatsverzeichnis aus meinem aktuellen Verzeichnis als Volume unter /certs im Container ein, übergeben einige Umgebungsvariablen zur Konfiguration der Registry und fügen ganz selbstverständlich das Flag --privileged hinzu, das Podman anweist, den Container im privilegierten Modus auszuführen.

Damit Podman die Registry verwenden kann, müssen wir anschließend einen Eintrag in /etc/containers/registries.conf hinzufügen:

[registries.insecure]
registries = ['localhost:5000']

Im privilegierten Modus funktioniert alles problemlos. Als Erstes wollte ich daher sehen, was passiert, wenn wir den Container ohne Privilegien ausführen. Das war schnell herausgefunden: Der Container startet, stürzt ab und wird erneut gestartet. In den Logs steht, dass er nicht auf das Zertifikat zugreifen kann.

[matt@localhost log]$ podman logs bd323f90c60b

time="2020-10-20T18:24:27.806128235Z" level=fatal msg="open /certs/domain.crt: permission denied"

Zunächst nahm ich an, dass dies mit Linux-Capabilities zusammenhing, denn das Flag --privileged ermöglicht dem Container unter anderem den Zugriff auf alle vom Kernel bereitgestellten Capabilities. Das lässt sich mit Podman überprüfen, während der Container im privilegierten Modus läuft:

[matt@localhost ~]$ podman top -l capeff
EFFECTIVE CAPS
full

Als Nächstes habe ich in den Linux-Manpages nachgeschlagen, um mir im Detail anzusehen, welche Aktionen die einzelnen Capabilities Prozessen ermöglichen. Mit Podman können wir auch prüfen, welche Capabilities unser laufender Container ohne Privilegien hat:

[matt@localhost ~]$ podman top b7cea04eb70e
USER   PID   PPID   %CPU    ELAPSED        TTY   TIME   COMMAND
root   1     0      0.000   1.400417458s   ?     0s     registry serve /etc/docker/registry/config.yml

[matt@localhost ~]$ podman top -l capeff
EFFECTIVE CAPS
AUDIT_WRITE,CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,MKNOD,NET_BIND_SERVICE,NET_RAW,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOT

Vergleicht man diese Liste mit den Linux-Manpages, sollten die effektiven Capabilities im nicht privilegierten Modus ausreichen, damit der Container Dateien lesen kann. Wahrscheinlich gewährt diese Liste sogar mehr Capabilities, als dieser spezielle Container benötigt. Darauf kommen wir später zurück.

Als Nächstes werfen wir einen Blick in die audit.log auf dem Host – und tatsächlich:

type=AVC msg=audit(1603218166.554:4674): avc:  denied  { read } for  pid=223435 comm="registry" name="domain.crt" dev="dm-0" ino=6582402 scontext=system_u:system_r:container_t:s0:c127,c779 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

Dieser Eintrag zeigt, dass SELinux den Lesezugriff auf die Datei domain.crt blockiert hat. Auf CentOS- und RHEL-Systemen ist SELinux standardmäßig im Enforcing-Modus konfiguriert. Mit dem Tool sestatus können wir das überprüfen:

[matt@localhost ~]$ sudo sestatus
[sudo] password for matt: 
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      31

SELinux versieht Dateien und Verzeichnisse im Enforcing-Modus mit Labels, um den Zugriff darauf zu verwalten. Diese Kennzeichnung wirkt sich auch auf das Verhalten von Container-Engines aus. Sie starten Prozesse mit dem Label container_t und kennzeichnen den eigentlichen Container mit container_file_t. SELinux stellt sicher, dass Prozesse nur mit entsprechend gekennzeichneten Dateien interagieren können. Der Zugriff auf Dateien außerhalb des Containers wird standardmäßig verweigert. Wenn wir das Flag --privileged verwenden, werden Labels deaktiviert, und der Container läuft mit dem Label, mit dem die Container-Engine gestartet wurde. Das sehen wir, wenn wir unsere Container mit Podman untersuchen. Hier ein privilegierter Container:

[matt@localhost ~]$ podman top -l label
LABEL
unconfined_u:system_r:container_runtime_t:s0

Und hier ein Container ohne Privilegien:

[matt@localhost ~]$ podman top -l label
LABEL
System_u:system_r:container_t:s0:c23,c603

Unser Zertifikatsverzeichnis im Host-Dateisystem hat das Label user_home_t:

[matt@localhost ~]$ ls -lZ certs
total 8
-rw-rw-r--. 1 matt matt unconfined_u:object_r:user_home_t:s0 1944 Oct 20 17:54 domain.crt
-rw-------. 1 matt matt unconfined_u:object_r:user_home_t:s0 3272 Oct 20 17:53 domain.key

Das bedeutet in der Praxis, dass diese Dateien im nicht privilegierten Modus für den Container nicht zugänglich sind – selbst dann nicht, wenn sie per Bind-Mount in das Image eingebunden sind.

Anstatt unseren Container also mit --privileged auszuführen, haben wir mehrere Möglichkeiten, das Problem zu beheben. Zunächst können wir Labels vollständig deaktivieren, indem wir in der Podman-Befehlszeile --security-opts label=disable verwenden. Aus Sicherheitssicht ist das natürlich nicht ideal. Deshalb bieten sowohl Podman als auch Docker die Möglichkeit, Mounts neu zu labeln: privat mit dem Schalter Z oder – bei gemeinsam genutzten Mounts – mit dem Schalter z.

Um unseren Container zu reparieren, können wir ihn also einfach so ausführen:

podman run -d --name registry -p 5000:5000 -v "$(pwd)"/certs:/certs:Z  --restart=always -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key registry:2

Abschließend noch einmal zu den Capabilities: Es ist immer eine gute Idee, Container mit möglichst wenigen aktivierten Capabilities auszuführen. In meiner Testkonfiguration lässt sich dieser Registry-Container sogar ohne jegliche Capabilities starten.

podman run -d --name registry -p 5000:5000 -v "$(pwd)"/certs:/certs:Z  --restart=always -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key --cap-drop=all registry:2

Es gibt verschiedene Möglichkeiten herauszufinden, welche Capabilities Ihr Container benötigt. Sie können beispielsweise im Audit-Log nachsehen, welche Capabilities SELinux während des Betriebs blockiert, und die erforderlichen anschließend mit dem Argument --cap-add wieder hinzufügen. Der Grundsatz der geringsten Berechtigungen ist immer die beste Wahl! Die Moral dieser Geschichte: Schütten Sie nicht das Kind mit dem Bade aus.

Kurz zusammengefasst

Container mit --privileged zu kennzeichnen, ist selbst in User-Namespaces keine gute Praxis und widerspricht den Prinzipien der geringsten Berechtigungen und Zero Trust. Finden Sie vor dem Start heraus, was Ihr Container tatsächlich benötigt. Nutzen Sie beispielsweise die Ausgaben von Tools wie SELinux, um zu prüfen, welche Capabilities und Berechtigungen Ihr Container-Image anfordert, und legen Sie in der Produktionsumgebung nur diese fest. Oft sind die tatsächlichen Anforderungen erstaunlich gering.

Container-Sicherheit mit Fokus auf Entwickler

Snyk findet und behebt automatisch Schwachstellen in Container-Images und Kubernetes-Workloads.

Weiterlesen

feature customer snowflake
Article

Sicherheit von Anfang an: Die Snyk Studio-Integration für Snowflake Cortex Code

Snyk Studio ist in Snowflake Cortex Code integriert und scannt KI-generierten Code, Abhängigkeiten und Container während der Entwicklung auf Schwachstellen.

blog feature pypi spoof
Article

Die gesamte Snyk AI Security Platform – kostenlos für Open-Source-Maintainer

Open-Source-Maintainer werden von echten Schwachstellenmeldungen überflutet und brauchen Unterstützung dabei, diese zu priorisieren, zu beheben und schneller Korrekturen bereitzustellen. Das Secure Developer Program von Snyk bietet qualifizierten Projekten kostenlosen Zugang zur Snyk AI Security Platform.

feature insights announcement
Blog

Node-gyp-Supply-Chain-Kompromittierung: Ein sich selbst verbreitender npm-Wurm, der sich in binding.gyp versteckt

Ein neuer npm-Wurm missbraucht binding.gyp, um node-gyp während der Installation auszuführen. So können schädliche Pakete Code ohne Lifecycle-Skripte ausführen. Der Wurm stiehlt Zugangsdaten, setzt sich in GitHub fest und verbreitet sich selbstständig über Maintainer.