Skip to content

Pod Security Standards and Admission

Overview

Pod Security Standards (PSS) define three progressively restrictive security profiles for pods. The Pod Security Admission (PSA) controller enforces these standards at the namespace level, providing a built-in mechanism to prevent insecure pod configurations without external tools.

PSS Replaces PodSecurityPolicy

PodSecurityPolicy (PSP) was deprecated in Kubernetes v1.21 and removed in v1.25. Pod Security Admission is the built-in replacement. It is simpler, uses namespace labels instead of RBAC-bound policies, and is enabled by default in modern Kubernetes clusters.

Pod Security Standards Levels

Level Comparison Table

ControlPrivilegedBaselineRestricted
HostProcess (Windows)AllowedDeniedDenied
Host Namespaces (hostNetwork, hostPID, hostIPC)AllowedDeniedDenied
Privileged ContainersAllowedDeniedDenied
CapabilitiesUnrestrictedCannot add beyond defaults; cannot add SYS_ADMINMust drop ALL; only NET_BIND_SERVICE can be added
HostPortUnrestrictedLimited rangeLimited range
AppArmorAnyRuntimeDefault or definedRuntimeDefault or defined
SELinuxAnyCannot set custom typeCannot set custom type
/proc Mount TypeAnyDefault onlyDefault only
SeccompAnyAnyRuntimeDefault or Localhost required
SysctlsAnyLimited safe setLimited safe set
Volume TypesAnyRestricted (no hostPath)Further restricted
Privilege EscalationAllowedAllowedMust be false
Running as RootAllowedAllowedMust run as non-root

Pod Security Admission Controller

The PSA controller is a built-in Kubernetes admission controller (enabled by default since v1.23 as beta, GA in v1.25) that enforces Pod Security Standards using namespace labels.

Enforcement Modes

PSA supports three modes that can be applied independently:

ModeBehaviorLabel
enforcePods violating the policy are rejectedpod-security.kubernetes.io/enforce
auditViolations are logged in the API server audit log, but pods are allowedpod-security.kubernetes.io/audit
warnViolations generate a warning shown to the user, but pods are allowedpod-security.kubernetes.io/warn

Combining Modes

Best practice is to use all three modes together. For example, enforce at baseline, warn and audit at restricted. This blocks dangerous pods while warning about pods that could be more secure.

Namespace Label Format

pod-security.kubernetes.io/<mode>: <level>
pod-security.kubernetes.io/<mode>-version: <version>

Where:

  • <mode> is enforce, audit, or warn
  • <level> is privileged, baseline, or restricted
  • <version> is a Kubernetes version like v1.30 or latest

Applying Pod Security Standards

Example 1: Enforce Baseline

bash
# Label namespace to enforce baseline
kubectl label namespace my-namespace \
  pod-security.kubernetes.io/enforce=baseline \
  pod-security.kubernetes.io/enforce-version=latest
yaml
# Or apply via namespace manifest
apiVersion: v1
kind: Namespace
metadata:
  name: my-namespace
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: latest

Example 2: Enforce Restricted

yaml
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/audit: restricted
    pod-security.kubernetes.io/audit-version: latest
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest
yaml
apiVersion: v1
kind: Namespace
metadata:
  name: staging
  labels:
    # Enforce baseline -- block clearly dangerous pods
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: latest
    # Warn and audit at restricted -- show what needs fixing
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: latest

Example 4: Privileged Namespace (System Components)

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: kube-system
  labels:
    pod-security.kubernetes.io/enforce: privileged
    pod-security.kubernetes.io/enforce-version: latest

kube-system Namespace

System components in kube-system (kube-proxy, CNI plugins, etc.) often need privileged access. The kube-system namespace typically uses the privileged level. Never apply restricted to kube-system without careful testing.

Testing Pod Security Admission

Test 1: Baseline Enforcement

bash
# Create a namespace with baseline enforcement
kubectl create namespace test-baseline
kubectl label namespace test-baseline \
  pod-security.kubernetes.io/enforce=baseline

