CKS — Pod Security Admission (PSA)

9 minute read

Pod Security Admission (PSA)

Pod Security Admission is the built-in admission controller that enforces Pod Security Standards. It operates at the namespace level using labels — no custom resources, no webhooks, no external dependencies. It’s been stable since Kubernetes 1.23 and is the only built-in Pod security enforcement mechanism since PSP was removed in 1.25.


1. PSA Modes

PSA supports three modes that control how violations are handled:

Mode Behavior When a Pod Violates the Level
enforce Reject the Pod Pod is denied admission — it does not run
audit Log the violation Pod is allowed to run; violation recorded in the API server audit log
warn Warn the user Pod is allowed to run; user sees a warning message in kubectl output
Pod Submission → PSA Admission Controller
                 ├── enforce: restricted → REJECT (Pod does not run)
                 ├── audit: restricted   → ALLOW + write to audit log
                 └── warn: restricted    → ALLOW + show warning to user

Why Three Modes?

You can combine modes for a gradual rollout strategy:

# Phase 1: See what would break without blocking anything
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted

# Phase 2: Enforce Baseline now, warn about Restricted
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted

# Phase 3: Full Restricted enforcement
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted

Exam Tip: The CKS exam may ask you to apply PSA labels to a namespace. Know the exact label format: pod-security.kubernetes.io/<mode>: <level>.


2. Namespace Labels

PSA is configured entirely through namespace labels. No CRDs, no YAML files, no webhook configuration.

Label Format

pod-security.kubernetes.io/<MODE>: <LEVEL>
pod-security.kubernetes.io/<MODE>-version: <VERSION>

Where:

  • <MODE> = enforce, audit, or warn
  • <LEVEL> = privileged, baseline, or restricted
  • <VERSION> = Kubernetes minor version (e.g., v1.30) or latest

Complete Namespace Example

apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: v1.30
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.30
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.30

Applying Labels with kubectl

# Apply Restricted enforcement to an existing namespace
kubectl label ns production \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/warn=restricted

# Verify labels
kubectl get ns production --show-labels

# Remove a label (if rolling back)
kubectl label ns production \
  pod-security.kubernetes.io/enforce-

3. Version Pinning

Version pinning locks the PSS definitions to a specific Kubernetes version. This prevents new restrictions added in future versions from unexpectedly breaking workloads after a cluster upgrade.

# Pin to v1.30 definitions
pod-security.kubernetes.io/enforce-version: v1.30

# Use the latest definitions (whatever the cluster version supports)
pod-security.kubernetes.io/enforce-version: latest

When to Pin

Scenario Recommendation
Production clusters with strict change control Pin to current version
Development/testing clusters Use latest
After cluster upgrade Review new PSS additions, then update pinned version

Best Practice: Always pin in production so cluster upgrades don’t silently enforce new restrictions.


4. Cluster-Level PSA Configuration

You can set default PSA behavior for all namespaces by configuring the API server’s AdmissionConfiguration.

AdmissionConfiguration File

# /etc/kubernetes/psa/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:
      usernames: []
      runtimeClasses: []
      namespaces:
      - kube-system
      - kube-node-lease
      - kube-public

Applying to the API Server

# /etc/kubernetes/manifests/kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
  name: kube-apiserver
  namespace: kube-system
spec:
  containers:
  - name: kube-apiserver
    command:
    - kube-apiserver
    - --admission-control-config-file=/etc/kubernetes/psa/admission-config.yaml
    # ... other flags
    volumeMounts:
    - name: psa-config
      mountPath: /etc/kubernetes/psa
      readOnly: true
  volumes:
  - name: psa-config
    hostPath:
      path: /etc/kubernetes/psa
      type: DirectoryOrCreate

After modifying the static Pod manifest, the kubelet automatically restarts the API server.

# Verify the API server restarted
kubectl get pods -n kube-system | grep kube-apiserver

# Verify the admission config is loaded
kubectl logs -n kube-system kube-apiserver-controlplane | grep PodSecurity

Exam Tip: The CKS exam may ask you to configure cluster-level PSA defaults. Know the file format and the API server flag --admission-control-config-file.


5. PSA Exemptions

Exemptions allow specific workloads to bypass PSA enforcement. This is essential for system components that need elevated privileges.

Exemption Types

Exemption Scope Example
usernames Specific users or service accounts system:serviceaccount:kube-system:calico-node
runtimeClasses Pods using a specific RuntimeClass gvisor (sandbox runtime is already isolated)
namespaces Entire namespaces kube-system, kube-node-lease

Configuration

