Privilegierte Pods und Container-Escape: Hinweise im Log
Vorbereitung eines Container-Escapes in Kubernetes-Audit-Logs erkennen: privilegierte Pods, hostPID, hostNetwork, hostPath auf / und DaemonSets in kube-system.
TL;DR. Das Audit-Log sieht nicht, wie ein Container-Escape passiert, aber es sieht die Pod-Spec, die ihn trivial macht. Suchen Sie Workload-Schreibzugriffe (pods, deployments, daemonsets, jobs, cronjobs…), deren Body securityContext.privileged: true setzt, SYS_ADMIN hinzufügt, hostPID / hostNetwork / hostIPC teilt oder einen hostPath auf /, /etc, /var/lib/kubelet oder einen Container-Runtime-Socket mountet. Ein privilegierter Container mit dem Root-Dateisystem oder PID-Namespace des Nodes bedeutet Node-Übernahme. Folgen Sie dann dem exec, das ihn genutzt hat (chroot /host, nsenter). All das setzt Request-Bodies im Log voraus.
ATT&CK teilt das in T1610, Deploy Container und T1611, Escape to Host. In Kubernetes liegen beide meist weniger als eine Minute auseinander, und das Audit-Log zeichnet den ersten Schritt vollständig auf.
Warum diese Einstellungen wichtig sind
Ein Container ist eine Sammlung von Linux-Namespaces und Einschränkungen auf einem gemeinsamen Kernel. Jede dieser Pod-Einstellungen entfernt eine Schicht:
| Einstellung | Was der Container bekommt |
|---|---|
securityContext.privileged: true | Alle Capabilities, Zugriff auf Host-Geräte; kann die Host-Disk mounten |
capabilities.add: ["SYS_ADMIN"] (oder ALL) | Das meiste von privileged, einschließlich Mounts |
hostPID: true | Sieht jeden Prozess des Nodes und kann ihm Signale senden; nsenter -t 1 betritt den Host |
hostNetwork: true | Nutzt den Netzwerkstack des Nodes: lokale Dienste, Cloud-Metadaten-Endpunkt, Mitschneiden |
hostIPC: true | Teilt den IPC-Namespace des Nodes |
hostPath auf / | Das gesamte Dateisystem des Nodes: Kubelet-Zugangsdaten, Volumes anderer Pods, chroot |
hostPath auf /var/run/docker.sock, containerd.sock, crio.sock | Spricht mit der Container-Runtime: startet Container außerhalb von Kubernetes |
hostPath auf /etc, /root, /var/lib/kubelet | Host-Konfiguration, SSH-Schlüssel, Kubelet-Zugangsdaten und Pod-Secrets |
Die RBAC Good Practices von Kubernetes halten fest, dass das Recht, Workloads oder PersistentVolumes anzulegen, das Anlegen von hostPath-Volumes und damit Zugriff auf das Host-Dateisystem einschließen kann. Das Baseline-Profil der Pod Security Standards verbietet deshalb alle oben genannten Einstellungen.
Was das Audit-Log zeigt
Ein fiktives DaemonSet, auf das Wesentliche gekürzt:
{
"verb": "create",
"user": { "username": "system:serviceaccount:kubernetes-dashboard:kubernetes-dashboard" },
"objectRef": { "resource": "daemonsets", "namespace": "kube-system", "name": "kube-proxy-monitor", "apiGroup": "apps" },
"requestObject": {
"spec": { "template": { "spec": {
"hostPID": true,
"hostNetwork": true,
"containers": [{
"name": "monitor",
"image": "docker.io/library/alpine:3.20",
"securityContext": { "privileged": true },
"volumeMounts": [{ "name": "host", "mountPath": "/host" }]
}],
"volumes": [{ "name": "host", "hostPath": { "path": "/", "type": "Directory" } }]
} } }
},
"responseStatus": { "code": 201 }
}
Ein schlichtes Image, ein Name, der kube-proxy imitiert, der Namespace kube-system und alle Escape-Einstellungen auf einmal. Ein DaemonSet legt auf jedem Node eine Kopie an: Das ist nicht ein übernommener Node, das sind alle.
Wer es angelegt hat: die Person, nicht der Controller
Die Pods selbst legt der DaemonSet-, ReplicaSet- oder Job-Controller an, eine System-Identität. Wer nur auf pods-Anlagen schaut, schreibt den Pod system:serviceaccount:kube-system:daemon-set-controller zu. Verantwortlich ist die Identität, die das Workload-Objekt geschrieben hat. Der Analyzer auf der Startseite liest das Pod-Template in Deployments, DaemonSets, StatefulSets, ReplicaSets, Jobs und CronJobs, sodass Findings auf die Person oder das Token zeigen, das die Spec geschrieben hat.
Angelegt heißt nicht immer laufend
Mit Pod Security Admission gilt der Modus enforce für Pods, nicht für Workload-Objekte: Laut PSA-Dokumentation bekommen Workload-Ressourcen nur audit und warn. Ein privilegiertes DaemonSet kann also angenommen werden (201), während jeder Pod, den es anzulegen versucht, abgelehnt wird. Prüfen Sie die create-Events der Pods durch den Controller und deren Response-Codes, bevor Sie schließen, dass der Escape stattgefunden hat. Im Modus audit fügt PSA dem Audit-Event eine Annotation mit der Verletzung hinzu, was für sich schon ein nützliches Signal ist.
Erkennungen
| Regel | Schweregrad | Logik |
|---|---|---|
| Container-Escape auf den Node | Kritisch | Privilegierter Container und (hostPath / oder hostPID) |
| Privilegierter Container bereitgestellt | Hoch | privileged: true oder SYS_ADMIN / ALL hinzugefügt |
| Sensibler Host-Pfad gemountet | Hoch | hostPath auf /, /etc, /etc/kubernetes, /root, /home, /proc, /sys, /dev, /boot, /var/lib/kubelet oder einen Docker-, containerd- oder CRI-O-Socket |
| Workload teilt sich PID-/Netzwerk-/IPC-Namespace mit dem Node | Mittel | hostPID, hostNetwork oder hostIPC |
| DaemonSet in kube-system erstellt | Hoch | Jedes DaemonSet, das dort eine Nicht-System-Identität anlegt |
Alle gelten für create, update und patch: Ein bestehendes Deployment zu patchen und einen hostPath hinzuzufügen, ist so wirksam wie ein neues anzulegen und weniger auffällig.
Rechnen Sie mit legitimen Treffern. CNI-Plugins, CSI-Treiber, Log-Shipper, Node-Exporter und Security-Agents laufen als privilegierte DaemonSets mit Host-Mounts. Sie werden normalerweise von einer bekannten Pipeline-Identität, zu einem bekannten Zeitpunkt, aus einer bekannten Registry installiert. Ein neues, per kubectl von einem Laptop angelegtes, oder von einem Service Account, der nie etwas deployt, dagegen nicht.
Dann nach dem exec suchen
Auf die Vorbereitung folgt die Nutzung. Filtern Sie pods/exec-Events auf die Pods des verdächtigen Workloads (ihre Namen beginnen mit dem Workload-Namen) und lesen Sie die Befehle:
chroot /host shoderchroot /host bash: eine Shell auf dem Dateisystem des Nodes;nsenter --target 1 --mount --uts --ipc --net --pid: eine Shell in den Namespaces des Nodes über PID 1;cat /host/var/lib/kubelet/…,ls /host/etc/kubernetes: Suche nach Zugangsdaten;crictl,ctroderdockergegen einen gemounteten Socket: Container außerhalb von Kubernetes gestartet.
Wie Befehle in requestURI erscheinen, zeigt kubectl exec erkennen. Gibt es gar kein exec, erledigt vielleicht der Container-Befehl selbst die Arbeit: Lesen Sie command und args in der Spec.
Was Sie nicht sehen werden
Sobald er auf dem Node ist, ist der Angreifer außerhalb des Blickfelds des API-Servers. Prozesse, neue SSH-Schlüssel, wiederverwendete Kubelet-Zugangsdaten und direkt über den Runtime-Socket gestartete Container erzeugen keine Kubernetes-Audit-Events. Sie brauchen Beweise auf Node-Ebene: Runtime-Erkennung wie Falco, EDR, die Logs des Nodes und einen Disk-Snapshot. Der Artikel zu den Grenzen beschreibt, wie Sie beides kombinieren. Der Analyzer kann Falco-Alerts als JSON neben den Audit-Logs laden und auf dieselbe Timeline legen.
Behebung
- Sichern Sie die Workload-Specs als Beweis, löschen Sie dann die Workloads und suchen Sie Kopien in anderen Namespaces.
- Behandeln Sie jeden Node, auf dem der Pod lief, als kompromittiert: Cordon, Drain, Snapshot, ersetzen. Rotieren Sie Kubelet-Zertifikate und die Cloud-Instanzrolle oder Managed Identity des Nodes.
- Rotieren Sie die Secrets aller Pods, die auf diesen Nodes liefen: Sie waren vom Host aus lesbar.
- Erzwingen Sie Pod Security Admission
baselineoderrestrictedauf jedem Namespace, auch inkube-systemmit engen, namentlichen Ausnahmen. - Beschränken Sie, wer in
kube-systemWorkloads anlegen darf.