Privileged Pods and Container Escape: Audit Log Indicators
Spot container escape preparation in Kubernetes audit logs: privileged pods, hostPID, hostNetwork, hostPath of / or runtime sockets, kube-system DaemonSets.
TL;DR. The audit log cannot see a container escape happen, but it sees the pod spec that makes it trivial. Look for workload writes (pods, deployments, daemonsets, jobs, cronjobs…) whose body sets securityContext.privileged: true, adds SYS_ADMIN, shares hostPID / hostNetwork / hostIPC, or mounts a hostPath of /, /etc, /var/lib/kubelet or a container runtime socket. A privileged container with the node's root filesystem or PID namespace is node takeover. Follow it with the exec that used it (chroot /host, nsenter). All of this requires request bodies in the log.
ATT&CK splits this into T1610, Deploy Container and T1611, Escape to Host. In Kubernetes the two usually happen within a minute of each other, and the audit log records the first one in full.
Why these settings matter
A container is a set of Linux namespaces and restrictions on a shared kernel. Each of these pod settings removes one layer:
| Setting | What it gives the container |
|---|---|
securityContext.privileged: true | All capabilities, access to host devices; can mount the host disk |
capabilities.add: ["SYS_ADMIN"] (or ALL) | Most of what privileged gives, including mounts |
hostPID: true | Sees and can signal every process on the node; nsenter -t 1 enters the host |
hostNetwork: true | Uses the node's network stack: node-local services, cloud metadata endpoint, sniffing |
hostIPC: true | Shares the node's IPC namespace |
hostPath of / | The node's whole filesystem: kubelet credentials, other pods' volumes, chroot |
hostPath of /var/run/docker.sock, containerd.sock, crio.sock | Talks to the container runtime: starts new containers outside Kubernetes |
hostPath of /etc, /root, /var/lib/kubelet | Host configuration, SSH keys, kubelet credentials and pod secrets |
The Kubernetes RBAC good practices note that permission to create workloads or PersistentVolumes can include creating hostPath volumes and therefore host filesystem access. The Pod Security Standards Baseline profile forbids all of the settings above for that reason.
What the audit log shows
A fictional DaemonSet, trimmed to the parts that matter:
{
"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 }
}
A plain image, a name imitating kube-proxy, the kube-system namespace, and every escape setting at once. A DaemonSet puts one copy on every node, so this is not one node taken over but all of them.
Who created it: the person, not the controller
The pods themselves are created by the DaemonSet, ReplicaSet or Job controller, a system identity. If you only look at pods creations you will attribute the pod to system:serviceaccount:kube-system:daemon-set-controller. The accountable identity is the one that wrote the workload object. The analyzer on the home page reads the pod template inside Deployments, DaemonSets, StatefulSets, ReplicaSets, Jobs and CronJobs, so findings point to the person or token that wrote the spec.
Created does not always mean running
With Pod Security Admission, enforce mode applies to pods, not to workload objects: the PSA documentation says workload resources only get audit and warn. A privileged DaemonSet can therefore be accepted (201) while every pod it tries to create is rejected. Check the controller's pod create events and their response codes before concluding that the escape happened. In audit mode, PSA adds an annotation to the audit event describing the violation, which is a useful signal on its own.
Detections
| Rule | Severity | Logic |
|---|---|---|
| Container escape to the node | Critical | Privileged container and (hostPath / or hostPID) |
| Privileged container deployed | High | privileged: true or SYS_ADMIN / ALL added |
| Sensitive host path mounted | High | hostPath on /, /etc, /etc/kubernetes, /root, /home, /proc, /sys, /dev, /boot, /var/lib/kubelet or a Docker, containerd or CRI-O socket |
| Workload shares the node's PID / network / IPC namespace | Medium | hostPID, hostNetwork or hostIPC |
| DaemonSet created in kube-system | High | Any DaemonSet created there by a non-system identity |
All of them apply to create, update and patch: patching an existing Deployment to add a hostPath is as effective as creating a new one, and less visible.
Expect some legitimate matches. CNI plugins, CSI drivers, log shippers, node exporters and security agents run as privileged DaemonSets with host mounts. They are normally installed by a known pipeline identity, at a known time, from a known registry. A new one created with kubectl from a laptop, or by a service account that never deploys anything, is not.
Then look for the exec
Preparation is followed by use. Filter pods/exec events on the pods created from the suspicious workload (the pod names start with the workload name) and read the commands:
chroot /host shorchroot /host bash: a shell on the node's filesystem;nsenter --target 1 --mount --uts --ipc --net --pid: a shell in the node's namespaces via PID 1;cat /host/var/lib/kubelet/…,ls /host/etc/kubernetes: credential hunting;crictl,ctrordockeragainst a mounted socket: containers started outside Kubernetes.
See detecting kubectl exec for how commands appear in requestURI. If there is no exec at all, the container's own command may do the work: read command and args in the spec.
What you will not see
Once on the node, the attacker is outside the API server's view. Processes, new SSH keys, kubelet credential reuse and containers started directly through the runtime socket do not produce Kubernetes audit events. You need node-level evidence: runtime detection such as Falco, EDR, the node's own logs and a disk snapshot. The limitations article covers how to pair the two. The analyzer can load Falco JSON alerts next to the audit logs and put them on the same timeline.
Remediation
- Save the workload specs as evidence, then delete the workloads and look for copies in other namespaces.
- Treat every node that ran the pod as compromised: cordon, drain, snapshot, replace. Rotate kubelet certificates and the cloud instance role or managed identity the node used.
- Rotate secrets of every pod that ran on those nodes: they were readable from the host.
- Enforce Pod Security Admission
baselineorrestrictedon every namespace, includingkube-systemwith narrow, named exemptions. - Restrict who can create workloads in
kube-system.