Debian 13 (Trixie): Rootless, Secrets & seccomp – Container im Produktivbetrieb (Teil 6)
Teil 5 hat angekündigt, was jetzt kommt: der Container-Daemon ohne Root, echte Secrets statt Umgebungsvariablen und eine zweite Schutzschicht aus seccomp & AppArmor. Teil 6 schließt die Debian-13-Serie mit dem Produktivbetrieb ab – inklusive Podman als rootless Alternative.
Warum Rootless?
In Teil 5 gab es eine Warnung mit Folgen: Wer zur docker-Gruppe gehört, hat faktisch Root-Rechte auf dem Host – die docker.sock ist eine Root-Shell mit Wartezeit. Genau dieses Problem löst Rootless Docker: Der Daemon läuft als normaler Benutzer in einem eigenen User-Namespace. Ein Angreifer, der den Daemon oder einen Container überwindet, landet nicht als Root im Host, sondern als eingeschränkter Benutzer in einem abgekapselten Namespace.
- Weniger Angriffsfläche: Kein Root-Daemon, keine Root-Socket mehr – die Privilegien-Eskalation über die Docker-Gruppe existiert nicht.
- Konsequenz aus Teil 5: Secrets via
docker inspectsind nur noch für den Besitzer des Rootless-Sockets lesbar, nicht für jeden Host-Benutzer. - Passend zur Serie: Der gehärtete Host aus Teil 1–4 bleibt unverändert – der Daemon zieht nur in dein User-Home um.
sudo im laufenden Betrieb.Rootless Docker einrichten
Die Installation läuft in zwei Schritten: erst die Abhängigkeiten, dann das Setup als normaler Benutzer – niemals als root:
sudo apt install dbus-user-session uidmap slirp4netns
# Rootful-Daemon abschalten, damit nur der Rootless-Daemon lauscht
sudo systemctl disable --now docker.service docker.socket
# Jetzt als NORMALER Benutzer ausführen (nicht mit sudo!):
dockerd-rootless-setuptool.sh install
# User-Dienst dauerhaft aktivieren und Linger setzen:
systemctl --user enable --now docker
sudo loginctl enable-linger $USER
uidmap liefert die Sub-UIDs/Sub-GIDs für den User-Namespace, slirp4netns stellt das Netzwerk bereit. enable-linger ist der entscheidende Schritt: Damit läuft deine User-Session auch nach dem Ausloggen weiter – sonst stoppen alle Container beim nächsten SSH-Disconnect.
# Context prüfen und auf rootless umstellen:
docker context ls
docker context use rootless
# Kontrolle – Ausgabe muss rootless zeigen:
docker info | grep -iE "rootless|username|seccomp"
docker context inspect | grep -i host
Der Rootless-Socket liegt danach unter $XDG_RUNTIME_DIR/docker.sock (meist /run/user/1000/docker.sock). Alle Befehle aus Teil 5 – docker compose, docker stats, Watchtower – funktionieren unverändert, sobald der Context aktiv ist.
sudo docker starten, sondern immer in der eigenen User-Session.Grenzen & wann Rootful bleiben darf
Rootless ist kein Allheilmittel. Diese Punkte solltest du kennen, bevor du umstellst:
- Ports unter 1024: Ein unprivilegierter Benutzer darf keine privilegierten Ports binden. Lösung aus der Serie: Der Reverse-Proxy (Artikel nginx/acme.sh) bindet
80/443und leitet auf die Container-Ports im lokalen Netz weiter. - Datei-Rechte auf Volumes: Container laufen im Sub-UID-Bereich des Benutzers. Host-Ordner, die du per Bind-Mount bereitstellst, gehören dem Benutzer – nicht root. Bei Altdaten aus der Rootful-Zeit die Ownership prüfen:
sudo chown -R $USER:$USER ./volumes. - Spezial-Fälle: Docker-in-Docker, Zugriff auf Kernel-Module oder Geräte (
--device),--privilegedund manche Netzwerk-Modi funktionieren rootless nicht oder nur eingeschränkt. - AppArmor im Container: Das Setzen von AppArmor-Profilen braucht Capabilities, die im User-Namespace fehlen – der Daemon kann Container nicht mit hostweiten Profilen belegen (Details in Abschnitt 5).
Docker Secrets statt Umgebungsvariablen
In Teil 5 stand: Env-Variablen kann jeder lesen, der docker inspect darf. Das gilt auch rootless noch innerhalb deiner eigenen Session – und Geheimnisse in Klartext-Variablen landen außerdem in Backups, Logs und Healthchecks. Für echte Secrets stellt Docker Compose /run/secrets bereit: kleine Dateien, die nur im Container gemountet werden.
# 1) Secret-Datei anlegen – nur für deinen Benutzer lesbar
printf 'LangesZufaelligesDBPasswort\n' > ./db_password.txt
chmod 600 ./db_password.txt
# 2) docker-compose.yml
services:
app:
image: beispiel/app:latest
secrets: # Secret-Liste direkt unter dem Dienst
- db_password # Name = Eintrag in der globalen secrets:-Liste
environment:
# Apps, die Datei-Pfade unterstützen, bekommen den Pfad als Variable
- DB_PASSWORD_FILE=/run/secrets/db_password
# 3) Globale Definition am Datei-Ende – hier wird die Quelle festgelegt
secrets:
db_password:
file: ./db_password.txt
Die Anwendung liest das Passwort dann aus /run/secrets/db_password – nicht aus einer Umgebungsvariablen. Was nicht in der Variable steht, taucht auch nicht in docker inspect, Prozesslisten oder Crash-Logs auf.
- Secret-Dateien gehören wie die
.envaus Teil 5 nicht ins Git –.gitignoreundchmod 600sind Pflicht. - Akzeptiert eine Anwendung nur Env-Variablen, bleibt die
.env-Lösung aus Teil 5 die pragmatische Wahl – Dokumentation sagt dann deutlich, wo das Limit liegt. - Ein Secret wechseln heißt: Datei ändern und den Stack neu erstellen –
docker compose up -dgenügt nicht immer, da Datei-Secrets beim Neustart neu gelesen werden.
.env und Secret-Dateien. Beim Restore einfach die Rechte (chmod 600) wiederherstellen – mehr ist nicht nötig.podman compose – das native Compose-Plugin unter Debian 13 – behandelt die secrets:-Definition identisch zu Docker. Das ältere Python-Tool podman-compose unterstützt Datei-Secrets erst in neueren Versionen zuverlässig; wer darauf angewiesen ist, nutzt besser podman compose oder bleibt bei der .env-Lösung aus Teil 5.seccomp & AppArmor für Container
Teil 5 hat mit cap_drop und no-new-privileges die Rechte der Prozesse begrenzt. seccomp ergänzt das auf der Syscall-Ebene: Der Kernel blockt einzelne Systemaufrufe, bevor sie den Prozess erreichen. Docker bringt dafür ein gut austariertes Default-Profil mit, das automatisch aktiv ist:
docker info | grep -i seccomp
# Ausgabe: name=seccomp,profile=builtin
Für Dienste mit besonderem Risiko lohnt ein eigenes Profil, das zusätzlich einzelne Aufrufe sperrt – z. B. unshare (Namespace-Eskapaden) in einem Web-Container:
# profile.json – eigene Syscall-Blockliste
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{ "names": ["unshare"], "action": "SCMP_ACT_ERRNO" }
]
}
# docker-compose.yml – Profil pro Dienst aktivieren
services:
web:
image: nginx:alpine
security_opt:
- seccomp=./profile.json
Das eigene Profil startet mit SCMP_ACT_ALLOW (alles erlaubt) und sperrt gezielt – so riskierst du keine kaputten Dienste. Das ausführliche Docker-Default-Profil dient als Referenz für syscalls, die du sicher blocken kannst.
AppArmor: Auf Debian 13 ist AppArmor seit Teil 2 aktiv. Rootful Docker belegt Container automatisch mit dem Profil docker-default; unter Rootless greift es – wie in Abschnitt 3 beschrieben – nicht in den Container hinein. Dort übernehmen seccomp, User-Namespace und no-new-privileges die Schutzfunktion. Auf einem Rootful-Ausnahme-Host (Abschnitt 3) lohnt ein eigenes Profil:
# /etc/apparmor.d/docker.nginx
abi <abi/3.0>,
#include <tunables/global>
profile docker.nginx flags=(attach_disconnected) {
#include <abstractions/base>
network inet tcp,
/etc/nginx/** r,
/var/log/nginx/** w,
/var/cache/nginx/** w,
deny /etc/shadow rw,
}
# Profil laden und dem Container zuweisen
sudo apparmor_parser -r /etc/apparmor.d/docker.nginx
sudo aa-status | grep nginx
# docker-compose.yml:
# security_opt:
# - apparmor=docker.nginx
Podman: die rootless Alternative
Podman ist daemonless und rootless von Haus aus – der Blog-Artikel „Podman vs. Docker“ hat den Vergleich geliefert, hier kommt der Betrieb. Die Container-Kommandos sind weitgehend identisch, für Stacks gibt es die Compose-Unterstützung von Podman:
# Als normaler Benutzer – kein Daemon nötig
sudo apt install podman podman-compose
# Compose-Stacks aus Teil 5 laufen lassen:
podman compose -f /opt/stacks/app/docker-compose.yml up -d
Der elegante Unterschied ist Quadlet: Podman übersetzt Container-Definitionen direkt in systemd-User-Units – dein Dienst wird damit ein ganz normaler, überwachbarer systemd-Dienst:
# ~/.config/containers/systemd/app.container
[Unit]
Description=Beispiel-Dienst
[Container]
Image=docker.io/library/nginx:1.27-alpine
PublishPort=127.0.0.1:8080:80
[Service]
Restart=always
[Install]
WantedBy=default.target
# Unit aktivieren – systemd übernimmt Start, Restart und Autostart
systemctl --user daemon-reload
systemctl --user enable --now app
systemctl --user status app
- Migration:
docker-Alias aufpodmansetzen – die meisten Befehle aus Teil 5 laufen unverändert durch. - Kein Daemon: Kein zentraler Prozess mit Root-Socket, jeder Dienst gehört seiner eigenen User-Unit.
- Für Einsteiger: Wer das Ökosystem aus Teil 5 (Watchtower, Compose) weiternutzen will, bleibt bei Rootless Docker – Podman ist die Wahl, wenn du systemd als Kontrollinstanz willst.
Häufige Probleme
| Problem | Lösung |
|---|---|
| Container laufen nach Reboot nicht mehr | sudo loginctl enable-linger $USER setzen und systemctl --user enable docker prüfen. |
| docker zeigt wieder den Rootful-Daemon | docker context ls prüfen und docker context use rootless setzen – nie sudo docker verwenden. |
| Port 80/443 lässt sich nicht binden | Unprivilegierte Benutzer dürfen keine Ports unter 1024 binden – Reverse-Proxy übernimmt 80/443 und leitet auf Container-Ports weiter. |
| Volumes gehören plötzlich falschen Besitzern | Rootless-Container laufen im Sub-UID-Bereich – Ownership der Host-Ordner auf deinen Benutzer setzen (chown -R $USER:$USER). |
| AppArmor-Profil wird nicht angewendet | Rootless kann keine hostweiten Profile setzen – seccomp + User-Namespace übernehmen; AppArmor nur auf Rootful-Ausnahme-Hosts. |
| Backup-Skripte aus Teil 4/5 finden den Daemon nicht | Der Root-Timer aus Teil 4 läuft im System-Kontext und sieht den Rootless-Socket von lena nicht – docker compose stop stoppt dann nichts (Backup bei laufendem Container = riskant) oder bricht ab. Deshalb die Variable im Skript vor dem Compose-Befehl setzen: export DOCKER_HOST="unix:///run/user/1000/docker.sock" (UID bei Bedarf anpassen). Alternativ läuft das Backup als systemd-User-Timer in der Session von lena – dann entfällt die Variable. |
| Secret-Datei wird nicht gemountet | Pfad unter secrets: prüfen und Stack neu erstellen – Datei-Secrets werden beim Neustart gelesen. |
Fazit & Serien-Ende
Damit ist die Debian-13-Serie komplett: installieren (Teil 1), härten (Teil 2), abschotten (Teil 3), überwachen und sichern (Teil 4), Container betreiben (Teil 5) – und jetzt den Container-Betrieb auf das Niveau des Hosts heben. Rootless nimmt dem Daemon die Krone, Secrets und seccomp schließen die Lücken, die in Teil 5 bewusst offen blieben. Wer mag, wechselt mit Podman den Motor, ohne die Konzepte zu verlernen.
/run/secrets, nicht in Variablen ③ seccomp blockt Syscalls, AppArmor blockt Dateizugriffe – beide ergänzen Teil 5 ④ Linger setzen, Context prüfen, Restore testen: Der Betrieb ist erst mit Kontrolle fertigWeiterlesen: Die einzelnen Bausteine – Watchtower, Uptime Kuma, Podman vs. Docker, Portainer und Dockge – sind als eigene Artikel auf dem Blog vertieft. Die Serie endet hier, die Dienste-Liste wächst weiter.