Kyverno entscheidet, was in den Cluster darf. Falco erkennt, was ein laufender Prozess tatsächlich tut. Beide Ebenen haben eine gemeinsame Lücke: Sie ändern nichts daran, welche Syscalls ein Container aufrufen kann, sobald er einmal läuft. Ein Angreifer, der Code in einem Container zur Ausführung bringt, trifft dort auf den vollen Werkzeugkasten des Linux Kernels, sofern du ihn vorher nicht eingeschränkt hast. Genau das leisten seccomp und AppArmor. Sie entziehen einem kompromittierten Prozess die Mittel, bevor er sie überhaupt braucht. Admission Control und Falco arbeiten auf der Ebene „was darf laufen“ und „was fällt auf“. Dieser Artikel arbeitet auf der Ebene darunter: was darf ein laufender Prozess technisch überhaupt tun.
Warum RuntimeDefault ein Anfang ist, aber kein Ziel
Seit Kubernetes 1.19 ist securityContext.seccompProfile Teil der API, seit 1.22 lässt sich RuntimeDefault per Feature-Gate clusterweit als Vorgabe erzwingen, seit 1.27 ist dieses Feature-Gate GA. Das Profil kommt vom Container Runtime (containerd oder Docker) und blockiert eine Reihe von Syscalls, die für normale Workloads praktisch nie gebraucht werden, etwa mount, reboot, ptrace oder kexec_load. Das ist ein sinnvoller Startpunkt, weil er ohne Aufwand für praktisch jeden Container funktioniert.
Der Haken: RuntimeDefault ist ein generisches Profil für beliebige Container. Es kennt deine Anwendung nicht. Eine Node-App, ein Go-Binary und ein Python-Script mit nativen Bibliotheken brauchen jeweils nur einen Bruchteil der freigegebenen Syscalls. Alles, was RuntimeDefault zulässt, aber deine Anwendung nie aufruft, ist eine Angriffsfläche, die ungenutzt herumliegt. Ein eigenes, auf die Anwendung zugeschnittenes Profil schließt genau diese Lücke, kostet aber Pflegeaufwand bei jedem Funktionswechsel im Code.
seccomp: ein eigenes Profil ableiten
Ein eigenes seccomp-Profil ist eine JSON-Datei mit einer Default-Aktion (meist SCMP_ACT_ERRNO, also verweigern) und einer Liste erlaubter Syscalls mit SCMP_ACT_ALLOW. Die Kunst liegt nicht in der Syntax, sondern darin, herauszufinden, welche Syscalls deine Anwendung tatsächlich braucht.
Der pragmatische Weg: den Container unter realistischer Last mit strace -f -c laufen lassen und die tatsächlich aufgerufenen Syscalls protokollieren. Für containerisierte Workloads eignet sich auch ein Trace direkt gegen den Container-Prozess auf dem Node (strace -f -p <pid>), sofern der Node das erlaubt. Aus der gesammelten Liste baust du das Allowlist-JSON, testest es zunächst im Log-Modus (SCMP_ACT_LOG statt SCMP_ACT_ERRNO als Default) und beobachtest im Kernel-Audit-Log, was zusätzlich anfällt, bevor du auf Deny umstellst. Das Profil liegt danach auf jedem Node im Seccomp-Profilverzeichnis des Kubelets und wird per Localhost-Typ referenziert:
apiVersion: v1kind: Podmetadata: name: hardened-appspec: securityContext: seccompProfile: type: RuntimeDefault containers: - name: app image: example/app:latest securityContext: seccompProfile: type: Localhost localhostProfile: profiles/app-strict.json
AppArmor: Pfade und Capabilities statt Syscalls
seccomp filtert, welche Syscalls ein Prozess aufrufen darf. AppArmor filtert etwas anderes: auf welche Dateipfade ein Prozess zugreifen darf, welche Capabilities er nutzen darf, und mit welchen Netzwerkfunktionen er arbeiten darf. Beides ergänzt sich, weil es an unterschiedlichen Stellen im Kernel ansetzt: seccomp am Syscall-Interface, AppArmor als Linux Security Module (LSM) an der Ressourcenebene.
In Kubernetes wird ein AppArmor-Profil ab Version 1.30 nativ über securityContext.appArmorProfile zugewiesen (vorher nur per Annotation, die Annotation-API gilt seit 1.30 als deprecated). Das Profil selbst muss vorher auf dem Node geladen sein, über apparmor_parser, ein Node-Image mit vorinstallierten Profilen oder eine DaemonSet-basierte Verteilung:
apiVersion: v1kind: Podmetadata: name: hardened-appspec: containers: - name: app image: example/app:latest securityContext: appArmorProfile: type: Localhost localhostProfile: app-strict
Läuft dein Cluster noch unter 1.30, bleibt nur der Weg über die Annotation container.apparmor.security.beta.kubernetes.io/<container>: localhost/app-strict. Welchen Weg du nutzt, hängt von deiner Cluster-Version ab, das solltest du gegen deine eigene Kubernetes-Version prüfen.
Grenzen, die du vorher kennen musst
AppArmor ist kein universelles Kernel-Feature. Es läuft auf Debian- und Ubuntu-basierten Systemen standardmäßig, auf der RHEL-Familie setzt der Kernel stattdessen auf SELinux, AppArmor ist dort in der Regel nicht aktiv. Ein Profil auf einem Node ohne AppArmor-Unterstützung läuft ins Leere, im schlechtesten Fall verhindert es den Start des Pods. Das musst du gegen deine konkreten Node-Images prüfen, bevor du es im Cluster ausrollst, nicht danach.
Der zweite Punkt ist Wartungsaufwand. Ein eigenes seccomp- oder AppArmor-Profil ist Code, der zur Anwendung gehört. Ändert sich die Anwendung, etwa durch eine neue Bibliothek oder ein neues Feature, das zusätzliche Syscalls oder Pfadzugriffe braucht, muss das Profil mitwachsen. Ohne diesen Prozess produzierst du irgendwann Ausfälle, die aussehen wie Bugs, aber eigentlich zu strenge Profile sind.
Deshalb gilt für beide Mechanismen dieselbe Regel: erst im Audit-Modus ausrollen. Bei seccomp heißt das SCMP_ACT_LOG statt SCMP_ACT_ERRNO, bei AppArmor der complain-Modus statt enforce. Erst wenn die Logs über einen repräsentativen Zeitraum keine unerwarteten Verstöße mehr zeigen, schaltest du auf Enforce um.
Fazit
Admission Control entscheidet vorab, Falco beobachtet zur Laufzeit, seccomp und AppArmor entziehen einem kompromittierten Prozess die technischen Mittel, überhaupt Schaden anzurichten. Keine der drei Ebenen ersetzt die anderen, jede schließt eine andere Lücke. Der Aufwand für eigene Profile ist real, aber er ist ein einmaliger Bauaufwand mit wiederkehrender, kleiner Pflege, nicht ein Fass ohne Boden.
Wie sich das in eine größere Security-Strategie für Cloud-native Umgebungen einordnet, findest du drüben auf digital-business.blog.
Hinterlasse einen Kommentar