# In AdmissionConfiguration
exemptions:
  usernames:
  - system:serviceaccount:kube-system:calico-node
  - system:serviceaccount:kube-system:kube-proxy
  runtimeClasses:
  - gvisor
  - kata
  namespaces:
  - kube-system
  - kube-node-lease
  - kube-public
  - monitoring       # Prometheus node-exporter needs hostNetwork

How Exemptions Work

Pod Admission Request
  ├── Is namespace exempted? → YES → Skip PSA check
  ├── Is user/SA exempted?   → YES → Skip PSA check
  ├── Is runtimeClass exempted? → YES → Skip PSA check
  └── None exempted → Apply PSA enforcement

Exemptions apply to all three modes (enforce, audit, warn). An exempted Pod won’t generate audit entries or warnings either.


6. Testing PSA: Rejection in Action

Step 1: Create a Restricted Namespace

kubectl create namespace secure-ns

kubectl label namespace secure-ns \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/warn=restricted

Step 2: Deploy a Privileged Pod (should be rejected)

# privileged-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: privileged-test
  namespace: secure-ns
spec:
  containers:
  - name: test
    image: nginx:1.27
kubectl apply -f privileged-pod.yaml

Expected output:

Error from server (Forbidden): error when creating "privileged-pod.yaml":
pods "privileged-test" is forbidden: violates PodSecurity "restricted:latest":
  allowPrivilegeEscalation != false
    (container "test" must set securityContext.allowPrivilegeEscalation=false)
  unrestricted capabilities
    (container "test" must set securityContext.capabilities.drop=["ALL"])
  runAsNonRoot != true
    (pod or container "test" must set securityContext.runAsNonRoot=true)
  seccompProfile
    (pod or container "test" must set securityContext.seccompProfile.type
     to "RuntimeDefault" or "Localhost")

Step 3: Deploy a Compliant Pod (should succeed)

# compliant-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: compliant-test
  namespace: secure-ns
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: test
    image: nginx:1.27-alpine
    securityContext:
      allowPrivilegeEscalation: false
      capabilities:
        drop: ["ALL"]
kubectl apply -f compliant-pod.yaml
# pod/compliant-test created

7. Combining Enforce + Warn for Gradual Rollout

The recommended migration strategy uses all three modes together:

Phase 1: Audit Only (Discovery)

kubectl label ns production \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/warn=restricted
  • Existing Pods keep running
  • New non-compliant Pods show warnings in kubectl
  • Violations are logged in the audit log
  • Review the audit log to identify all non-compliant workloads
# Check audit log for PSA violations
grep "PodSecurity" /var/log/kubernetes/audit/audit.log | \
  jq '{pod: .objectRef.name, namespace: .objectRef.namespace, message: .annotations["pod-security.kubernetes.io/audit-violations"]}'

Phase 2: Enforce Baseline, Warn Restricted

kubectl label ns production \
  pod-security.kubernetes.io/enforce=baseline \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted
  • Dangerous Pods (hostNetwork, privileged) are blocked immediately
  • Pods that only violate Restricted still run but show warnings
  • Developers see what needs to be fixed for Restricted compliance

Phase 3: Full Restricted Enforcement

kubectl label ns production --overwrite \
  pod-security.kubernetes.io/enforce=restricted
  • Only fully hardened Pods are allowed
  • All violations are blocked at admission

8. PSA and Workload Controllers

PSA enforces at the Pod level, but warnings and audit entries also apply to workload controllers (Deployments, DaemonSets, Jobs, etc.) that create Pods.

# A Deployment with a non-compliant Pod template triggers a warning
kubectl apply -f deployment-non-compliant.yaml
# Warning: would violate PodSecurity "restricted:latest":
#   allowPrivilegeEscalation != false ...
# deployment.apps/web created   ← Deployment is created!
#                                  But Pods will be REJECTED if enforce is set

Important: In enforce mode, the Deployment object is created, but the ReplicaSet cannot create Pods. Check ReplicaSet events:

kubectl describe rs -n production
# Events:
#   Warning  FailedCreate  ... pods "web-abc123" is forbidden:
#     violates PodSecurity "restricted:latest"

This is a common troubleshooting scenario on the CKS exam.


Summary

Component Detail
What is PSA Built-in admission controller enforcing Pod Security Standards
Configuration Namespace labels — no CRDs or webhooks needed
Modes enforce (reject), audit (log), warn (warning)
Label format pod-security.kubernetes.io/<mode>: <level>
Version pinning pod-security.kubernetes.io/<mode>-version: v1.30
Cluster defaults --admission-control-config-file on API server
Exemptions By username, runtimeClass, or namespace
Rollout strategy Start with warn/audit → enforce Baseline → enforce Restricted
Key behavior Validation only — no mutation of Pod specs

Practice Questions

