Certified Kubernetes Security Specialist (CKS) #9: Pod Security Admission (PSA, Pod Security Standards)

Up through #8 kernel hardening, capabilities, /proc protection we dealt with confining nodes and containers at the Linux kernel level. Starting with this post we move into the Minimize Microservice Vulnerabilities domain and look at how to make the cluster refuse a Pod that carries dangerous settings in the first place. The first tool for that is Pod Security Admission (PSA), built into Kubernetes.

PSA is an admission controller baked into the API server — no separate install required. Attach a single label to a namespace, and every Pod that comes into that namespace is checked against a set security standard, with creation itself rejected on a violation. On the exam, the recurring patterns are “apply the restricted standard to this namespace in enforce mode” and “fix the Pod that’s being rejected so it meets the standard.”

PodSecurityPolicy is gone #

First, the background in one line. In the past a different admission controller, PodSecurityPolicy (PSP), did the same job, but it was deprecated in v1.21 — its authorization model was complex and hard to use — and fully removed in v1.25. Its replacement, stable since v1.25, is Pod Security Admission. In the current CKS exam version PSP no longer exists, so you can treat all Pod-level security standards as handled by PSA.

Unlike PSP, PSA requires no separate policy resource and no RBAC wiring. All you do is pick one of the three predefined Pod Security Standards levels and apply it with a namespace label.

The three Pod Security Standards levels #

The Pod Security Standards are standard profiles that split security strength into three levels. The higher the level, the fewer dangerous settings a Pod is allowed.

LevelMeaningAllowed scope
privilegedNo restrictionsAll settings allowed. Host namespaces, privileged containers, even arbitrary capabilities — everything is possible
baselineThe minimum line that blocks known privilege escalationForbids privileged containers, hostNetwork, hostPID, dangerous capabilities, and so on. Other default settings are mostly allowed
restrictedHardened best practiceOn top of baseline’s prohibitions, it actively requires runAsNonRoot, seccomp RuntimeDefault, capabilities drop ALL, and the like

The difference between the three levels in one line: privileged blocks nothing, baseline blocks only the obvious escape routes, and restricted enforces least privilege. privileged is normally used only for workloads that genuinely need host access — system components or node agents — while the recommended direction is to apply baseline or restricted to ordinary application namespaces.

What baseline blocks #

baseline blocks the known routes by which a container takes over a node. The main prohibited items are these.

  • Host namespace sharing such as hostNetwork, hostPID, hostIPC
  • privileged: true containers
  • hostPath volumes (with a few exceptions)
  • Dangerous added capabilities other than NET_RAW
  • Explicitly setting the seccomp profile to Unconfined

What restricted additionally requires #

restricted includes all of baseline’s prohibitions and, on top of that, requires the following to be set without fail. To write a Pod that passes restricted on the exam, this list is your checklist.

  • runAsNonRoot: true (no running as root)
  • allowPrivilegeEscalation: false
  • seccompProfile.type: RuntimeDefault (or Localhost)
  • capabilities.drop must include ALL. The only one additionally allowed back is NET_BIND_SERVICE
  • Only safe volume types, excluding types like hostPath

These settings have to go into both the Pod-level securityContext and the container-level securityContext in the right places. In particular, runAsNonRoot and seccompProfile typically go at the Pod level, while allowPrivilegeEscalation and capabilities go at the container level.

The three PSA modes #

For the same standard, you choose what happens on a violation via the mode. PSA offers three modes, and you can even apply different levels at once in a single namespace.

ModeBehaviorUse
enforceRejects creation of a Pod that violates the standard. The Pod is not createdActual blocking
auditAllows creation despite the violation, but records the violation in the audit logImpact analysis
warnAllows creation despite the violation, but shows a warning message in the kubectl responseUser guidance

In operations, before applying a new standard directly in enforce, you usually examine the impact first with warn and audit, then raise it to enforce once you’ve confirmed it’s safe. That said, the exam usually asks for enforce alone and clearly, so when a question says “apply restricted in enforce mode,” you just attach the enforce label correctly.

Apply it with a namespace label #

PSA’s unit of application is the namespace. Attach a label of the following form to a namespace and the standard applies to all Pods in it.

pod-security.kubernetes.io/<mode>: <level>
pod-security.kubernetes.io/<mode>-version: <version>
  • <mode> is one of enforce, audit, warn.
  • <level> is one of privileged, baseline, restricted.
  • The -version label pins the Kubernetes version of the standard to apply. Omit it and it acts as latest; when specifying, write it like v1.30.

For example, the labels that apply restricted in enforce mode to the app namespace look like this.

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

Applying enforce and warn together like this means a violating Pod is rejected while a human-readable warning is shown at the same time. To handle this fast on the exam, it’s worth learning how to apply it with kubectl label instead of a manifest.

# Apply enforce restricted to an already existing namespace
kubectl label namespace app \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest --overwrite

