Docker Container Security: User, Capabilities, Read-Only-FS und Seccomp
AI generated
Docker Security · Container Hardening · Linux Capabilities · Seccomp
Container Security:
User, Capabilities, Read-Only-FS und Seccomp

Ein Container, der als root läuft, mit allen Linux-Capabilities und beschreibbarem Filesystem, ist technisch gesehen ein privilegierter Prozess mit einem dünnen Namespace-Vorhang. Docker bietet mehrere Sicherheitsschichten, die sich ergänzen: Non-root USER, minimale Capabilities, Read-Only-Filesystem und Seccomp-Profile. Dieser Artikel zeigt, wie diese Schichten funktionieren und wie sie sich in realen Dockerfiles und Compose-Konfigurationen implementieren lassen.

15 Min. Lesezeit Non-root · Capabilities · Read-Only-FS · Seccomp · AppArmor · Docker Bench Docker 25+ · Linux Kernel 5.x+ · CIS Docker Benchmark

1. Das Container-Bedrohungsmodell verstehen

Das Missverständnis über Container Security beginnt oft mit einem falschen mentalen Modell: Container sind keine virtuellen Maschinen. Sie teilen sich den Linux-Kernel mit dem Host und mit anderen Containern auf demselben Host. Eine Schwachstelle im Kernel-Namespace-System, eine fehlerhafte Syscall-Implementierung oder ein Kernel-Exploit kann dazu führen, dass ein angreifender Prozess innerhalb eines Containers auf den Host-Kontext eskaliert. Die Frage ist nicht, ob Container eine Isolation bieten – das tun sie. Die Frage ist, wie stark und gegen welche Angriffsvektoren diese Isolation hält.

Das realistische Angriffsszenario für Web-Container: Eine Anwendung hat eine Remote-Code-Execution-Schwachstelle (RCE). Ein Angreifer schafft es, beliebigen Code innerhalb des Containers auszuführen. Jetzt entscheidet die Container Security-Konfiguration darüber, was dieser Code tun kann: Kann er auf das Dateisystem schreiben? Kann er Netzwerkverbindungen aufbauen? Kann er privilegierte Syscalls ausführen? Kann er auf andere Container auf demselben Host zugreifen? Jede Sicherheitsschicht – Non-root, Read-Only-FS, Capabilities-Drop, Seccomp – beantwortet eine dieser Fragen mit "Nein" und beschränkt damit den Radius eines erfolgreichen Angriffs.

Defense-in-Depth ist das Prinzip hinter den verschiedenen Container-Security-Schichten: Keine Einzelmaßnahme ist absolut sicher, aber mehrere Schichten zusammen erhöhen den Aufwand für einen Angreifer exponentiell. Ein Non-root-Container mit Read-Only-Filesystem, minimierten Capabilities und Seccomp-Profil ist erheblich schwerer zu missbrauchen als ein root-Container mit allen Defaults. Dieser Artikel behandelt jede Schicht einzeln, erklärt ihren Schutzmechanismus und zeigt, wie sie sich in produktiven Dockerfiles und Compose-Konfigurationen implementieren lässt.

2. Non-root USER: die wichtigste Einzelmaßnahme

Standardmäßig laufen Docker-Container als root (UID 0). Das bedeutet: Ein Prozess innerhalb des Containers hat dieselben Rechte wie root auf dem Host, sofern keine anderen Einschränkungen greifen. Für viele Angriffspfade – Escape über Namespace-Schwachstellen, Mountpoint-Exploits – ist der root-Kontext innerhalb des Containers eine Voraussetzung. Die einfachste und wirkungsvollste Container Security-Maßnahme ist deshalb, den Container als nicht-privilegierter Benutzer laufen zu lassen: Die USER-Direktive im Dockerfile legt den Benutzer für alle nachfolgenden RUN-, CMD- und ENTRYPOINT-Befehle fest.

Die praktische Umsetzung erfordert Sorgfalt: Dateien und Verzeichnisse, auf die die Anwendung Zugriff benötigt, müssen dem Application-User gehören. Ports unter 1024 dürfen nur root binden – Container sollten deshalb auf Ports über 1024 (typischerweise 8080 statt 80) hören und der externe Load-Balancer oder Reverse-Proxy übernimmt die Portweiterleitung. Das USER-Konzept wird durch User Namespace Remapping auf Daemon-Ebene ergänzt: Mit aktiviertem User Namespace Remapping wird root im Container auf einen nicht-privilegierten Host-UID gemappt, was einen weiteren Schutzring gegen Container-Escape-Exploits hinzufügt.


