Skip to content

This tool is not affiliated with, endorsed by or sponsored by The Linux Foundation, the Cloud Native Computing Foundation (CNCF) or the Kubernetes project. Kubernetes and K8s are registered trademarks of The Linux Foundation. EKS, GKE, AKS and other names are trademarks of their respective owners.

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.

Published on 5 min read

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:

SettingWhat it gives the container
securityContext.privileged: trueAll capabilities, access to host devices; can mount the host disk
capabilities.add: ["SYS_ADMIN"] (or ALL)Most of what privileged gives, including mounts
hostPID: trueSees and can signal every process on the node; nsenter -t 1 enters the host
hostNetwork: trueUses the node's network stack: node-local services, cloud metadata endpoint, sniffing
hostIPC: trueShares 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.sockTalks to the container runtime: starts new containers outside Kubernetes
hostPath of /etc, /root, /var/lib/kubeletHost 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

RuleSeverityLogic
Container escape to the nodeCriticalPrivileged container and (hostPath / or hostPID)
Privileged container deployedHighprivileged: true or SYS_ADMIN / ALL added
Sensitive host path mountedHighhostPath 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 namespaceMediumhostPID, hostNetwork or hostIPC
DaemonSet created in kube-systemHighAny 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 sh or chroot /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, ctr or docker against 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

  1. Save the workload specs as evidence, then delete the workloads and look for copies in other namespaces.
  2. 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.
  3. Rotate secrets of every pod that ran on those nodes: they were readable from the host.
  4. Enforce Pod Security Admission baseline or restricted on every namespace, including kube-system with narrow, named exemptions.
  5. Restrict who can create workloads in kube-system.

Related articles

Detect crypto-mining in Kubernetes clusters from audit logs: miner images and arguments, unusual registries, CronJob and DaemonSet persistence, and clean-up.
Find Kubernetes RBAC privilege escalation in audit logs: cluster-admin bindings, escalate, bind and impersonate verbs, impersonated calls, anonymous grants.
Detect Kubernetes secrets theft and stolen service account tokens in audit logs: cluster-wide lists, read bursts, TokenRequest, public-IP replay, can-i recon.

This tool is not affiliated with, endorsed by or sponsored by The Linux Foundation, the Cloud Native Computing Foundation (CNCF) or the Kubernetes project. Kubernetes and K8s are registered trademarks of The Linux Foundation. EKS, GKE, AKS and other names are trademarks of their respective owners.