Zum Inhalt

Block 7 – Rootless Podman

Dauer: ca. 20 Minuten
Ziel: Verstehen was rootless bedeutet, warum es wichtig ist – und wie Container dauerhaft laufen.

💡 Roter Faden: Wir richten unseren nginx so ein, dass er nach dem Ausloggen weiterläuft.


7.1 Was bedeutet Rootless?

Rootless Podman bedeutet: Container werden als normaler User gestartet – ohne sudo, ohne root-Rechte.

Rootful (Docker / sudo podman):      Rootless (podman als User):
┌───────────────────────────┐        ┌───────────────────────────┐
│  dockerd (root)           │        │  Kein Daemon              │
│  └── Container (root)     │        │  teilnehmer01             │
│       └── Prozess (root)  │        │  └── Container            │
│                           │        │       └── Prozess         │
│  Kompromittiert = root    │        │  Kompromittiert = User    │
└───────────────────────────┘        └───────────────────────────┘

"Mit rootless Podman ist ein kompromittierter Container schlimmstenfalls mit den Rechten deines Users unterwegs – nicht mit root-Rechten auf dem ganzen System."

⚠️ Die Gefahr von sudo podman / sudo docker

Wer sudo podman, sudo docker ausführen darf – oder Zugriff auf den Docker-Socket /var/run/docker.sock hat – hat effektiv root-Rechte auf dem ganzen Host. Das ist kein theoretisches Risiko:

# Host-Filesystem als root lesen – trivial
sudo podman run --rm -v /:/host alpine cat /host/etc/shadow
# → /etc/shadow – Passwort-Hashes aller User

# Shell als root auf dem Host – ein Befehl
sudo podman run --rm -it -v /:/host alpine chroot /host
# → Vollständiger root-Zugriff auf den Host!

Das gleiche gilt für Docker:

# Docker-Socket Zugriff = root
docker run --rm -v /:/host alpine chroot /host
# → Gleicher Angriff, gleiche Wirkung

"sudo podman ist wie sudo bash – wer Container mit root-Rechten starten darf, kann auf alles auf dem Host zugreifen. Das ist in GTFOBins dokumentiert als bekannter Privilege-Escalation-Weg."

Rootless schützt davor:

# Als normaler User – kein Schaden möglich ausserhalb des User-Namespaces
podman run --rm -v /:/host alpine cat /host/etc/shadow
# → Permission denied! Dein User darf /etc/shadow nicht lesen

Fazit für den Alltag: - sudo podman / sudo docker nur wenn wirklich nötig - Niemandem sudo podman geben der nicht bereits volle sudo-Rechte hat - Rootless als Standard – nicht als Option


7.2 UID/GID-Mapping

Das ist das technische Herzstück von rootless Podman. Da ein normaler User keine system-weiten UIDs vergeben kann, bekommt jeder User einen privaten UID-Range zugewiesen:

# Deinen UID-Range anzeigen
cat /etc/subuid
# teilnehmer01:100000:65536
# → teilnehmer01 darf UIDs 100000–165535 verwenden

cat /etc/subgid
# gleiche Struktur

Was passiert konkret?

Im Container läuft Prozess als:   uid=0 (root)
Auf dem Host sieht man:           uid=100000 (sub-uid von teilnehmer01)
# Container starten
podman run -d --name mein-nginx nginx

# Im Container – ist root
podman exec mein-nginx id
# uid=0(root) gid=0(root)

# Auf dem Host – ist sub-uid
ps aux | grep nginx
# teilnehm+ 12345 nginx: master process
# → Läuft als User-Prozess, nicht als system-root!

"Container-root ist nicht Host-root. Das ist der grosse Sicherheitsvorteil von rootless – auch wenn der Prozess im Container als root läuft, ist er auf dem Host ein normaler User-Prozess."

UID-Mapping anzeigen

podman unshare springt kurz in den User-Namespace von Podman rein – die gleiche isolierte Umgebung in der Container laufen. Von dort lesen wir die UID-Mapping-Tabelle:

# Im Namespace bin ich root...
podman unshare id
# uid=0(root) gid=0(root)

# ...aber auf dem Host bin ich mein normaler User
id
# uid=1000(teilnehmer01)
# Das vollständige UID-Mapping anzeigen
podman unshare cat /proc/self/uid_map

# Beispiel-Ausgabe:
#    0       1000       1
# 1000     100000   65536
#
# Spalten: Namespace-UID   Host-UID   Anzahl
#
# Zeile 1: UID 0 im Namespace (Container-root) = UID 1000 auf dem Host (dein User)
# Zeile 2: UIDs 1000–66535 im Namespace = UIDs 100000–165535 auf dem Host (sub-uid Range)

