Im Juni 2026 betreffen 60 % aller Cloud-Umgebungen noch immer die runc-Schwachstelle CVE-2024-21626 (CVSS 8.6), die es Angreifern erlaubt, aus einem Container auszubrechen und Root-Rechte auf dem Host zu erlangen. Docker Desktop enthielt in diesem Jahr allein fünf kritische Sicherheitslücken, darunter CVE-2026-33990 mit einem CVSS-Score von 9.1. Wer Container produktiv betreibt, ohne eine durchdachte Sicherheitsstrategie, setzt seinen gesamten Server auf Spiel. Dieses Tutorial zeigt, wie Sie Docker-Container in 12 konkreten Schritten absichern, mit Codebeispielen, typischen Fehlern und einem vollständigen Praxisprojekt.
Warum Docker-Sicherheit 2026 nicht optional ist
Docker ist das am häufigsten genutzte Container-Format weltweit. Hinter der Bequemlichkeit von docker run verbergen sich jedoch erhebliche Angriffsrisiken, wenn Standard-Konfigurationen unverändert bleiben. Die Schwachstelle CVE-2024-21626 im runc-Container-Runtime zeigte 2024, wie ein einziges manipuliertes Image den Ausbruch aus der Container-Isolation ermöglicht. Laut Orca Security waren 60 % aller gescannten Cloud-Umgebungen betroffen. Der Fix erfordert runc 1.1.12 oder neuer.
Docker Desktop selbst listete in der ersten Hälfte 2026 mehrere kritische CVEs: CVE-2026-33990 (CVSS 9.1, kritische Lücke im OCI Registry Client des Docker Model Runners), CVE-2026-28400 (Runtime-Flag-Injektion, behoben in Docker Desktop 4.67.0) und CVE-2025-9074 (unauthentifizierter Zugriff auf die Docker Engine API, behoben in Docker Desktop 4.44.3). Ein weiteres Problem: CVE-2025-3911 legte sensible Daten einschließlich Umgebungsvariablen in Docker-Desktop-Logdateien offen. Das ist der direkte Beweis dafür, warum Geheimnisse niemals als Umgebungsvariablen gespeichert werden dürfen.
Supply-Chain-Angriffe auf Container-Images nehmen zu. Angreifer schleusen Schadcode in populäre Basis-Images ein, die Millionen von Deployments ziehen. Die OWASP Container Security Cheat Sheet und das CIS Docker Benchmark bieten strukturierte Gegenmaßnahmen. Dieses Tutorial setzt beides um, Schritt für Schritt, mit produktionsreifem Code.
Voraussetzungen
Für dieses Tutorial benötigen Sie:
- Docker Engine 27.x oder neuer (Docker Desktop 4.67.0+ für macOS/Windows)
- Linux-Server oder -VM mit Ubuntu 22.04/24.04 oder Debian 12 (empfohlen für Produktion)
- Docker Compose 2.x für Mehrcontainer-Setups
- Trivy 0.52+ für Image-Scanning (wird in Schritt 9 installiert)
- cosign 2.x für Image-Signing (wird in Schritt 10 installiert)
- Grundkenntnisse in Dockerfile-Syntax und Linux-Benutzerrechten
- Root- oder sudo-Zugang zum Host-System
Die geschätzte Zeit für dieses Tutorial beträgt 45 Minuten bei Befolgung aller Schritte. Produktionsumgebungen sollten alle 12 Schritte umsetzen. Für Entwicklungsumgebungen sind mindestens die Schritte 1 bis 8 empfehlenswert.
Schritt 1: Docker aktuell halten und Daemon absichern
Die wichtigste Sicherheitsmaßnahme ist die aktuellste Docker-Version. CVE-2024-21626 war monatelang ungepatcht in Produktionsumgebungen aktiv, weil Administratoren keine automatischen Updates eingerichtet hatten. Prüfen Sie zunächst Ihre aktuelle Version und konfigurieren Sie den Docker-Daemon mit Sicherheitsoptionen:
# Docker-Version und runc-Version prüfen
docker version
runc --version
# Docker-Daemon-Konfiguration absichern
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<'EOF'
{
"no-new-privileges": true,
"userns-remap": "default",
"live-restore": true,
"userland-proxy": false,
"icc": false,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOF
sudo systemctl restart docker
# Automatische Sicherheitsupdates einrichten (Ubuntu/Debian)
sudo apt-get install -y unattended-upgrades
echo 'Unattended-Upgrade::Allowed-Origins {
"Docker:${distro_codename}";
};' | sudo tee /etc/apt/apt.conf.d/51docker-security
Die Option "no-new-privileges": true verhindert, dass Prozesse innerhalb von Containern über setuid-Binärdateien zusätzliche Privilegien erlangen. "userns-remap": "default" mappt den Root-User im Container auf einen unprivilegierten Benutzer auf dem Host, was den Impact von Container-Escapes erheblich reduziert. "icc": false deaktiviert die Inter-Container-Kommunikation standardmäßig und erzwingt explizite Netzwerkregeln. "userland-proxy": false entfernt den Docker Proxy-Prozess für Port-Mappings und nutzt stattdessen direkte iptables-Regeln, was die Angriffsfläche verkleinert.
Prüfen Sie nach dem Neustart, ob die Konfiguration korrekt geladen wurde: docker info | grep -E "Security|Runtimes|remap". Das Ergebnis sollte userns-remap: dockremap und die konfigurierten Security-Optionen anzeigen.
Schritt 2: Minimale Basis-Images verwenden
Das größte Angriffspotenzial in Docker-Images liegt in unnötigen Paketen. Ein ubuntu:latest-Image enthält Hunderte von Binärdateien, die ein Angreifer nach einem erfolgreichen Exploit nutzen kann. Distroless-Images von Google und Alpine Linux reduzieren die Angriffsfläche auf ein Minimum.
| Basis-Image | Komprimierte Größe | Pakete (ca.) | Typische kritische CVEs | Empfehlung |
|---|---|---|---|---|
ubuntu:24.04 | ~29 MB | ~400 | 5-15 | Nur für Entwicklung |
debian:bookworm-slim | ~31 MB | ~100 | 2-8 | Akzeptabel |
alpine:3.20 | ~3,3 MB | ~14 | 0-2 | Gut geeignet |
gcr.io/distroless/nodejs22-debian12 | ~60 MB | 0 (nur Node.js) | 0-1 | Beste Wahl für Node.js |
scratch | 0 MB | 0 | 0 | Nur für statische Binärdateien |
Distroless-Images enthalten keine Shell, keinen Paketmanager und keine unnötigen Systemwerkzeuge. Ein Angreifer, der in einen Distroless-Container einbricht, findet weder bash noch curl noch wget, was Post-Exploitation erheblich erschwert. Für Node.js-Anwendungen ist gcr.io/distroless/nodejs22-debian12 die erste Wahl für Produktionsumgebungen. Das :nonroot-Tag verwendet automatisch den Benutzer mit UID 65532 ohne Root-Rechte.
Alpine Linux ist ebenfalls eine gute Wahl, nutzt jedoch musl libc statt glibc, was bei manchen npm-Paketen mit nativen Erweiterungen zu Kompatibilitätsproblemen führen kann. Testen Sie Alpine immer gründlich vor dem Einsatz in der Produktion. Für Python-Workloads eignet sich python:3.12-slim-bookworm als sichere Alternative zu python:3.12.
Schritt 3: Multi-Stage Builds für sichere, schlanke Images
Multi-Stage Builds trennen die Build-Umgebung von der Laufzeitumgebung. Build-Tools wie Compiler, npm, pip und Make verbleiben ausschließlich im Build-Stage und erscheinen nicht im finalen Image. Das reduziert die Angriffsfläche erheblich und verkleinert Images typischerweise um 60 bis 90 Prozent. Außerdem landen keine Build-Geheimnisse (SSH-Keys, npm-Tokens) im fertigen Image.
# Dockerfile mit Multi-Stage Build für eine Node.js-Anwendung
# Stage 1: Abhängigkeiten installieren und Build erstellen
FROM node:22-alpine AS builder
WORKDIR /app
# Nur package.json zuerst kopieren (besseres Layer-Caching)
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force
# Quellcode kopieren und kompilieren
COPY src/ ./src/
# Stage 2: Minimales Produktions-Image (Distroless, kein Root)
FROM gcr.io/distroless/nodejs22-debian12:nonroot AS production
WORKDIR /app
# Nur die benötigten Artefakte aus Stage 1 kopieren
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/src ./src
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["src/server.js"]
Ein häufiger Fehler bei Multi-Stage Builds ist das Kopieren des gesamten Build-Kontexts mit COPY . ., bevor die .dockerignore-Datei korrekt konfiguriert wurde. Das führt dazu, dass .env-Dateien, private Schlüssel oder Git-Historien in den Build-Kontext gelangen. Die .dockerignore-Datei (Schritt 3b) ist daher ebenso wichtig wie das Dockerfile selbst.
Schritt 4: Container als Nicht-Root-Benutzer ausführen
Standardmäßig führt Docker Container als Root-Benutzer (UID 0) aus. Das ist das Container-Sicherheitsäquivalent von “als Administrator im Internet surfen”. Wenn ein Angreifer Remote-Code-Execution im Container erlangt, handelt er mit Root-Rechten innerhalb des Namespace. Bei einem Container-Escape erhält er potenziell Root-Rechte auf dem Host. Jeder Container sollte als dedizierter Nicht-Root-Benutzer laufen.
# Dockerfile: Nicht-Root-Benutzer anlegen und verwenden
FROM node:22-alpine
WORKDIR /app
# Systembenutzer und -gruppe anlegen (kein Home-Verzeichnis, keine Login-Shell)
RUN addgroup -S appgroup -g 1001 && \
adduser -S appuser -u 1001 -G appgroup -s /sbin/nologin
# Abhängigkeiten als Root installieren (npm benötigt ggf. Schreibrechte)
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# App-Dateien mit korrekten Berechtigungen kopieren
COPY --chown=appuser:appgroup src/ ./src/
RUN chown -R appuser:appgroup /app
# Zu Nicht-Root-Benutzer wechseln
USER appuser
EXPOSE 3000
CMD ["node", "src/server.js"]
Beim Start können Sie den Benutzer zusätzlich per Flag erzwingen und prüfen:
# Container mit explizitem Benutzer starten
docker run --user 1001:1001 --rm myapp:latest
# Prüfen, welcher Benutzer im laufenden Container aktiv ist
docker exec CONTAINER_ID whoami
docker exec CONTAINER_ID id
# Alle laufenden Container auf Root-Ausführung prüfen
docker ps -q | xargs docker inspect --format='{{.Name}}: User={{.Config.User}}'
Wichtig: Manche Basis-Images setzen USER root als letzten Befehl. Prüfen Sie mit docker inspect IMAGE_NAME | grep -A2 '"User"', welcher Benutzer tatsächlich beim Start aktiv ist. Wenn das Ergebnis leer ist, bedeutet das Root. Setzen Sie immer explizit USER im Dockerfile als letzten Befehl vor CMD oder ENTRYPOINT.
Schritt 5: Read-Only-Dateisystem und tmpfs konfigurieren
Ein Read-Only-Dateisystem verhindert, dass Angreifer Persistenz im Container herstellen, indem sie Dateien modifizieren oder neue Executables ablegen. Mit --read-only wird das gesamte Container-Dateisystem schreibgeschützt. Verzeichnisse, die zur Laufzeit Schreibzugriff benötigen (z. B. für temporäre Dateien), werden als tmpfs gemountet, also im RAM ohne Persistenz nach dem Container-Stopp.
# Container mit Read-Only-Dateisystem starten
docker run \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--tmpfs /var/run:rw,noexec,nosuid,size=10m \
--user 1001:1001 \
--rm \
myapp:latest
# In Docker Compose (compose.yml)
services:
app:
image: myapp:latest
read_only: true
tmpfs:
- /tmp:size=64m,mode=1777,exec=0
- /var/run:size=10m,mode=755,exec=0
user: "1001:1001"
Die Optionen noexec und nosuid für tmpfs verhindern die Ausführung von Binärdateien aus dem temporären Verzeichnis und unterbinden SUID-Bit-Exploits. Kombiniert mit --read-only entsteht eine Umgebung, in der ein Angreifer keine dauerhaften Änderungen vornehmen kann. Manche Anwendungen benötigen Schreibzugriff auf bestimmte Verzeichnisse wie /app/logs. Mounten Sie diese als Named Volumes, nicht als Bind Mounts vom Host, um versehentliche Host-Dateisystem-Exposition zu vermeiden.
Schritt 6: Docker-Netzwerke segmentieren
Standardmäßig verbindet Docker alle Container im selben bridge-Netzwerk, was Inter-Container-Kommunikation ohne Einschränkungen erlaubt. Ein kompromittierter Datenbankcontainer kann dann direkt mit dem API-Container kommunizieren und umgekehrt. Mit der Daemon-Option "icc": false aus Schritt 1 und dedizierten Netzwerken werden alle Kommunikationswege explizit definiert.
# compose.yml mit isolierten Netzwerken
services:
nginx:
image: nginx:alpine
ports:
- "443:443"
networks:
- frontend
read_only: true
api:
image: myapp:latest
networks:
- frontend
- backend
read_only: true
user: "1001:1001"
environment:
NODE_ENV: production
postgres:
image: postgres:16-alpine
networks:
- backend
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- pgdata:/var/lib/postgresql/data
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # Kein Internetzugang fuer Backend-Container
secrets:
db_password:
file: ./secrets/db_password.txt
volumes:
pgdata:
Das internal: true-Flag für das Backend-Netzwerk verhindert, dass die Datenbank selbst Verbindungen ins Internet aufbauen kann. Das ist entscheidend: Viele Malware-Samples exfiltrieren Daten nach dem Einbruch nach draußen. Ein internes Netzwerk unterbricht diesen Kanal vollständig. Der Nginx-Container ist das einzige Gateway nach außen, der API-Container kommuniziert mit beiden Netzwerken, und die Datenbank ist vollständig isoliert.
Schritt 7: Linux-Capabilities einschränken
Docker-Container erhalten standardmäßig eine Reihe von Linux-Capabilities, die in den meisten Anwendungsfällen nicht benötigt werden. Das Prinzip des minimalen Privilegs gilt auch hier: Entfernen Sie alle Capabilities mit --cap-drop=ALL und fügen Sie nur die tatsächlich benötigten wieder hinzu.
| Capability | Standardmäßig aktiv | Beschreibung | Empfehlung für Web-API |
|---|---|---|---|
| NET_BIND_SERVICE | Ja | Ports unter 1024 binden | Entfernen, wenn Port > 1024 |
| CHOWN | Ja | Dateibesitzer ändern | Entfernen nach dem Build |
| SETUID | Ja | Benutzer-ID wechseln | Immer entfernen |
| SETGID | Ja | Gruppen-ID wechseln | Immer entfernen |
| SYS_ADMIN | Nein | Systemadministration | Niemals hinzufügen |
| NET_RAW | Ja | RAW-Sockets (Ping etc.) | In Produktion entfernen |
| SYS_PTRACE | Nein | Prozesse debuggen | Nur in Entwicklung |
# Alle Capabilities entfernen, nur benoetigte hinzufuegen
docker run \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt no-new-privileges:true \
--user 1001:1001 \
--read-only \
myapp:latest
# In Docker Compose
services:
app:
image: myapp:latest
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Nur wenn Port unter 1024 benoetigt
security_opt:
- no-new-privileges:true
Für Webserver und API-Dienste, die auf Ports über 1024 lauschen (z. B. 3000, 8080), benötigen Sie keine einzige Capability über die Grundfunktionen hinaus. Wenn Ihre Anwendung auf Port 3000 läuft, können Sie sogar auf NET_BIND_SERVICE verzichten. Verwenden Sie immer einen Reverse Proxy (Nginx, Traefik, Caddy) vor Ihrem Anwendungscontainer, der sich um die Portweiterleitung von 443 auf 3000 kümmert.
Schritt 8: Secrets sicher verwalten
CVE-2025-3911 belegte es: Docker Desktop-Logdateien enthielten Klartextdarstellungen von Umgebungsvariablen, darunter Passwörter, API-Schlüssel und Datenbankverbindungsstrings. Umgebungsvariablen sind eine der unsichersten Methoden zur Weitergabe von Geheimnissen an Container. Sie sind sichtbar in docker inspect, in CI/CD-Ausgaben, in /proc/PID/environ und in Logdateien.
# Docker Secret erstellen
echo "mein_sehr_sicheres_passwort" | docker secret create db_password -
# Fuer Docker Compose ohne Swarm: Secret-Datei verwenden
mkdir -p ./secrets
chmod 700 ./secrets
echo "mein_sehr_sicheres_passwort" > ./secrets/db_password.txt
chmod 600 ./secrets/db_password.txt
# Secrets in .gitignore eintragen
echo "secrets/" >> .gitignore
# In der Anwendung (Node.js): Secret aus /run/secrets/ lesen
// config/database.js
import fs from 'node:fs';
function readSecret(name) {
const secretPath = `/run/secrets/${name}`;
try {
return fs.readFileSync(secretPath, 'utf8').trim();
} catch {
// Fallback auf Umgebungsvariable (nur fuer lokale Entwicklung)
const envKey = name.toUpperCase().replace(/-/g, '_');
if (process.env.NODE_ENV !== 'production') {
return process.env[envKey];
}
throw new Error(`Secret '${name}' nicht gefunden und Produktion aktiv`);
}
}
export const dbConfig = {
host: process.env.DB_HOST || 'postgres',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'myapp',
password: readSecret('db_password'),
};
Für komplexere Geheimnisverwaltung in größeren Infrastrukturen empfiehlt sich die Integration von HashiCorp Vault, das dynamische Credentials, automatische Rotation und detailliertes Audit-Logging bietet. Vault unterstützt native Integration mit Docker und Kubernetes über den Vault Agent oder den Secrets Store CSI Driver.
Schritt 9: Container-Images mit Trivy scannen
Trivy ist der führende Open-Source-Vulnerability-Scanner für Container-Images, IaC-Dateien, Git-Repositories und Secrets. Das Tool scannt Images auf bekannte CVEs in OS-Paketen und Sprachpaketen (npm, pip, Go-Module) und findet gleichzeitig Fehlkonfigurationen sowie hartcodierte Zugangsdaten.
# Trivy installieren (Ubuntu/Debian)
sudo apt-get install -y wget apt-transport-https gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo deb https://aquasecurity.github.io/trivy-repo/deb generic main | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install -y trivy
# Image auf kritische und hohe Schwachstellen scannen
trivy image --severity CRITICAL,HIGH myapp:latest
# Dockerfile auf Fehlkonfigurationen pruefen
trivy config ./Dockerfile
# Geheimnisse im Quellcode scannen (vor dem Build)
trivy fs --scanners secret .
# Im CI/CD-Pipeline: Build fehlschlagen lassen bei CRITICAL
trivy image --exit-code 1 --severity CRITICAL myapp:latest
# JSON-Bericht fuer SIEM oder Compliance exportieren
trivy image --format json --output trivy-report.json myapp:latest
Integrieren Sie Trivy in Ihre CI/CD-Pipeline, damit jeder Build automatisch auf Sicherheitslücken geprüft wird. Das folgende GitHub Actions-Beispiel scannt das Image und lädt die Ergebnisse in den GitHub Security Tab hoch:
# .github/workflows/security.yml
name: Container Security Scan
on:
push:
branches: [main, develop]
jobs:
trivy-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker Image bauen
run: docker build -t myapp:${{ github.sha }} .
- name: Trivy Vulnerability Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: 1
- name: Ergebnisse hochladen
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
Trivy prüft bei npm-Packages auch die package-lock.json, die genauere Versionsinformationen enthält als package.json. Stellen Sie sicher, dass die Lock-Datei im Build-Kontext verfügbar ist und durch npm ci (nicht npm install) verwendet wird, um reproduzierbare, auditierbare Builds zu erhalten. Richten Sie außerdem wöchentliche automatische Rebuilds Ihrer Images ein, um Basis-Image-Patches aufzunehmen.
Schritt 10: Image-Signing mit Cosign und Sigstore
Image-Signing stellt sicher, dass ein Container-Image tatsächlich von Ihrem CI/CD-System erstellt und nicht durch einen Supply-Chain-Angriff manipuliert wurde. Sigstore und das zugehörige Tool Cosign bieten eine moderne, schlüssellose Signaturinfrastruktur, die auf Certificate Transparency basiert. Seit April 2018 müssen alle öffentlich vertrauenswürdigen TLS-Zertifikate in CT-Logs protokolliert werden. Cosign nutzt dasselbe Prinzip für Container-Images.
# cosign installieren
curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
sudo chmod +x /usr/local/bin/cosign
# Schlüsselpaar erstellen (klassische Methode)
cosign generate-key-pair
# Erstellt cosign.key (privat, sicher aufbewahren!) und cosign.pub (oeffentlich)
# Image signieren (nach dem Push in die Registry)
docker build -t registry.example.com/myapp:1.0.0 .
docker push registry.example.com/myapp:1.0.0
cosign sign --key cosign.key registry.example.com/myapp:1.0.0
# Signatur verifizieren (in CI/CD oder Admission Controller)
cosign verify --key cosign.pub registry.example.com/myapp:1.0.0
# Schluesselloser Modus in GitHub Actions (kein Schluessel erforderlich)
# Die Identitaet wird durch GitHub-OIDC-Token bewiesen
cosign sign --yes registry.example.com/myapp:1.0.0
Im schlüssellosen Modus kommuniziert Cosign mit der öffentlichen Sigstore Fulcio CA, die ein kurzlebiges Zertifikat ausstellt, das an Ihre GitHub-Identität gebunden ist. Das Zertifikat und die Signatur werden im öffentlichen Rekor-Transparenz-Log unveränderlich protokolliert. Für eine vollständige Supply-Chain-Sicherheit in Kubernetes kombinieren Sie Cosign mit dem Admission Controller Connaisseur oder Kyverno, die nur signierte Images aus vertrauenswürdigen Quellen zulassen.
Schritt 11: Ressourcenbeschränkungen und Prozesslimits
Ohne Ressourcenlimits kann ein einzelner kompromittierter Container den gesamten Host durch CPU-Mining (Cryptojacking) oder Speicher-Exhaustion lahmlegen. Ressourcenlimits sind eine kritische Schutzmaßnahme gegen Denial-of-Service von innen und begrenzen den Schaden eines kompromittierten Containers.
# Container mit vollstaendigen Ressourcenlimits starten
docker run \
--memory="512m" \
--memory-swap="512m" \
--cpus="0.5" \
--pids-limit=100 \
--ulimit nofile=1024:1024 \
--user 1001:1001 \
--read-only \
myapp:latest
# In Docker Compose
services:
app:
image: myapp:latest
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.25"
memory: 256M
ulimits:
nofile:
soft: 1024
hard: 1024
pids_limit: 100
# Ressourcenverbrauch live ueberwachen
# docker stats --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}"
Die Option --memory-swap="512m" auf denselben Wert wie --memory zu setzen, deaktiviert Swap für den Container. Das verhindert, dass ein Speicher-Exhaustion-Angriff auf Festplattenspeicher ausweicht und den gesamten Swap-Bereich des Hosts belegt. --pids-limit=100 begrenzt die Anzahl der Prozesse im Container und schützt vor Fork-Bomb-Angriffen. Passen Sie diese Werte an den tatsächlichen Bedarf Ihrer Anwendung an: zu niedrige Limits führen zu legitimen Fehlern, zu hohe Limits bieten keinen ausreichenden Schutz.
Schritt 12: AppArmor und Seccomp-Profile anpassen
AppArmor und Seccomp sind zwei Kernel-Sicherheitsmechanismen, die Docker standardmäßig mit einem konservativen Default-Profil nutzt. Das Default-Seccomp-Profil von Docker blockiert bereits 44 der gefährlichsten Syscalls, darunter ptrace, reboot und kexec_load. Für kritische Produktionsumgebungen lohnt es sich, noch restriktivere, anwendungsspezifische Profile zu erstellen.
# AppArmor-Status pruefen
sudo aa-status | grep docker
# Container mit explizitem Seccomp-Profil starten
docker run \
--security-opt seccomp=seccomp-node.json \
--security-opt no-new-privileges:true \
--security-opt apparmor=docker-default \
--user 1001:1001 \
myapp:latest
# Seccomp-Profil im Audit-Modus testen (loggt statt zu blockieren)
docker run \
--security-opt 'seccomp={"defaultAction":"SCMP_ACT_LOG","syscalls":[]}' \
myapp:latest
# Verwendete Syscalls ermitteln (fuer eigenes Profil)
docker run --cap-add SYS_PTRACE myapp:latest &
strace -p $(docker inspect -f '{{.State.Pid}}' CONTAINER_ID) \
-e trace=syscalls -c 2>&1 | head -30
# Ergebnis: nur erlaubte Syscalls in finales Profil aufnehmen
# und defaultAction auf SCMP_ACT_ERRNO setzen
Für Node.js-Anwendungen sind die kritischen Syscalls: read, write, open, close, socket, connect, bind, listen, accept, epoll_*, futex, clone, getrandom und Datei-I/O. Gefährliche Calls wie mount, pivot_root, keyctl und ptrace müssen gesperrt bleiben. Das Erstellen eines präzisen Seccomp-Profils erfordert einmalig etwas Aufwand, bietet aber langfristig erheblichen Schutz gegen Kernel-Exploits.
Häufige Fehler und Sicherheitsfallen
Fehler 1: --privileged-Flag in der Produktion. Das --privileged-Flag gibt dem Container nahezu alle Kernel-Capabilities und deaktiviert AppArmor und Seccomp. Es ist das Äquivalent zu “Container-Sicherheit vollständig deaktiviert”. Selbst wenn es für lokale Tests bequem ist, gehört es niemals in Produktions-Deployments. Prüfen Sie alle laufenden Container: docker ps -q | xargs docker inspect --format='{{.Name}}: {{.HostConfig.Privileged}}'.
Fehler 2: Secrets als Umgebungsvariablen. Umgebungsvariablen werden in docker inspect, in Logdateien und in CI/CD-Ausgaben sichtbar. CVE-2025-3911 zeigte, dass selbst Docker-Desktop-interne Logdateien Umgebungsvariablen enthüllten. Verwenden Sie stattdessen Docker Secrets, externe Secrets-Manager oder lesen Sie Geheimnisse aus Dateien, die per Volume gemountet sind.
Fehler 3: ADD statt COPY im Dockerfile. ADD unterstützt Remote-URLs und automatisches Entpacken von Tar-Archiven. Das macht es zu einem potenziellen Angriffspunkt, wenn URLs aus nicht vertrauenswürdigen Quellen stammen oder Tar-Bombs verarbeitet werden. Verwenden Sie immer COPY für lokale Dateien. Für Remote-Ressourcen nutzen Sie curl mit expliziter SHA256-Prüfsummenvalidierung.
Fehler 4: latest-Tag für Basis-Images. Das latest-Tag ist undeterministisch. Heute funktioniert der Build, morgen ändert sich das Basis-Image und bricht Ihre Anwendung oder führt unbekannte Vulnerabilities ein. Pinnen Sie auf einen spezifischen SHA256-Digest: FROM node:22-alpine@sha256:abc123.... Holen Sie den Digest mit docker pull node:22-alpine && docker inspect node:22-alpine | grep '"Id"'.
Fehler 5: Fehlende oder unvollständige .dockerignore. Ohne .dockerignore landet der gesamte Projektordner im Build-Kontext, einschließlich .git, .env, node_modules und privaten Schlüsseln. Mindest-Inhalt:
# .dockerignore - minimale Konfiguration
.git
.gitignore
.env
.env.*
node_modules
npm-debug.log*
*.pem
*.key
secrets/
.DS_Store
coverage/
*.test.js
README.md
docker-compose*.yml
Dockerfile*
Fehler 6: Docker-Socket in Container mounten. Das Mounten von /var/run/docker.sock gibt dem Container vollständige Kontrolle über den Docker-Daemon, gleichbedeutend mit Root-Zugriff auf den Host. CI/CD-Tools wie Jenkins oder GitLab Runner benötigen diesen Zugriff manchmal, aber er sollte auf dedizierte Build-Container beschränkt und durch strikte Firewall-Regeln abgesichert werden. Alternativ: rootless Docker oder externe Build-Dienste.
Fehler 7: Kein automatisches Scanning und keine Updates. CVE-2024-21626 war in 60 % der gescannten Cloud-Umgebungen noch aktiv, weil kein automatischer Scanning-Prozess existierte. Richten Sie wöchentliche Rebuilds und tägliche Trivy-Scans aller Produktions-Images ein. Nutzen Sie Docker Scout oder Renovate Bot für automatische Update-Benachrichtigungen.
Troubleshooting: 8 häufige Probleme und Lösungen
Problem 1: Container startet nicht nach Aktivierung von --read-only.
Symptom: Error: EROFS: read-only file system oder ähnliche Fehler beim Start.
Ursache: Anwendung schreibt beim Start in ein Verzeichnis, das nun schreibgeschützt ist.
Lösung: Starten Sie den Container mit strace oder nutzen Sie docker run --read-only --tmpfs /tmp:exec myapp bash -c "node src/server.js 2>&1 | grep -E 'EROFS|permission'", um das betroffene Verzeichnis zu identifizieren. Mounten Sie es dann als tmpfs.
Problem 2: Permission denied beim Start als Nicht-Root-Benutzer.
Symptom: Error: EACCES: permission denied, open '/app/config.json'
Ursache: Dateien im Image gehören Root, aber der Container läuft als UID 1001.
Lösung: Fügen Sie im Dockerfile COPY --chown=1001:1001 src/ ./src/ oder RUN chown -R 1001:1001 /app hinzu. Prüfen Sie Berechtigungen mit docker run --entrypoint ls myapp:latest -la /app.
Problem 3: Netzwerkverbindungen zwischen Containern schlagen fehl.
Symptom: Error: connect ECONNREFUSED postgres:5432
Ursache: Container sind in unterschiedlichen Docker-Netzwerken oder icc: false blockiert Verbindungen.
Lösung: Stellen Sie sicher, dass beide Container im selben Docker Compose Netzwerk definiert sind. Testen Sie mit docker exec api_container ping postgres und docker network inspect NETWORK_NAME.
Problem 4: Trivy findet keine Vulnerabilität, obwohl welche bekannt sind.
Symptom: Trivy meldet 0 Vulnerabilties trotz bekanntem CVE.
Ursache: Veraltete Vulnerability-Datenbank oder fehlerhafte Package-Detektion.
Lösung: trivy image --download-db-only && trivy image myapp:latest. Trivy aktualisiert die Datenbank automatisch, wenn eine Internetverbindung besteht. Für Offline-Umgebungen: trivy image --skip-db-update --offline-scan myapp:latest.
Problem 5: Docker-Daemon startet nicht nach Änderung von daemon.json.
Symptom: systemctl status docker zeigt JSON-Parse-Fehler.
Ursache: Ungültige JSON-Syntax in /etc/docker/daemon.json.
Lösung: python3 -m json.tool /etc/docker/daemon.json validiert die Datei und zeigt Syntaxfehler. Typische Fehler: fehlende Kommas, nicht geschlossene Klammern oder trailing commas nach dem letzten Feld.
Problem 6: userns-remap bricht Volume-Berechtigungen.
Symptom: Named Volumes sind nach Aktivierung von userns-remap: default nicht mehr beschreibbar.
Ursache: User Namespace Remapping ändert die effektiven UID-Mappings für Volume-Dateien.
Lösung: Bestehende Volumes müssen nach Aktivierung von userns-remap neu erstellt werden. Entfernen Sie das alte Volume mit docker volume rm VOLUME_NAME und lassen Sie Docker Compose es neu erstellen.
Problem 7: Cosign-Verifikation schlägt fehl.
Symptom: Error: no signatures found for image
Ursache: Das Image wurde nach dem Signing neu getaggt oder in eine andere Registry verschoben. Signaturen sind an den Image-Digest gebunden, nicht an Tags.
Lösung: Signieren Sie immer nach dem Push in die Zielregistry. Verwenden Sie den Image-Digest direkt: cosign sign --key cosign.key registry.example.com/myapp@sha256:ABC123.
Problem 8: Seccomp-Profil verhindert legitime Syscalls.
Symptom: Container startet, stürzt aber nach kurzer Zeit mit Operation not permitted ab.
Ursache: Die Anwendung ruft einen Syscall auf, der nicht im Seccomp-Profil erlaubt ist.
Lösung: Setzen Sie das Profil vorübergehend auf SCMP_ACT_LOG statt SCMP_ACT_ERRNO und prüfen Sie dmesg | grep seccomp oder auditd-Logs, um die fehlenden Syscalls zu identifizieren und ins Profil aufzunehmen.
Komplettes Sicherheitsprojekt: Gehärtete Node.js-API
Das folgende vollständige Projekt kombiniert alle 12 Schritte in einem produktionsreifen Setup. Erstellen Sie die Verzeichnisstruktur:
secure-api/
├── src/
│ └── server.js
├── secrets/
│ └── db_password.txt # In .gitignore!
├── .dockerignore
├── .github/
│ └── workflows/
│ └── security.yml
├── Dockerfile
├── compose.yml
└── seccomp-node.json
Das Dockerfile kombiniert Multi-Stage Build, Nicht-Root-Benutzer und Distroless-Basis-Image. Ersetzen Sie SHA256_HASH mit dem aktuellen Digest von docker pull node:22-alpine && docker inspect node:22-alpine --format='{{index .RepoDigests 0}}':
# Dockerfile - produktionsreif mit allen Sicherheitsmassnahmen
FROM node:22-alpine AS builder
# Fuer reproduzierbare Builds: SHA256-Digest pinnen
# FROM node:22-alpine@sha256:HASH AS builder
WORKDIR /build
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY src/ ./src/
FROM gcr.io/distroless/nodejs22-debian12:nonroot
WORKDIR /app
COPY --from=builder /build/node_modules ./node_modules
COPY --from=builder /build/src ./src
COPY --from=builder /build/package.json ./
EXPOSE 3000
CMD ["src/server.js"]
Das vollständige compose.yml mit allen Sicherheitsmassnahmen:
# compose.yml - alle 12 Sicherheitsmassnahmen kombiniert
services:
api:
build: { context: ., target: production }
image: secure-api:latest
read_only: true
user: "65532:65532"
tmpfs:
- /tmp:size=32m,mode=1777,exec=0
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
- seccomp:./seccomp-node.json
networks: [frontend, backend]
environment:
NODE_ENV: production
DB_HOST: postgres
secrets: [db_password]
deploy:
resources:
limits: { cpus: "0.5", memory: 256M }
healthcheck:
test: ["CMD", "node", "-e",
"require('http').get('http://localhost:3000/health',
r=>process.exit(r.statusCode===200?0:1))"]
interval: 30s
timeout: 5s
retries: 3
pids_limit: 100
postgres:
image: postgres:16-alpine
networks: [backend]
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets: [db_password]
volumes: [pgdata:/var/lib/postgresql/data]
read_only: true
tmpfs: [/tmp, /var/run/postgresql]
deploy:
resources:
limits: { cpus: "0.5", memory: 512M }
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true
secrets:
db_password:
file: ./secrets/db_password.txt
volumes:
pgdata:
Erweiterte Tipps für Produktionsumgebungen
Docker Rootless Mode: Seit Docker 20.10 ist der Rootless-Modus stabil. In Docker 27 ist er vollständig für Produktionsumgebungen unterstützt. Im Rootless-Modus läuft der Docker-Daemon selbst als normaler Benutzer ohne Root-Rechte. Das eliminiert eine gesamte Klasse von Host-Escape-Schwachstellen. Einrichten mit: dockerd-rootless-setuptool.sh install. Haupteinschränkungen: kein --net=host, kein --privileged, und overlay2 benötigt Kernel 5.11+.
Docker Bench for Security: Das offizielle CIS Docker Benchmark Audit-Tool prüft Ihren Host und Ihre Container auf Hunderte von Sicherheitskonfigurationen. Führen Sie es nach jeder größeren Infrastrukturänderung aus:
# Docker Bench for Security ausfuehren
docker run --rm -it \
--net host --pid host --userns host \
--cap-add audit_control \
-v /etc:/etc:ro \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
--label docker_bench_security \
docker/docker-bench-security
Runtime-Security mit Falco: Falco ist ein Open-Source-Tool für die Laufzeit-Sicherheitsüberwachung von Containern. Es analysiert Kernel-Syscalls in Echtzeit und löst Alarme aus, wenn verdächtige Aktivitäten erkannt werden: ein Shell-Prozess in einem Container, der normalerweise keine Shell enthält, ungewöhnliche Netzwerkverbindungen oder verdächtige Dateioperationen. Falco ist de facto Standard für Container-Runtime-Security in Kubernetes-Umgebungen.
Image-Digest-Pinning automatisieren: Renovate Bot erstellt automatisch Pull Requests, wenn neue gepinnte Image-Versionen verfügbar sind. Konfigurieren Sie Renovate, Dockerfile-Image-Digests wöchentlich zu aktualisieren und dabei Trivy-Scans der neuen Version auszuführen, bevor der PR genehmigt wird. Das kombiniert Sicherheit mit Aktualität, ohne manuelle Eingriffe.
Kubernetes-Übergang: Beim Wechsel von Docker Compose zu Kubernetes übernehmen Kubernetes-native Konzepte die Sicherheitsmaßnahmen: SecurityContext für Capabilities und Nicht-Root-User, NetworkPolicy für Netzwerksegmentierung, PodSecurityStandards für Richtlinienerzwingung und Kubernetes Secrets für Geheimnisse. Lesen Sie dazu auch unseren Artikel über Nginx Reverse Proxy mit HTTPS für die TLS-Terminierung vor Kubernetes-Deployments.
Verwandte Artikel
- OpenSSL-Tutorial: Schlüssel und Zertifikate in 12 Schritten
- Nginx Reverse Proxy: HTTPS in 12 Schritten einrichten
- HashiCorp Vault 2.0: Sichere Secrets in 12 Schritten
- HTTP Security Headers in Node.js: 12 Schritte mit Helmet.js
- Nmap-Tutorial: Docker-Netzwerke und exponierte Ports prüfen
- Security-Hub: Alle Sicherheits-Tutorials im Überblick
FAQ: Docker Container absichern
Muss ich alle 12 Schritte für eine lokale Entwicklungsumgebung umsetzen?
Nein. Für lokale Entwicklung sind Schritt 1 (aktuelle Version), Schritt 2 (minimale Basis-Images) und Schritt 8 (keine Secrets als Umgebungsvariablen) am wichtigsten. Die restriktiven Maßnahmen wie Read-Only-Dateisystem und Seccomp-Profile können in der Entwicklung den Workflow verlangsamen. Trennen Sie klar zwischen Entwicklungs- und Produktionskonfiguration mit separaten Compose-Dateien.
Wie überprüfe ich, ob mein Container als Root läuft?
Führen Sie docker exec CONTAINER_ID whoami aus oder prüfen Sie beim Start mit docker inspect CONTAINER_ID | grep -A2 '"User"'. Wenn das Ergebnis leer ist, bedeutet das Root. Für alle laufenden Container: docker ps -q | xargs docker inspect --format='{{.Name}}: User={{.Config.User}}'.
Was ist CVE-2024-21626 und bin ich betroffen?
CVE-2024-21626 ist eine runc-Schwachstelle (CVSS 8.6), bei der ein manipuliertes Docker-Image durch einen “Leaky File Descriptor” im WORKDIR-Handling aus dem Container ausbrechen und Root-Rechte auf dem Host erlangen kann. Prüfen Sie Ihre runc-Version mit runc --version. Sie benötigen runc 1.1.12 oder neuer. Der Fix ist in Docker Engine 25.0.4+ und Docker Desktop 4.28.0+ enthalten.
Ist Docker Desktop oder Docker Engine empfehlenswerter für Produktion?
Für Produktionsserver auf Linux empfehlen sich Docker Engine (Community Edition) direkt oder containerd ohne Docker-Overhead. Docker Desktop ist für Entwicklungs-Workstations konzipiert und enthielt 2025/2026 mehrere kritische CVEs. Halten Sie Docker Desktop auf mindestens Version 4.67.0 aktuell. Für macOS und Windows-Entwicklung ist Docker Desktop akzeptabel mit aktivierten automatischen Updates.
Wie oft sollte ich Docker-Images neu bauen?
Für Produktionsumgebungen empfiehlt sich ein wöchentlicher automatischer Rebuild, um Basis-Image-Sicherheitsupdates aufzunehmen. Richten Sie eine CI/CD-Pipeline ein, die jede Woche automatisch rebuildet, Trivy scannt und bei kritischen Vulnerabilties einen Alert auslöst. Tools wie Renovate Bot oder Dependabot helfen dabei, Image-Digests aktuell zu halten.
Was tun bei einem Container-Sicherheitsvorfall?
Isolieren Sie den Container sofort vom Netzwerk: docker network disconnect NETWORK CONTAINER_ID. Erstellen Sie einen forensischen Snapshot: docker export CONTAINER_ID > evidence-$(date +%Y%m%d).tar. Stoppen Sie den Container ohne ihn zu löschen (docker stop, nicht docker rm). Benachrichtigen Sie Ihr Security-Team und folgen Sie Ihrem Incident-Response-Plan. Untersuchen Sie Docker-Logs mit docker logs CONTAINER_ID und Host-Audit-Logs.
Was ist der Unterschied zwischen Docker Secrets und Kubernetes Secrets?
Docker Secrets (im Swarm-Modus) werden als temporäre In-Memory-Dateien unter /run/secrets/ eingehängt und sind nicht in Images oder Logs sichtbar. Kubernetes Secrets werden standardmäßig base64-kodiert im etcd gespeichert, was keine echte Verschlüsselung darstellt. Für Kubernetes empfehlen sich externe Secrets-Manager wie HashiCorp Vault oder AWS Secrets Manager mit dem Kubernetes External Secrets Operator. Aktivieren Sie in Kubernetes mindestens etcd-Verschlüsselung at rest.
Kann ich Docker Security Benchmarks automatisch prüfen?
Ja. Docker Bench for Security führt automatisch CIS Docker Benchmark-Checks durch und gibt Bestanden/Fehlgeschlagen-Berichte aus. Das Tool kann als Container gestartet werden und benötigt ca. 30 Sekunden pro Host. Integrieren Sie es in Ihren CI/CD-Prozess oder nutzen Sie kommerzielle Tools wie Aqua Security, Prisma Cloud oder Snyk Container für kontinuierliches Compliance-Monitoring.
Quellen: Docker Engine Security Documentation | OWASP Docker Security Cheat Sheet | CVE-2024-21626 (runc Container Escape) | Trivy Vulnerability Scanner | Cosign / Sigstore Image Signing | runc Security Advisory (CVE-2024-21626)




