Serie: Podman-Bausteine – Capabilities (Teil 10)

  indibart   Lesezeit: 9 Minuten Auf Mastodon ansehen

Der Artikel erklärt Linux Capabilities anhand einer Schlüssel-Analogie und zeigt, warum die von Caddy empfohlene NET_ADMIN-Capability bei einem rootless Container ihr eigentliches Ziel verfehlt.

serie: podman-bausteine – capabilities (teil 10)

Eine Tutorialreihe zum Einsatz von Podman: Anwendung, Vorteile und Funktionsweise.

Inhaltsverzeichnis

  1. Teil: Einleitung und Installation
  2. Teil: Podman CLI
  3. Teil: Wie funktioniert die Restart-Policy? Oder: Podman hat keinen Daemon
  4. Teil: Quadlets
  5. Teil: Updates
  6. Teil: Healthchecks
  7. Teil: Ports publishing – rootful & rootless
  8. Teil: Volumes & Mounts
  9. Teil: Netzwerk: Kommunikation zwischen Containern
  10. Teil: Capabilities (Das ist dieser Artikel)
  11. Teil: Pods und multi-container Apps
  12. Teil: Environment Variables & Secrets

Einleitung

In den bisherigen Artikeln haben wir IT Tools zum Laufen gebracht und Caddy als Proxy davorgehängt. Diese laufen als rootless Container. Bei Caddy haben wir die privilegierten Ports gepublisht und die Kommunikation unter den beiden Containern mittels eines Netzwerks sichergestellt. Alles läuft.

Wenn man sich die Anleitung des Caddy Docker Image ansieht, dann ist die Rede von Linux capabilities. Dem Caddy-Container wird dort die Capability NET_ADMIN gegeben, damit es selbständig gewisse Netzwerk-Einstellungen anpassen kann. Dies ist optional und kann gewisse Sicherheitsrisiken bergen. Dies bestätigt auch die Caddy-Anleitung.

Wie gesagt, läuft bei uns schon alles, eine Capability ist eine rein optionale Ergänzung. Der Vollständigkeit halber möchte ich mich trotzdem kurz den Capabilities widmen, denn die Caddy-Anleitung geht davon aus, dass der Container rootful läuft. Unsere Architektur mit rootless Containern ändert die Auswirkung der Capabilities, was zu Verwirrung führen könnte.

Capabilities

Stell dir vor, du gründest eine Firma. Du mietest Räumlichkeiten und erhältst den Schlüssel. Dieser Schlüssel öffnet alle Türen. Er ist der Generalschlüssel für deine Firma. Für jede Person, die zusätzlich bei dir arbeitet, musst du also entscheiden: Bekommt die Person einen Schlüssel oder nicht? Dies war traditionell die Art, wie Linux mit Prozessen umgegangen ist: Entweder ist der Prozess privilegiert, was bedeutet, dass der ausführende User UID 0 hatte oder eben root war, oder der Prozess ist unprivilegiert, ausgeführt durch einen User mit UID ungleich 0.

Nun wächst deine Firma und hat immer mehr Angestellte, die jeweils nur eine einzige Tür brauchen. Trotzdem händigst du jeder Person einen Generalschlüssel aus, der alle Türen öffnet. Irgendwann macht das keinen Sinn mehr und du beschliesst, die Räume in verschiedene Bereiche zu unterteilen. Jedem Bereich ordnest du je einen eigenen Schlüssel zu. So kannst du genauer bestimmen, wer welche Schlüssel benötigt. Genau das ist in Linux seit Version 2.2 der Fall: Die Root-Privilegien (der Generalschlüssel) wurden in ca. vierzig Capabilities (einzelne Schlüssel) unterteilt. Jetzt können einem Prozess entsprechende Capabilities zugeordnet werden.

Um nicht jedes Mal neu entscheiden zu müssen, wem du welche Schlüssel aushändigst, machst du dir Listen entsprechend der Rollen, die Angestellte in deiner Firma haben. Neue Angestellte bekommen dann einen Schlüsselbund entsprechend der Rolle, die sie belegen werden. So erhält jemand aus dem Finanzwesen andere Schlüssel als jemand aus der Produktion. Schlüssel, die nicht auf der entsprechenden Rollenliste stehen, werden auch nicht ausgehändigt. So hat schliesslich jede Person einen Schlüsselbund, entsprechend ihrer Rollenliste.

Linux kennt ein Pendant zu diesen Rollenlisten: Das Bounding Set. Es legt fest, welche Capabilities ein Prozess maximal erhalten kann. Das Pendant zum Schlüsselbund sind die Permitted Capabilities eines Prozesses. Effective Capability entspricht dem Schlüssel, den die Person jetzt gerade nutzt. Der Kernel testet immer nur die Effective Capability.

Zu guter Letzt gibt es die Inheritable Capabilities. In unserer Analogie ist das eine Liste, die mit dem Schlüsselbund mitgegeben wird. Dort sind diejenigen Schlüssel aufgelistet, die an andere Personen weitergereicht werden dürfen. Welche Schlüssel darf beispielsweise eine Bereichsleiterin einer Mitarbeiterin weitergeben? Die Inheritable-Liste bestimmt, welche Schlüssel beim Wechsel zu einem neuen Programm mitgenommen werden dürfen.

Noch ein Wort zur Rollenliste: Sagen wir, du entscheidest, dass die Finanzabteilung keinen Zugang zum Archiv mehr haben soll. Also streichst du den Schlüssel von der Rollenliste. Du rennst aber nicht jeder Person hinterher und nimmst ihr den Schlüssel aus dem Bund. Die angepasste Rollenliste kommt erst zum Zug, wenn jemand neue Schlüssel erhält, zum Beispiel beim Wechsel auf eine neue Stelle.

Genauso verhalten sich Bounding Set und Permitted Capabilities: Entfernst du eine Capability aus dem Bounding Set, behält ein laufender Prozess sie trotzdem in seinen Permitted Capabilities. Das Bounding Set kann ausserdem nur schrumpfen, nie wachsen. Soll ein Prozess eine zusätzliche Capability bekommen, musst du ihn neu starten.

Capabilities und Container

Wenn du einen Container startest, übernimmt Podman die Rolle des Schlüsselverwalters. Podman schreibt die Rollenliste, also das Bounding Set, bevor der erste Prozess im Container startet. Podman verwendet dabei standardmässig eine bewusst kurze Liste. Mit dem Start kriegt ein Prozess die entsprechenden Capabilities. Prozesse im Container können diese Liste für ihre eigenen Kindprozesse weiter kürzen, aber nie verlängern.

Beim Build-Prozess eines Container-Images kann diese Funktion genutzt werden, um die Sicherheit des Containers zu erhöhen: Der erste Prozess kriegt hohe Caps, um alles aufbauen zu können (beispielsweise Netzwerk-Einstellungen vornehmen), dann erzeugt er einen Kindprozess mit weniger Capabilities und lässt ihn als unprivilegierter Nutzer laufen. Dieser Kindprozess ist der eigentliche, laufende Worker-Prozess des Containers.

Wie gesagt, ist dies ein Vorgehen beim Build des Images. Wir nutzen jedoch das fertige Image und können lediglich via Podman das Bounding Set des ersten Prozesses beeinflussen. Genau das Gleiche tut die Caddy-Anleitung: Sie erweitert das Bounding Set um die Capability NET_ADMIN und gibt somit dem ersten Prozess die Möglichkeit, das Netzwerk des Hosts zu beeinflussen.

Capabilities kommen mit unterschiedlicher Mächtigkeit. NET_ADMIN ist eine der mächtigeren, da Prozesse mit dieser Capability das gesamte Netzwerk beeinflussen können.

Aber: Die Caddy-Anleitung geht davon aus, dass Caddy rootful gestartet wird. Sobald wir rootless arbeiten, verändert sich die Situation. Jetzt werden User Namespaces wichtig.

User Namespace

Im Artikel zu Podman-Netzwerken haben wir uns mit Network-Namespaces auseinandergesetzt. Dort haben wir gelernt, dass jeder rootless Container einen eigenen Netzwerk-Namespace erhält. Wir haben auch gelernt, dass es noch weitere Namespace-Arten gibt.

Eine weitere Art ist der User Namespace. Jeder User hat einen eigenen User Namespace. Angenommen, wir haben ein System mit zwei Usern: anna mit UID 1001 und bob mit UID 1002. Es gibt also zwei User Namespaces, einer gehört UID 1001 und einer gehört UID 1002. Zentral sind immer die UIDs und nicht die Namen, was bei Containern schnell wichtig werden kann. Die User Namespaces besitzen die Container, die der entsprechende User erstellt hat.

Wenn also User anna zwei rootless Container startet, gehören beide dem gleichen User Namespace UID 1001. Jeder Container hat aber, wie wir gelernt haben, einen eigenen Network-Namespace. Startet User bob zwei rootless Container, gehören die dem User Namespace UID 1002 und haben wiederum je einen eigenen Network-Namespace. Wir haben jetzt also zwei User Namespaces und vier Network-Namespaces.

Für die Capabilities sind lediglich die User Namespaces wichtig. Wir wollen uns daher nicht mehr mit den Network-Namespaces aufhalten. Ich wollte lediglich vermeiden, dass User und Network-Namespaces verwechselt werden.

Capabilities und User Namespaces

