SCC Limits & Resource Guardrails
Duration: ~20 minutes
Overview
Some vendor or legacy images cannot be rebuilt for non-root or high ports. Learn to see why they fail under restricted SCC, what anyuid actually allows, and when a documented risk acceptance is safer than silencing the failure with a broad SCC grant.
Also apply LimitRange and ResourceQuota so namespaces get defaults and ceilings for CPU and memory, instead of unbounded requests that starve neighbors.
Prove why privileged ports and root fail under restricted SCC, then see what anyuid unlocks—and apply LimitRange plus ResourceQuota to bound CPU and memory.
Why it matters
OpenShift assigns a random non-root UID. Images that hard-code root or fixed ownership fail until someone grants anyuid or rebuilds the image. Granting anyuid gets the pod running, and it also makes root-in-container normal again. Prefer fixing or replacing the image; if you cannot, record why the exception exists and who owns it.
Without LimitRange and ResourceQuota, a single workload can request huge CPU and memory and push others out. LimitRange sets per-container defaults and maxes. ResourceQuota caps the namespace. Together they keep shared clusters predictable and make capacity problems show up as events instead of mystery outages.
Permission Requirements for This Lab
Most steps require only project-level access. Granting the anyuid SCC needs cluster-admin (or delegated). LimitRange & ResourceQuota creation usually needs elevated (cluster-admin or delegated) rights. Forbidden errors are expected guardrails.
|
What does it solve
-
Root-by-default workloads creeping into production
-
Habitual
anyuidgrants instead of image fixes -
Hard-coded UIDs that only work with SCC exceptions
-
Namespaces with no CPU/memory ceilings
-
Silent neighbor starvation and surprise scale-outs
Attackers prefer pods that already run as root. Refusing that by default removes an easy foothold.
Your Mission
Refuse the privilege ladder attackers want (anyuid, UID 0, port 80) and stop noisy-neighbor resource grabs with LimitRange and ResourceQuota. Complete Part A, then Part B.
Click each step only if you need a hint.
Part A: SCC Limits & Non-Root Realities
An unpatched vendor image wants root and port 80—exactly the privilege ladder attackers climb after breakout. Prove restricted SCC blocks it, see what anyuid would unlock if you caved, and document any unavoidable exception instead of silently normalizing root.
Open a sandbox for the privilege test
oc new-project 101-04-s-scc-demo
Attacker-shaped deploy — bind privileged port 80
oc create deployment vendor-app --image=registry.access.redhat.com/ubi9/python-311 -- python3 -m http.server 80
oc logs -l app=vendor-app --tail=50
Expect CrashLoopBackOff with PermissionError (binding port 80).
|
Force UID 0 — expect the platform to refuse
oc patch deployment vendor-app --type='json' -p='[{"op":"add","path":"/spec/template/spec/securityContext","value":{"runAsUser":0}}]'
oc rollout restart deployment/vendor-app
oc get events --sort-by=.lastTimestamp | grep vendor-app | tail -n 5 || true
| Look for invalid UID range / runAsUser errors. That refusal is the win. |
anyuid is the trap — try it only to understand the cost (cluster-admin)
Granting anyuid is how teams accidentally gift attackers root-in-container. Observe what it unlocks, then treat it as an anti-pattern.
oc adm policy add-scc-to-user anyuid -z default || echo 'Expected Forbidden if not admin'
oc rollout restart deployment/vendor-app
oc exec deploy/vendor-app -- id -u || true
oc logs -l app=vendor-app --tail=10 || true
If it succeeded (not recommended): id -u = 0 and the server listens on 80 — privilege restored for whoever owns the pod next.
If you must accept risk, make the exception loud
For third-party apps you cannot rebuild, register a discoverable risk acceptance instead of a silent, forever grant.
Multiple deployments? Prefer per-Deployment annotations; for a namespace index, use keys suffixed by deployment name.
oc annotate namespace 101-04-s-scc-demo \
openshift.io/risk-accepted.vendor-app="Requires anyuid for privileged port" \
openshift.io/risk-accepted-approved-by.vendor-app="InfoSec Team" \
--overwrite
Prove the exception is audited
oc get ns 101-04-s-scc-demo -o yaml | grep -E 'openshift.io/risk-accepted'
| Keep this for exceptional cases. The preferred path is to rebuild images so they run under the restricted SCC without anyuid. |
Cleanup
oc delete project 101-04-s-scc-demo --wait=false
=== Part B: Resource Usage (Quotas & Limits) Without quotas, one runaway or attacker-controlled scale-up eats the node’s CPU and memory—classic noisy-neighbor denial. Apply LimitRange and ResourceQuota so oversized grabs fail loudly.
Namespace: 101-04-s-resources-demo. Image: UBI sleep infinity.
oc new-project 101-04-s-resources-demo
oc project
LimitRange — default and cap every container
oc apply -f - <<'EOF'
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: 101-04-s-resources-demo
spec:
limits:
- type: Container
defaultRequest:
cpu: 100m
memory: 128Mi
default:
cpu: 500m
memory: 256Mi
min:
cpu: 50m
memory: 64Mi
max:
cpu: "1"
memory: 512Mi
maxLimitRequestRatio:
cpu: "5"
memory: "4"
EOF
Verify defaults are now enforced
oc get limitrange default-limits -o jsonpath='{.spec.limits[0]}' | sed 's/,/\n/g'
ResourceQuota — stop cluster-wide resource theft
oc create quota compute-quota \
--hard=requests.cpu=2 \
--hard=requests.memory=2Gi \
--hard=limits.cpu=4 \
--hard=limits.memory=4Gi \
--hard=pods=10
oc describe quota compute-quota | sed -n '1,25p'
Deploy without oversized requests — defaults fill in
oc create deployment stress --image=registry.access.redhat.com/ubi9/ubi -- /bin/sh -c "sleep infinity"
oc wait --for=condition=Available deployment/stress --timeout=90s
oc get pod -l app=stress -o jsonpath='{.items[0].spec.containers[0].resources}{"\n"}'
| Expect requests 100m/128Mi and limits 500m/256Mi. |
Resize within the allowed envelope
oc set resources deploy/stress --requests=cpu=300m,memory=256Mi --limits=cpu=800m,memory=512Mi
oc rollout restart deploy/stress
oc wait --for=condition=Available deployment/stress --timeout=90s
oc get pod -l app=stress -o jsonpath='{.items[0].spec.containers[0].resources}{"\n"}'
Reject the oversize grab (admission stops the DoS path)
oc set resources deployment/stress --limits=cpu=2,memory=1Gi
oc rollout restart deploy/stress
oc get deploy stress -o jsonpath='{range .status.conditions[*]}{.type}:{.reason}:{.message}{"\n"}{end}' | grep -i FailedCreate || true
oc get pod -l app=stress -o jsonpath='{.items[0].spec.containers[0].resources}{"\n"}'
oc get deploy stress -o jsonpath='{.spec.template.spec.containers[0].resources.limits}{"\n"}'
Reject replica flood beyond pod quota
oc scale deployment stress --replicas=25 || true
oc get events --sort-by=.lastTimestamp | grep -E 'FailedCreate.*compute-quota' | tail -1 || true
oc get quota compute-quota
Cleanup
oc delete project 101-04-s-resources-demo --wait=false
Debrief
You saw restricted SCC refuse root/privileged-port patterns, and LimitRange plus ResourceQuota stop oversized and noisy-neighbor deployments in a shared namespace.
What breaks without this:
-
Hard-coded root or port 80 → SCC failures or pressure for
anyuid -
Broad
anyuid→ long-lived privilege that is hard to roll back -
No LimitRange/ResourceQuota → scheduling chaos and cluster resource theft
Controls that matter: stay on restricted SCC, fix or replace images before granting exceptions, apply LimitRange defaults/caps and ResourceQuota ceilings, and audit exceptions.
Quick facts: random UIDs are intentional for portability; requests drive scheduling, limits throttle. Prefer image fixes over SCC debt.
