Initializing Enclave...

Fixing Kubernetes PodSecurityAdmission 'runAsUser: 0' Violation Under the Restricted Policy (K8s 1.29+)

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH (deployment hard-blocked) | Time to Fix: 10–20 mins


TL;DR

  • What broke: Kubernetes PodSecurityAdmission (PSA) enforcing the restricted policy in 1.29 hard-rejects any pod spec with runAsUser: 0 (root) — the pod never schedules, deployment stalls immediately.
  • How to fix it: Set runAsNonRoot: true + a non-zero runAsUser (≥1000 recommended) in every securityContext block, and satisfy all remaining restricted profile requirements (allowPrivilegeEscalation: false, capabilities.drop: [ALL], seccompProfile.type: RuntimeDefault).
  • Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your failing manifest and get a compliant YAML back without sending your config to any external server.

The Incident (What does the error mean?)

Raw admission webhook rejection:

Error from server (Forbidden): error when creating "deployment.yaml":
pods "api-server-6d4f8b9c7-x2k9p" is forbidden:
violation policy check failed for pod: [{"message":"violation: pod must not set runAsUser to 0","reason":"Forbidden"}]

Starting with Kubernetes 1.25, PSA replaced PodSecurityPolicy (PSP). In 1.29, many clusters that migrated from PSP with enforce: baseline are now being tightened to enforce: restricted — and the restricted profile is unambiguous: root UID is forbidden, full stop. The admission controller fires synchronously at kubectl apply time. The pod object is never written to etcd. Your Deployment, StatefulSet, or Job sits at 0/N replicas with no further error surface unless you check events.

Check events immediately:

kubectl get events -n <namespace> --field-selector reason=FailedCreate --sort-by='.lastTimestamp'

The Attack Vector / Blast Radius

This is not a theoretical risk. A container running as UID 0 inside a pod means:

  • Container escape = full node compromise. If any CVE allows a container breakout (runc, containerd, kernel vuln), the attacker lands on the node as root. No privilege escalation step required.
  • Shared PID namespace abuse. Root containers can ptrace or kill processes in other containers on the same node if hostPID: true is ever set, or signal processes via /proc.
  • Volume mount privilege abuse. Root can read/write hostPath mounts, emptyDir volumes shared with sidecar containers, and potentially access service account tokens of other pods via filesystem traversal.
  • Blast radius in multi-tenant clusters: One compromised root container in a namespace can pivot to kube-system via node-level access, exfiltrate all secrets stored in etcd if the node has kubelet credentials, and laterally move to cloud provider metadata APIs (IMDS) to steal IAM credentials.

The PSA restricted policy is the correct defense-in-depth layer. The violation being surfaced now is the policy working as designed — your image or chart was always misconfigured.


How to Fix It (The Solution)

Basic Fix — Pod/Deployment securityContext

The minimal change to pass restricted admission:

 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: api-server
 spec:
   template:
     spec:
+      securityContext:
+        runAsNonRoot: true
+        runAsUser: 1000
+        runAsGroup: 1000
+        fsGroup: 1000
+        seccompProfile:
+          type: RuntimeDefault
       containers:
         - name: api-server
           image: myrepo/api-server:2.1.4
           securityContext:
-            runAsUser: 0
+            runAsUser: 1000
+            runAsNonRoot: true
+            allowPrivilegeEscalation: false
+            capabilities:
+              drop:
+                - ALL

⚠️ If your image's entrypoint requires root (e.g., binding port <1024, writing to /etc, running apt): fix the Dockerfile. Bind to port 8080+, use USER 1000 in the image, and move any root-required init logic to an init container with a tightly scoped securityContext — or better, eliminate it.

Enterprise Best Practice — Namespace-Level PSA Labels + OPA Gatekeeper Backstop

Don't rely solely on PSA labels. Layer it:

1. Enforce at namespace level with dry-run warn before hard enforce:

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

2. OPA Gatekeeper ConstraintTemplate as a second enforcement layer:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPAllowedUsers
metadata:
  name: psp-pods-no-root-users
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    runAsUser:
      rule: MustRunAsNonRoot

3. Validate before apply with kubectl --dry-run:

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

The server-side dry-run runs admission webhooks including PSA. If it fails here, it will fail in production. Make this mandatory in your CI pipeline.


💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

The only acceptable answer is shift-left. This violation must never reach a running cluster.

1. Checkov (fastest to add)

# In your GitHub Actions / GitLab CI pipeline
pip install checkov
checkov -f deployment.yaml --check CKV_K8S_6,CKV_K8S_8,CKV_K8S_9,CKV_K8S_28,CKV_K8S_30

Key checks: CKV_K8S_6 = do not admit root containers, CKV_K8S_28 = non-root must be enforced, CKV_K8S_30 = seccomp profile required.

2. Kyverno ClusterPolicy (in-cluster prevention)

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-run-as-non-root
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-runAsNonRoot
      match:
        resources:
          kinds: [Pod]
      validate:
        message: "Containers must not run as root. Set runAsNonRoot: true and runAsUser >= 1000."
        pattern:
          spec:
            containers:
              - securityContext:
                  runAsNonRoot: true
                  runAsUser: ">=1000"

3. Helm chart linting with helm lint + kubeconform

If you're shipping Helm charts, add kubeconform with the PSS restricted schema to your chart CI:

helm template my-release ./chart | kubeconform -strict -kubernetes-version 1.29.0

4. Dockerfile enforcement

# Every application Dockerfile must end with this
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 --ingroup appgroup appuser
USER 1001

Enforce this in your base image pipeline. If USER is not set or is set to 0 or root, fail the image build in CI using a hadolint rule:

hadolint --error DL3002 Dockerfile
# DL3002: Last USER should not be root

Bottom line: The restricted PSA profile is non-negotiable in any cluster handling real workloads. Every image in your registry should be buildable and runnable as UID ≥ 1000. Audit your base images now — node:lts, python:3.12, and many vendor images still default to root. Pin to -slim or -nonroot variants, or build your own hardened base.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →