If you learned Kubernetes on kind or minikube and then deploy the same manifests to OpenShift, the first surprise is often not Ingress or Routes. It is a Pod that refuses to start with a message about Security Context Constraints. The container image worked elsewhere. The YAML looks fine. OpenShift says no.

That reaction is normal. OpenShift is still Kubernetes, but it adds an admission layer that vanilla clusters handle differently. Security Context Constraints — SCCs — decide which Pods may run as root, which Linux capabilities they may keep, which volume types they may mount, and whether they may use host paths or privileged mode. The scheduler and kubelet matter, but for many OpenShift failures the SCC check happens earlier, at admission time.

This post is for the moment when a Deployment is stuck at zero ready replicas and the Events tab mentions unable to validate against any security context constraint. The goal is not to memorise every SCC field. The goal is to understand why SCCs exist, recognise the common failure patterns, and debug denials without defaulting to anyuid because it was the fastest fix.

Why SCCs exist when Pod securityContext already does

Kubernetes lets you set securityContext on a Pod or container: run as a non-root UID, drop capabilities, forbid privilege escalation, choose a seccomp profile. That is necessary but not always sufficient on a shared platform. A manifest can omit securityContext entirely and still ask the API server to create a Pod. Someone has to enforce defaults and upper bounds.

Pod Security Standards and Pod Security Admission on upstream Kubernetes push namespaces toward restricted, baseline, or privileged profiles through labels. OpenShift predates much of that ecosystem and implements its own model. SCCs are cluster-scoped objects. Admission checks every Pod against the set of SCCs the Pod’s ServiceAccount is allowed to use. The first matching SCC wins. If none match, the Pod is rejected.

That design serves platform teams. A developer namespace can default to restricted while infrastructure namespaces use tighter or looser profiles as policy requires. The enforcement is centralized. You cannot bypass it by forgetting to label a namespace, the way PSA sometimes allows if nobody applied labels yet.

SCCs also encode OpenShift-specific concerns: which UIDs are valid on the platform, whether host networking is permitted, whether an arbitrary FSGroup is allowed on volumes, and how supplemental groups behave. If you only read generic Kubernetes security docs, you may fix runAsUser in YAML and still fail because the SCC does not allow the volume type or the requested UID range.

Think of securityContext as what the workload requests. Think of the SCC as what the platform permits for that ServiceAccount. Both must align.

What an SCC actually controls

An SCC is a policy document. Important fields you will see in oc describe scc restricted and in documentation:

  • runAsUser — strategy MustRunAsRange with a min/max UID, or RunAsAny, or MustRunAs for a fixed UID.
  • fsGroup — same idea for volume ownership groups.
  • supplementalGroups — extra groups added to the container process.
  • allowPrivilegedContainer — whether privileged: true is permitted.
  • allowPrivilegeEscalation — whether a process can gain more privileges than its parent.
  • allowedCapabilities / requiredDropCapabilities — which Linux capabilities may be added or must be dropped.
  • readOnlyRootFilesystem — whether the root filesystem must be read-only.
  • volumes — allowed volume types such as configMap, secret, persistentVolumeClaim, emptyDir, projected; host paths are usually restricted.
  • allowHostNetwork, allowHostPID, allowHostIPC — host namespace sharing.
  • seLinuxContext — SELinux labeling strategy on RHEL CoreOS nodes.

You do not need every field on day one. When a Pod fails, start with runAsUser, fsGroup, capabilities, and volumes. Those four account for most tickets I have seen.

List SCCs and inspect the defaults:

oc get scc
oc describe scc restricted
oc describe scc anyuid
oc describe scc nonroot

The names are hints, not promises. Always read the live object in your cluster. Upgrades can adjust defaults.

restricted, anyuid, and when custom SCCs make sense

OpenShift ships several built-in SCCs. Three names appear constantly in practice.

restricted is the default for many namespaces. It runs Pods as an arbitrary UID from a platform-defined range, drops all capabilities, disallows privileged containers, and limits volume types. It aligns with the spirit of Kubernetes restricted Pod Security. Images that assume UID 0, expect to write to /, or need CAP_NET_BIND_SERVICE without declaring it often fail here. That is intentional.

anyuid allows running as any UID, including 0. It is the well-known escape hatch when a vendor image hard-codes root and cannot be rebuilt quickly. It is also broader than most applications need. Granting anyuid to a ServiceAccount solves many startup failures and removes much of what restricted was protecting. Platform teams often treat it as a reviewed exception, not a default for every namespace.

nonroot sits between the two in spirit: the container must run as a non-zero UID, but the exact UID is more flexible than restricted in some configurations. Some clusters use it for legacy apps that cannot use the random UID range but no longer require root.

