Skip to content
docktionary

Kubernetes

Pod Security Standards

A non-root USER and a dropped capability set (see Docker runtime hardening) only hold if every Pod spec actually sets them -- nothing stops the next manifest from skipping it. Pod Security Standards is how a cluster enforces that at the namespace level instead of trusting every author to remember.

Three cumulative levels

  • Privileged — no restrictions. The equivalent of no policy at all.
  • Baseline — blocks the known, obvious privilege-escalation paths (host namespaces, privileged containers, most dangerous capabilities) while still allowing a default, unspecified Pod to run without complaint.
  • Restricted — current Pod hardening best practice. Everything Baseline blocks, plus requiring the specific hardening flags that page covers to actually be set, not just allowed.

What Restricted actually requires

Specifically, every container (and init and ephemeral container) in a Pod must set:

  • runAsNonRoot: true — at the Pod level, or on every container individually.
  • runAsUser, if set at all, must not be 0.
  • allowPrivilegeEscalation: false.
  • seccompProfile.type set to RuntimeDefault or Localhost -- explicitly. Neither Unconfined nor leaving it unset at all is allowed, which is a real difference from Docker's own default of just quietly applying its default profile for you.
  • capabilities.drop must include ALL, and the only capability capabilities.add is allowed to add back is NET_BIND_SERVICE.
  • Volumes are limited to a safe set -- configMap, secret, emptyDir, persistentVolumeClaim, projected, downwardAPI, csi, and ephemeral. Notably absent: hostPath.
restricted-container.yaml
containers:
  - name: app
    image: docktionary/app:1.4.0
    securityContext:
      runAsNonRoot: true
      allowPrivilegeEscalation: false
      seccompProfile:
        type: RuntimeDefault
      capabilities:
        drop: ["ALL"]

Applying it: namespace labels, not a Pod field

Pod Security Admission -- the thing that actually reads and enforces these levels -- is built into the API server and configured entirely through labels on the Namespace object itself, with no separate policy resource to create:

namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: staging
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

Three independent modes, each with its own level: enforce rejects a violating Pod outright, audit allows it but adds an annotation to the audit log, and warn allows it but returns a warning to whoever ran kubectl apply. A namespace can run all three at different levels at once -- which is exactly how you roll this out without an outage.

Rolling it out without breaking what's already running

Setting enforce on a namespace does not retroactively touch Pods that are already running. Pod Security Admission checks happen at Pod creation (and on updates to most fields), not continuously -- an existing non-compliant Pod just keeps running undisturbed until something replaces it. That's exactly what makes a staged rollout possible:

terminal
# 1. warn + audit only -- see what would break, change nothing yet
kubectl label --overwrite ns staging \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted

# 2. Fix whatever the warnings surfaced, then flip enforcement on.
# Existing, now-compliant Pods aren't affected either way -- this
# only changes what's accepted from here on.
kubectl label --overwrite ns staging \
  pod-security.kubernetes.io/enforce=restricted

Going straight to enforce on a namespace with Pods already running skips the one step that tells you what's about to start failing -- the next deploy, not the Pods sitting there right now.

Checking a manifest before it ever reaches a cluster

A real cluster with the labels set is the authoritative check, but it needs a cluster. For a quick local pass before that -- including the specific checks this page covers, like missing runAsNonRoot or a privileged container -- paste the manifest into the Kubernetes manifest linter. Against a real cluster, a dry run against the namespace is the exact check Pod Security Admission itself would do, with no Pod actually created:

terminal
kubectl apply --dry-run=server -n staging -f deployment.yaml

What replaced PodSecurityPolicy

If you've seen PodSecurityPolicy (PSP) mentioned anywhere, it's the thing this replaced -- deprecated in Kubernetes 1.21 and removed entirely in 1.25. PSP was its own cluster-scoped resource with its own RBAC-based binding model, which was a large part of why it was hard to use correctly; Pod Security Admission trades that flexibility for the three fixed levels above, applied with a label instead of a resource and a binding.