Capabilities beziehen sich immer auf den User Namespace. Eine Capability gilt also nur für Ressourcen, die dem entsprechenden User Namespace gehören. Alles wird vom Linux-Kernel gegen den User Namespace geprüft. Wenn der User bob mittels podman run --cap-add=NET_ADMIN docker.io/my_container einen Container mit der Capability NET_ADMIN erstellt, dann hat dieser Container die gleichen Netzwerk-Rechte, die der User bob hat, da die Capability NET_ADMIN vom Kernel gegen den User Namespace UID 1002 geprüft wird.

Dasselbe gilt bei rootful Containern. Die werden ja von root, also UID 0, gestartet. Wenn root (oder bob mit sudo) mittels podman run --cap-add=NET_ADMIN docker.io/my_container einen Container mit der Capability NET_ADMIN erstellt, dann hat dieser Container die gleichen Netzwerk-Rechte, die root hat.

Ein rootless Container mit der Capability NET_ADMIN kann beispielsweise das eigene Loopback-Interface (lo) aus- und wieder einschalten: ip link set lo down \&\& ip link set lo up. Das funktioniert, weil es sich um eine Namespace-lokale Einstellung handelt.

Host-globale Netzwerk-Einstellungen können nur von root getätigt werden. Die Caddy-Anleitung will mit dem Hinzufügen der Capability NET_ADMIN erreichen, dass der Container eine ebensolche Host-globale Netzwerk-Einstellung verändern kann. Da wir unser Caddy jedoch rootless laufen lassen, kann es höchstens die Rechte unseres eigenen Users erhalten und somit nur Namespace-lokale Einstellungen ändern.

Capabilities und rootless Container

Das Beispiel oben mit dem Loopback-Interface hat gezeigt, dass Capabilities auch bei rootless Containern ihre Funktion haben können. Doch sind sie immer auf den User Namespace beschränkt, zu dem der Container gehört. Alle Host-globalen Einstellungen sind daher für rootless Container, unabhängig von den zugeordneten Capabilities, nicht erreichbar.

Wie bei den privilegierten Ports müssen wir also auch hier nach anderen Möglichkeiten suchen, um diese Einstellungen vornehmen zu können. Beispielsweise indem wir die Einstellungen von Hand vornehmen.

Das ist vielleicht etwas mühsamer, doch Sicherheit ist mir wichtig, wenn ich Container rootless laufen lasse. Diese Sicherheit dann wieder mit einer Capability durchbrechen zu wollen, die so mächtig ist wie NET_ADMIN, macht wenig Sinn.

Fazit und Ausblick

Capabilities zerlegen die Root-Rechte in einzelne Bausteine — aber sie bleiben immer an den User Namespace gebunden. Ein rootless Container mit NET_ADMIN kann deshalb nur Namespace-lokale Einstellungen ändern (wie das Loopback-Interface), niemals host-globale. Genau das verlangt aber die Caddy-Anleitung mit ihrem NET_ADMIN-Vorschlag, weshalb er in unserem rootless Setup sein eigentliches Ziel verfehlt. Ich verzichte deshalb bewusst darauf: Die zusätzliche Macht von NET_ADMIN würde die Sicherheit von rootless aushöhlen, ohne dass wir den eigentlichen Nutzen davon überhaupt erreichen könnten.

Damit ist unser Caddy-Setup nun vollständig erklärt — von den Ports über Volumes und das Netzwerk bis zu den Capabilities.

In den nächsten Artikeln wollen wir mit Mealie eine zusätzliche App in unser Setup einbinden. Dort haben wir eine neue Situation: Eine App besteht aus mehreren Containern. Hier kommen Pods ins Spiel — der namensgebende Bestandteil von Podman.

TL;DR

  • Capabilities teilen Root-Rechte in ca. 40 einzelne Berechtigungen auf (z. B. NET_ADMIN).
  • Bounding Set = maximal mögliche Capabilities eines Containers, von Podman vor dem Start festgelegt, kann danach nur schrumpfen, nie wachsen.
  • Capabilities werden immer gegen den User Namespace geprüft, nicht gegen den Network-Namespace.
  • Rootless + --cap-add=NET_ADMIN gibt nur die Rechte des eigenen Users – reicht für Namespace-lokale Änderungen (ip link set lo down \&\& ip link set lo up), nicht für host-globale Einstellungen.
  • Caddys NET_ADMIN-Empfehlung (grössere HTTP/3-Puffer) setzt rootful voraus und bringt rootless daher nichts.
  • Fazit: Für unser Setup lassen wir NET_ADMIN bewusst weg.

Quellen und Dokumentation

Tags

Capabilities, User Namespace, Bounding Set, NET_ADMIN, Rootless, Container, Podman, Caddy

Es wurden noch keine Kommentare verfasst, sei der erste!