Container-Sicherheit - die wichtigsten Stellschrauben

Container sind keine Sicherheitsgrenze — sie sind eine Isolationsschicht. Wer das verwechselt, hat ein Problem.

Das Missverständnis aufräumen

Ein Container läuft auf demselben Kernel wie der Host. Keine VM, kein Hypervisor dazwischen. Das bedeutet: eine Kernel-Schwachstelle, ein falsch konfigurierter Mount, zu viele Rechte — und die Isolierung ist weg.

Images aktuell halten und schlank halten

Das größte Risiko sitzt oft im Basis-Image. latest zu pullen und darauf zu vertrauen ist keine Strategie — der Tag ändert sich still, und niemand weiß was drin ist.

Images vor dem Deploy auf bekannte Sicherheitslücken scannen:

# Trivy scannt Images auf bekannte CVEs
trivy image nginx:latest

# Nur kritische und schwerwiegende Lücken anzeigen
trivy image --severity HIGH,CRITICAL myapp:1.2.3

Alpine- oder Distroless-Basis-Images helfen zusätzlich: weniger vorinstallierte Pakete bedeuten weniger potenzielle Angriffspunkte. Ein vollständiges Ubuntu als Basis für eine einzelne Go-Binary zu verwenden ist schlicht unnötig.

Multi-Stage Builds verhindern außerdem, dass Build-Tools oder Zugangsdaten im finalen Image landen:

FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o /app/server

FROM gcr.io/distroless/base-debian12
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

Container mit minimalen Rechten starten

Der falsche Weg: Container mit --privileged starten. Das gibt dem Container nahezu vollen Zugriff auf den Host — wird trotzdem regelmäßig gemacht, weil irgendwas "nicht funktioniert hat" und das die schnelle Lösung war.

Der richtige Weg: kein Root, kein --privileged, so wenig Rechte wie möglich:

services:
  app:
    image: myapp:1.0.0
    user: "1000:1000"
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp

no-new-privileges verhindert, dass Prozesse im Container sich über Umwege zusätzliche Rechte verschaffen. read_only: true macht das Dateisystem des Containers schreibgeschützt — /tmp wird per tmpfs trotzdem beschreibbar gehalten, weil viele Anwendungen das brauchen.

Secrets nicht ins Image backen

Klassischer Fehler: Passwörter oder API-Keys direkt im Dockerfile via ENV setzen oder .env-Dateien ins Image kopieren. Beides landet in den Image-Layern und ist im Nachhinein auslesbar:

docker history --no-trunc myapp:1.0.0

Zugangsdaten gehören zur Laufzeit injiziert — nicht beim Build. Mindestens eine .dockerignore die verhindert dass sensible Dateien überhaupt ins Image gelangen:

.env
.env.*
*.pem
*.key

Netzwerk zwischen Containern einschränken

Standardmäßig können alle Container im selben Docker-Netzwerk miteinander kommunizieren. In einem Compose-Stack mit mehreren Services bedeutet das: jeder Service kann jeden anderen direkt ansprechen — auch die Datenbank.

Explizite Netzwerke lösen das:

networks:
  frontend:
  backend:

services:
  nginx:
    networks:
      - frontend
  app:
    networks:
      - frontend
      - backend
  db:
    networks:
      - backend  # nur app erreicht die DB

Checkliste

  • Images: regelmäßig scannen, minimale Basis, Multi-Stage Builds
  • Laufzeit: kein Root, kein --privileged, no-new-privileges, read-only Filesystem
  • Secrets: nie im Image, immer zur Laufzeit injiziert
  • Netzwerk: explizite Segmentierung statt Default-Netz