# This pod will be REJECTED (privileged: true violates baseline)
kubectl -n test-baseline run test-privileged \
  --image=nginx --overrides='{
    "spec": {
      "containers": [{
        "name": "nginx",
        "image": "nginx",
        "securityContext": {"privileged": true}
      }]
    }
  }'
# Error: pods "test-privileged" is forbidden: 
# violates PodSecurity "baseline:latest": privileged

# This pod will SUCCEED (no privileged settings)
kubectl -n test-baseline run test-normal --image=nginx
# pod/test-normal created

Test 2: Restricted Enforcement

bash
# Create a namespace with restricted enforcement
kubectl create namespace test-restricted
kubectl label namespace test-restricted \
  pod-security.kubernetes.io/enforce=restricted

# This simple pod will be REJECTED (doesn't meet restricted requirements)
kubectl -n test-restricted run test-nginx --image=nginx
# Error: pods "test-nginx" is forbidden: violates PodSecurity "restricted:latest":
# allowPrivilegeEscalation != false, unrestricted capabilities, 
# runAsNonRoot != true, seccompProfile

# This fully-hardened pod will SUCCEED
cat <<'EOF' | kubectl apply -n test-restricted -f -
apiVersion: v1
kind: Pod
metadata:
  name: restricted-pod
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: python:3.12-slim
    command: ["python", "-m", "http.server", "8080"]
    securityContext:
      allowPrivilegeEscalation: false
      capabilities:
        drop:
          - ALL
EOF
# pod/restricted-pod created

Test 3: Warning Mode

bash
# Create namespace with warn at restricted
kubectl create namespace test-warn
kubectl label namespace test-warn \
  pod-security.kubernetes.io/warn=restricted

# Create a pod that violates restricted
kubectl -n test-warn run test-nginx --image=nginx
# Warning: would violate PodSecurity "restricted:latest": 
# allowPrivilegeEscalation != false, unrestricted capabilities,
# runAsNonRoot != true, seccompProfile
# pod/test-nginx created   <-- Pod is still created!

What Each Level Restricts in Detail

Baseline Level Restrictions

The Baseline policy prevents known privilege escalation vectors. Pods must not:

yaml
# All of these are DENIED under Baseline:

# 1. No privileged containers
securityContext:
  privileged: true          # DENIED

# 2. No host namespaces
spec:
  hostNetwork: true         # DENIED
  hostPID: true             # DENIED
  hostIPC: true             # DENIED

# 3. No hostPath volumes
volumes:
- name: host
  hostPath:                 # DENIED
    path: /

# 4. No adding SYS_ADMIN capability
securityContext:
  capabilities:
    add: ["SYS_ADMIN"]     # DENIED

# 5. No Unmasked procMount
securityContext:
  procMount: Unmasked       # DENIED

# 6. Restricted hostPort range
ports:
- hostPort: 9999            # Limited (0-65535 range check)

Restricted Level Restrictions (in Addition to Baseline)

yaml
# Everything from Baseline PLUS:

# 1. Must drop ALL capabilities (only NET_BIND_SERVICE allowed to add)
securityContext:
  capabilities:
    drop: ["ALL"]           # REQUIRED
    add: ["NET_RAW"]        # DENIED (only NET_BIND_SERVICE allowed)

# 2. Must run as non-root
securityContext:
  runAsNonRoot: true        # REQUIRED
  # OR runAsUser with non-zero value

# 3. Must disable privilege escalation
securityContext:
  allowPrivilegeEscalation: false  # REQUIRED

# 4. Must set seccomp profile
securityContext:
  seccompProfile:
    type: RuntimeDefault    # REQUIRED (RuntimeDefault or Localhost)

# 5. Limited volume types (only these allowed):
# - configMap, csi, downwardAPI, emptyDir, ephemeral,
#   persistentVolumeClaim, projected, secret
volumes:
- name: data
  hostPath:                 # DENIED under Restricted
    path: /data

PSS Enforcement Flow

Migration from PodSecurityPolicy to PSA

Migration Path

PodSecurityPolicy was removed in Kubernetes v1.25. If you are working with clusters that previously used PSP, here is the migration approach:

