When Privileged Mode Turned Into Node Access
A penetration test demonstrated a container escape from a Kubernetes pod running in privileged mode: root on the node in under 2 minutes. We were running Kubernetes 1.28 on Ubuntu 22.04 worker nodes, with a mix of FortiGate edge controls on FortiOS 7.4.3 and Python 3.11 automation around deployment checks. The pod looked boring in the inventory. The attacker path was not boring at all.
My first assumption was wrong. I treated the finding like a missing hardening option, something we could close with a tighter image, fewer packages, and better RBAC. The real problem was simpler and uglier: the pod was privileged because a developer needed to run a system monitoring tool, and the privileged flag was added and never removed after the debugging session.
That one flag changed the security model. A privileged container could see host devices, load kernel-facing tooling, and interact with the node in ways our normal application containers could not. If hostPID or hostNetwork had been present too, we would have had even less separation between the compromised workload and the machine beneath it.
The boundary was gone.
I do not consider privileged pods an application setting anymore. In our environment, they are an infrastructure exception with production blast radius, and I want them reviewed like firewall rule changes, not merged like harmless YAML.
Why HostPID, HostNetwork, And Privileged Are Different
We had already blocked obvious mistakes: no SSH in images, no package managers in runtime containers, no broad Kubernetes API tokens. That helped, but it did not matter enough when the runtime configuration handed the pod host-level reach. Container isolation depends on namespaces, cgroups, Linux capabilities, and kernel enforcement working together. Privileged mode punches through too many of those controls at once.
hostPID lets a pod observe host process IDs, which can expose sensitive process arguments and make process-targeting attacks easier. hostNetwork places the pod directly on the node network namespace, bypassing assumptions we made about Kubernetes networking and service-level segmentation. Privileged mode is worse because it grants broad device and capability access that most workloads never need.
- I block privileged mode unless a named platform owner approves it.
- I reject hostPID for normal application pods.
- I reject hostNetwork unless the workload is node infrastructure.
- I remove added Linux capabilities before arguing about adding new ones.
- I treat hostPath mounts as node access until proven otherwise.
What I didn’t expect was how many exceptions had boring names. They were not called escape-demo or node-admin. They were called metrics-agent, debug-runner, and hardware-checker, which made them easier to ignore during normal review.
Names lie.
My opinion is blunt here: if a container needs host-level privileges, it should carry the same operational friction as any other path to host administration.
Applying Pod Security Standards Without Theater
We moved from informal review to Kubernetes Pod Security Standards because I wanted the cluster to reject bad pod specs before a person had to notice them. The baseline profile caught common risky settings, but the restricted profile was where the escape path started closing. We applied restricted enforcement to application namespaces and left a small, documented carve-out for platform namespaces where node agents lived.
The practical shift was label-driven admission control. We did not need a giant custom admission controller for the first pass. We needed namespace labels, deployment ownership, and a rule that application teams could not silently opt out. After implementing Pod Security Standards with the restricted profile and removing all privileged pods, the pen test team could not reproduce the escape in 4 subsequent tests.
apiVersion: v1
kind: Namespace
metadata:
name: production-apps
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.28
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.28
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.28
The before and after was measurable. Before the change, the pen test team reached root on the node in under 2 minutes from the privileged pod. After the change, they failed to reproduce that escape in 4 out of 4 follow-up attempts, using the same initial compromised workload assumption.
Admission control beats memory.
I prefer Pod Security Standards because they make unsafe configurations visible at deployment time, where I can still stop the change without pretending a dashboard alert will save me later.
Restricting Syscalls With seccomp And AppArmor
Pod Security Standards removed the worst foot-guns, but we still needed runtime containment for compromised containers that stayed inside the allowed policy. That is where seccomp and AppArmor earned their place. On Ubuntu 22.04 nodes, we used the container runtime default seccomp profile as the floor, then reviewed workloads that requested unconfined mode. I do not like unconfined mode in production unless we can explain the exact syscall failure that forced it.
seccomp limits which system calls a process can make into the Linux kernel. AppArmor constrains file access, execution patterns, and other behavior through Linux Security Module policy. Neither one is magic, but both reduce the attack surface that post-exploitation tooling expects to find. When attackers bring generic escape tooling, boring restrictions become useful fast.
We also checked the image build pipeline. Python 3.11 scripts that generated manifests had allowed teams to set securityContext fields without enough guardrails, so we added validation before deployment. That helped catch drift from Helm values, local overrides, and emergency patches copied from old runbooks.
The defaults mattered more than the exceptions.
My view is that seccomp and AppArmor should be treated as normal production plumbing, not advanced hardening reserved for regulated workloads.
Watch For Escape Behavior In Runtime
Prevention did most of the work, but I still wanted runtime signals for attempts that got past policy. We tuned monitoring for suspicious hostPath access, unexpected writes under sensitive paths, namespace entry attempts, kernel module activity, and processes launched from temporary directories. Our manufacturing environment has maintenance windows, vendor tools, and old habits, so context mattered as much as raw alerts.
We tied runtime detections back to response playbooks. If a pod tried to access host process namespaces or touch node devices unexpectedly, we wanted the pod identity, namespace, node name, image digest, service account, and recent deployment event in the first alert. Without that context, an alert turns into a scavenger hunt at 2:00 a.m.
I also stopped trusting “temporary” debug access. The privileged monitoring pod existed because someone needed visibility during an outage, and nobody owned removing the exception afterward. Now we put an expiration date on exceptions, track them in change records, and query for them automatically.
Temporary becomes permanent.
We still run node-level tools when the platform needs them, but I want those workloads isolated, named, reviewed, and watched. A compromised pod should have to fight layers of control before it reaches the host, and privileged mode removes too many of those layers for my taste.

