I once inherited a cluster where every single pod ran as root with privileged access. "It was the only way to make it work," they said. It's amazing what you can justify when you're in a hurry.
Turns out Kubernetes has built-in tools to prevent this. Pod Security Standards have been GA since 1.25 and they're way simpler than the old PodSecurityPolicy mess.
The Problem
By default, Kubernetes lets you run containers however you want. Privileged mode? Sure. Root user? No problem. Host network? Go for it.
This is bad. A compromised privileged container basically owns the node. And from the node, lateral movement to other pods and the control plane becomes much easier.
Three Security Levels
Pod Security Standards define three levels:
Privileged: No restrictions. The "I know what I'm doing" mode. Use sparingly.
Baseline: Prevents known privilege escalations. Blocks privileged containers, hostPath mounts, host networking. This is reasonable for most workloads.
Restricted: Maximum security. Non-root, drops all capabilities, read-only root filesystem. What you should aim for.
Enforcing With Pod Security Admission
The Pod Security Admission controller is built into Kubernetes. You just need to enable it.
Three enforcement modes:
- enforce: Violations reject the pod
- audit: Violations log but allow
- warn: Violations show warning but allow
Start with warn/audit to see what would break, then move to enforce.
Apply via namespace labels:
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
Now any pod in production that violates restricted standards gets rejected.
A Practical Migration
Don't flip everything to restricted overnight. That's a great way to break production at 2 AM.
- Label namespaces with warn mode first:
labels:
pod-security.kubernetes.io/warn: restricted
-
Deploy and watch for warnings. You'll see which pods need fixes.
-
Fix the violations. Usually this means:
- Setting
runAsNonRoot: true - Dropping capabilities
- Removing privileged flags
- Using emptyDir instead of hostPath
- Setting
-
Move to enforce once violations are fixed.
What Restricted Actually Requires
The restricted policy requires:
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true # recommended
Most well-designed apps can run this way. If yours can't, that's worth investigating.
Exemptions
Some things legitimately need elevated privileges (CNI plugins, monitoring agents, etc.). You can configure exemptions at the cluster level or use the Baseline level for infrastructure namespaces.
Just don't use exemptions as an excuse to skip security for everything. That defeats the purpose.
Why Not Just Use OPA/Kyverno?
You can, and those tools are more flexible. But Pod Security Admission is:
- Built in (no extra components)
- Simple to configure (just namespace labels)
- Well documented and supported
For many teams, it's enough. Add policy engines when you need more granular control.
The Real Win
The best part isn't just blocking bad configs. It's that developers start thinking about security earlier. "Why does my container need root?" becomes a question they ask during development, not after an incident.
Apply baseline to everything. Get to restricted where you can. Your future self will thank you when there's a container escape CVE and you're already protected.
Sources: