Zum Inhalt springen
sw
en

Tippe um zu suchen

Virtualisierung

Container-Sicherheit und Haertung

Docker-Container professionell absichern: Image-Scanning, Rootless-Mode, Seccomp/AppArmor, Secrets-Management und CIS-Benchmark im KMU-Praxiseinsatz.

12 Min Lesezeit Expert Zuletzt aktualisiert:

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:

  1. Das Image selbst – veraltete Basis-Images mit bekannten CVEs, eingebettete Secrets, unnoetige Tools (curl, bash, ssh) die eine Post-Exploitation erleichtern.
  2. Die Laufzeitkonfiguration – Container laeuft als root, --privileged, gemountete Docker-Socket (/var/run/docker.sock), fehlende Ressourcenlimits.
  3. Der Kernel selbst – ueber Syscalls, die dem Container eigentlich nicht erlaubt sein sollten, kann ein kompromittierter Prozess versuchen, aus dem Namespace auszubrechen.
  4. 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
KriteriumTrivyGrype
ScopeOS-Pakete, App-Abhaengigkeiten, IaC, Secrets, Lizenzen, KubernetesReines Vulnerability-Matching
GeschwindigkeitGutCa. 30–40% schneller bei reinem CVE-Scan
Risiko-PriorisierungCVSS-basiertCVSS + EPSS + CISA-KEV-Score
SBOM-IntegrationEingebautUeber Syft (gleicher Anbieter)
Ideal fuerAll-in-One-Gate in CI/CDZweitmeinung / dedizierte SBOM-Pipeline
LizenzApache 2.0Apache 2.0
Trivy Gratis Open Source linux mac windows

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.

KriteriumDocker SecretsHashiCorp Vault
VoraussetzungSwarm-ModusEigener Vault-Server/-Cluster
VerschluesselungAES-256-GCM (Swarm-Manager)Konfigurierbar, inkl. HSM-Support
RotationManuellAutomatisch (dynamische Secrets)
Audit-TrailNeinJa, vollstaendig
BetriebsaufwandSehr geringMittel bis hoch
EinsatzempfehlungKleine Setups, keine Compliance-PflichtWachsende 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-remap konfiguriert
Docker Bench for Security Gratis Open Source linux

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:

  1. Trivy/Grype haetten die CVE schon beim Image-Build gemeldet.
  2. cap_drop: ALL + Seccomp haetten viele Post-Exploitation-Techniken blockiert.
  3. Ohne gemounteten Docker-Socket haette der Angreifer keine neuen Container starten koennen.
  4. Netzwerksegmentierung haette verhindert, dass der Monitoring-Container ueberhaupt die Datenbank erreicht.

Weiterlernen

Videos

YouTube
Docker – Security & Co. // deutsch

Kommentare

Frage, Verbesserungsvorschlag oder eigene Erfahrung zu diesem Artikel? Schreib einen Kommentar. Neue Beiträge erscheinen nach kurzer Moderation.

  • Lade Kommentare …
Kommentar schreiben