Container-Sicherheit und Haertung
Docker-Container professionell absichern: Image-Scanning, Rootless-Mode, Seccomp/AppArmor, Secrets-Management und CIS-Benchmark im KMU-Praxiseinsatz.
Warum Container-Sicherheit anders funktioniert als bei VMs
Bei einer virtuellen Maschine hast du eine klare Isolationsgrenze: eigener Kernel, eigenes Betriebssystem, Hypervisor dazwischen. Ein Container teilt sich den Kernel mit dem Host und mit allen anderen Containern auf derselben Maschine. Das ist der Grund, warum Container so leichtgewichtig sind – und gleichzeitig der Grund, warum eine Fehlkonfiguration hier viel schneller zum Ausbruch aus der Isolation (Container Escape) fuehrt als bei einer VM.
In der Praxis siehst du in vielen KMU-Umgebungen Docker-Setups, die exakt nach dem Quickstart-Tutorial aufgebaut wurden: Container laufen als root, Images kommen ungeprueft von Docker Hub, Secrets stehen im Klartext in der docker-compose.yml, und alle Container haengen im selben Default-Netzwerk. Funktioniert – bis es eben nicht mehr funktioniert. Dieser Artikel baut auf den Docker-Grundlagen auf und geht die sechs wichtigsten Haertungsebenen durch, die in jedem produktiven Container-Setup gehoeren.
Die Angriffsflaeche im Ueberblick
Bevor du einzelne Massnahmen umsetzt, hilft ein mentales Modell der Angriffsflaeche. Ein Angreifer kann grundsaetzlich an vier Stellen ansetzen:
- Das Image selbst – veraltete Basis-Images mit bekannten CVEs, eingebettete Secrets, unnoetige Tools (curl, bash, ssh) die eine Post-Exploitation erleichtern.
- Die Laufzeitkonfiguration – Container laeuft als root,
--privileged, gemountete Docker-Socket (/var/run/docker.sock), fehlende Ressourcenlimits. - Der Kernel selbst – ueber Syscalls, die dem Container eigentlich nicht erlaubt sein sollten, kann ein kompromittierter Prozess versuchen, aus dem Namespace auszubrechen.
- Das Netzwerk – ein kompromittierter Frontend-Container, der ungehindert auf die Datenbank oder das interne Management-Interface zugreifen kann (Lateral Movement).
Fuer jede dieser vier Ebenen gibt es ein etabliertes Gegenmittel: Image-Scanning, Rootless/Least-Privilege, Seccomp/AppArmor/Capabilities und Netzwerksegmentierung. Secrets-Management und das CIS-Benchmark-Audit ziehen sich quer durch alle vier.
Image-Scanning mit Trivy und Grype
Die meisten Container-Kompromittierungen fangen nicht mit einem exotischen Zero-Day an, sondern mit einer laengst gepatchten CVE in einer veralteten Basis-Image-Schicht (Alpine, Debian-Slim, OpenSSL, glibc). Image-Scanner gleichen die installierten Pakete im Image gegen CVE-Datenbanken ab und geben dir eine priorisierte Liste, bevor das Image ueberhaupt deployed wird.
Trivy – der Allrounder
Trivy von Aqua Security scannt nicht nur OS-Pakete, sondern auch Sprachabhaengigkeiten (npm, pip, Go-Module), Infrastructure-as-Code-Dateien, eingebettete Secrets und kann SBOMs (Software Bill of Materials) erzeugen – alles in einem Binary.
# Trivy installieren (Linux, offizielles Install-Skript)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Image scannen, nur HIGH/CRITICAL anzeigen
trivy image --severity HIGH,CRITICAL nginx:1.25-alpine
# Ergebnis als JSON fuer die CI/CD-Pipeline
trivy image --format json --output scan-result.json myapp:1.4.2
# Build in der Pipeline hart stoppen, wenn CRITICAL-CVEs gefunden werden
trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed myapp:1.4.2
Der Schalter --ignore-unfixed ist in der Praxis wichtig: Er blendet CVEs aus, fuer die der Upstream-Maintainer noch keinen Patch veroeffentlicht hat. Ohne diesen Schalter blockierst du sonst jeden Build wegen Findings, die du gar nicht beheben kannst.
Grype – schlank und schnell
Grype von Anchore macht bewusst nur eine Sache: Vulnerability-Matching gegen Pakete, dafuer mit einem zusaetzlichen Risiko-Score (CVSS kombiniert mit EPSS-Exploit-Wahrscheinlichkeit und CISA-KEV-Status), der dir hilft, unter 200 Findings die 5 wirklich relevanten zu finden.
# Grype installieren
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Image direkt scannen
grype myapp:1.4.2
# Zusammen mit Syft: SBOM einmal erzeugen, mehrfach gegen die CVE-DB pruefen
syft myapp:1.4.2 -o json > sbom.json
grype sbom:sbom.json
| Kriterium | Trivy | Grype |
|---|---|---|
| Scope | OS-Pakete, App-Abhaengigkeiten, IaC, Secrets, Lizenzen, Kubernetes | Reines Vulnerability-Matching |
| Geschwindigkeit | Gut | Ca. 30–40% schneller bei reinem CVE-Scan |
| Risiko-Priorisierung | CVSS-basiert | CVSS + EPSS + CISA-KEV-Score |
| SBOM-Integration | Eingebaut | Ueber Syft (gleicher Anbieter) |
| Ideal fuer | All-in-One-Gate in CI/CD | Zweitmeinung / dedizierte SBOM-Pipeline |
| Lizenz | Apache 2.0 | Apache 2.0 |
Open-Source-Scanner fuer Container-Images, IaC, Secrets und mehr
github.com
Rootless Container und User-Namespaces
Standardmaessig laeuft der Docker-Daemon als root, und Prozesse innerhalb eines Containers laufen ebenfalls als root – auch wenn dieses “root” durch die Namespace-Isolation eigentlich harmlos wirken soll. Das Problem: Bei einem Container-Escape (z.B. durch eine Kernel-Schwachstelle oder eine fehlerhafte Mount-Konfiguration) landet der Angreifer direkt mit Root-Rechten auf dem Host.
Zwei Mechanismen loesen das:
User-Namespace-Remapping (userns-remap)
Der Docker-Daemon selbst laeuft weiterhin als root, aber die UIDs innerhalb der Container werden auf einen unprivilegierten UID-Bereich auf dem Host abgebildet. UID 0 im Container wird z.B. auf UID 231072 auf dem Host gemappt – ein Prozess, der versucht, aus dem Container auszubrechen, landet als praktisch rechtloser Host-User.
# /etc/subuid und /etc/subgid – mind. 65536 UIDs/GIDs reservieren
dockremap:231072:65536
// /etc/docker/daemon.json
{
"userns-remap": "default"
}
sudo systemctl restart docker
# Pruefen, welcher Bereich aktiv ist
grep dockremap /etc/subuid /etc/subgid
Rootless Mode – der Daemon selbst laeuft ohne root
Rootless Mode geht einen Schritt weiter: Hier laeuft nicht nur der Container, sondern der gesamte Docker-Daemon als normaler Benutzer, komplett ohne SUID-Binaries (bis auf newuidmap/newgidmap, die fuer UID-Mapping noetig sind).
# Voraussetzung: uidmap-Paket installiert, /etc/subuid + /etc/subgid befuellt
sudo apt install -y uidmap dbus-user-session
# Rootless-Docker als eigener Benutzer installieren
curl -fsSL https://get.docker.com/rootless | sh
# Umgebungsvariablen setzen (in .bashrc persistieren)
export PATH=$HOME/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
# Dienst aktivieren, damit er Reboots uebersteht
systemctl --user enable docker
loginctl enable-linger $(whoami)
Zusaetzlich solltest du auch innerhalb des Containers nie als root arbeiten, unabhaengig vom Host-Setup:
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["node", "server.js"]
Seccomp- und AppArmor-Profile, Capabilities droppen
Seccomp – Syscalls einschraenken
Der Linux-Kernel bietet ueber 300 Syscalls an, aber ein typischer Webserver-Container braucht vielleicht 50 davon. Seccomp (Secure Computing Mode) filtert per Whitelist, welche Syscalls ein Prozess ueberhaupt aufrufen darf. Docker liefert bereits ein Default-Profil mit, das rund 44 gefaehrliche Syscalls blockiert (u.a. ptrace, mount, reboot, kexec_load).
# Container mit dem Standard-Profil starten (ist bereits Default)
docker run --security-opt seccomp=default.json nginx
# Ein eigenes, restriktiveres Profil anwenden
docker run --security-opt seccomp=./custom-seccomp.json myapp
# Seccomp komplett deaktivieren (NUR zum Debuggen, nie in Produktion!)
docker run --security-opt seccomp=unconfined myapp
Ein minimales Custom-Profil, das nur explizit erlaubte Syscalls durchlaesst (Ausschnitt):
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read", "write", "open", "close", "stat", "fstat",
"mmap", "mprotect", "munmap", "brk", "rt_sigaction",
"rt_sigprocmask", "ioctl", "access", "socket", "connect",
"accept", "bind", "listen", "epoll_wait", "clone", "exit_group"],
"action": "SCMP_ACT_ALLOW"
}
]
}
AppArmor – Dateisystem- und Netzwerkzugriff einschraenken
Waehrend Seccomp auf Kernel-Syscall-Ebene arbeitet, kontrolliert AppArmor, auf welche Dateipfade, Capabilities und Netzwerkressourcen ein Prozess zugreifen darf. Docker aktiviert automatisch das mitgelieferte docker-default-Profil, sofern AppArmor auf dem Host aktiv ist (Standard bei Ubuntu/Debian).
# Pruefen, ob AppArmor aktiv ist
sudo aa-status
# Eigenes Profil laden
sudo apparmor_parser -r -W /etc/apparmor.d/docker-nginx-custom
# Container mit dem eigenen Profil starten
docker run --security-opt apparmor=docker-nginx-custom nginx
# Auszug /etc/apparmor.d/docker-nginx-custom
profile docker-nginx-custom flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network inet tcp,
deny /proc/sys/kernel/** wklx,
deny /sys/** wklx,
/var/log/nginx/** rw,
/usr/sbin/nginx ix,
deny /etc/shadow r,
}
Linux Capabilities minimal halten
Ein Container startet standardmaessig mit 14 Capabilities (u.a. CHOWN, SETUID, NET_BIND_SERVICE). Die meisten Anwendungen brauchen keine einzige davon.
# Alle Capabilities entfernen, nur das Noetigste zurueckgeben
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
# docker-compose.yml – gehaertete Beispiel-Konfiguration
services:
web:
image: nginx:1.25-alpine
read_only: true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
- seccomp=./custom-seccomp.json
tmpfs:
- /var/cache/nginx
- /var/run
no-new-privileges:true verhindert zusaetzlich, dass ein Prozess im Container ueber SUID-Binaries seine Rechte nachtraeglich erweitert – eine der wirkungsvollsten Einzelmassnahmen ueberhaupt, mit praktisch keinem Funktionsverlust.
Secrets-Management: Docker Secrets vs. Vault
Der haeufigste Fehler in KMU-Setups: Datenbank-Passwoerter und API-Keys landen als Umgebungsvariable direkt in der docker-compose.yml oder im Dockerfile. Umgebungsvariablen sind ueber docker inspect, /proc/<pid>/environ und oft auch in Log-Aggregatoren im Klartext einsehbar.
Docker Secrets (Swarm-Modus)
Docker Secrets funktionieren nur im Swarm-Modus, verschluesseln die Werte aber mit AES-256-GCM und mounten sie als tmpfs-Datei in /run/secrets/, statt sie als Umgebungsvariable sichtbar zu machen.
# Swarm initialisieren (falls noch nicht aktiv)
docker swarm init
# Secret aus einer Datei anlegen
echo "MeinSicheresDbPasswort123!" | docker secret create db_password -
# Service mit Secret starten
docker service create --name db --secret db_password postgres:16
Im Container liegt das Secret dann unter /run/secrets/db_password und kann von der Anwendung gelesen werden, ohne je als Umgebungsvariable sichtbar zu sein.
HashiCorp Vault – wenn Rotation und Audit-Trail Pflicht sind
Vault ist die deutlich maechtigere Loesung: dynamische, kurzlebige Secrets (z.B. DB-Credentials, die nach 1 Stunde automatisch ablaufen), vollstaendiges Audit-Logging, Verschluesselung als Service (Transit Engine) und Unterstuetzung fuer Hardware-Security-Module.
# Vault-Dev-Server lokal starten (NUR zum Testen, nicht produktiv!)
vault server -dev
# KV-Secret-Engine aktivieren und Secret ablegen
vault secrets enable -path=kv kv-v2
vault kv put kv/myapp/db password="MeinSicheresDbPasswort123!"
# Secret per CLI auslesen
vault kv get kv/myapp/db
# vault-agent-config.hcl – Secret automatisch in eine Datei im Container schreiben
vault {
address = "https://vault.internal.example.ch:8200"
}
auto_auth {
method "approle" {
config = {
role_id_file_path = "/vault/config/role-id"
secret_id_file_path = "/vault/config/secret-id"
}
}
}
template {
source = "/vault/templates/db-creds.tpl"
destination = "/vault/secrets/db-creds.env"
}
Der Vault-Agent-Sidecar-Container liest das Secret dynamisch, schreibt es in eine gemountete Datei, und die eigentliche Anwendung liest nur diese Datei – Vault selbst muss der Anwendung nicht bekannt sein.
| Kriterium | Docker Secrets | HashiCorp Vault |
|---|---|---|
| Voraussetzung | Swarm-Modus | Eigener Vault-Server/-Cluster |
| Verschluesselung | AES-256-GCM (Swarm-Manager) | Konfigurierbar, inkl. HSM-Support |
| Rotation | Manuell | Automatisch (dynamische Secrets) |
| Audit-Trail | Nein | Ja, vollstaendig |
| Betriebsaufwand | Sehr gering | Mittel bis hoch |
| Einsatzempfehlung | Kleine Setups, keine Compliance-Pflicht | Wachsende Umgebungen, regulierte Branchen |
Netzwerksegmentierung zwischen Containern
Im Default-Bridge-Netzwerk koennen alle Container miteinander sprechen – ein kompromittierter Webserver kann so ungehindert die interne Datenbank oder das Monitoring-Interface erreichen. Custom-Netzwerke und die --internal-Option loesen das.
# Isoliertes internes Netzwerk ohne Internetzugang anlegen
docker network create --internal backend-net
# Frontend-Netzwerk mit Internetzugang
docker network create frontend-net
# docker-compose.yml – Drei-Zonen-Modell: frontend, backend, db
services:
reverse-proxy:
image: nginx:1.25-alpine
networks: [frontend-net]
ports: ["443:443"]
app:
image: myapp:1.4.2
networks: [frontend-net, backend-net]
db:
image: postgres:16
networks: [backend-net]
# kein "ports:" -> von aussen komplett unerreichbar
networks:
frontend-net:
backend-net:
internal: true
In diesem Modell hat nur app Zugriff auf beide Netzwerke, db ist ausschliesslich ueber backend-net erreichbar und hat keinerlei Route nach aussen. Selbst wenn reverse-proxy kompromittiert wird, kommt ein Angreifer nicht direkt an die Datenbank heran, weil reverse-proxy gar nicht im backend-net haengt.
CIS Docker Benchmark und Docker Bench for Security
Der CIS Docker Benchmark ist der De-facto-Standard fuer Docker-Haertung, herausgegeben vom Center for Internet Security. Er deckt vier Bereiche ab: Host-Konfiguration, Docker-Daemon-Konfiguration, Docker-Daemon-Konfigurationsdateien und Container-Images/Runtime.
Das offizielle Docker Bench for Security-Skript automatisiert die Pruefung gegen diesen Benchmark:
# Docker Bench for Security als Container ausfuehren
docker run --rm --net host --pid host --userns host --cap-add audit_control \
-e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /etc:/etc:ro \
--label docker_bench_security \
docker/docker-bench-security
Typische Findings, die in fast jedem ungehaerteten KMU-Setup auftauchen:
- 4.1 Container laufen als root statt mit dediziertem
USER - 5.4
--privileged-Flag wird verwendet, wo es gar nicht noetig waere - 5.9 Der Docker-Socket (
/var/run/docker.sock) ist in einen Container gemountet – das ist praktisch root auf dem Host - 5.25 Fehlende
--read-only-Root-Dateisysteme - 2.5 Kein
userns-remapkonfiguriert
Offizielles CIS-Benchmark-Audit-Skript von Docker
github.com
Entscheidungsbaum: Welches Haertungslevel brauchst du?
Container verarbeitet sensible Daten (Kunden, Finanzen, Gesundheit)?
├── NEIN → Basis-Haertung reicht:
│ cap_drop ALL, no-new-privileges, Trivy-Scan in CI/CD,
│ Custom-Netzwerke statt Default-Bridge
│
└── JA → Erweiterte Haertung noetig:
├── Regulatorische Pflicht (DSGVO/nDSG, Finma, ISO 27001)?
│ └── JA → Vault statt Docker Secrets, vollstaendiger
│ CIS-Benchmark-Pass, Audit-Logging aktiv
│
├── Mehrere Teams/Mandanten auf demselben Host?
│ └── JA → Rootless Mode oder userns-remap zwingend,
│ eigene Seccomp/AppArmor-Profile pro Workload
│
└── Internetfacing (direkt erreichbar von aussen)?
└── JA → Netzwerksegmentierung mit --internal-Netzwerken,
read_only-Filesystem, Custom-Seccomp-Profil
Praxis-Szenario: Der kompromittierte Monitoring-Container
Ein typischer Fall aus der Praxis: Ein Cacti- oder Grafana-Container laeuft mit einer veralteten Version, weil “das laeuft doch, warum updaten”. Ein Scanner findet eine kritische CVE, ein automatisierter Bot nutzt sie aus, bekommt eine Shell im Container – und weil der Container ohne cap_drop, ohne Netzwerksegmentierung und mit gemountetem Docker-Socket lief, kann der Angreifer von dort aus einen neuen, privilegierten Container starten und hat effektiv Root auf dem Host.
Mit den in diesem Artikel beschriebenen Massnahmen waere die Kill-Chain an mehreren Stellen unterbrochen worden:
- Trivy/Grype haetten die CVE schon beim Image-Build gemeldet.
cap_drop: ALL+ Seccomp haetten viele Post-Exploitation-Techniken blockiert.- Ohne gemounteten Docker-Socket haette der Angreifer keine neuen Container starten koennen.
- Netzwerksegmentierung haette verhindert, dass der Monitoring-Container ueberhaupt die Datenbank erreicht.
Weiterlernen
- Docker Security – Rootless Mode (offizielle Doku)
- Docker Security – User Namespace Remapping
- Trivy Dokumentation (Aqua Security)
- Grype auf GitHub (Anchore)
- CIS Docker Benchmarks
- HashiCorp Vault Dokumentation
Videos
Kommentare
Frage, Verbesserungsvorschlag oder eigene Erfahrung zu diesem Artikel? Schreib einen Kommentar. Neue Beiträge erscheinen nach kurzer Moderation.
- Lade Kommentare …