Putting Guardrails to Work – Workload Hardening (Custom SCC, Seccomp, SELinux)
Duration: ~25 minutes
Overview
You’ll author and apply a hardened SecurityContextConstraints object, combine it with seccomp and SELinux context discipline, and prove it works through positive and negative deployment tests.
Harden a workload with a custom SCC where needed, plus seccomp and SELinux controls, and verify the pod still runs under stronger constraints.
Why it matters
Default settings are like issuing every warehouse worker a multi‑tool with blades, saws, and pry hooks when they only needed a flat screwdriver. Extra capabilities become unintended weapons if someone misuses or compromises the account. Hardened runtime boundaries (SCC + seccomp + SELinux) strip the “just in case” powers that attackers love to discover after a simple pod foothold.
End goal is a lower chance a minor pod compromise escalates into node‑level panic, plus clearer evidence of least privilege for auditors.
What does it solve
Broad SCC usage silently enables:
-
Leftover Linux capabilities (each one is an exploration vector)
-
Accidental root UID usage (scripts work as root, drift ignored)
-
Looser SELinux contexts (more permissive file/process access)
-
Unfiltered syscalls (toolbox open for exploitation chains)
Your Mission
Prove restricted-plus is not enough for this workload: apply a tighter SCC, run a good pod, then watch capability-add and root-UID pods fail.
Click each step only if you need a hint.
Inspect SCC restricted
Know the baseline before you invent a custom SCC. Prefer restricted plus an image fix when that already fits.
oc get scc restricted -o yaml | head -n 40
You should see allowPrivilegedContainer: false and a non-root runAsUser strategy.
|
Apply SCC hardened-nonroot
This SCC drops all capabilities, forces a UID range, sets SELinux, and pins runtime/default seccomp. SCCs are cluster-scoped (they cannot live in a project), but an unbound SCC is inert: empty users and groups means admission will not pick it for OpenShift system pods or anyone else. The next bind step grants use only to this lab’s service accounts.
Do not add system:authenticated or system:serviceaccounts to this object. That would make it eligible for every pod, including platform workloads.
oc apply -f - <<'EOF'
apiVersion: security.openshift.io/v1
kind: SecurityContextConstraints
metadata:
name: hardened-nonroot
allowPrivilegedContainer: false
allowHostDirVolumePlugin: false
allowHostIPC: false
allowHostNetwork: false
allowHostPID: false
allowHostPorts: false
allowedCapabilities: []
defaultAllowPrivilegeEscalation: false
requiredDropCapabilities: ["ALL"]
runAsUser:
type: MustRunAsRange
uidRangeMin: 1000660000
uidRangeMax: 1000669999
fsGroup:
type: MustRunAs
ranges:
- min: 1000660000
max: 1000669999
seLinuxContext:
type: MustRunAs
seLinuxOptions:
level: s0:c26,c30
seccompProfiles:
- runtime/default
volumes:
- configMap
- emptyDir
- projected
- secret
- downwardAPI
users: []
groups: []
priority: 10
EOF
securitycontextconstraints.security.openshift.io/hardened-nonroot created (or configured). Platform namespaces keep their existing SCCs (restricted-v2, privileged, and so on).
|
Create project 201-02-w-harden
Stay in this project for the pod tests.
oc new-project 201-02-w-harden
Now using project "201-02-w-harden".
|
Bind SCC hardened-nonroot to this project’s service accounts
An SCC does nothing until identities can use it. This grant is system:serviceaccounts:201-02-w-harden only — not every service account in the cluster.
oc adm policy add-scc-to-group hardened-nonroot system:serviceaccounts:201-02-w-harden
oc create rolebinding default-edit --clusterrole=edit --serviceaccount=201-02-w-harden:default -n 201-02-w-harden || true
The SCC is now available to SAs in 201-02-w-harden. OpenShift pods in other namespaces are unchanged. default-edit lets that SA create pods for the deny tests without using your cluster-admin privileged SCC.
|
Apply pod good
Match the SCC UID range and drop privilege escalation. This pod should Run.
oc apply -n 201-02-w-harden -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: good
annotations:
seccomp.security.alpha.kubernetes.io/pod: runtime/default
spec:
securityContext:
runAsUser: 1000660001
fsGroup: 1000660001
containers:
- name: app
image: registry.access.redhat.com/ubi9/ubi
command: ["sh","-c","id; sleep 1000"]
securityContext:
allowPrivilegeEscalation: false
EOF
pod/good created.
|
Read the SCC annotation on pod good
Confirm the scheduler actually used hardened-nonroot, not restricted.
oc get pod good -n 201-02-w-harden -o jsonpath='{.metadata.annotations.openshift\.io/scc}{"\n"}'
Output is hardened-nonroot.
|
Apply pod bad-cap with NET_RAW as the project SA
Adding a capability the SCC does not allow is the attacker’s “just one more tool” request. Create as the project SA, not your bastion user. lab-user can use privileged, so the pod is admitted and you only see a PodSecurity warning — not an SCC deny.
oc create rolebinding default-edit --clusterrole=edit --serviceaccount=201-02-w-harden:default -n 201-02-w-harden || true
oc delete pod bad-cap -n 201-02-w-harden --ignore-not-found
oc apply -n 201-02-w-harden --as=system:serviceaccount:201-02-w-harden:default -f - <<'EOF' || echo DENIED
apiVersion: v1
kind: Pod
metadata:
name: bad-cap
spec:
containers:
- name: app
image: registry.access.redhat.com/ubi9/ubi
command: ["sh","-c","sleep 1000"]
securityContext:
capabilities:
add: ["NET_RAW"]
EOF
Forbidden plus DENIED is the correct result. The long SCC provider list is normal; look for NET_RAW and hardened-nonroot.
|
Error from server (Forbidden): pods "bad-cap" is forbidden: unable to validate against any security context constraint: ... capabilities.add: Invalid value: "NET_RAW" ... provider "hardened-nonroot": ... NET_RAW is not allowed
DENIED
Apply pod bad-uid with runAsUser 0 as the project SA
Root in the container is the privilege ladder from 101-04. This SCC’s UID range must reject it. Same impersonation as bad-cap.
oc delete pod bad-uid -n 201-02-w-harden --ignore-not-found
oc apply -n 201-02-w-harden --as=system:serviceaccount:201-02-w-harden:default -f - <<'EOF' || echo DENIED
apiVersion: v1
kind: Pod
metadata:
name: bad-uid
spec:
securityContext:
runAsUser: 0
containers:
- name: app
image: registry.access.redhat.com/ubi9/ubi
command: ["sh","-c","sleep 1000"]
EOF
Forbidden plus DENIED is the correct result. Look for runAsUser: Invalid value: 0 and must be in the ranges.
|
Error from server (Forbidden): pods "bad-uid" is forbidden: unable to validate against any security context constraint: ... runAsUser: Invalid value: 0: must be in the ranges: [{1000660000 1000669999}] ...
DENIED
Exec unshare on pod good
seccomp runtime/default should block a risky syscall such as unshare. Failure is the correct result.
oc exec good -- unshare -m true || echo "Blocked or not permitted (expected)"
| The command fails and the expected message prints. |
unshare: unshare failed: Function not implemented
command terminated with exit code 1
Blocked or not permitted (expected)
Debrief
Hardened SCC, seccomp, capability drops, and SELinux close gaps that restricted defaults alone leave for sophisticated workloads.
What breaks without this:
-
Leftover capabilities / loose syscalls → privilege escalation after container compromise
-
Invented custom SCCs everywhere → unmaintainable exception sprawl
|
Treat a custom SCC as an exception, not a platform default. Start from Layer the runtime controls instead of stuffing everything into the SCC:
|
|
If the workload already runs under restricted after an image fix, stop there. A second SCC is another cluster-scoped object to bind, audit, and forget. The sprawl mode is a new SCC per team “because production is special,” which becomes an unreviewable allow-list. Watch for drift after the exception ships: a later Deployment bump that adds |