"podman unshare ist wie kurz in den Podman-Namespace reinspringen um von innen zu schauen – ohne einen Container zu starten."


7.3 Einschränkungen von Rootless

Diese Einschränkungen sind keine Podman-Fehler – sie sind Kernel-Sicherheitsmechanismen:

Einschränkung Grund Lösung
Ports < 1024 nicht erlaubt Kernel: privilegierte Ports brauchen CAP_NET_BIND_SERVICE Ports ≥ 1024 oder Reverse-Proxy davor
Kein --network host Netzwerk-Namespace braucht Rechte custom network verwenden
Kein Zugriff auf Host-Devices /dev/* ohne CAP_MKNOD rootful verwenden wenn nötig
NFS-Homedirs nicht unterstützt NFS versteht User-Namespaces nicht, verweigert UID-Mapping Home auf lokalem Filesystem, Storage woanders konfigurieren
Home mit noexec/nodev Podman kann keine Overlays mounten Mount-Optionen des Homedirs prüfen
Images nicht zwischen Usern geteilt Jeder User hat eigenen Store podman image scp – kennen wir aus Block 2
Keine Resource-Limits auf cgroups v1 cgroups v1 unterstützt User-Namespaces nicht für Limits RHEL 9 verwendet cgroups v2 – kein Problem
Inter-Container-Kommunikation mit pasta pasta (Podman 5 Default) kopiert Host-IP – Loops möglich custom network erstellen, kein Standard-Bridge
# Ports unter 1024 – schlägt fehl
podman run -p 80:80 nginx
# Error: permission denied

# Lösung 1: anderen Port verwenden
podman run -p 8080:80 nginx   # ✓

# Lösung 2: sysctl anpassen (als root, systemweit)
sudo sysctl net.ipv4.ip_unprivileged_port_start=443
# → rootless Container dürfen dann Ports ≥ 443 binden

# NFS-Problem prüfen
mount | grep home
# → Wenn "nfs" in der Ausgabe: rootless Podman wird Probleme haben

💡 pasta vs. slirp4netns: Seit Podman 5.0 ist pasta das Standard-Netzwerk-Backend für rootless. Falls Inter-Container-Kommunikation nicht funktioniert, explizit ein custom network erstellen – das löst das Problem zuverlässig.


7.4 Lingering – Container laufen nach dem Ausloggen

Das ist eine der wichtigsten Einstellungen für Rootless Podman im produktiven Betrieb.

Das Problem ohne Lingering

teilnehmer01 loggt sich ein
→ systemd erstellt User-Session
→ Container starten

teilnehmer01 loggt sich aus
→ systemd beendet User-Session
→ ALLE Prozesse des Users werden beendet
→ Container sind weg!

"Ohne Lingering ist es wie ein Prozess den du ohne nohup gestartet hast – Terminal zu, Prozess weg."

Die Lösung: Lingering aktivieren

# Als root aktivieren
sudo loginctl enable-linger teilnehmer01

# Prüfen
loginctl show-user teilnehmer01 | grep Linger
# Linger=yes   ← muss so aussehen

# Für dich selbst (als der betroffene User)
loginctl show-user $(whoami) | grep Linger

"Lingering ist wie @reboot in der crontab des Users – systemd sorgt dafür dass seine Prozesse auch ohne aktive Session laufen."

Mit Lingering bleibt die systemd-User-Instanz aktiv – auch wenn niemand eingeloggt ist.


7.5 Autostart mit systemd und Quadlets

Lingering allein reicht nicht – wir müssen auch definieren welche Container beim Booten gestartet werden sollen. Das geschieht mit Quadlets.

Was ist ein Quadlet?

Ein Quadlet ist eine systemd-Unit-Datei speziell für Container. Podman generiert daraus automatisch systemd-Services – du schreibst keinen systemd-Code von Hand, sondern nur eine einfache INI-Datei die den Container beschreibt.

~/.config/containers/systemd/mein-nginx.container
         ↓ (automatisch beim daemon-reload)
systemd-Service: mein-nginx.service

Alter Weg vs. Quadlet

Vor Quadlets gab es podman generate systemd – das ist heute deprecated und sollte nicht mehr verwendet werden. Der Vergleich zeigt warum:

# ALTER WEG – podman generate systemd (deprecated)
podman run -d --name mein-nginx -p ${PORT}:80 nginx   # 1. Container muss laufen
podman generate systemd --name mein-nginx \         # 2. Unit generieren
  > ~/.config/systemd/user/mein-nginx.service
systemctl --user daemon-reload                      # 3. Aktivieren
systemctl --user enable --now mein-nginx
# → Generierter Code ist unhandlich, schwer zu lesen
# → Container ändern = von vorne beginnen
# NEUER WEG – Quadlet (empfohlen ab Podman 4.4 / RHEL 9)
# ~/.config/containers/systemd/mein-nginx.container
# → Saubere, lesbare Datei
# → Änderung = Datei editieren + daemon-reload, fertig
# → Versionierbar im Git-Repository
podman generate systemd Quadlet
Lesbarkeit Generierter, unhandlicher Code Saubere INI-Syntax
Änderungen Neu generieren, neu aktivieren Datei editieren + daemon-reload
Versionierbar Schwierig Einfach – liegt als Datei
RHEL-Support Deprecated ab Podman 4.4 Empfohlener Weg
RHEL 8 Funktioniert Ab Podman 4.4 (backport)

Quadlet erstellen

# Verzeichnis erstellen
mkdir -p ~/.config/containers/systemd/

# Quadlet-Datei schreiben
cat > ~/.config/containers/systemd/mein-nginx.container << 'EOF'
[Unit]
Description=Mein nginx Container
After=network-online.target

[Container]
Image=docker.io/library/nginx:latest
PublishPort=${PORT}:80
Volume=%h/webroot:/usr/share/nginx/html:ro
Network=kurs-netz

[Service]
Restart=always

[Install]
WantedBy=default.target
EOF

Platzhalter in Quadlets

Quadlets unterstützen systemd-Variablen – das macht sie portabel:

Volume=%h/webroot:/usr/share/nginx/html:ro
#       ↑
#       %h = Home-Verzeichnis des Users
#       Bei teilnehmer01: /home/teilnehmer01/webroot
#       Bei teilnehmer02: /home/teilnehmer02/webroot
# → Gleiche Datei funktioniert für jeden User

Quadlet-Typen

Nicht nur Container können als Quadlet definiert werden:

Dateiendung Zweck
.container Einzelner Container als Service
.pod Pod als Service
.network Netzwerk deklarativ definieren
.volume Volume deklarativ definieren
# Beispiel: Netzwerk als Quadlet
# ~/.config/containers/systemd/kurs-netz.network
[Network]
Driver=bridge

Quadlet aktivieren

⚠️ Wichtig: systemctl --user und journalctl --user funktionieren nur in einer echten SSH-Session als dieser User. Via sudo -u teilnehmer01 bash oder su - teilnehmer01 fehlt der systemd User-Bus und du bekommst:

Failed to connect to bus: No medium found
Lösung: Entweder direkt als der User einloggen – oder XDG_RUNTIME_DIR manuell setzen:
export XDG_RUNTIME_DIR=/run/user/$(id -u)
systemctl --user status mein-nginx

# systemd neu laden
systemctl --user daemon-reload

# Service starten
systemctl --user start mein-nginx

# Status prüfen
systemctl --user status mein-nginx

# Autostart aktivieren
systemctl --user enable mein-nginx

# Logs
journalctl --user -u mein-nginx -f
# Falls "Failed to connect to bus": export XDG_RUNTIME_DIR=/run/user/$(id -u)
# Alternative: podman logs -f mein-nginx

"Mit Lingering + Quadlet verhält sich der Container wie ein normaler systemd-Dienst – startet beim Booten, startet nach Absturz neu, loggt nach journald."

Zusammenspiel: Lingering + Quadlet

Server startet
→ systemd startet User-Instanz von teilnehmer01 (wegen Lingering)
→ systemd startet mein-nginx.service (wegen enable + WantedBy)
→ Podman startet Container
→ Container läuft – ohne dass jemand eingeloggt ist

Übung 7 – Autostart einrichten

🔴 Roter Faden: Wir richten unseren nginx als dauerhaften Service ein.

Aufgabe 7a – Lingering prüfen

# Ist Lingering aktiv?
loginctl show-user $(whoami) | grep Linger
# Linger=yes  ← sollte schon aktiv sein (durch setup-teilnehmer.sh)

Aufgabe 7b – UID-Mapping verstehen

# Container starten
podman run -d --name uid-test nginx

# Im Container – welcher User bin ich?
podman exec uid-test id
# → uid=0(root) im Container

# Auf dem Host – welcher User wirklich?
ps aux | grep nginx
# → Läuft als dein User, nicht als system-root!

# Kurz in den User-Namespace springen
podman unshare id
# → uid=0(root) im Namespace

# ...aber wer bin ich wirklich?
id
# → uid=1000(${USER_NAME}) auf dem Host

# Das vollständige UID-Mapping lesen
podman unshare cat /proc/self/uid_map
# Spalten: Namespace-UID   Host-UID   Anzahl
# → Zeigt wie Container-UIDs auf Host-UIDs übersetzt werden

# Aufräumen
podman rm -f uid-test

Fragen: 1. Welche UID hat nginx im Container? 2. Welche UID sieht der Host für den gleichen Prozess? 3. Was beweist das über die Sicherheit von rootless Podman?

Aufgabe 7c – Quadlet erstellen

mkdir -p ~/.config/containers/systemd/

cat > ~/.config/containers/systemd/mein-nginx.container << 'EOF'
[Unit]
Description=Mein nginx Container
After=network-online.target

[Container]
Image=docker.io/library/nginx:latest
PublishPort=${PORT}:80
Volume=%h/webroot:/usr/share/nginx/html:ro

[Service]
Restart=always

[Install]
WantedBy=default.target
EOF

# Alten Container entfernen
podman stop mein-nginx && podman rm mein-nginx 2>/dev/null || true

# systemd neu laden und Service starten
systemctl --user daemon-reload
systemctl --user start mein-nginx
systemctl --user status mein-nginx

curl http://localhost:${PORT}

Musterlösung Übung 7

7b

podman exec uid-test id
# uid=0(root) gid=0(root)   ← Im Container: root

ps aux | grep nginx
# teilnehm+ 12345  ← Auf dem Host: User-Prozess!

podman unshare id
# uid=0(root)   ← Im Namespace: root

id
# uid=1000(teilnehmer01)   ← Auf dem Host: normaler User

podman unshare cat /proc/self/uid_map
#    0       1000       1      ← Container-root (0) = Host-User (1000)
# 1000     100000   65536      ← Weitere UIDs auf sub-uid Range gemappt

Antworten: 1. nginx läuft im Container als uid=0 (root) 2. Auf dem Host sieht der gleiche Prozess wie uid=1000 (${USER_NAME}) aus 3. Selbst wenn ein Angreifer Container-root übernimmt, hat er auf dem Host nur die Rechte des normalen Users – kein system-root

7c

systemctl --user status mein-nginx
# ● mein-nginx.service
#    Loaded: loaded
#    Active: active (running)

curl http://localhost:${PORT}
# → Eigene index.html

7.6 Zusammenfassung – Rootless und bestehende Infrastruktur

Rootless Podman ist die schonendste Container-Technologie für eine bestehende Infrastruktur:

Podman rootless Podman rootful Docker k8s / k3s
iptables-Eingriffe ✅ keine ⚠️ NAT-Regeln ⚠️ aggressiv, eigene Chain ⚠️ sehr viele Regeln
Root-Rechte nötig ✅ nein ❌ ja ❌ ja (Daemon) ❌ ja
Systemdienst ✅ kein Daemon ✅ kein Daemon ❌ dockerd läuft immer ❌ kubelet läuft immer
Bestehende Firewall ✅ kein Konflikt ⚠️ möglich ⚠️ umgeht firewalld ⚠️ ersetzt Teile
Ports < 1024 ❌ nicht erlaubt ✅ erlaubt ✅ erlaubt ✅ erlaubt
NFS-Homedirs ❌ nicht unterstützt
Sicherheit bei Kompromittierung ✅ User-Rechte ⚠️ root ❌ root via Daemon ⚠️ root

"Podman rootless hinterlässt ausserhalb des User-Namespaces praktisch keine Spuren – kein Daemon, keine iptables-Eingriffe, kein root. Der Container läuft als normaler User-Prozess. Das macht ihn besonders geeignet wenn man auf einem Server nicht der einzige ist der etwas betreibt."

Wann rootless, wann rootful?

Situation Empfehlung
Standard-Applikation auf Port > 1024 Rootless
Muss auf Port 80/443 Rootless + Reverse-Proxy davor
Braucht Zugriff auf /dev/* Rootful
Home-Verzeichnis auf NFS Rootful oder Storage umkonfigurieren
Maximale Sicherheit Rootless

Thema Link
Rootless Podman docs.podman.io – Rootless tutorial
Rootless Einschränkungen (offiziell) github.com/containers/podman – rootless.md
Podman Quadlets docs.podman.io – Quadlet
Red Hat – Rootless containers access.redhat.com – Rootless containers