Fractal Techware

Guides /

Migrate a namespace to Pod Security Admission restricted

Manifests validated with kubeconform -strict against the Kubernetes 1.35 and 1.37 schemas. The Deployment was checked against the restricted profile offline, and its image was run with the same constraints (non-root, read-only root filesystem, all capabilities dropped) to confirm it starts. The kubectl commands are standard; run them against your cluster.

Pod Security Admission (PSA) is built into Kubernetes and needs no extra software. You label a namespace, and the API server checks every pod against one of three profiles: privileged (no checks), baseline (blocks the obviously dangerous: privileged containers, host namespaces, hostPath) and restricted (non-root, no privilege escalation, dropped capabilities, seccomp).

Setting enforce: restricted on a busy namespace in one step usually breaks something. This is the order that doesn’t.

Step 1: find out what would break (no changes)

A server-side dry run of the label evaluates every existing pod in the namespace against the profile and prints the violations. Nothing is changed:

kubectl label --dry-run=server --overwrite ns shop \
  pod-security.kubernetes.io/enforce=restricted

The output looks roughly like this:

Warning: existing pods in namespace "shop" violate the new PodSecurity enforce level "restricted:latest"
Warning: web-7d9c...: allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile
namespace/shop labeled (server dry run)

Run it for every namespace to get a quick inventory:

for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
  echo "== $ns"
  kubectl label --dry-run=server --overwrite ns "$ns" pod-security.kubernetes.io/enforce=restricted 2>&1 | grep Warning
done

Step 2: enforce baseline, warn and audit on restricted

Most workloads already pass baseline. Enforce that now and turn on restricted as warn and audit only:

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

Run step 1 with enforce=baseline first. If that dry run shows warnings, fix those pods before applying this.

warn and audit also check workload resources (Deployments, StatefulSets, Jobs and so on), so kubectl apply of a Deployment shows the warning immediately. enforce checks pods only, as the next steps explain.

Step 3: make workloads compliant

A Deployment that passes restricted:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: shop
spec:
  replicas: 2
  selector:
    matchLabels: {app: web}
  template:
    metadata:
      labels: {app: web}
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        fsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: web
          image: nginxinc/nginx-unprivileged:1.31-alpine
          ports:
            - containerPort: 8080
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: [ALL]
          resources:
            requests: {cpu: 50m, memory: 64Mi}
            limits: {memory: 128Mi}
          volumeMounts:
            - {name: tmp, mountPath: /tmp}
      volumes:
        - name: tmp
          emptyDir: {}

What restricted actually requires, and the field that satisfies it:

Requirement Field
Must not run as root runAsNonRoot: true (and an image or runAsUser with a non-zero UID)
No privilege escalation allowPrivilegeEscalation: false on every container
Drop all capabilities capabilities.drop: [ALL] (only NET_BIND_SERVICE may be added back)
Seccomp profile seccompProfile.type: RuntimeDefault (or Localhost)
Restricted volume types configMap, secret, emptyDir, projected, downwardAPI, persistentVolumeClaim, ephemeral, csi

readOnlyRootFilesystem and resources are not required by PSA. They are included because they are good practice, and the /tmp emptyDir is what makes a read-only root filesystem work for nginx.

Common fixes:

Step 4: enforce restricted, pinned

When the warnings stop, re-run the dry run from step 1. When it prints no warnings, switch:

apiVersion: v1
kind: Namespace
metadata:
  name: shop
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: v1.35
    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

Pin enforce-version to the Kubernetes minor you tested against. If a future release tightens the restricted profile, an upgrade will not start rejecting pods. Set warn and audit to latest so you see what the next version would reject before you bump the pinned version.

Existing pods keep running when you change the label. PSA only checks pods when they are created. The dry run in step 1 is how you find pods that would fail on their next restart.

Because enforce only checks pods, a non-compliant Deployment is still accepted by the API server. Its ReplicaSet then fails to create pods. kubectl apply succeeds (with a warning), but the rollout hangs. Look for FailedCreate events with kubectl -n shop get events or kubectl -n shop describe rs.

Validate before applying

kubeconform -strict -summary -kubernetes-version 1.35.0 namespace.yaml deployment.yaml

Kubeconform checks the schema, not the security profile. For the profile itself, use the server-side dry run in a real cluster:

kubectl apply --dry-run=server -f deployment.yaml

In a namespace with warn: restricted, a non-compliant Deployment prints Warning: would violate PodSecurity "restricted:..." followed by each failing field.

Pitfalls

Next steps