Container-Sicherheit ist ein eigenes Thema, weil ein Container per Definition Prozesse auf einem gemeinsamen Kernel ausführt. Die Isolation ist gut, aber nicht so stark wie bei einer virtuellen Maschine. Wer Container produktiv einsetzt, sollte wissen, welche Grenzen der Kernel zieht – und wie man sie enger stellt.

Was Container isolieren – und was nicht

Isoliert (mit eigenen Namensraeumen / namespaces):
  Prozesse        eigene PID-Nummerierung
  Netzwerk        eigene Schnittstellen, Routing-Tabellen, Firewall
  Einhängepunkte  eigene Sicht auf das Dateisystem
  Benutzer/Gruppen eigene UID/GID-Zuordnung
  Rechnername     eigener Hostname
  Zwischenprozess eigene IPC-Puffer

NICHT isoliert:
  Der Kernel selbst
  -> Eine Luecke im Kernel ist aus jedem Container heraus nutzbar.
  -> Ein ungepatchter Host-Kernel ist die groesste Gefahr
     im gesamten Containerbetrieb.

  Hardware und Geraete
  -> Wer /dev/* durchreicht, gibt viel Kontrolle ab.
     /dev/mem, /dev/sda oder der Docker-Socket sind praktisch
     gleichbedeutend mit Root auf dem Host.

  Ressourcen ohne explizite Begrenzung
  -> Ohne Speichergrenze kann ein Container den Host
     in den Speichermangel treiben.

Die zehn wichtigsten Maßnahmen

  1. Der Host-Kernel wird aktuell gehalten. Das ist wirksamer als jede Containerkonfiguration. Ein Kernel-Ausbruch betrifft alle Container gleichzeitig.
  2. Nicht als Root im Container arbeiten. Im Dockerfile einen Benutzer anlegen und mit USER umschalten. Wer als Root im Container läuft, hätte bei einem Ausbruch die höchsten Rechte.
    FROM debian:bookworm-slim
    RUN groupadd -r app && useradd -r -g app -d /srv/app -s /usr/sbin/nologin app
    WORKDIR /srv/app
    COPY --chown=app:app . .
    USER app
    CMD ["/srv/app/server"]
  3. Dateisystem nur lesen. --read-only verhindert, dass ein Angreifer Schadsoftware ablegt. Schreibpfade einzeln freigeben.
    docker run -d \
      --read-only \
      --tmpfs /tmp:rw,noexec,nosuid,size=64m \
      --tmpfs /run:rw,noexec,nosuid,size=16m \
      --mount type=volume,src=app-daten,dst=/srv/app/daten \
      myapp:1.0
  4. Fähigkeiten (Capabilities) entziehen. Ein Standard-Root-Container hat erhebliche Kernel-Rechte. Alle entfernen und nur die nötigen zurückgeben.
    docker run -d \
      --cap-drop ALL \
      --cap-add NET_BIND_SERVICE \
      --security-opt no-new-privileges \
      myapp:1.0
    
    # Besonders gefaehrlich: SYS_ADMIN (fast gleichbedeutend mit Root),
    # SYS_PTRACE, SYS_MODULE, NET_ADMIN ohne Not, und --privileged.
    # --privileged hebt praktisch JEDE Isolation auf.
  5. Namensräume des Hosts nicht teilen. --pid=host, --network=host oder --ipc=host sind Sonderfälle mit gutem Grund – nicht der Standard.
  6. Ressourcen begrenzen. Speicher- und CPU-Grenzen verhindern, dass ein Container den Host lahmlegt.
    docker run -d \
      --memory 512m --memory-swap 512m \
      --cpus 1.5 --pids-limit 200 \
      myapp:1.0
    #   --pids-limit schuetzt vor einer Prozesslawine
    #   (Fork-Bombe) und sollte IMMER gesetzt werden.
  7. Signatur und Herkunft der Images prüfen. Nur Images aus vertrauenswürdigen Quellen und mit festgeschriebener Version per Hash – nicht :latest.
    docker pull myapp:1.0
    docker inspect myapp:1.0 --format '{{.RepoDigests}}'
    #   Immer per Digest pinnen - der Tag kann sich aendern!
    docker run -d myapp@sha256:abc123...
    
    # Image auf bekannte Schwaechen pruefen
    trivy image myapp:1.0
    grype myapp:1.0
    #   In die Pipeline einbauen: Abbruch bei kritischen Funden.
    
    # Fuer Docker Swarm und Kubernetes zusaetzlich:
    cosign sign myapp:1.0
    cosign verify myapp:1.0
    docker trust inspect --pretty myapp:1.0
  8. Keine Geheimnisse im Image. Weder im Dockerfile noch in Umgebungsvariablen (docker inspect zeigt sie jedem mit Zugriff). Geheimnisse zur Laufzeit einhängen.
    # NICHT: ENV DB_PASS=geheim      -> dauerhaft im Image!
    # NICHT: -e DB_PASS=geheim       -> sichtbar in "docker inspect"!
    
    # Richtig: Datei einhaengen, nur zur Laufzeit vorhanden
    docker run -d \
      --mount type=bind,src=/srv/secrets/db_pass,dst=/run/secrets/db_pass,ro \
      myapp:1.0
    #   Docker Swarm: docker secret create
    #   Kubernetes: Secret als Volume einhaengen
    #   Neuer: --mount type=tmpfs fuer Geheimnisse, die nur im Speicher liegen
  9. Den Docker-Socket schützen. Wer /var/run/docker.sock einhängt, kann Container erstellen, die als Root alles dürfen. Das ist praktisch gleichbedeutend mit uneingeschränktem Root-Zugriff auf den Host.
    # NIEMALS so:
    #   -v /var/run/docker.sock:/var/run/docker.sock
    # Wenn es sich nicht vermeiden laesst:
    #   - Socket-Proxy mit eingeschraenktem Befehlsumfang
    #   - Rootless-Docker
    #   - Podman mit dediziertem Socket und Rechtepruefung
  10. Logging und Erkennung. Ohne Sichtbarkeit lässt sich ein Vorfall weder erkennen noch aufklären.
    docker logs -f mein-container
    journalctl -u docker -f
    #   Den Ereignispuffer vergroessern, sonst sind Protokolle
    #   nach wenigen Minuten weg:
    #   /etc/docker/daemon.json:
    #     { "log-driver": "json-file",
    #       "log-opts": {"max-size": "50m", "max-file": "5"} }
    
    # Laufzeitpruefung auf Systemebene (Falco, Tetragon):
    #   Meldet ungewoehnliche Vorgaenge IM Container -
    #   etwa einen Shell-Start in einem Dienstcontainer, der nie
    #   eine Shell starten sollte. Das ist der wirkungsvollste
    #   einzelne Punkt in der Erkennung.

Images klein und nachvollziehbar halten

# 1. Schlankes Ausgangsimage
FROM debian:bookworm-slim          # statt debian:latest
# oder distroless für Produktion - keine Shell, keine Paketverwaltung:
# FROM gcr.io/distroless/static-debian12

# 2. Mehrstufiger Aufbau: Werkzeuge rein, Artefakt raus
FROM golang:1.23 AS bauen
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/server

FROM gcr.io/distroless/static-debian12
COPY --from=bauen /out/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]
#   Ergebnis: kein Compiler, kein Paketmanager, keine Shell im
#   fertigen Image. Ein Angreifer findet kaum Werkzeuge.

# 3. Kein "latest", keine unnoetigen Pakete
# 4. .dockerignore nutzen - verhindert, dass .git, .env,
#    Schluessel oder Testdaten ins Image gelangen
#    .git
#    .env
#    *.pem
#    *.key
#    node_modules
#    tests/

# 5. Pruefen, was drin ist
docker run --rm -it --entrypoint sh mein-image -c 'ls -la /; which sh; which curl'
docker history --no-trunc mein-image | head -20
dive mein-image        # Schichten interaktiv ansehen

Prüfliste für den Betrieb

  1. Läuft der Container als nicht-privilegierter Benutzer?
  2. Ist das Dateisystem schreibgeschützt, mit einzelnen Ausnahmen?
  3. Sind alle Fähigkeiten entfernt und nur die nötigen ergänzt?
  4. Ist no-new-privileges gesetzt (verhindert Rechteerhöhung durch setuid-Programme)?
  5. Sind Speicher, CPU und Prozessanzahl begrenzt?
  6. Ist das Image per Digest festgeschrieben und auf Schwachstellen geprüft?
  7. Sind Geheimnisse zur Laufzeit eingehängt und nicht im Image?
  8. Ist der Docker-Socket nirgends eingehängt?
  9. Werden Protokolle aufbewahrt und auf ungewöhnliche Vorgänge geprüft?
  10. Sind die Container-Ports nur dort veröffentlicht, wo es nötig ist – und Datenbanken nie auf allen Schnittstellen?

Verwandte Grundlagen: Container-Technologie, Linux-Fähigkeiten, seccomp, Docker-Netzwerke.