Q1: Write the kubectl command to enforce the Restricted PSS level on namespace app-ns, with warnings and audit logging also set to Restricted, pinned to version v1.30.
kubectl label namespace app-ns \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=v1.30 \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/audit-version=v1.30 \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/warn-version=v1.30

To verify:

kubectl get namespace app-ns -o yaml | grep pod-security

Expected output:

labels:
  pod-security.kubernetes.io/audit: restricted
  pod-security.kubernetes.io/audit-version: v1.30
  pod-security.kubernetes.io/enforce: restricted
  pod-security.kubernetes.io/enforce-version: v1.30
  pod-security.kubernetes.io/warn: restricted
  pod-security.kubernetes.io/warn-version: v1.30
Q2: You applied enforce: restricted to a namespace. A Deployment was created successfully, but no Pods are running. What's happening and how do you diagnose it?

What’s happening:

PSA enforces at the Pod level, not the Deployment level. The Deployment and ReplicaSet objects are created, but the ReplicaSet’s attempt to create Pods is blocked by PSA.

Diagnosis steps:

# 1. Check Deployment status
kubectl get deploy -n the-namespace
# NAME   READY   UP-TO-DATE   AVAILABLE
# web    0/3     0            0

# 2. Check ReplicaSet
kubectl get rs -n the-namespace
# NAME          DESIRED   CURRENT   READY
# web-abc123    3         0         0

# 3. Check ReplicaSet events — this shows the PSA rejection
kubectl describe rs -n the-namespace web-abc123
# Events:
#   Warning  FailedCreate  replicaset-controller
#   pods "web-abc123-xyz" is forbidden:
#     violates PodSecurity "restricted:latest":
#     allowPrivilegeEscalation != false ...

# 4. Fix the Deployment's Pod template to be Restricted-compliant
kubectl edit deploy web -n the-namespace
# Add required security context fields

Key insight: Always check ReplicaSet events when Pods aren’t being created. The Deployment controller won’t surface PSA errors directly.

Q3: Write an AdmissionConfiguration file that sets cluster-wide defaults to enforce Baseline and warn/audit for Restricted, exempting kube-system and monitoring namespaces.
# /etc/kubernetes/psa/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:
      usernames: []
      runtimeClasses: []
      namespaces:
      - kube-system
      - kube-node-lease
      - kube-public
      - monitoring

Apply by adding --admission-control-config-file=/etc/kubernetes/psa/admission-config.yaml to the API server arguments and mounting the config directory.

Q4: What is the difference between enforce and warn mode? If both are set to restricted, what happens when a non-compliant Pod is submitted?
Mode Action on Non-Compliant Pod
enforce Rejects the Pod — returns a Forbidden error, Pod does not run
warn Allows the Pod — returns a warning header that kubectl displays to the user

When both are set to restricted:

kubectl apply -f non-compliant-pod.yaml

Result:

  • enforce triggers first: the Pod is rejected
  • warn also triggers: a warning message is shown

The user sees both the rejection error and the warning:

Warning: would violate PodSecurity "restricted:latest": ...
Error from server (Forbidden): pods "test" is forbidden:
  violates PodSecurity "restricted:latest": ...

Why set both? The warn mode adds a human-readable warning for kubectl users, while the audit mode writes to the audit log for automated monitoring. Using all three modes together provides the most complete visibility:

  • enforce — actual enforcement (blocks Pods)
  • warn — immediate feedback to developers
  • audit — historical record for security teams
Q5: A DaemonSet in kube-system runs with hostNetwork: true and privileged: true. You want to enforce Restricted on all namespaces. How do you handle kube-system?

Option 1: Exempt kube-system via cluster-level configuration (recommended)

# In AdmissionConfiguration
exemptions:
  namespaces:
  - kube-system
  - kube-node-lease
  - kube-public

This exempts kube-system entirely — all Pods in that namespace bypass PSA.

Option 2: Apply Privileged level to kube-system via namespace label

kubectl label namespace kube-system \
  pod-security.kubernetes.io/enforce=privileged \
  --overwrite

Namespace-level labels override cluster defaults. Setting kube-system to Privileged allows all system workloads while other namespaces enforce Restricted.

Option 3: Exempt specific service accounts

# In AdmissionConfiguration
exemptions:
  usernames:
  - system:serviceaccount:kube-system:calico-node
  - system:serviceaccount:kube-system:kube-proxy

This is the most granular approach — only specific DaemonSet service accounts are exempted.

Recommendation: Use namespace-level exemption for kube-system. System components like CNI plugins, kube-proxy, and node-local-dns all require elevated privileges. Trying to restrict kube-system breaks core cluster functionality.


Previous Up Next
Pod Security Standards Pod Security Security Contexts Deep Dive

Updated: