Right, let’s talk about Pod Security Standards. This is where we move from the abstract, hand-wavy world of “you should secure your pods” to the concrete, “here’s exactly what that means” rules of the road. The PSS are a set of three predefined security profiles that give you a sane, graduated path from “let’s just get it running” to “Fort Knox on a good day.”

Think of them as the security settings on your phone: Privileged is the developer mode you use once and regret, Baseline is the default that stops the most egregious nonsense, and Restricted is the “max security” that makes your phone ask for a fingerprint to change the volume. Kubernetes needed this because the default security posture was, frankly, a carnival ride designed by a madman. You could do anything. The PSS are the safety harnesses.

The Three Flavors of Sanity

Let’s break down what each profile actually means. They’re essentially a checklist of Pod Security Admission controls, bundled into sensible tiers.

Privileged: This is the “I renounce all protections” profile. It’s intentionally permissive and is meant for system-level workloads that absolutely need to escalate privileges, like CNI plugins or storage drivers running on the node itself. You would never, ever run your application pods under this unless you enjoy living dangerously. It’s the Kubernetes equivalent of running your web app as root because you couldn’t figure out file permissions.

Baseline: This is the default you should be starting with. It’s designed to prevent the most obvious privilege escalations while still being compatible with the vast majority of “normal” applications. It stops the truly terrifying stuff, like running as root by default or sharing the host’s process namespace. If your app breaks under the Baseline profile, it’s almost certainly doing something it shouldn’t, and you should have a very good reason for needing to bypass it.

Restricted: This is the gold standard. It builds on Baseline and enforces much stricter hardening measures, like forcing all containers to run as a non-root user and making the filesystem read-only. This is what you should be aiming for for all your production workloads. It’s a pain to get to, I won’t lie, but the reduction in your attack surface is massive.

Applying the Standards: Labels are Law

You don’t “set” these profiles on a Pod directly. Instead, you enforce them by labeling a namespace. The Pod Security Admission controller (which is built into the API server now, no need for a separate webhook) watches for Pod creation and checks it against the namespace’s rules.

Here’s how you set a namespace to enforce the Baseline profile in warn mode (more on modes in a second). This is your best first step: it’ll yell at you for violations but won’t actually stop you. It’s like a very loud backseat driver.

apiVersion: v1
kind: Namespace
metadata:
  name: my-app-namespace
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: v1.30 # Always pin the version!

The key here is pinning the version (v1.30 in this case). The standards evolve with new Kubernetes releases. Pinning the version means your security policy doesn’t change unexpectedly when you upgrade your cluster, which is a fantastic way to avoid a 3 AM outage. You can then upgrade the policy version on your own terms.

The Three Enforcement Modes

Notice I used enforce in that label. You have three modes, and they’re your real knobs for tuning:

  • enforce: The bouncer. Policy violations will be rejected outright. This is for production.
  • warn: The nagging parent. Violations trigger a user-facing warning in the API response, but the Pod is still created. Perfect for development and CI.
  • audit: The silent observer. Violations are recorded in the audit logs, but the user sees nothing. Great for discovering what messes exist before you turn on enforcement.

You can and should combine them. A great setup is to have enforce=baseline to stop the truly bad stuff, and warn=restricted to start nagging developers about getting to the higher standard.

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/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.30

The Devil’s in the Details (aka The Exemptions)

Here’s where the designers made a choice you’ll either love or hate: the entire policy is all-or-nothing on a namespace. You can’t easily say “this one Pod in this namespace is special.” This is annoying for legitimate edge cases (e.g., a initContainer that needs privileged to configure a device).

The “blessed” way to handle this is to use exemption rules in the Pod Security Admission configuration file, which is a pain to manage. The pragmatic way everyone actually does it? You put the special snowflake workload in its own dedicated namespace labeled with privileged and then lock down access to that namespace with RBAC. It’s a bit clunky, but it works and is auditable.

A Real-World Example: Breaking a Pod

Let’s see this in action. Here’s a sloppy Pod spec that would make any security engineer weep:

apiVersion: v1
kind: Pod
metadata:
  name: bad-pod
spec:
  containers:
  - name: bad-container
    image: nginx
    securityContext:
      runAsUser: 0 # Runs as root, because why not?
      privileged: true # And requests privileged mode, for extra oof.

If you try to create this in our my-app-namespace from earlier (which has enforce: baseline), the API server will instantly shut you down. You’ll get a beautifully clear error message telling you exactly what you did wrong:

Error from server (Forbidden): error when creating "bad-pod.yaml": pods "bad-pod" is forbidden: violates PodSecurity "baseline:latest": privileged (container "bad-container" must not set securityContext.privileged=true), runAsUser=0 (container "bad-container" must not set runAsUser=0)

No ambiguity. It’s not working because you’re trying to do two things the Baseline profile explicitly forbids. To fix it, you’d drop the privileged: true and set a non-root runAsUser. The PSS forces you to be explicit about your security choices, which is the entire point. It makes the secure path the default path, and that’s a very, very good thing.