You need to add --overwrite so it can overwrite even when a different value already exists.

How a violating Pod is rejected #

When you try to create a Pod that violates the standard in a namespace with enforce on, the API server blocks it at the admission stage and returns an error message. For example, creating a plain nginx Pod with no settings in a namespace where restricted is enforced produces a rejection message similar to this.

Error from server (Forbidden): error when creating "pod.yaml": pods "nginx" is forbidden:
violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false (...),
unrestricted capabilities (...), runAsNonRoot != true (...),
seccompProfile (...) must be set to "RuntimeDefault" or "Localhost"

This message is itself the fix guide. Because the violated items are listed as-is, filling in each field named in the message one by one makes it pass. When the exam says “fix the Pod that’s being rejected,” the fast flow is to apply it as-is first to get the rejection message, then reflect the listed items into securityContext.

Pods of a Deployment that were already running are unaffected. Since PSA acts only at the admission stage, only newly created Pods get checked after you attach the label. So if you applied restricted to an existing workload, you have to redeploy the Deployment to create new Pods before any actual violation surfaces.

A Pod that passes restricted — an example #

A Pod that’s created normally even in a namespace where restricted is enforced is written as follows. The checklist above is all reflected.

apiVersion: v1
kind: Pod
metadata:
  name: secure-nginx
  namespace: app
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: nginx
      image: nginxinc/nginx-unprivileged:stable
      ports:
        - containerPort: 8080
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop:
            - ALL

Let me restate the key points.

  • Put runAsNonRoot: true and seccompProfile.type: RuntimeDefault in the Pod-level securityContext.
  • Put allowPrivilegeEscalation: false and capabilities.drop: [ALL] in the container-level securityContext.
  • The image has to be one that runs as a non-root user. The ordinary nginx image binds port 80 as root, which conflicts with runAsNonRoot. Use an unprivileged image as in the example above, or change the port to 1024 or higher.

That last item is the easy place to slip. If you fill in the securityContext fields but keep using an image that runs as root, creation passes but the container fails at the startup stage. Both a manifest that meets the standard and an image that actually runs unprivileged have to line up.

Cluster-wide defaults (AdmissionConfiguration) #

Instead of attaching a label per namespace, you can also lay down a default standard cluster-wide. You hand an AdmissionConfiguration file to the API server to set the defaults and exemptions of the PodSecurity plugin. System namespaces (such as kube-system) are usually pulled out as exemptions.

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
      exemptions:
        namespaces:
          - kube-system

You wire this file in with kube-apiserver’s --admission-control-config-file flag. That said, the common form on the exam is a per-namespace label rather than a cluster-wide setting, so it’s safer to give priority to the label approach.

Exam points #

  • PSP was removed in v1.25 and its replacement is PSA. You must not use PSP syntax.
  • The three levels get stronger in the order privileged → baseline → restricted. restricted is the strictest.
  • The three modes are enforce (reject), audit (record), warn (warning). Only enforce actually blocks.
  • The unit of application is the namespace, and the label keys are pod-security.kubernetes.io/<mode> and pod-security.kubernetes.io/<mode>-version.
  • The restricted pass checklist is runAsNonRoot: true, allowPrivilegeEscalation: false, seccompProfile.type: RuntimeDefault, capabilities.drop: [ALL].
  • Since the rejection message lists the violated items as-is, the fast flow is to apply first, get the message, and fix it exactly as listed.
  • An image that runs as root fails to start even if you fill in only the securityContext. You have to take care of the unprivileged image as well.

PSA’s hardening direction uses the same fields as the Pod-level security settings covered in CKAD #15 securityContext and Pod security; the only difference is that PSA enforces those settings at the namespace level. If CKAD was the stage where you got the recommended settings into your hands, CKS is the stage where you make the cluster check those settings.

Wrap-up #

What this post locked in:

  • After PodSecurityPolicy was retired, Pod Security Admission took its place. It’s built into the API server with no separate install.
  • The three Pod Security Standards levels (privileged, baseline, restricted) narrow the allowed scope as the level rises.
  • The three modes (enforce, audit, warn) let you choose the behavior on a violation, and the actual rejection is enforce.
  • You apply it with the namespace labels pod-security.kubernetes.io/enforce: restricted and enforce-version.
  • A Pod that passes restricted has runAsNonRoot, allowPrivilegeEscalation: false, seccomp RuntimeDefault, and capabilities drop ALL, and must run on an unprivileged image.

Next — Secrets management #

Pod security standards can now be enforced with PSA. But what about the sensitive data a Pod handles — the Secret itself? A default Secret is stored in etcd as near-plaintext base64, so anyone with access to etcd can read it as-is.

In #10 Secrets management: etcd encryption, External Secrets, we’ll work through how to encrypt Secrets stored in etcd with an EncryptionConfiguration, key rotation, and the External Secrets pattern that integrates an external secret store — all from an exam point of view.

X