Beyond builtins, custom SCCs let you express least privilege for one team or one application: allow only NET_BIND_SERVICE, permit a specific UID range, allow emptyDir and PVC but not hostPath. Custom SCCs are the long-term answer when restricted is almost right and anyuid is far too wide.

Compare strategies side by side:

# Fragment — restricted-style (conceptual; real SCCs are cluster objects, not Pod YAML)
runAsUser:
  type: MustRunAsRange
  uidRangeMin: 1000660000
  uidRangeMax: 1000669999
requiredDropCapabilities:
  - KILL
  - MKNOD
  - SETUID
  - SETGID
allowPrivilegedContainer: false
volumes:
  - configMap
  - downwardAPI
  - emptyDir
  - persistentVolumeClaim
  - projected
  - secret
# Fragment — anyuid-style (conceptual)
runAsUser:
  type: RunAsAny
allowPrivilegedContainer: false
# Still not the same as privileged; read the full SCC

When someone proposes anyuid, ask what specifically failed under restricted. If the answer is “it runs as root,” the next question is whether the image can be rebuilt with a non-root user. If the answer is “it binds port 80,” the fix may be a custom SCC that adds NET_BIND_SERVICE or a Service targetPort above 1024 — not full root.

How SCCs attach to your Pod

Pods do not reference an SCC by name in their manifest. Admission derives allowed SCCs from the Pod’s ServiceAccount. OpenShift binds SCCs to users and groups through RBAC-like resources: ClusterRoleBindings such as system:openshift:scc:restricted grant the ability to use a named SCC.

Typical flow:

  1. You create a Pod (or Deployment) with serviceAccountName: myapp.
  2. Admission loads SCCs that myapp may use.
  3. Admission tries to match Pod securityContext and spec fields against each SCC.
  4. The first valid match is recorded on the Pod annotation openshift.io/scc.

Check which SCC a running Pod received:

oc get pod myapp-7d4f8b9c6-xk2lm -o jsonpath='{.metadata.annotations.openshift\.io/scc}'; echo
oc get pod myapp-7d4f8b9c6-xk2lm -o yaml | grep -A2 openshift.io/scc

See which SCCs a ServiceAccount can use:

oc adm policy who-can use scc restricted -n myproject
oc describe sa myapp -n myproject
oc get rolebinding,clusterrolebinding -n myproject | grep myapp

New namespaces often get the restricted SCC for default ServiceAccounts through the system:openshift:scc:restricted binding. If your Pod uses a custom ServiceAccount with no SCC binding, it may match nothing — a common “works in default, fails when I created a SA” story.

Grant a ServiceAccount access to an SCC (platform admin operation):

oc adm policy add-scc-to-user restricted -z myapp -n myproject
oc adm policy add-scc-to-user anyuid -z legacy-batch -n myproject

Prefer adding a custom SCC to a named group rather than sprinkling anyuid on individual accounts when many workloads share the same gap.

Common Pod failures that look like application bugs

SCC denials often surface as CreateContainerConfigError, FailedCreate, or Deployments with replicas stuck at zero. The container never starts, so there are no application logs — only Events.

Run as root. Image Dockerfile ends with USER root or omits USER entirely and the entrypoint expects UID 0. restricted rejects it. Fix: rebuild the image to run as non-root, or set securityContext.runAsUser to a value in the allowed range if the image supports arbitrary UIDs.

Fixed UID outside the range. Some images expect UID 1001 because the vendor said so. OpenShift’s range may not include 1001. Fix: adjust runAsUser to a value in range, or request a custom SCC range aligned with the image.

Writeable root filesystem. readOnlyRootFilesystem: true in the SCC or Pod spec conflicts with apps that write logs or temp files under /. Fix: mount emptyDir for /tmp or logs, rebuild paths, or relax read-only in a custom SCC if justified.

Capabilities. Binding to ports below 1024 without NET_BIND_SERVICE, or images that add CHOWN/SETUID. Fix: drop the capability need (listen on 8080) or allow only the capability in a custom SCC.

Forbidden volume types. hostPath, NFS in some setups, or exotic volume plugins. Fix: use PVC, ConfigMap, or Secret instead; hostPath on worker nodes is rarely appropriate for app teams.

FSGroup and volume permissions. Pod sets fsGroup that the SCC disallows, or storage permissions do not match the assigned UID. Fix: align fsGroup with SCC strategy and ensure PVC mount permissions support the group.