# Dockerfile — Non-root user setup for PHP-FPM application
FROM php:8.4-fpm-alpine

# Create application user with specific UID/GID
# Use fixed IDs for predictable ownership in volume mounts
RUN addgroup -g 1001 -S appgroup && \
    adduser  -u 1001 -S appuser -G appgroup

WORKDIR /var/www/html

# Install dependencies as root (they modify system dirs)
RUN --mount=type=cache,id=apk-cache,target=/var/cache/apk \
    apk add --no-cache libzip libpng

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# Copy application — owned by appuser from the start
COPY --chown=appuser:appgroup composer.json composer.lock ./

# Install dependencies as root, but with COMPOSER_CACHE_DIR owned by appuser
RUN --mount=type=cache,id=composer,uid=1001,gid=1001,target=/composer-cache \
    COMPOSER_CACHE_DIR=/composer-cache composer install \
      --no-dev --no-interaction --prefer-dist --no-scripts --optimize-autoloader

COPY --chown=appuser:appgroup . .

# PHP-FPM config: listen as appuser
RUN sed -i 's/www-data/appuser/g' /usr/local/etc/php-fpm.d/www.conf

# Switch to non-root user — all subsequent operations run as appuser
USER appuser

# Health check runs as appuser
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
  CMD php-fpm -t || exit 1

EXPOSE 9000
CMD ["php-fpm"]

3. Linux Capabilities: minimale Rechte für Container

Linux teilt root-Privilegien in granulare Einheiten auf: Linux Capabilities. Docker gibt Containern standardmäßig eine definierte Teilmenge dieser Capabilities, darunter CHOWN, DAC_OVERRIDE, SETUID, NET_BIND_SERVICE und andere. Jede dieser Capabilities ist ein potenzieller Angriffspunkt für Privilege-Escalation. Das Prinzip der minimalen Rechte empfiehlt, alle Capabilities zu entziehen (--cap-drop=ALL) und nur die explizit benötigten wieder hinzuzufügen (--cap-add=NET_BIND_SERVICE). Für die meisten Web-Applikationen werden außer NET_BIND_SERVICE (falls auf Port < 1024 gebunden wird) keine Capabilities benötigt.

In der Praxis ist die schwierigste Frage: Welche Capabilities braucht meine Anwendung tatsächlich? Das lässt sich mit docker run --cap-drop=ALL --cap-add=... schrittweise herausfinden, indem Capabilities einzeln hinzugefügt werden bis die Anwendung funktioniert. Ein schnellerer Weg ist docker run --security-opt=no-new-privileges:true: Dieser Flag verhindert, dass Prozesse im Container neue Privileges über SUID-Binaries oder Syscalls erwerben können – auch wenn die Capabilities noch vorhanden sind. no-new-privileges ist eine sinnvolle Ergänzung zu --cap-drop und schützt vor einer Klasse von Privilege-Escalation-Angriffen, die cap-drop allein nicht abdeckt.

4. Read-Only Filesystem: Schutz vor Laufzeit-Modifikation

Das Read-Only-Filesystem (--read-only) verhindert, dass Prozesse innerhalb des Containers das Dateisystem des Container-Images modifizieren. Das ist ein starker Schutz gegen eine häufige Angriffstechnik: Nach einer initialen RCE-Schwachstelle schreiben Angreifer Malware, Web-Shells oder Backdoors ins Dateisystem, die beim nächsten Request oder Container-Start ausgeführt werden. Mit --read-only ist kein Schreibzugriff möglich – solche Persistenzmechanismen funktionieren nicht.

Das praktische Problem: Viele Anwendungen müssen in temporäre Verzeichnisse schreiben – PHP-Session-Files, Log-Dateien, temporäre Upload-Dateien. Das löst man mit selektiven tmpfs-Mounts auf genau die Verzeichnisse, die beschreibbar sein müssen. Ein tmpfs-Mount ist ein RAM-Dateisystem, das beim Container-Stop verschwindet – es bietet keinen Persistenz-Angriffspunkt für Malware. Mit --tmpfs /tmp:noexec,nosuid,nodev,size=256m wird /tmp beschreibbar, aber Executables können dort nicht ausgeführt werden (noexec). Für PHP-Applikationen typische tmpfs-Mounts: /tmp, /var/lib/php/sessions, /run und applikationsspezifische Cache-Verzeichnisse.


# docker-compose.yml — Full security hardening for a PHP web container
services:
  web:
    image: registry.example.com/app:${APP_VERSION}
    user: "1001:1001"
    read_only: true

    # Grant write access only to specific directories via tmpfs (in-memory, no exec)
    tmpfs:
      - /tmp:mode=1777,noexec,nosuid,nodev,size=128m
      - /var/lib/php/sessions:mode=0700,noexec,nosuid,size=64m
      - /var/run:noexec,nosuid,size=16m

    # Drop ALL capabilities, add back only what is strictly needed
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE   # Only needed if binding port < 1024 (prefer 8080!)

    security_opt:
      - no-new-privileges:true
      - seccomp:./seccomp/app-profile.json    # Custom seccomp profile
      - apparmor:docker-app-profile            # AppArmor profile (if available)

    # Prevent container from running as privileged
    privileged: false

    # Limit resources to reduce DoS blast radius
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 512M
        reservations:
          memory: 128M

    # Health check verifies actual application health
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

5. Seccomp-Profile: Syscall-Filterung für Container

Seccomp (Secure Computing Mode) ist ein Linux-Kernel-Feature, das Syscalls von Prozessen auf eine erlaubte Whitelist beschränkt. Docker wendet standardmäßig ein Seccomp-Profil an, das bereits ~44 gefährliche Syscalls blockiert – darunter ptrace, perf_event_open und einige Namespace-Syscalls, die für Container-Escape-Exploits genutzt werden können. Für maximale Härtung kann ein anwendungsspezifisches Seccomp-Profil erstellt werden, das nur die tatsächlich benötigten Syscalls erlaubt.

Das Erstellen eines minimalen Seccomp-Profils für eine Anwendung ist aufwändig, aber möglich: Mit strace -f -e trace=all ... 2>&1 | grep syscall oder dem audit-Modus von Seccomp lässt sich beobachten, welche Syscalls eine Anwendung unter realen Bedingungen tatsächlich nutzt. Diese Liste wird dann zur Whitelist des Seccomp-Profils. Das Docker-Standard-Profil ist ein guter Startpunkt; der Unterschied liegt im spezifischen Blockieren von Syscalls, die für eine PHP-Anwendung nie benötigt werden (mount, swapon, reboot, alle Namespace-Syscalls). Das Format des Seccomp-Profils ist JSON und wird via --security-opt seccomp=profil.json angewendet.

6. AppArmor und LSM: Mandatory Access Control

AppArmor ist ein Linux Security Module (LSM), das Mandatory Access Control (MAC) auf Pfadbasis implementiert. Ein AppArmor-Profil für einen Container definiert, auf welche Dateipfade der Container lesend und schreibend zugreifen darf, welche Netzwerkoperationen erlaubt sind und welche Capabilities genutzt werden können. Docker wendet auf Ubuntu und Debian-basierten Hosts standardmäßig das Profil docker-default an, das einen soliden Baseline-Schutz bietet.

Für kritische Container ist ein applikationsspezifisches AppArmor-Profil sinnvoll. Es wird auf dem Host installiert (apparmor_parser -r -W /etc/apparmor.d/docker-app-profile) und dem Container via --security-opt apparmor=docker-app-profile zugewiesen. Die Kombination aus Non-root-User, Capabilities-Drop, Read-Only-Filesystem, Seccomp und AppArmor bildet eine Defense-in-Depth-Architektur, bei der jede Schicht einen anderen Angriffsvektor abdeckt und der Ausfall einer Schicht nicht zum vollständigen Kompromittierung führt.

7. Security-Konfiguration in Docker Compose

Docker Compose übersetzt Security-Konfigurationen direkt in Docker-Run-Parameter. Die wichtigsten Felder: user für den Container-User, read_only: true für das Read-Only-Filesystem, cap_drop und cap_add für Capabilities, security_opt für Seccomp-Profile und AppArmor, privileged: false (explizit setzen um Defaults zu dokumentieren) und tmpfs für schreibbare In-Memory-Verzeichnisse. Diese Konfigurationen gelten pro Service und können in einer Base-Compose-Datei als Anker-Vorlage definiert werden, die andere Services via extends einbinden.

Ein häufiger Fehler: privileged: true für debugging-Zwecke gesetzt und dann vergessen. In der Produktion bedeutet privileged: true praktisch komplette Abschaltung aller Container Security-Schichten – der Container hat vollen Zugriff auf den Host. Ein automatischer Lint-Schritt in der CI-Pipeline, der Compose-Dateien auf privileged: true prüft, verhindert dieses Versehen. docker compose config gibt die resolvte Konfiguration aus und zeigt, welche Security-Parameter tatsächlich aktiv sind, inklusive der Defaults. Das ist ein wertvolles Debugging-Tool für Container Security-Audits.

