Skip to main content
Security·8 min read

Preventing Container Escape & Privilege Escalation in Kubernetes

Secure Kubernetes pods against container escape and privilege escalation. Implement Pod Security Standards, Kyverno policies, seccomp profiles, and non-root containers in production.

DT

DevOps Engineer & Technical Writer

TL;DR — Quick Fix

# Add to every pod spec — minimum security baseline

spec:

containers:

- name: app

securityContext:

runAsNonRoot: true

runAsUser: 1000

allowPrivilegeEscalation: false

readOnlyRootFilesystem: true

capabilities:

drop: ["ALL"]

These 5 lines block the most common container escape vectors.

---

Why Container Security Matters

CONTAINER ESCAPE — ATTACK PATH COMPROMISED APP RCE in pod Running as root ESCALATE Mount host FS CAP_SYS_ADMIN ESCAPE Access node Full host access LATERAL MOVE All pods Cluster takeover DEFENSE IN DEPTH — BLOCK AT EVERY STAGE: 1. Non-root user 2. Drop all capabilities 3. Read-only filesystem 4. No privilege escalation 5. Seccomp profile 6. Network policies

A container running as root with default Linux capabilities has almost everything it needs to escape to the host node. Once on the node, the attacker can access the kubelet, steal service account tokens, and take over the entire cluster.

Pod Security Standards (PSS) — Built-in Kubernetes

Kubernetes has three built-in security profiles enforced at the namespace level:

LevelWhat it blocksUse case
<strong>Privileged</strong>NothingSystem namespaces (kube-system)
<strong>Baseline</strong>Known privilege escalationsDefault for most workloads
<strong>Restricted</strong>Everything not strictly neededSensitive workloads

Enforce Restricted Profile on a Namespace

apiVersion: v1

kind: Namespace

metadata:

name: production

labels:

pod-security.kubernetes.io/enforce: restricted

pod-security.kubernetes.io/enforce-version: latest

pod-security.kubernetes.io/warn: restricted

pod-security.kubernetes.io/audit: restricted

What the Restricted Profile Requires

apiVersion: v1

kind: Pod

metadata:

name: secure-app

spec:

securityContext:

runAsNonRoot: true

seccompProfile:

type: RuntimeDefault

containers:

- name: app

image: my-app:latest

securityContext:

allowPrivilegeEscalation: false

runAsNonRoot: true

runAsUser: 1000

readOnlyRootFilesystem: true

capabilities:

drop: ["ALL"]

volumeMounts:

- name: tmp

mountPath: /tmp

volumes:

- name: tmp

emptyDir: {}

Security Context Deep Dive

The 5 Critical Fields

securityContext:

# 1. Never run as root

runAsNonRoot: true

runAsUser: 1000

runAsGroup: 1000

# 2. Block privilege escalation (setuid, setgid)

allowPrivilegeEscalation: false

# 3. Read-only root filesystem

readOnlyRootFilesystem: true

# 4. Drop ALL Linux capabilities

capabilities:

drop: ["ALL"]

# Only add back what you absolutely need:

# add: ["NET_BIND_SERVICE"] # for binding to ports < 1024

# 5. Apply seccomp profile

seccompProfile:

type: RuntimeDefault

What Each Field Prevents

FieldAttack it blocks
<code class="inline-code">runAsNonRoot: true</code>Attacker can't use root UID to access host files
<code class="inline-code">allowPrivilegeEscalation: false</code>Blocks setuid/setgid binaries (no sudo, no su)
<code class="inline-code">readOnlyRootFilesystem: true</code>Attacker can't write malicious binaries to disk
<code class="inline-code">capabilities.drop: [&quot;ALL&quot;]</code>Removes CAP_SYS_ADMIN, CAP_NET_RAW, etc.
<code class="inline-code">seccompProfile: RuntimeDefault</code>Blocks dangerous syscalls (mount, ptrace, etc.)

Handling Apps That Need Writable Directories

spec:

containers:

- name: app

securityContext:

readOnlyRootFilesystem: true

volumeMounts:

- name: tmp

mountPath: /tmp

- name: cache

mountPath: /app/.cache

- name: logs

mountPath: /var/log/app

volumes:

- name: tmp

emptyDir: { sizeLimit: "100Mi" }

- name: cache

emptyDir: { sizeLimit: "500Mi" }

- name: logs

emptyDir: { sizeLimit: "200Mi" }

Policy Enforcement with Kyverno

Install Kyverno

helm repo add kyverno https://kyverno.github.io/kyverno/

helm install kyverno kyverno/kyverno -n kyverno --create-namespace

Policy: Require Non-Root Containers

apiVersion: kyverno.io/v1

kind: ClusterPolicy

metadata:

name: require-run-as-non-root

spec:

validationFailureAction: Enforce

rules:

- name: run-as-non-root

match:

any:

- resources:

kinds: ["Pod"]

exclude:

any:

- resources:

namespaces: ["kube-system", "kyverno"]

validate:

message: "Containers must run as non-root"

pattern:

spec:

containers:

- securityContext:

runAsNonRoot: true

allowPrivilegeEscalation: false

Policy: Block Privileged Containers

apiVersion: kyverno.io/v1

kind: ClusterPolicy

metadata:

name: disallow-privileged

spec:

validationFailureAction: Enforce

rules:

- name: no-privileged

match:

any:

- resources:

kinds: ["Pod"]

validate:

message: "Privileged containers are not allowed"

pattern:

spec:

containers:

- securityContext:

privileged: "!true"

- name: no-host-namespaces

match:

any:

- resources:

kinds: ["Pod"]

validate:

message: "Host namespaces are not allowed"

pattern:

spec:

=(hostPID): false

=(hostIPC): false

=(hostNetwork): false

Policy: Require Read-Only Root Filesystem

apiVersion: kyverno.io/v1

kind: ClusterPolicy

metadata:

name: require-ro-rootfs

spec:

validationFailureAction: Enforce

rules:

- name: read-only-root

match:

any:

- resources:

kinds: ["Pod"]

exclude:

any:

- resources:

namespaces: ["kube-system"]

validate:

message: "Root filesystem must be read-only"

pattern:

spec:

containers:

- securityContext:

readOnlyRootFilesystem: true

Building Non-Root Container Images

Dockerfile Best Practices

FROM node:20-slim AS build

WORKDIR /app

COPY package*.json ./

RUN npm ci --only=production

FROM node:20-slim

RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser

WORKDIR /app

COPY --from=build /app/node_modules ./node_modules

COPY . .

RUN chown -R appuser:appuser /app

USER appuser

EXPOSE 8080

CMD ["node", "server.js"]

Distroless Images (Most Secure)

FROM golang:1.22 AS builder

WORKDIR /app

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -o /server

FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=builder /server /server

USER nonroot:nonroot

ENTRYPOINT ["/server"]

Distroless images have no shell, no package manager, and no unnecessary binaries.

Network Policies (Blast Radius Reduction)

apiVersion: networking.k8s.io/v1

kind: NetworkPolicy

metadata:

name: payment-service-policy

namespace: production

spec:

podSelector:

matchLabels:

app: payment-service

policyTypes:

- Ingress

- Egress

ingress:

- from:

- podSelector:

matchLabels:

app: api-gateway

ports:

- protocol: TCP

port: 8080

egress:

- to:

- podSelector:

matchLabels:

app: postgres

ports:

- protocol: TCP

port: 5432

- to:

- namespaceSelector: {}

podSelector:

matchLabels:

k8s-app: kube-dns

ports:

- protocol: UDP

port: 53

Security Scanning in CI/CD

# GitHub Actions — scan with Trivy
  • name: Scan container image
uses: aquasecurity/trivy-action@master

with:

image-ref: 'my-app:${{ github.sha }}'

format: 'sarif'

severity: 'CRITICAL,HIGH'

exit-code: '1'

# Kube-score — check security best practices

kube-score score deployment.yaml

# Kubesec — security risk scoring

kubesec scan deployment.yaml

# Checkov — policy-as-code scanning

checkov -d ./k8s-manifests/ --framework kubernetes

---

Frequently Asked Questions

Why shouldn't containers run as root?

Running as root (UID 0) inside a container means that if an attacker achieves code execution, they have root privileges within the container namespace. Combined with certain Linux capabilities or misconfigurations, root inside the container can escape to root on the host node. Running as non-root eliminates this entire class of attacks.

What are Linux capabilities and why drop them all?

Linux capabilities split root's powers into granular permissions. Docker/Kubernetes grants several by default (NET_RAW, SETUID, SETGID, etc.). CAP_SYS_ADMIN alone allows mounting filesystems, creating namespaces, and dozens of other operations that enable container escape. Dropping all capabilities and adding back only what's needed follows least-privilege principles.

How do I handle apps that need to write to disk?

Use readOnlyRootFilesystem: true and mount writable emptyDir volumes at specific paths the app needs (/tmp, /var/cache, /var/log). This prevents attackers from writing malicious binaries to the container filesystem while allowing your app to function normally.

What's the difference between Pod Security Standards and Kyverno?

Pod Security Standards (PSS) are built into Kubernetes and provide three predefined profiles. Kyverno allows custom policies with more granularity — you can enforce image registries, resource limits, label requirements, and any arbitrary validation logic. Use PSS as a baseline and Kyverno for custom organizational policies.

Should I use Kyverno or OPA Gatekeeper?

Both are production-ready. Kyverno uses YAML-native policies (easier for Kubernetes teams). OPA Gatekeeper uses Rego (more powerful but steeper learning curve). Choose Kyverno if your team prefers Kubernetes-native YAML. Choose OPA if you need policies across non-Kubernetes systems too.

---