Example Deployment that often works under restricted without root:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: myproject
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      serviceAccountName: web
      containers:
        - name: app
          image: registry.example.org/shop/web:2.4.1
          ports:
            - containerPort: 8080
          securityContext:
            allowPrivilegeEscalation: false
            runAsNonRoot: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}

If that Pod still fails, the image itself may still start as root internally. Inspect with oc describe pod and image metadata, not only the YAML you wrote.

How to debug SCC denials step by step

When Events mention SCC validation, treat it as an admission problem. Debugging is repeatable.

1. Read Events first.

oc describe pod failing-pod-name -n myproject
oc get events -n myproject --sort-by='.lastTimestamp' | tail -20

Look for text like unable to validate against any security context constraint or provider restricted: .containers[0].runAsUser: Invalid value. The provider name tells you which SCC almost matched and which field failed.

2. Compare Pod spec to the SCC.

oc get pod failing-pod-name -n myproject -o yaml > /tmp/pod.yaml
oc describe scc restricted

Walk field by field: UID strategy, capabilities, volumes, privileged, host namespaces.

3. Check the ServiceAccount’s SCC permissions.

oc describe sa web -n myproject
oc adm policy who-can use scc anyuid -n myproject
oc auth can-i use scc/restricted --as=system:serviceaccount:myproject:web -n myproject

If the ServiceAccount cannot use any SCC that fits the Pod, admission fails even when the Pod spec is otherwise reasonable.

4. Use audit logs on clusters where you have access. OpenShift audit entries may record SCC admission decisions at a detail level Events omit. Your platform team may need to run this query; it is still worth knowing the trail exists.

5. Reproduce with an minimal Pod. Strip init containers, volumes, and sidecars until a bare Pod fails or succeeds. Add pieces back. SCC errors are easier to read on a five-line Pod than on a Helm chart with twelve templates.

oc run scc-test --image=registry.example.org/shop/web:2.4.1 \
  --restart=Never -n myproject --overrides='
{
  "spec": {
    "serviceAccountName": "web",
    "containers": [{
      "name": "scc-test",
      "image": "registry.example.org/shop/web:2.4.1"
    }]
  }
}'
oc describe pod scc-test -n myproject
oc delete pod scc-test -n myproject

6. Fix in the right order. Adjust the Pod spec and image first. Then a custom SCC. Then, with review, a broader builtin like anyuid. Document why.

Least privilege when restricted is almost enough

Platform security is not a debate between restricted and anyuid only. Most mature teams spend their time on custom SCCs and image hardening.

Hardening first: non-root USER in the Dockerfile, listen on non-privileged ports, write temp data to mounted volumes, document required capabilities. Many commercial images ship runAsUser: auto friendly layouts once you read the vendor’s OpenShift guide.

When you need a custom SCC, copy the closest builtin and narrow or widen one concern:

allowHostDirVolumePlugin: false
allowHostIPC: false
allowHostNetwork: false
allowHostPID: false
allowHostPorts: false
allowPrivilegeEscalation: false
allowPrivilegedContainer: false
allowedCapabilities:
  - NET_BIND_SERVICE
defaultAddCapabilities: null
requiredDropCapabilities:
  - KILL
  - MKNOD
  - SETUID
  - SETGID
runAsUser:
  type: MustRunAsRange
  uidRangeMin: 1000660000
  uidRangeMax: 1000669999
users: []
groups: []

Apply with platform process: create the SCC cluster-wide, then bind it to a group:

oc create -f custom-net-bind-scc.yaml
oc adm policy add-scc-to-group custom-net-bind-scc system:serviceaccounts:myproject

Review custom SCCs periodically. An SCC created for one release often outlives the constraint that justified it. Remove NET_BIND_SERVICE when the app moves to 8080. Remove anyuid when the image is rebuilt.

For day-to-day work, keep a short note in the repo: required securityContext, which SCC the ServiceAccount uses, and why. Future you during an incident should not rediscover that the batch job needs an exception.

Practical checklist

Before opening a platform ticket or granting anyuid:

  1. Read Pod Events and identify the failing SCC provider and field.
  2. Confirm the ServiceAccount can use at least one SCC that could match.
  3. Inspect the image user, ports, and volume writes — not only Deployment YAML.
  4. Try runAsNonRoot, dropped capabilities, and emptyDir for temp paths.
  5. Prefer a custom SCC over anyuid when one capability or UID range is the only gap.
  6. Record the exception: who approved it, expiry review date, owning team.

SCCs are not an arbitrary OpenShift quirk. They are the cluster’s way of saying which runtime power each workload is allowed to exercise. Once you read denials as structured feedback instead of mystery errors, OpenShift security feels less like a wall and more like a checklist — strict, but learnable.