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 podmanist wiesudo 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 unshareist 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
pastadas 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
nohupgestartet 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
@rebootin 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:
Lösung: Entweder direkt als der User einloggen – odersystemctl --userundjournalctl --userfunktionieren nur in einer echten SSH-Session als dieser User. Viasudo -u teilnehmer01 bashodersu - teilnehmer01fehlt der systemd User-Bus und du bekommst:XDG_RUNTIME_DIRmanuell setzen:
# 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 |
Weiterführende Links
| 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 |