Step 1: Audit Current State

bash
# Check if PSP is still in use
kubectl get psp 2>/dev/null

# Map PSP policies to PSS levels
# PSP with: privileged: false, runAsUser: MustRunAsNonRoot,
#           allowPrivilegeEscalation: false
# Maps to: PSS Restricted

# PSP with: privileged: false (only)
# Maps to: PSS Baseline

# PSP with: privileged: true
# Maps to: PSS Privileged

Step 2: Apply PSA Labels in Warn Mode First

bash
# Apply warn labels to all namespaces
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
  kubectl label namespace $ns \
    pod-security.kubernetes.io/warn=baseline \
    pod-security.kubernetes.io/warn-version=latest \
    --overwrite
done

Step 3: Check for Warnings

bash
# Create test pods in each namespace and check for warnings
kubectl -n <namespace> run test --image=nginx --dry-run=server -o yaml

# Review audit logs for violations
kubectl logs -n kube-system kube-apiserver-<node> | grep "PodSecurity"

Step 4: Enforce

bash
# Once validated, switch to enforce mode
kubectl label namespace production \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest \
  --overwrite

Common Exam Scenarios

Scenario 1: Label a Namespace to Enforce Restricted

bash
# Task: Ensure all pods in namespace "secure-apps" meet the restricted standard

kubectl label namespace secure-apps \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest

# Verify
kubectl get namespace secure-apps --show-labels

Scenario 2: Create a Namespace with Baseline Enforcement

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: baseline-ns
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest

Scenario 3: Fix a Pod That Violates Restricted Standard

yaml
# Original pod (violates restricted):
apiVersion: v1
kind: Pod
metadata:
  name: web-app
spec:
  containers:
  - name: web
    image: nginx:1.27
    # Missing: seccompProfile, runAsNonRoot, capabilities, allowPrivilegeEscalation

---
# Fixed pod (meets restricted standard):
apiVersion: v1
kind: Pod
metadata:
  name: web-app
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: web
    image: nginx:1.27
    securityContext:
      allowPrivilegeEscalation: false
      capabilities:
        drop:
          - ALL
        add:
          - NET_BIND_SERVICE

Scenario 4: Identify Which PSS Level a Pod Meets

bash
# Check with dry-run against a restricted namespace
kubectl label namespace test-ns \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest

# Try creating the pod -- if rejected, check the error message
kubectl -n test-ns apply -f pod.yaml --dry-run=server

# The error message tells you exactly what's wrong:
# Error: violates PodSecurity "restricted:latest":
#   allowPrivilegeEscalation != false (container "web" ...)
#   unrestricted capabilities (container "web" ...)
#   runAsNonRoot != true (pod or container "web" ...)

Quick Reference

Exam Speed Reference

Label namespace for restricted enforcement:

bash
kubectl label namespace <ns> \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest

Label namespace for baseline + warn restricted:

bash
kubectl label namespace <ns> \
  pod-security.kubernetes.io/enforce=baseline \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/warn-version=latest

Minimum pod spec for Restricted level:

yaml
spec:
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  containers:
  - securityContext:
      allowPrivilegeEscalation: false
      capabilities:
        drop: ["ALL"]

Check namespace labels:

bash
kubectl get ns <namespace> --show-labels

Dry-run test against a namespace:

bash
kubectl apply -f pod.yaml -n <namespace> --dry-run=server

Key Exam Takeaways

  1. PSS has three levels: Privileged (unrestricted), Baseline (prevent escalation), Restricted (hardened)
  2. PSA has three modes: enforce (reject), audit (log), warn (user warning)
  3. Standards are applied via namespace labels -- no custom resources needed
  4. The label format is pod-security.kubernetes.io/<mode>: <level>
  5. The Restricted level requires: runAsNonRoot, drop ALL, allowPrivilegeEscalation: false, seccompProfile
  6. Always use --dry-run=server to test if a pod meets a namespace's PSS requirements
  7. Use warn + audit at a stricter level than enforce for gradual rollout
  8. kube-system typically needs privileged level -- do not restrict it

Released under the MIT License.