Einleitung: Warum Container-Sicherheit 2026 über Erfolg oder Desaster entscheidet
Container haben die Art und Weise, wie wir Software entwickeln und betreiben, grundlegend verändert. Docker, Podman, Kubernetes — ohne diese Technologien geht in modernen IT-Infrastrukturen eigentlich nichts mehr. Aber mit der massiven Verbreitung wächst eben auch die Angriffsfläche. Und die Zahlen sind, ganz ehrlich, alarmierend: Laut dem Cloud Native Security Report 2024 weisen 87 Prozent aller produktiven Container-Images mindestens eine schwerwiegende Schwachstelle auf. Seit Anfang 2025 haben Angriffe auf Build-Systeme, ungeprüfte Registry-Pulls und fehlkonfigurierte Kubernetes-Deployments massiv zugenommen.
Im August 2025 sorgte CVE-2025-9074 für Schlagzeilen — eine kritische Container-Escape-Schwachstelle in Docker Desktop mit einem CVSS-Score von 9,3. Ein einfacher Web-Request aus einem Container heraus genügte, um vollständige Kontrolle über den Host zu erlangen. Und das war kein Einzelfall. Auch in runC, der Low-Level-Container-Runtime hinter Docker und Containerd, wurden 2025 mehrere Escape-Schwachstellen entdeckt (CVE-2025-52565, CVE-2025-31133).
Die Botschaft ist klar: Container-Isolation ist kein absoluter Schutz. Sie ist eine Schicht — eine wichtige, aber eben nur eine — in einem mehrschichtigen Verteidigungskonzept, das sorgfältig aufgebaut und kontinuierlich gepflegt werden muss.
In diesem Leitfaden gehen wir systematisch durch alle Ebenen der Container-Sicherheit unter Linux. Von den Kernel-Grundlagen über Image-Härtung und Schwachstellen-Scanning bis hin zum Laufzeitschutz mit Falco. Jeder Abschnitt enthält sofort einsetzbare Konfigurationen und Befehle. Also, los geht's.
Linux-Kernel-Grundlagen: Die Sicherheitsprimitive hinter Containern
Bevor wir uns mit Docker und Podman beschäftigen, müssen wir kurz verstehen, worauf Container-Sicherheit eigentlich aufbaut. Container sind keine virtuellen Maschinen — sie teilen sich den Host-Kernel. Das wird gerne vergessen. Die Isolation entsteht durch mehrere Kernel-Mechanismen, die zusammenspielen müssen.
Namespaces: Isolation der Sichtbarkeit
Linux-Namespaces partitionieren Kernel-Ressourcen, sodass ein Container nur seine eigene isolierte Sicht auf das System hat. Die wichtigsten Namespaces für Container-Sicherheit sind:
- PID-Namespace: Isoliert die Prozess-IDs. Prozesse in einem Container sehen nur ihre eigenen Prozesse — der Container-Hauptprozess hat PID 1.
- Network-Namespace: Jeder Container bekommt seinen eigenen Netzwerk-Stack mit eigenen Interfaces, Routing-Tabellen und Firewall-Regeln.
- Mount-Namespace: Isoliert das Dateisystem. Ein Container sieht nur seine eigenen Mount-Points.
- User-Namespace: Ermöglicht das Mapping von Container-UIDs auf Host-UIDs. Der Root-Benutzer im Container kann auf dem Host einem unprivilegierten Benutzer zugeordnet werden — ein entscheidender Sicherheitsgewinn.
- IPC-Namespace: Isoliert Inter-Process-Communication-Ressourcen wie Shared Memory und POSIX Message Queues.
- UTS-Namespace: Erlaubt jedem Container einen eigenen Hostnamen (klingt trivial, ist aber für bestimmte Anwendungen relevant).
# Namespaces eines laufenden Containers anzeigen
CONTAINER_PID=$(docker inspect --format '{{.State.Pid}}' mein_container)
ls -la /proc/$CONTAINER_PID/ns/
# Ausgabe zeigt die einzelnen Namespace-Verknüpfungen:
# lrwxrwxrwx 1 root root 0 ... cgroup -> 'cgroup:[4026531835]'
# lrwxrwxrwx 1 root root 0 ... ipc -> 'ipc:[4026532456]'
# lrwxrwxrwx 1 root root 0 ... mnt -> 'mnt:[4026532454]'
# lrwxrwxrwx 1 root root 0 ... net -> 'net:[4026532459]'
# lrwxrwxrwx 1 root root 0 ... pid -> 'pid:[4026532457]'
# lrwxrwxrwx 1 root root 0 ... user -> 'user:[4026531837]'
# User-Namespace-Mapping prüfen
cat /proc/$CONTAINER_PID/uid_map
cat /proc/$CONTAINER_PID/gid_map
cgroups v2: Ressourcenlimitierung
Control Groups (cgroups) begrenzen, wie viele Ressourcen ein Container verbrauchen darf — CPU, Speicher, I/O und Prozessanzahl. Seit 2025 ist cgroups v2 auf allen gängigen Enterprise-Distributionen der Standard, und das ist auch gut so. Die Vorteile gegenüber v1 sind erheblich:
- Einheitliche Hierarchie: Alle Ressourcen-Controller werden in einer einzigen Baumstruktur verwaltet, statt in separaten Hierarchien pro Controller.
- Rootless-Delegation: Ein unprivilegierter Benutzer kann eine Unterhierarchie der cgroup-Struktur kontrollieren, wenn systemd die Delegation übernimmt. Rootless-Container können damit echte Ressourcen-Isolation durchsetzen — ganz ohne Root-Rechte.
- Pressure Stall Information (PSI): Liefert Echtzeitdaten über Ressourcenengpässe in CPU, Speicher und I/O.
# Prüfen ob cgroups v2 aktiv ist
mount | grep cgroup2
# Erwartete Ausgabe: cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)
# Container mit Ressourcenlimits starten
docker run -d --name webapp \
--memory=512m \
--memory-swap=512m \
--cpus=1.5 \
--pids-limit=256 \
--read-only \
nginx:alpine
# Ressourcenverbrauch überwachen
cat /sys/fs/cgroup/system.slice/docker-$(docker inspect --format '{{.Id}}' webapp).scope/memory.current
cat /sys/fs/cgroup/system.slice/docker-$(docker inspect --format '{{.Id}}' webapp).scope/cpu.stat
Linux Capabilities: Feingranulare Rechte statt Root
Traditionell hatte ein Prozess entweder volle Root-Rechte oder gar keine. Alles oder nichts — nicht gerade ideal. Linux Capabilities zerlegen Root-Privilegien in einzelne, granulare Berechtigungen. Für Container heißt das konkret: Anstatt einem Container vollen Root-Zugang zu geben, kann man gezielt nur die Capabilities zuweisen, die er tatsächlich braucht.
# Standard-Capabilities eines Docker-Containers anzeigen
docker run --rm alpine sh -c 'cat /proc/1/status | grep Cap'
# Alle Capabilities entfernen und nur benötigte hinzufügen
docker run --rm \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
nginx:alpine
# Capabilities eines laufenden Containers prüfen
docker inspect --format '{{.HostConfig.CapAdd}}' mein_container
docker inspect --format '{{.HostConfig.CapDrop}}' mein_container
Goldene Regel: Immer --cap-drop=ALL setzen und dann nur die tatsächlich benötigten Capabilities einzeln hinzufügen. Und niemals — wirklich niemals — --privileged in Produktion verwenden. Das gibt dem Container alle 40+ Capabilities und deaktiviert praktisch sämtliche Isolationsmechanismen.
Docker vs. Podman: Sicherheitsarchitektur im Vergleich
Docker und Podman sind die beiden wichtigsten Container-Runtimes unter Linux. Ihre Sicherheitsarchitekturen unterscheiden sich aber fundamental — und das hat direkte Auswirkungen auf die Angriffsfläche.
Docker: Der Daemon als zentrales Risiko
Docker arbeitet mit einem zentralen Daemon (dockerd), der als Root-Prozess läuft. Jede Interaktion mit Containern — Erstellen, Starten, Stoppen — läuft über diesen Daemon. Das hat eine kritische Konsequenz: Wer Zugriff auf den Docker-Socket (/var/run/docker.sock) hat, hat effektiv Root-Zugang zum gesamten Host.
Ich habe das in der Praxis schon öfter gesehen, als mir lieb ist. Die Schwachstelle CVE-2025-9074 hat genau dieses Problem illustriert: Container konnten die Docker Engine API direkt über eine interne IP-Adresse erreichen — ganz ohne Authentifizierung. Ein einziger API-Call genügte, um das Host-Dateisystem in einen Container zu mounten und beliebigen Code mit Host-Rechten auszuführen.
Podman: Daemonlos und rootless by Design
Podman verfolgt einen grundlegend anderen Ansatz. Es gibt keinen zentralen Daemon — Podman nutzt das Fork-Exec-Modell, bei dem jeder Container-Prozess direkt vom aufrufenden Benutzer gestartet wird. Die Sicherheitsvorteile sind erheblich:
- Kein Single Point of Failure: Ein kompromittierter Container kann nicht über einen zentralen Daemon andere Container oder den Host angreifen.
- Rootless by Default: Container laufen standardmäßig als unprivilegierter Benutzer, mit User-Namespaces für UID/GID-Mapping.
- Nahtlose SELinux-Integration: Podman wurde für die Zusammenarbeit mit SELinux entwickelt und wendet automatisch korrekte Kontexte auf Container-Dateisysteme an.
- Keine Socket-Exposition: Es gibt schlicht keinen Docker-Socket-äquivalenten Angriffspunkt.
# Podman rootless einrichten (als normaler Benutzer)
# Subuid/Subgid-Bereiche prüfen
grep $USER /etc/subuid /etc/subgid
# Falls keine Einträge vorhanden:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER
# Rootless-Container starten
podman run -d --name webapp \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid \
--security-opt no-new-privileges:true \
docker.io/library/nginx:alpine
# Überprüfen, dass der Container tatsächlich rootless läuft
podman inspect webapp --format '{{.HostConfig.Privileged}}'
# Ausgabe: false
# UID-Mapping anzeigen
podman top webapp user huser
# Zeigt: root im Container = unprivilegierter User auf dem Host
Vergleichsübersicht
Um das Ganze etwas übersichtlicher zu machen, hier die wichtigsten Unterschiede auf einen Blick:
| Sicherheitsmerkmal | Docker | Podman |
|---|---|---|
| Daemonlose Architektur | Nein (dockerd als Root) | Ja (Fork-Exec) |
| Rootless by Default | Nein (manuell konfigurierbar) | Ja |
| SELinux-Integration | Manuell | Nativ eingebaut |
| Seccomp-Unterstützung | Ja | Ja |
| CIS-Benchmark-Tools | Docker Bench | Podman Security Bench |
| Single Point of Failure | Ja (Docker-Daemon) | Nein |
Container-Images härten: Von der Basis bis zum Build
Die Sicherheit eines Containers beginnt lange bevor er überhaupt gestartet wird — sie beginnt beim Image. Ein schlecht gebautes Image ist wie ein Haus mit offenen Fenstern: egal wie gut das Schloss an der Tür ist, der Einbrecher kommt trotzdem rein.
Minimale Base-Images verwenden
Jedes installierte Paket im Base-Image ist eine potenzielle Angriffsfläche. Die Faustregel ist simpel: Je weniger im Image steckt, desto weniger kann ausgenutzt werden.
# Schlecht: Volles Ubuntu-Image (78 MB, hunderte Pakete)
FROM ubuntu:24.04
# Besser: Alpine-basiert (7 MB, minimale Pakete)
FROM alpine:3.21
# Am besten: Distroless (nur die Anwendung, kein OS)
FROM gcr.io/distroless/static-debian12
# Oder: Chainguard-Images (regelmäßig aktualisiert, SBOM inklusive)
FROM cgr.dev/chainguard/python:latest
Multi-Stage Builds für minimale Angriffsfläche
Multi-Stage Builds sind meiner Meinung nach eines der besten Features moderner Dockerfiles. Sie trennen die Build-Umgebung sauber von der Runtime-Umgebung. Build-Tools, Compiler und Entwicklungsabhängigkeiten landen nicht im finalen Image — und das reduziert die Angriffsfläche enorm.
# Dockerfile mit Multi-Stage Build
# Stage 1: Build
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server
# Stage 2: Runtime (nur die kompilierte Binary)
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]
Sicherheitsrichtlinien im Dockerfile
# Nie als Root laufen
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Keine Secrets im Image
# FALSCH:
# ENV DB_PASSWORD=geheim123
# RICHTIG: Secrets zur Laufzeit über Mounts oder Env-Variablen injizieren
# .dockerignore pflegen — verhindert, dass sensible Dateien ins Image gelangen
# Inhalt von .dockerignore:
# .env
# .git
# *.key
# *.pem
# credentials.json
# Healthchecks definieren
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:8080/health || exit 1
Schwachstellen-Scanning mit Trivy und Grype
Ein gehärtetes Dockerfile schützt nicht vor bekannten Schwachstellen in den verwendeten Paketen. Dafür braucht man automatisierte Vulnerability-Scanner — und zwar am besten welche, die in die CI/CD-Pipeline integriert sind. Die beiden führenden Open-Source-Tools sind Trivy (von Aqua Security) und Grype (von Anchore).
Trivy: Das Schweizer Taschenmesser
Trivy ist ein All-in-One-Scanner, der Schwachstellen, IaC-Fehlkonfigurationen, eingebettete Secrets und Lizenzverstöße erkennt. Die Datenbank wird täglich aktualisiert und umfasst NVD, OS-spezifische Advisories und sprachspezifische Datenbanken. Ehrlich gesagt, es gibt kaum einen Grund, Trivy nicht einzusetzen.
# Trivy installieren
# Debian/Ubuntu
sudo apt-get install -y wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] 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
# Container-Image scannen
trivy image nginx:alpine
# Nur kritische und hohe Schwachstellen anzeigen
trivy image --severity CRITICAL,HIGH nginx:alpine
# SBOM (Software Bill of Materials) generieren
trivy image --format spdx-json --output nginx-sbom.json nginx:alpine
# Dockerfile auf Fehlkonfigurationen prüfen
trivy config ./Dockerfile
# Filesystem eines laufenden Containers scannen
trivy fs --security-checks vuln,secret /path/to/project
# In CI/CD: Exit-Code bei kritischen Schwachstellen
trivy image --exit-code 1 --severity CRITICAL mein-image:latest
Grype: Tiefe Schwachstellenanalyse mit Risikobewertung
Grype konzentriert sich ausschließlich auf Vulnerability-Scanning, bietet dafür aber eine richtig gute Risikobewertung. Jeder Fund erhält einen zusammengesetzten Score aus CVSS-Schweregrad, EPSS-Ausnutzungswahrscheinlichkeit und KEV-Katalogstatus (Known Exploited Vulnerabilities der CISA). Das hilft enorm bei der Priorisierung — denn nicht jede CVE verdient sofortige Panik.
# Grype installieren
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Image scannen
grype nginx:alpine
# Nur fixbare Schwachstellen anzeigen
grype nginx:alpine --only-fixed
# SBOM-basierter Workflow (empfohlen für CI/CD)
# Schritt 1: SBOM mit Syft generieren
syft nginx:alpine -o spdx-json > nginx-sbom.json
# Schritt 2: SBOM mit Grype scannen (schneller als erneutes Image-Scanning)
grype sbom:nginx-sbom.json
# Ausgabeformat für SARIF (GitHub/GitLab Security Tab)
grype nginx:alpine -o sarif > results.sarif
Trivy und Grype kombinieren
Die beste Strategie? Trivy für die Breite, Grype für die Tiefe. Trivy deckt mit seinem Multi-Scanner-Ansatz Schwachstellen, Secrets und IaC-Fehlkonfigurationen ab. Grype ergänzt mit seiner SBOM-first-Architektur und der EPSS-basierten Priorisierung. So wissen Sie nicht nur, welche Schwachstellen existieren, sondern auch welche davon tatsächlich aktiv ausgenutzt werden. In der Praxis hat sich diese Kombination bei mir bewährt.
Seccomp-Profile: System-Calls unter Kontrolle
Seccomp (Secure Computing Mode) ist ein Kernel-Mechanismus, der filtert, welche System-Calls ein Prozess ausführen darf. Da Container den Host-Kernel teilen, ist die Einschränkung der verfügbaren Syscalls eine der wirkungsvollsten Härtungsmaßnahmen überhaupt.
Das Standard-Seccomp-Profil
Docker und Podman liefern ein Standard-Seccomp-Profil mit, das bereits rund 44 von über 300 System-Calls blockiert. Das ist ein brauchbarer Ausgangspunkt — aber für sicherheitskritische Anwendungen reicht es bei Weitem nicht aus.
Eigenes Seccomp-Profil erstellen
Der Ansatz ist im Grunde einfach: Zuerst beobachten, welche Syscalls die Anwendung tatsächlich verwendet, dann ein maßgeschneidertes Profil erstellen. Klingt nach Arbeit? Ist es auch, aber der Sicherheitsgewinn ist den Aufwand definitiv wert.
# Schritt 1: Syscalls der Anwendung mit strace aufzeichnen
strace -c -f -S name docker run --rm nginx:alpine sh -c 'nginx -t' 2>&1 | tail -30
# Schritt 2: Alternativ mit OCI Seccomp BPF Hook die Syscalls aufzeichnen
# Installation: https://github.com/containers/oci-seccomp-bpf-hook
sudo dnf install oci-seccomp-bpf-hook # Fedora/RHEL
# Container mit Aufzeichnung starten
podman run --annotation io.containers.trace-syscall="of:/tmp/nginx-seccomp.json" \
nginx:alpine sh -c 'nginx && sleep 5'
# Schritt 3: Generiertes Profil anpassen
cat /tmp/nginx-seccomp.json
Ein angepasstes Seccomp-Profil für einen Nginx-Container könnte so aussehen:
{
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_AARCH64"
],
"syscalls": [
{
"names": [
"accept4", "access", "arch_prctl", "bind", "brk",
"clone", "close", "connect", "dup2", "epoll_create1",
"epoll_ctl", "epoll_wait", "eventfd2", "execve",
"exit", "exit_group", "fchown", "fcntl", "fstat",
"futex", "getdents64", "getpid", "getuid", "ioctl",
"listen", "lseek", "mmap", "mprotect", "munmap",
"nanosleep", "newfstatat", "openat", "pipe2",
"pread64", "read", "recvfrom", "recvmsg", "rt_sigaction",
"rt_sigprocmask", "rt_sigreturn", "sendfile", "sendmsg",
"set_robust_list", "set_tid_address", "setgid", "setgroups",
"setuid", "socket", "socketpair", "uname", "write",
"writev"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
# Profil anwenden
docker run -d \
--security-opt seccomp=/etc/docker/seccomp/nginx-strict.json \
--name webapp \
nginx:alpine
# Mit Podman
podman run -d \
--security-opt seccomp=/etc/containers/seccomp/nginx-strict.json \
--name webapp \
docker.io/library/nginx:alpine
SELinux und AppArmor für Container
Während Seccomp einzelne System-Calls filtert, bieten SELinux und AppArmor eine umfassendere Zugriffskontrolle auf Dateien, Netzwerk und IPC-Ressourcen. Welches der beiden zum Einsatz kommt, hängt von der Distribution ab: RHEL, Fedora und CentOS setzen auf SELinux, Ubuntu und SUSE auf AppArmor. Beides hat seine Berechtigung — aber man sollte auf jeden Fall eines davon aktiv nutzen.
SELinux mit Podman
Podman integriert SELinux nativ. Jeder Container erhält automatisch den SELinux-Typ container_t, und Volume-Mounts werden mit der Option :Z (private Label) oder :z (shared Label) korrekt gelabelt. Das klingt nach einem Detail, spart in der Praxis aber enorm viel Ärger.
# SELinux-Status prüfen
getenforce
# Erwartete Ausgabe: Enforcing
# SELinux-Kontext eines Containers anzeigen
podman run --rm alpine cat /proc/1/attr/current
# Ausgabe: system_u:system_r:container_t:s0:c123,c456
# Volume mit korrektem SELinux-Label mounten
podman run -d \
-v /srv/webapp/data:/data:Z \
--name webapp \
nginx:alpine
# SELinux-Label des gemounteten Verzeichnisses prüfen
ls -laZ /srv/webapp/data/
# Ausgabe zeigt: system_u:object_r:container_file_t:s0:c123,c456
# Eigene SELinux-Policy für einen Container erstellen
# Schritt 1: Audit-Log im permissiven Modus sammeln
sudo semanage permissive -a container_t
# Container normal betreiben, dann:
sudo audit2allow -a -M mein_container_policy
sudo semodule -i mein_container_policy.pp
sudo semanage permissive -d container_t
AppArmor mit Docker
# Standard-AppArmor-Profil für Docker prüfen
sudo aa-status | grep docker
# Eigenes AppArmor-Profil für einen Container erstellen
cat > /etc/apparmor.d/docker-nginx << 'EOF'
#include
profile docker-nginx flags=(attach_disconnected,mediate_deleted) {
#include
#include
# Netzwerkzugriff erlauben
network inet tcp,
network inet udp,
network inet6 tcp,
# Nginx-spezifische Pfade
/usr/sbin/nginx mr,
/etc/nginx/** r,
/var/log/nginx/** rw,
/var/cache/nginx/** rw,
/run/nginx.pid rw,
# Dateisystemzugriff einschränken
deny /etc/shadow r,
deny /etc/passwd w,
deny /proc/*/mem rw,
deny /sys/firmware/** r,
}
EOF
# Profil laden
sudo apparmor_parser -r /etc/apparmor.d/docker-nginx
# Container mit benutzerdefiniertem Profil starten
docker run -d \
--security-opt apparmor=docker-nginx \
--name webapp \
nginx:alpine
Laufzeitschutz mit Falco: Angriffe in Echtzeit erkennen
Image-Scanning und Seccomp-Profile sind präventive Maßnahmen — und die sind wichtig. Aber was passiert, wenn ein Angreifer trotzdem einen Weg reinfindet? Hier kommt Falco ins Spiel — ein CNCF-graduiertes Open-Source-Tool für Runtime-Security, das verdächtiges Verhalten in Echtzeit erkennt.
Wie Falco funktioniert
Falco beobachtet System-Calls auf Kernel-Ebene und vergleicht sie mit definierbaren Regeln. Das Clevere daran: Anstatt nach bekannten Signaturen zu suchen, erkennt Falco abweichendes Verhalten. Ein Container, der plötzlich /etc/shadow liest, eine Shell startet oder eine Netzwerkverbindung zu einer unbekannten IP aufbaut — das sind alles Warnsignale, die Falco sofort meldet.
Seit den Versionen 0.39 und 0.40 setzt Falco auf den modernen eBPF-Treiber, der sicherer und leistungsfähiger ist als der ältere Kernel-Modul-Ansatz.
Falco installieren und konfigurieren
# Falco auf Debian/Ubuntu installieren
curl -fsSL https://falco.org/repo/falcosecurity-packages.asc | \
sudo gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] https://download.falco.org/packages/deb stable main" | \
sudo tee /etc/apt/sources.list.d/falcosecurity.list
sudo apt-get update && sudo apt-get install -y falco
# Falco auf RHEL/Fedora installieren
sudo rpm --import https://falco.org/repo/falcosecurity-packages.asc
cat << 'EOF' | sudo tee /etc/yum.repos.d/falcosecurity.repo
[falcosecurity]
name=Falco Security Repository
baseurl=https://download.falco.org/packages/rpm/
enabled=1
gpgcheck=1
gpgkey=https://falco.org/repo/falcosecurity-packages.asc
EOF
sudo dnf install -y falco
# Falco mit modernem eBPF-Treiber starten
sudo falco --modern-bpf
# Als systemd-Service aktivieren
sudo systemctl enable --now falco-modern-bpf.service
Benutzerdefinierte Falco-Regeln für Container
Die mitgelieferten Standard-Regeln sind ein guter Anfang. Aber die wahre Stärke von Falco zeigt sich in benutzerdefinierten Regeln, die auf Ihre spezifische Umgebung zugeschnitten sind:
# /etc/falco/rules.d/container-security.yaml
# Shell in Container erkennen
- rule: Shell in Container gestartet
desc: Erkennt, wenn eine Shell in einem Container gestartet wird
condition: >
spawned_process and container
and proc.name in (bash, sh, zsh, dash, ash, csh, ksh, fish)
and not proc.pname in (entrypoint.sh, docker-entrypoint)
output: >
Shell in Container gestartet
(container=%container.name image=%container.image.repository
user=%user.name shell=%proc.name parent=%proc.pname
cmdline=%proc.cmdline)
priority: WARNING
tags: [container, shell]
# Sensitive Dateien lesen
- rule: Container liest sensitive Host-Dateien
desc: Container versucht sensitive Dateien wie /etc/shadow zu lesen
condition: >
open_read and container
and fd.name in (/etc/shadow, /etc/sudoers, /etc/pam.d)
output: >
Sensitiver Dateizugriff in Container
(container=%container.name file=%fd.name image=%container.image.repository
user=%user.name command=%proc.cmdline)
priority: CRITICAL
tags: [container, filesystem, sensitive_data]
# Unerwartete Netzwerkverbindung
- rule: Container stellt ausgehende Verbindung her
desc: Container öffnet eine Verbindung zu einer unbekannten IP
condition: >
outbound and container
and not fd.sip in (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
and not k8s.ns.name = "kube-system"
output: >
Ausgehende Verbindung aus Container
(container=%container.name image=%container.image.repository
connection=%fd.name user=%user.name)
priority: NOTICE
tags: [container, network]
# Crypto-Miner erkennen
- rule: Verdacht auf Crypto-Mining in Container
desc: Erkennt typische Crypto-Mining-Aktivitäten
condition: >
spawned_process and container
and (proc.name in (xmrig, minerd, minergate, cpuminer)
or proc.cmdline contains "stratum+tcp"
or proc.cmdline contains "pool.mining")
output: >
Möglicher Crypto-Miner in Container erkannt
(container=%container.name image=%container.image.repository
process=%proc.name cmdline=%proc.cmdline)
priority: CRITICAL
tags: [container, cryptomining]
Falco Talon: Automatische Reaktion auf Bedrohungen
Erkennung ist gut, automatische Reaktion ist besser. Mit Falco Talon können auf erkannte Bedrohungen automatisch Aktionen ausgelöst werden — etwa einen verdächtigen Container stoppen, Netzwerkverbindungen kappen oder Alerts an SIEM-Systeme senden.
# Falco Talon Konfiguration
# /etc/falco-talon/rules.yaml
- action: Container bei kritischem Alert stoppen
match:
priority: CRITICAL
tags:
- container
actions:
- action: kubernetes:terminate
parameters:
grace_period: 5
- action: notification:slack
parameters:
channel: "#security-alerts"
message: "Container gestoppt: {{.Output}}"
DevSecOps: Container-Sicherheit in die CI/CD-Pipeline integrieren
Manuelle Sicherheitschecks skalieren nicht. Das ist keine Meinung, das ist eine Tatsache. Container-Sicherheit muss in die CI/CD-Pipeline integriert werden — automatisiert, konsistent und als Qualitäts-Gate, das den Deployment-Prozess stoppt, wenn kritische Schwachstellen gefunden werden.
GitHub Actions Pipeline mit Trivy und Grype
# .github/workflows/container-security.yml
name: Container Security Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Container-Image bauen
run: docker build -t ${{ github.repository }}:${{ github.sha }} .
- name: Trivy Vulnerability Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ github.repository }}:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: SBOM mit Syft generieren
uses: anchore/sbom-action@v0
with:
image: '${{ github.repository }}:${{ github.sha }}'
format: spdx-json
output-file: sbom.spdx.json
- name: Grype SBOM-Scan
uses: anchore/scan-action@v7
with:
sbom: sbom.spdx.json
fail-build: true
severity-cutoff: critical
- name: Dockerfile Lint mit Hadolint
uses: hadolint/[email protected]
with:
dockerfile: Dockerfile
- name: Ergebnisse in GitHub Security hochladen
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
Image-Signierung mit Cosign
Neben dem Scanning ist die Signierung von Images ein wichtiger Baustein — den viele Teams leider immer noch überspringen. Cosign (ein Sigstore-Projekt) ermöglicht es, Container-Images kryptografisch zu signieren und die Signatur vor dem Deployment zu verifizieren. So stellen Sie sicher, dass nur geprüfte und autorisierte Images in Produktion landen.
# Cosign installieren
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
# Schlüsselpaar generieren
cosign generate-key-pair
# Image signieren
cosign sign --key cosign.key registry.example.com/webapp:v1.2.3
# Signatur verifizieren (z.B. im Deployment-Skript)
cosign verify --key cosign.pub registry.example.com/webapp:v1.2.3
# In Kubernetes: Admission Controller für Signatur-Verifizierung
# Kyverno-Policy:
# apiVersion: kyverno.io/v1
# kind: ClusterPolicy
# metadata:
# name: verify-image-signature
# spec:
# validationFailureAction: Enforce
# rules:
# - name: verify-cosign
# match:
# resources:
# kinds: [Pod]
# verifyImages:
# - imageReferences: ["registry.example.com/*"]
# attestors:
# - entries:
# - keys:
# publicKeys: |-
# -----BEGIN PUBLIC KEY-----
# ...
# -----END PUBLIC KEY-----
Praxis-Checkliste: Container-Härtung in 15 Schritten
So, jetzt wird's konkret. Hier eine kompakte Checkliste, die Sie für jedes Container-Deployment durchgehen sollten. Drucken Sie sie sich aus, hängen Sie sie ans Whiteboard — Hauptsache, sie wird benutzt:
- Minimales Base-Image verwenden — Alpine, Distroless oder Chainguard-Images bevorzugen
- Multi-Stage Builds nutzen — Build-Tools gehören nicht ins Runtime-Image
- Nie als Root laufen —
USER-Direktive im Dockerfile, Rootless-Container bei Podman - Alle Capabilities droppen —
--cap-drop=ALLund nur benötigte einzeln hinzufügen - Read-only Dateisystem —
--read-onlyund nur nötige tmpfs-Mounts erlauben - no-new-privileges aktivieren —
--security-opt no-new-privileges:trueverhindert Privilege-Escalation - Ressourcenlimits setzen — Memory, CPU und PID-Limits über cgroups konfigurieren
- Seccomp-Profil anwenden — mindestens das Standard-Profil, idealerweise ein maßgeschneidertes
- SELinux oder AppArmor aktivieren — MAC-Layer für Dateisystem- und Netzwerkzugriff
- Images regelmäßig scannen — Trivy und Grype in die CI/CD-Pipeline integrieren
- SBOM generieren und pflegen — Transparenz über alle Abhängigkeiten schaffen
- Images signieren — Cosign für kryptografische Verifikation einsetzen
- Runtime-Monitoring einrichten — Falco für Echtzeit-Erkennung verdächtigen Verhaltens
- Docker-Socket nicht exponieren — Niemals
/var/run/docker.sockin Container mounten - Regelmäßig aktualisieren — Base-Images, Runtimes und Sicherheitstools stets auf aktuellem Stand halten
Netzwerk-Sicherheit für Container
Container-Netzwerke sind ein oft unterschätzter Angriffsvektor. Standardmäßig können alle Container im selben Docker-Netzwerk miteinander kommunizieren — ohne jede Einschränkung. In einer produktiven Umgebung mit mehreren Microservices ist das ein erhebliches Risiko: Wird ein Container kompromittiert, kann sich der Angreifer lateral durch das gesamte Container-Netzwerk bewegen.
Netzwerk-Segmentierung umsetzen
Die Grundregel ist simpel: Nur Container, die tatsächlich miteinander kommunizieren müssen, sollten im selben Netzwerk sein. Docker und Podman bieten benutzerdefinierte Bridge-Netzwerke, die genau diese Segmentierung ermöglichen.
# Separate Netzwerke für verschiedene Anwendungsschichten erstellen
docker network create --driver bridge \
--subnet 172.20.0.0/24 \
--opt com.docker.network.bridge.enable_icc=false \
frontend-net
docker network create --driver bridge \
--subnet 172.21.0.0/24 \
backend-net
docker network create --driver bridge \
--subnet 172.22.0.0/24 \
--internal \
database-net
# Frontend-Container: Nur im Frontend-Netzwerk
docker run -d --name nginx-proxy \
--network frontend-net \
-p 443:443 \
nginx:alpine
# API-Container: In Frontend UND Backend (vermittelt zwischen beiden)
docker run -d --name api-server \
--network frontend-net \
api:latest
docker network connect backend-net api-server
# Datenbank: Nur im internen Datenbank-Netzwerk (kein externer Zugang)
docker run -d --name postgres \
--network database-net \
-e POSTGRES_PASSWORD_FILE=/run/secrets/db_pass \
postgres:16-alpine
docker network connect backend-net postgres
Beachten Sie die Option --internal beim Datenbank-Netzwerk — damit hat das Netzwerk keinen Zugang zum Internet. Und enable_icc=false im Frontend-Netzwerk deaktiviert die Inter-Container-Kommunikation, sodass Container im selben Netzwerk nicht direkt miteinander sprechen können, sondern nur über explizit veröffentlichte Ports.
DNS-Auflösung einschränken
Container nutzen standardmäßig den DNS-Server des Hosts oder den eingebetteten Docker-DNS. Das klingt harmlos, ermöglicht einem kompromittierten Container aber, interne Hostnames aufzulösen und so die komplette Netzwerktopologie zu kartieren. Für sicherheitskritische Container sollten Sie den DNS-Zugriff einschränken:
# Container mit eingeschränktem DNS starten
docker run -d --name secure-worker \
--dns 127.0.0.1 \
--network backend-net \
--read-only \
worker:latest
# Oder mit Podman: Netzwerkzugriff komplett blockieren
podman run -d --name batch-processor \
--network none \
--read-only \
batch:latest
Firewall-Regeln für Container mit nftables
Docker manipuliert die iptables-Regeln des Hosts, um Port-Mappings und NAT umzusetzen. Das kann bestehende Firewall-Regeln umgehen — ein häufig übersehenes und ziemlich fieses Sicherheitsproblem. Mit nftables können Sie zusätzliche Regeln definieren, die den Container-Verkehr kontrollieren:
# /etc/nftables.d/docker-restrictions.nft
table inet docker_filter {
chain forward {
type filter hook forward priority 10; policy accept;
# Nur bestimmte Ziel-Ports für Container erlauben
iifname "docker0" tcp dport != { 80, 443, 5432 } drop
iifname "docker0" udp dport != { 53 } drop
# Ausgehenden Zugriff auf Metadata-Service blockieren
# (verhindert Cloud-Credential-Diebstahl)
iifname "docker0" ip daddr 169.254.169.254 drop
# Rate-Limiting für ausgehende Verbindungen
iifname "docker0" ct state new limit rate 50/second accept
iifname "docker0" ct state new drop
}
}
# Regeln laden
sudo nft -f /etc/nftables.d/docker-restrictions.nft
Besonders wichtig ist die Blockierung des Cloud-Metadata-Services unter 169.254.169.254. Wenn Container in Cloud-Umgebungen (AWS, GCP, Azure) laufen, kann ein Angreifer über diesen Endpunkt IAM-Credentials und andere sensitive Informationen abgreifen. Das ist eine der häufigsten Techniken bei Cloud-Container-Angriffen — und erschreckend einfach durchzuführen.
Secrets-Management: Zugangsdaten sicher in Container injizieren
Einer der häufigsten Sicherheitsfehler bei Container-Deployments, den ich immer wieder sehe: Zugangsdaten werden als Umgebungsvariablen oder sogar hartcodiert im Image übergeben. Das Problem dabei? Umgebungsvariablen sind in /proc/<PID>/environ lesbar, werden in Logs gedruckt und tauchen in docker inspect auf. Das ist kein sicheres Secrets-Management.
Docker Secrets und Podman Secrets
# Docker Secrets (Swarm-Modus oder Compose)
echo "mein_sicheres_passwort" | docker secret create db_password -
# docker-compose.yml mit Secrets
# version: '3.8'
# services:
# webapp:
# image: webapp:latest
# secrets:
# - db_password
# environment:
# DB_PASSWORD_FILE: /run/secrets/db_password
# secrets:
# db_password:
# external: true
# Podman Secrets
echo "mein_sicheres_passwort" | podman secret create db_password -
# Container mit Secret starten
podman run -d \
--secret db_password,type=mount,target=/run/secrets/db_password \
--name webapp \
webapp:latest
# Secrets werden als temporäre tmpfs-Mounts bereitgestellt
# und erscheinen NICHT in podman inspect oder den Umgebungsvariablen
Externe Secrets-Manager integrieren
Für produktive Umgebungen empfiehlt sich die Integration eines dedizierten Secrets-Managers wie HashiCorp Vault, AWS Secrets Manager oder Azure Key Vault. Damit werden Secrets zur Laufzeit abgerufen, automatisch rotiert und lückenlos auditiert — statt statisch im Container vorzuliegen.
# HashiCorp Vault Agent Sidecar (Kubernetes-Beispiel)
# Vault injiziert Secrets automatisch als Dateien in den Pod
# Pod-Annotation für Vault-Injection:
# vault.hashicorp.com/agent-inject: "true"
# vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/webapp"
# vault.hashicorp.com/role: "webapp"
# Alternativ: Vault CLI im Init-Container
vault kv get -field=password secret/webapp/database > /shared/db_password
CIS-Benchmarks und automatisierte Compliance-Prüfung
Der CIS Docker Benchmark (aktuell Version 1.7.0) definiert über 100 Best Practices für die sichere Docker-Konfiguration. Klingt nach viel? Ist es auch. Aber anstatt diese manuell durchzugehen, können automatisierte Tools die Compliance prüfen und Abweichungen melden.
Docker Bench for Security
# Docker Bench for Security ausführen
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 /usr/lib/systemd:/usr/lib/systemd:ro \
-v /etc:/etc:ro \
docker/docker-bench-security
# Podman Security Bench
git clone https://github.com/containers/podman-security-bench.git
cd podman-security-bench
sudo ./podman-security-bench.sh
# Ausgabe enthält Ergebnisse wie:
# [PASS] 2.1 - Restrict network traffic between containers
# [WARN] 2.2 - Set the logging level to info
# [PASS] 4.1 - Create a user for the container
# [WARN] 5.4 - Do not run containers with --privileged flag
Compliance-Reports für Audits
Für regulierte Branchen (Finanzsektor, Gesundheitswesen, öffentliche Verwaltung) müssen Container-Konfigurationen nachweislich den Sicherheitsstandards entsprechen. Die Kombination aus CIS-Benchmark-Checks, Trivy-Compliance-Scans und Falco-Audit-Logs ergibt ein solides Compliance-Paket:
# Trivy Compliance-Scan gegen NIST SP 800-190
trivy image --compliance nist-800-190 webapp:latest
# Compliance-Report als JSON für automatisierte Verarbeitung
trivy image --compliance docker-cis-1.7.0 --format json --output compliance-report.json webapp:latest
Frameworks wie NIST SP 800-190 (Application Container Security Guide) und der MITRE ATT&CK Container Matrix bieten die strukturierte Grundlage für eine systematische Bewertung der Container-Sicherheit. Die Container-spezifische MITRE ATT&CK Matrix umfasst Taktiken wie Initial Access über verwundbare Container-Images, Execution durch Container-Escape, Persistence über manipulierte Images in der Registry und Lateral Movement innerhalb des Container-Netzwerks.
Fazit: Container-Sicherheit ist eine Daueraufgabe
Container-Sicherheit ist kein Zustand, den man einmal erreicht und dann abhaken kann. Sie ist ein kontinuierlicher Prozess, der sich mit jeder neuen CVE, jedem Runtime-Update und jeder Änderung in der Bedrohungslandschaft weiterentwickeln muss.
Die gute Nachricht: Die Werkzeuge sind da — und sie sind ausgereift. Podman bietet rootless Container out of the box, Trivy und Grype automatisieren das Schwachstellen-Scanning, Falco überwacht die Laufzeit in Echtzeit, und SELinux/AppArmor liefern bewährte Zugriffskontrolle auf Kernel-Ebene.
Die Herausforderung besteht nicht im Mangel an Tools, sondern in der konsequenten Anwendung. In jeder Pipeline, auf jedem Host, für jeden Container. Kein Ausnahmen, keine Abkürzungen.
Wenn Sie die Konzepte und Konfigurationen aus diesem Leitfaden umsetzen, haben Sie eine solide Grundlage, die weit über die Standard-Container-Installation hinausgeht. Und falls Sie die anderen Teile dieser Serie noch nicht gelesen haben — der Leitfaden zur Boot-Chain-Sicherheit und systemd-Härtung sowie der SSH-Härtungsleitfaden ergänzen diesen Artikel zu einem umfassenden Linux-Sicherheitskonzept.