Skip to content

Restrict privileged Kubernetes containers

Severity: highApplies to: Kubernetes 1.29+Applies to: Kubernetes 1.34
The fix
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile: { type: RuntimeDefault }
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]

Enforcing this by hand on every pod is error-prone; let Pod Security Admission require it and use this as the spec that passes restricted.

A container is isolated from its node by namespaces, capabilities, seccomp and the user it runs as — the same mechanisms as Docker, because it’s the same runtime underneath. A pod that requests its way out of those is a route to the node, and a node is every pod scheduled on it plus a kubelet credential.

The dangerous requests, worst first:

  • privileged: true — disables the isolation entirely, exposes host devices. The container can mount the host disk. This is the Docker --privileged problem with a Kubernetes wrapper.
  • hostPID, hostNetwork, hostIPC — share the node’s process, network or IPC namespace. hostNetwork puts the pod on the node’s network, past network policies and onto the metadata service.
  • hostPath volumes — mount a node directory into the pod. hostPath: / is the node’s filesystem; even /var/run/docker.sock or the kubelet’s directory is a route to node control.
  • Running as root (runAsUser: 0, the default) — a container escape lands as root on the node instead of an unprivileged user.
  • allowPrivilegeEscalation: true (the default) — a setuid binary inside the container can re-elevate even after you’ve dropped to a non-root user.

The hardened securityContext, line by line

Section titled “The hardened securityContext, line by line”
Setting Effect
runAsNonRoot: true Refuse to start if the image runs as root
runAsUser: 10001 A specific unprivileged UID
allowPrivilegeEscalation: false No re-elevation via setuid (no_new_privs)
readOnlyRootFilesystem: true Can’t write a payload into the container
capabilities.drop: ["ALL"] Start from zero, add back only what’s needed
seccompProfile.type: RuntimeDefault Block the syscalls the runtime’s profile blocks

RuntimeDefault seccomp is worth calling out — Kubernetes does not apply a seccomp profile by default (historically Unconfined), so unless you set this, the container can make every syscall. It’s one line and it closes a lot.

Some settings live on the pod securityContext (runAsNonRoot, runAsUser, seccompProfile, fsGroup), some on the container (allowPrivilegeEscalation, readOnlyRootFilesystem, capabilities, privileged). Container-level overrides pod-level. Set both, and don’t assume a pod-level runAsNonRoot covers a container that sets its own.