Secure by Design – Rebuilding Images for OpenShift
Duration: ~20 minutes
Overview
Rebuild an image so it runs under the restricted SCC: non-root, permissions that tolerate a random UID, and an unprivileged listen port—without anyuid.
Rebuild an image on UBI so it runs non-root on a high port under restricted SCC, then build and deploy it on OpenShift without anyuid.
Why it matters
Shipping an image that needs root pushes teams toward anyuid and other SCC exceptions. Rebuild instead: non-root, high port, writable paths that work with a random UID. The same image then runs on restricted clusters without special cases.
What does it solve
-
Root-only service → escalation path
-
Fixed ownership mismatch → UID failure
-
Privileged port dependency → root need
-
Unknown base provenance → hidden CVEs
Your Mission
The public httpd image just crashed because it wants root and port 80—the same privilege attackers hope you unlock with anyuid. Rebuild on UBI so the app serves under restricted SCC, prove the UID stays non-root, and expose it without widening the blast radius.
Click each step only if you need a hint.
Open the rebuild sandbox
oc new-project 101-11-r-rebuild
Reproduce the attack-friendly image failure
oc create deployment webapp --image=httpd
oc logs -l app=webapp --tail=40 || true
| Expected failure resembles the following. |
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 10.128.2.191. Set the 'ServerName' directive globally to suppress this message
(13)Permission denied: AH00072: make_sock: could not bind to address [::]:80
(13)Permission denied: AH00072: make_sock: could not bind to address 0.0.0.0:80
no listening sockets available, shutting down
AH00015: Unable to open logs
Reason: Port 80 needs elevated privileges, root-owned log dirs, and clashes with random non-root UID injection.
Why this image is a defender’s problem:
-
Apache httpd on port 80 (privileged; pushes teams toward root)
-
Writes under root-owned paths (fails for random UIDs)
-
Not built for OpenShift restricted SCC
admission deny or crash-loop—do not “fix” with anyuid. Rebuild instead.
|
Rebuild on trusted UBI 9 httpd 2.4 (registry.access.redhat.com / registry.redhat.io). It runs non-root and listens on 8080. We copy a sample index.html; real apps place their site files the same way.
|
Build the defended image context
mkdir -p build && cd build
cat > index.html <<'EOF'
<html><body><h1>Secure UBI httpd App</h1></body></html>
EOF
cat > Dockerfile <<'EOF'
FROM registry.access.redhat.com/ubi9/httpd-24:latest
COPY index.html /var/www/html/index.html
USER 1001
EXPOSE 8080
EOF
Build inside OpenShift (no laptop privilege shortcuts)
oc new-build --name webapp --binary --strategy=docker
oc start-build webapp --from-dir=. --follow
oc get is
Roll the defended image into production position
oc set image deployment/webapp *=image-registry.openshift-image-registry.svc:5000/101-11-r-rebuild/webapp:latest
oc rollout status deployment/webapp
oc logs -l app=webapp --tail=40 || true
Prove non-root and restricted SCC still hold
oc exec deploy/webapp -- id -u
oc get pod -l app=webapp -o jsonpath='{.items[0].metadata.annotations.openshift\.io/scc}{"\n"}'
| Confirm both of the following: |
-
id -uis non-zero (NOT 0) -
SCC annotation shows
restricted-v2
What restricted-v2 denies the attacker:
-
Non-root only (no UID 0)
-
No privilege escalation; no hostPID/IPC/Network
-
Capabilities dropped; extra caps blocked
-
Default seccomp + SELinux MCS isolation
Expose the service without privilege growth
oc expose deployment webapp --port=8080 --target-port=8080
oc expose service webapp
ROUTE=$(oc get route webapp -o jsonpath='{.spec.host}')
curl -s http://$ROUTE | head -3
| Keep the project for the TLS lab (101-12). Delete only when finished with both. |
Cleanup
oc delete project 101-11-r-rebuild --wait=false
Debrief
You rebuilt for restricted SCC—non-root, high port, ownership that tolerates a random UID—then deployed without negotiating anyuid.
What breaks without this:
-
USER root/ port 80 / fixed ownership → every cluster becomes an SCC exception negotiation -
Unknown bases → hidden CVEs and drift from what you tested
Controls that matter: UBI (or maintained) bases, non-root, high ports, group-writable dirs, RHACS root policies, and Dockerfile history in GitOps.
Quick facts: fix the image once across environments; anyuid is long-term debt. Smaller images usually mean fewer CVEs.
