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
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:
| Level | What it blocks | Use case |
|---|---|---|
| <strong>Privileged</strong> | Nothing | System namespaces (kube-system) |
| <strong>Baseline</strong> | Known privilege escalations | Default for most workloads |
| <strong>Restricted</strong> | Everything not strictly needed | Sensitive 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
| Field | Attack 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: ["ALL"]</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.
---
Related Resources
- Open Source Security Scanners Guide — Trivy, Grype, and scanning tools
- SAST Complete Guide — Static application security testing
- Docker Production Best Practices — Secure container builds
- Secrets Management in DevOps — Managing sensitive data in clusters