Container-Sicherheit unter Linux 2026: Docker und Podman härten, Images scannen und Laufzeitschutz mit Falco

Praxisleitfaden zur Container-Sicherheit unter Linux: Docker und Podman härten, Schwachstellen-Scanning mit Trivy und Grype, Seccomp-Profile erstellen und Laufzeitschutz mit Falco einrichten. Mit sofort einsetzbaren Konfigurationen und CI/CD-Integration.

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:

  1. Minimales Base-Image verwenden — Alpine, Distroless oder Chainguard-Images bevorzugen
  2. Multi-Stage Builds nutzen — Build-Tools gehören nicht ins Runtime-Image
  3. Nie als Root laufenUSER-Direktive im Dockerfile, Rootless-Container bei Podman
  4. Alle Capabilities droppen--cap-drop=ALL und nur benötigte einzeln hinzufügen
  5. Read-only Dateisystem--read-only und nur nötige tmpfs-Mounts erlauben
  6. no-new-privileges aktivieren--security-opt no-new-privileges:true verhindert Privilege-Escalation
  7. Ressourcenlimits setzen — Memory, CPU und PID-Limits über cgroups konfigurieren
  8. Seccomp-Profil anwenden — mindestens das Standard-Profil, idealerweise ein maßgeschneidertes
  9. SELinux oder AppArmor aktivieren — MAC-Layer für Dateisystem- und Netzwerkzugriff
  10. Images regelmäßig scannen — Trivy und Grype in die CI/CD-Pipeline integrieren
  11. SBOM generieren und pflegen — Transparenz über alle Abhängigkeiten schaffen
  12. Images signieren — Cosign für kryptografische Verifikation einsetzen
  13. Runtime-Monitoring einrichten — Falco für Echtzeit-Erkennung verdächtigen Verhaltens
  14. Docker-Socket nicht exponieren — Niemals /var/run/docker.sock in Container mounten
  15. 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.

Über den Autor Editorial Team

Our team of expert writers and editors.