Skip to main content
cd ../config-traps
risk/register/kyverno-blocks-docker-sock-misses-containerd-sock.html
Kubernetes Runtime Securitycritical severityKubernetes

Kyverno Blocks docker.sock, Misses containerd.sock

Overview

A hostPath denylist written for Docker-era clusters checks for /var/run/docker.sock by exact string but never learned the containerd or CRI-O equivalents, so migrating the runtime silently reopens the same node-root escape the policy was written to close.

At a glance

Unsafe setting
A hostPath denylist policy matches only the literal string /var/run/docker.sock and ignores containerd or CRI-O socket paths.
Failure trigger
A pod requests a hostPath mount at /run/containerd/containerd.sock or /var/run/crio/crio.sock after a CRI migration, evading the outdated policy.
Blast radius
Direct containerd or CRI-O API access lets an attacker spawn a new container with host namespaces and root filesystem access, compromising the node and its scheduled workloads.
Recommended control
Replace literal socket path denylists with regex-based ValidatingAdmissionPolicy or Kyverno rules covering all known CRI socket variants, and restrict socket mounts to dedicated node DaemonSets.

The Trap

Runtime-socket hostPath denylisting by literal path string, rather than by regex or by capability class, in Kyverno, OPA Gatekeeper, or PodSecurity Admission exemptions.

The Default State

Most published Kyverno and Gatekeeper policy packs for blocking dangerous hostPath mounts were written when Docker was the default CRI and contain a hard-coded string match against /var/run/docker.sock. When a cluster migrates to containerd or CRI-O (as every cluster running Kubernetes 1.24+ has, following dockershim removal), the actual runtime socket moves to /run/containerd/containerd.sock or /var/run/crio/crio.sock. Nobody updates the policy, because the CI pipeline that validated it against docker.sock still passes. The denylist author assumed the socket path is the threat; the socket path is just one instance of it.

The Blast Radius

A pod that requests a hostPath volume at /run/containerd/containerd.sock is admitted cleanly because the string never matches the policy’s blocked-paths array. Inside the container, the containerd ttrpc client (or a statically compiled ctr binary) talks to that socket directly, bypassing Docker entirely. From there an attacker calls the containerd Tasks service to create and start a new container with host PID, host network, and a bind mount of the node’s root filesystem — no privileged: true flag required, because the exploit runs through the runtime API, not through kernel capabilities the pod spec declares. PodSecurity Admission’s restricted profile does not inspect hostPath target strings for semantic risk; it only checks that hostPath is present at all if you’ve configured that rule, and most clusters exempt kube-system or CI namespaces from restricted enforcement anyway. Once the shim is reachable, the attacker owns the node: kubelet client certs, every Secret volume mounted into pods scheduled there, and lateral movement into the API server via the node’s own service account token.

The Lead Mechanic Fix

Replace literal path matching with a regex-based Kyverno validate rule: pattern spec.volumes[].hostPath.path matched against ^/(var/)?run/(docker|containerd|crio|dockershim)(/.*)?.sock$, or better, deny all hostPath volumes outside an explicit allowlist enforced via a ValidatingAdmissionPolicy CEL expression that checks every volume’s hostPath.path against a maintained regex constant, not a per-runtime string list. Mount CRI sockets only inside node-level DaemonSets running under a dedicated ServiceAccount with no API server RBAC beyond what the CNI or CSI driver needs, and gate that DaemonSet’s namespace with a restricted PodSecurity label with no exemption. Audit existing clusters with kubectl get pods -A -o json | jq for any hostPath.path containing ‘.sock’ to find current exposure before writing the rule.

Kyverno Blocks docker.sock, Misses containerd.sock | Config Traps