Kubernetes Pod Security Standards: Restricted and Baseline
Pod Security Standards (PSS) replace the deprecated PodSecurityPolicy (PSP) that was removed in Kubernetes 1.25. PSS defines three human-readable security profiles — Privileged, Baseline, and Restricted — that cover the most important container security settings without requiring custom admission webhook code. The built-in Pod Security Admission controller enforces these profiles at the namespace level, making it straightforward to harden workloads across an entire cluster.
Table of Contents
The Three Pod Security Profiles
Kubernetes defines three Pod Security Standard profiles, ordered from most permissive to most restrictive:
- Privileged: Completely unrestricted. Allows hostPID, hostNetwork, privileged containers, and all capabilities. Appropriate only for trusted system-level workloads like CNI plugins and monitoring agents that must access host resources.
- Baseline: Prevents the most common privilege escalation paths while remaining compatible with most containerised applications. Blocks hostNetwork, hostPID, privileged containers, and dangerous capabilities, but allows running as root and using host ports above 1024.
- Restricted: The most secure profile. Enforces all Baseline controls plus: running as non-root, dropping all Linux capabilities, disabling privilege escalation, using a seccomp profile, and requiring a read-only root filesystem. Most production application pods should target this level.
Pod Security Admission Controller
Pod Security Admission (PSA) is a built-in admission controller that enforces PSS profiles. It operates in three modes per namespace:
- enforce: Pods that violate the policy are rejected. The pod is not created.
- audit: Violations are recorded in the audit log but pods are still created. Use this to identify violations without breaking workloads.
- warn: Violations produce a warning returned to the API client (visible in
kubectloutput) but pods are still created.
These modes are independent — you can enforce Baseline while auditing and warning on Restricted violations simultaneously:
apiVersion: v1
kind: Namespace
metadata:
name: team-payments
labels:
# Enforce Baseline — reject non-compliant pods
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: v1.30
# Audit Restricted — log violations without rejecting
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.30
# Warn Restricted — show warnings in kubectl output
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.30
The -version labels pin the policy version to a specific Kubernetes release, ensuring that cluster upgrades don't automatically tighten policies and break existing workloads.
Applying Profiles to Namespaces
Apply profiles to all existing namespaces at once using a label selector, or add labels during namespace creation with a Helm template or GitOps manifests.
# Apply Baseline enforcement to all non-system namespaces
for ns in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'); do
case $ns in
kube-system|kube-public|kube-node-lease)
echo "Skipping system namespace: $ns"
;;
*)
kubectl label namespace "$ns" \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/enforce-version=v1.30 \
--overwrite
echo "Labelled: $ns"
;;
esac
done
# Dry-run: simulate enforcing Restricted on a namespace to see violations
kubectl label namespace team-payments \
pod-security.kubernetes.io/enforce=restricted \
--dry-run=server --overwrite
Restricted Profile Requirements
A pod must satisfy all of these requirements to pass the Restricted profile:
apiVersion: v1
kind: Pod
metadata:
name: restricted-compliant-pod
spec:
securityContext:
runAsNonRoot: true # Pod must run as non-root
seccompProfile:
type: RuntimeDefault # Must have seccomp profile
containers:
- name: app
image: myapp:latest
securityContext:
allowPrivilegeEscalation: false # REQUIRED by Restricted
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true # Strongly recommended
capabilities:
drop:
- ALL # Drop ALL Linux capabilities
add: [] # Add back only what you need
seccompProfile:
type: RuntimeDefault
# Mount writable volumes for app data (not filesystem)
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /app/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
Security Contexts: Key Settings Explained
Understanding each security context field helps you write compliant manifests and explain rejections to your development team:
- runAsNonRoot: true: Kubernetes rejects the pod if the container image's USER instruction is root (UID 0). The image must be built with a non-root user.
- allowPrivilegeEscalation: false: Prevents a process in the container from gaining more privileges than its parent (e.g., via setuid binaries). Always set this to false.
- readOnlyRootFilesystem: true: The container's root filesystem is mounted read-only. The application must write to mounted volumes only. This prevents malware from writing to the container filesystem.
- capabilities.drop: [ALL]: Drops all Linux capabilities. Most applications do not need any capabilities. Add back only what is strictly required (e.g.,
NET_BIND_SERVICEto bind ports below 1024). - seccompProfile.type: RuntimeDefault: Applies the container runtime's default seccomp filter, which blocks ~300 rarely-used and dangerous syscalls.
# Check which pods in a namespace violate Restricted
kubectl get pods -n team-payments -o json | \
jq '.items[] | select(.spec.containers[].securityContext.allowPrivilegeEscalation != false)
| .metadata.name'
Migrating from PodSecurityPolicy
PodSecurityPolicy was removed in Kubernetes 1.25. If you are running a cluster older than 1.25 with PSP enabled, you must migrate before upgrading. The migration path is:
- Map each PodSecurityPolicy to the closest PSS profile (most restrictive PSPs → Restricted; permissive PSPs → Baseline)
- Add the equivalent namespace labels in
warnmode alongside existing PSPs - Fix any violations surfaced by the warnings
- Switch namespace labels from
warntoenforce - Disable PSP in the API server admission plugins before upgrading to 1.25
# List all PodSecurityPolicies currently in use
kubectl get psp
# Find which service accounts are bound to each PSP
kubectl get clusterrolebindings -o json | \
jq '.items[] | select(.roleRef.kind == "ClusterRole") |
{binding: .metadata.name, role: .roleRef.name, subjects: .subjects}'
Exemptions for System Workloads
Some legitimate system workloads require privileged access: monitoring agents (Falco, Datadog), CNI plugins, log collectors (Fluentbit DaemonSet), and CSI drivers. These must be exempted from Restricted or Baseline enforcement.
# kube-apiserver flag — configure PSA exemptions
--admission-control-config-file=/etc/kubernetes/admission-config.yaml
# /etc/kubernetes/admission-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1
kind: PodSecurityConfiguration
defaults:
enforce: "baseline"
enforce-version: "latest"
audit: "restricted"
audit-version: "latest"
warn: "restricted"
warn-version: "latest"
exemptions:
# Exempt system namespaces (already privileged)
namespaces:
- kube-system
- monitoring
- falco
# Exempt specific runtime classes (e.g., gVisor)
runtimeClasses: []
# Exempt specific usernames (e.g., cluster-admin)
usernames: []
OPA Gatekeeper as an Alternative
For organisations that need more granular policy control than PSS provides, OPA Gatekeeper is the most popular policy-as-code solution for Kubernetes. Gatekeeper lets you write custom Rego policies for any aspect of a resource manifest.
# Gatekeeper ConstraintTemplate: require non-root containers
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequirenonroot
spec:
crd:
spec:
names:
kind: K8sRequireNonRoot
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequirenonroot
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.securityContext.runAsNonRoot
msg := sprintf("Container %v must set runAsNonRoot: true", [container.name])
}
---
# Apply the constraint to all namespaces except system ones
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireNonRoot
metadata:
name: require-non-root-containers
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system
- monitoring