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
securityContextfields 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
securityContextdefined at all - Image built to run as
root(UID 0) with noUSERdirective in Dockerfile runAsNonRoot: trueset on pod but container imageUSERis still0allowPrivilegeEscalationnot explicitly set tofalseon a cluster runningrestricted-v2- Deploying to a namespace where only
restrictedSCC 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:
- Escapes to the node via a known container runtime CVE (runc, crun) — root in container + writable
/proc= node compromise. - Reads
/var/lib/kubelet/pods/*/volumes/kubernetes.io~secret/— harvests service account tokens from co-located pods. - Uses harvested tokens to pivot: calls the Kubernetes API as other service accounts, potentially cluster-admin if RBAC is loose.
- 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: 1001must match a UID that exists in the image or the image must be built withUSER 1001in the Dockerfile. Verify withdocker 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.