8. Security-Audit: Docker Bench und Trivy

Docker Bench for Security ist ein Open-Source-Script, das Docker-Installationen und Container-Konfigurationen gegen den CIS Docker Benchmark prüft. Es prüft Host-Konfiguration, Docker-Daemon-Einstellungen, Image-Sicherheit und Container-Runtime-Konfiguration. Ein automatischer Run von Docker Bench in der CI-Pipeline nach dem Build identifiziert Konfigurationsprobleme, bevor ein Image in die Produktion kommt. Die Ausgabe ist strukturiert: PASS, WARN und INFO-Einträge mit Referenzen auf den CIS Benchmark und konkreten Empfehlungen.

Trivy ergänzt Docker Bench durch Vulnerability-Scanning: Es prüft das Image gegen bekannte CVEs in Basis-Image-Paketen, Applikations-Dependencies (composer.lock, package-lock.json) und Konfigurationsdateien. Trivy kann direkt in CI-Pipelines integriert werden und schlägt bei kritischen CVEs fehl, bevor das Image deployed wird. Die Kombination aus statischem Image-Scan (Trivy) und Konfigurationsaudit (Docker Bench) deckt die wichtigsten Container Security-Aspekte automatisiert ab und macht Security zu einem Schritt in der normalen Deployment-Pipeline, nicht zu einer manuellen Prüfung.


# ci-security-check.sh — Automated container security validation in CI pipeline
set -euo pipefail

IMAGE="${1:?Image name required}"
FAIL_ON_SEVERITY="${2:-CRITICAL}"

echo "=== Trivy: Vulnerability scan ==="
trivy image \
  --exit-code 1 \
  --severity "${FAIL_ON_SEVERITY}" \
  --no-progress \
  --format table \
  "${IMAGE}"

echo "=== Trivy: Dockerfile misconfiguration check ==="
trivy config \
  --exit-code 1 \
  --severity HIGH,CRITICAL \
  --no-progress \
  Dockerfile

echo "=== Non-root user check ==="
USER=$(docker inspect "${IMAGE}" --format '{ {.Config.User} }')
if [ -z "$USER" ] || [ "$USER" = "root" ] || [ "$USER" = "0" ]; then
  echo "[FAIL] Container runs as root — set USER in Dockerfile"
  exit 1
fi
echo "[PASS] Container user: ${USER}"

echo "=== Docker Bench (subset): image checks ==="
docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  docker/docker-bench-security \
  -c container_images 2>/dev/null \
  | grep -E "^\[WARN\]|^\[PASS\]|^\[FAIL\]"

echo "=== All security checks passed ==="

9. Sicherheitsschichten im Vergleich

Die verschiedenen Container Security-Maßnahmen adressieren unterschiedliche Angriffsvektoren und haben unterschiedliche Implementierungsaufwände. Keine Einzelmaßnahme ist ausreichend.

Sicherheitsschicht Angriffsvektor Aufwand Wirkung
Non-root USER Privilege Escalation, Kernel Exploits Gering Hoch
Read-Only Filesystem Malware-Persistenz, Web-Shells Gering (+ tmpfs) Hoch
Capabilities Drop Privilege Escalation via Syscalls Mittel (testen nötig) Hoch
no-new-privileges SUID-Exploits, Privilege Escalation Minimal Mittel
Seccomp-Profil Kernel-Exploits via Syscalls Hoch (Profil erstellen) Sehr hoch

Für die meisten Produktions-Container ist der praktische Empfehlungs-Stack: Non-root USER (immer), Read-Only-Filesystem mit tmpfs für Write-Dirs (fast immer), --cap-drop=ALL mit minimalen --cap-add (für alle neuen Services), no-new-privileges (immer, ohne Kosten), Docker-Standard-Seccomp (automatisch aktiv, manuell verifizieren). Custom Seccomp-Profile lohnen sich für besonders kritische Services. Der Security-Stack ist kein Einmalaufwand – er muss Teil des normalen Dockerfile-Reviews und der CI-Pipeline sein.

Mironsoft

Container Security, Härtungs-Audits und Security-Pipeline-Integration

Container-Infrastruktur systematisch härten?

Wir analysieren eure Container-Konfigurationen gegen den CIS Benchmark, implementieren Non-root, Read-Only-FS, Capabilities-Drop und Seccomp-Profile und integrieren Trivy und Docker Bench in eure CI/CD-Pipeline.

Security-Audit

Dockerfile- und Compose-Konfigurationen gegen CIS Docker Benchmark prüfen

Härtungsimplementierung

Non-root, Read-Only-FS, Capabilities-Drop und Seccomp für Produktions-Container

CI-Security-Pipeline

Trivy-Scanning und Docker-Bench-Checks automatisch in jede Build-Pipeline integrieren

10. Zusammenfassung

Container Security ist keine Einzelmaßnahme, sondern ein Defense-in-Depth-Stack. Non-root USER verhindert, dass ein angreifender Prozess root-Rechte hat. Read-Only-Filesystem mit tmpfs für Write-Verzeichnisse verhindert Malware-Persistenz. Capabilities-Drop mit --cap-drop=ALL und minimalen --cap-add beschränkt privilegierte Operationen. no-new-privileges verhindert SUID-basierte Privilege-Escalation. Das Docker-Standard-Seccomp-Profil blockiert gefährliche Kernel-Syscalls. Diese Schichten zusammen reduzieren den Radius eines erfolgreichen Angriffs drastisch.

Die wichtigste praktische Erkenntnis: Die meisten dieser Maßnahmen sind keine Performance-Kosten und keine Entwicklungshindernisse. Non-root, no-new-privileges und das Standard-Seccomp-Profil sind faktisch kostenlos. Read-Only-FS mit tmpfs erfordert ein paar Zeilen Konfiguration. Capabilities-Drop erfordert Tests. Der Gesamtaufwand für ein solide gehärtetes Container-Setup ist ein bis zwei Tage Implementierungsarbeit – die Sicherheitsverbesserung ist dauerhaft und wirkt für jeden folgenden Deployment-Zyklus.

Container Security — Das Wichtigste auf einen Blick

Non-root USER

USER 1001:1001 im Dockerfile. Alle Anwendungsdateien mit --chown übergeben. Ports über 1024 nutzen — kein NET_BIND_SERVICE benötigt.

Read-Only Filesystem

--read-only + tmpfs für /tmp und /run mit noexec,nosuid. Verhindert Malware-Persistenz nach RCE-Exploits.

Capabilities

--cap-drop=ALL, dann nur explizit benötigte --cap-add. no-new-privileges:true immer setzen — verhindert SUID-Exploits.

Seccomp und Audit

Docker-Standard-Seccomp aktiv lassen oder Custom-Profil für kritische Services. Trivy + Docker Bench in CI-Pipeline integrieren.

11. FAQ: Docker Container Security

1Warum ist root im Container ein Risiko?
Container teilen den Kernel mit dem Host. Root-Prozesse haben bei Kernel-Exploits mehr Spielraum für Privilege-Escalation auf den Host.
2Warum Linux Capabilities reduzieren?
--cap-drop=ALL minimiert Angriffspunkte für Privilege-Escalation. Nur explizit benötigte Capabilities via --cap-add zurückgeben.
3--read-only und Anwendung schreibt in /tmp?
tmpfs-Mount auf /tmp:noexec,nosuid kombinieren — RAM-Dateisystem, beschreibbar, kein Persistenz-Angriffsvektor, verschwindet beim Stop.
4Was macht no-new-privileges?
Verhindert neue Privileges via SUID-Binaries oder execve(). Schützt vor Privilege-Escalation-Klasse auch wenn Capabilities noch vorhanden sind.
5Seccomp vs. AppArmor?
Seccomp: Syscall-Filterung (Kernel-Ebene). AppArmor: Pfad-basiertes Access Control (Datei, Netzwerk). Ergänzen sich für verschiedene Angriffsvektoren.
6Welche Capabilities braucht meine App?
Mit --cap-drop=ALL starten, Fehler beobachten. Oder strace/Seccomp-Audit-Modus für detailliertes Syscall-Logging nutzen.
7Was prüft Docker Bench for Security?
Host-Konfiguration, Docker-Daemon, Images und Container-Runtime gegen CIS Docker Benchmark. PASS/WARN/FAIL mit konkreten Empfehlungen.
8Was scannt Trivy?
CVEs in OS-Paketen, Application-Dependencies (composer.lock, package-lock.json) und Dockerfile-Misconfigurations. CI-Integration mit Build-Fail bei kritischen CVEs.
9Was ist privileged: true und warum gefährlich?
Deaktiviert alle Security-Beschränkungen — Seccomp, AppArmor, Capabilities. Praktisch root-Zugriff auf den Host. Niemals für Anwendungs-Container.
10Performance-Overhead von Security Hardening?
Praktisch null für Non-root, no-new-privileges, Seccomp und Capabilities-Drop. Aufwand liegt nur in initialer Implementierung und Tests.