Initializing Enclave...

Fixing OpenShift 'forbidden: unable to validate against any security context constraint' for Non-Root Container Images

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–30 mins depending on cluster RBAC access

TL;DR

  • What broke: Your pod's securityContext (or lack of one) doesn't satisfy any SCC bound to the pod's service account — OpenShift's admission controller hard-rejects the pod before it schedules.
  • How to fix it: Either patch the pod spec to match an existing SCC (restricted, nonroot, anyuid) or create a custom SCC and bind it to the workload's service account via RBAC.
  • Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your failing pod/deployment YAML and get a corrected spec with the right securityContext fields injected.

The Incident (What Does the Error Mean?)

Raw error from oc describe pod or oc get events:

Error creating: pods "my-app-7d9f8b-xkp2q" is forbidden:
unable to validate against any security context constraint:
[provider "anyuid": Forbidden: not usable by user or serviceaccount,
 provider "restricted": Forbidden: containers may not run as root,
 provider "nonroot": Forbidden: container has runAsNonRoot=false]

OpenShift's admission webhook evaluated every SCC bound to the pod's service account and the pod failed all of them. The pod never reaches Pending — it is rejected at the API server. Your deployment stalls at 0/N ready. If this is a rollout, the previous ReplicaSet stays live (best case) or you have zero replicas (worst case, e.g., initial deploy).

Common triggers:

  • Upstream Helm chart written for vanilla Kubernetes, no securityContext defined at all
  • Image built to run as root (UID 0) with no USER directive in Dockerfile
  • runAsNonRoot: true set on pod but container image USER is still 0
  • allowPrivilegeEscalation not explicitly set to false on a cluster running restricted-v2
  • Deploying to a namespace where only restricted SCC is available but chart requests host ports or capabilities

The Attack Vector / Blast Radius

This error is OpenShift doing its job — but misconfiguring the fix is where the real blast radius lives.

The dangerous anti-pattern is the "just make it work" fix: binding anyuid or privileged SCC to the default service account or to system:authenticated. This silently grants every pod in the namespace the ability to run as root.

What an attacker does with a root container in a shared cluster:

  1. Escapes to the node via a known container runtime CVE (runc, crun) — root in container + writable /proc = node compromise.
  2. Reads /var/lib/kubelet/pods/*/volumes/kubernetes.io~secret/ — harvests service account tokens from co-located pods.
  3. Uses harvested tokens to pivot: calls the Kubernetes API as other service accounts, potentially cluster-admin if RBAC is loose.
  4. Exfiltrates secrets from etcd if they reach a control-plane node.

Blast radius of the wrong fix: one oc adm policy add-scc-to-group anyuid system:authenticated command can compromise every tenant on a multi-tenant cluster.


How to Fix It

Step 0 — Diagnose Which SCC Fields Are Failing

# See which SCCs are available to a service account
oc adm policy who-can use scc restricted -n my-namespace

# Simulate SCC validation (OpenShift 4.x)
oc auth can-i use scc/restricted \
  --as=system:serviceaccount:my-namespace:my-serviceaccount

# Check what the pod is actually requesting
oc get pod my-app-pod -o jsonpath='{.spec.securityContext}'
oc get pod my-app-pod -o jsonpath='{.spec.containers[*].securityContext}'

Basic Fix — Patch the Pod Spec to Satisfy restricted SCC

The restricted SCC (default on most namespaces) requires: non-root UID, no privilege escalation, no added capabilities, MustRunAsRange fsGroup.

 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: my-app
 spec:
   template:
     spec:
+      securityContext:
+        runAsNonRoot: true
+        runAsUser: 1001
+        fsGroup: 1001
+        seccompProfile:
+          type: RuntimeDefault
       containers:
       - name: my-app
         image: my-registry/my-app:latest
-        securityContext: {}
+        securityContext:
+          allowPrivilegeEscalation: false
+          readOnlyRootFilesystem: true
+          capabilities:
+            drop:
+            - ALL

⚠️ The runAsUser: 1001 must match a UID that exists in the image or the image must be built with USER 1001 in the Dockerfile. Verify with docker inspect --format='{{.Config.User}}' my-image:tag.


Enterprise Best Practice — Custom SCC + Least-Privilege RBAC Binding

Do not grant broad SCCs to the default service account. Create a dedicated service account and bind a scoped SCC.

1. Create a minimal custom SCC:

+apiVersion: security.openshift.io/v1
+kind: SecurityContextConstraints
+metadata:
+  name: my-app-scc
+allowPrivilegeEscalation: false
+allowPrivilegedContainer: false
+allowedCapabilities: []
+defaultAddCapabilities: []
+requiredDropCapabilities:
+- ALL
+fsGroup:
+  type: MustRunAs
+  ranges:
+  - min: 1001
+    max: 1001
+runAsUser:
+  type: MustRunAsRange
+  uidRangeMin: 1001
+  uidRangeMax: 1001
+seLinuxContext:
+  type: MustRunAs
+seccompProfiles:
+- runtime/default
+volumes:
+- configMap
+- secret
+- emptyDir
+- persistentVolumeClaim
+users: []
+groups: []

2. Create a dedicated service account and bind the SCC:

+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: my-app-sa
+  namespace: my-namespace
# Bind SCC to service account — NOT to default SA, NOT to system:authenticated
oc adm policy add-scc-to-user my-app-scc \
  system:serviceaccount:my-namespace:my-app-sa

3. Reference the service account in the Deployment:

 spec:
   template:
     spec:
+      serviceAccountName: my-app-sa
       securityContext:
         runAsNonRoot: true

💡 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

1. Fail the Pipeline Before It Hits the Cluster

Checkov (static YAML scan):

checkov -f deployment.yaml \
  --check CKV_K8S_30,CKV_K8S_28,CKV_K8S_8,CKV_K8S_9
# CKV_K8S_30: Do not admit root containers
# CKV_K8S_28: Do not allow privilege escalation

kube-score:

kube-score score deployment.yaml --enable-optional-test container-security-context

2. OPA/Gatekeeper Policy (Cluster-Side Enforcement)

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPAllowPrivilegeEscalationContainer
metadata:
  name: psp-deny-privilege-escalation
spec:
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["Pod"]
    namespaces: ["production", "staging"]

3. Helm Chart Hardening Checklist (add to values.yaml defaults)

# values.yaml — ship these as defaults, not opt-in
securityContext:
  runAsNonRoot: true
  runAsUser: 1001
  fsGroup: 1001
containerSecurityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: ["ALL"]

4. Dockerfile Fix at the Source

 FROM ubi8/ubi-minimal:latest
 RUN microdnf install -y nodejs
-# No USER directive = runs as root
+USER 1001
 EXPOSE 8080
 CMD ["node", "server.js"]

5. Audit Existing SCC Bindings Regularly

# Find any service accounts with privileged or anyuid — run this weekly
oc get clusterrolebindings,rolebindings -A \
  -o json | jq -r '
  .items[] | select(.subjects[]?.name | 
  test("system:serviceaccount")) | 
  select(.roleRef.name | test("anyuid|privileged")) |
  "\(.metadata.namespace)/\(.metadata.name) -> \(.roleRef.name)"
'

Pipe this into your SIEM. Any new binding to anyuid or privileged outside of openshift-* namespaces should page your on-call.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →