Default Secure Behavior & Why OpenShift Differs from Vanilla Kubernetes
Duration: ~10 minutes
Overview
OpenShift assumes many container images were built for permissive environments. By default it runs containers as non-root and blocks privileged ports such as 80. Images that already follow those rules—non-root user, listen on 8080 or higher—deploy cleanly. Images that expect root or port 80 fail until you fix them or accept a risky exception.
For example, if you deploy a root-friendly public httpd image, it will fail under OpenShift defaults. If you then succeed with a UBI image on a high port and expose it with a Route, you will have a working service.
Why it matters
OpenShift does not assume your image was hardened. It applies guardrails first: random non-root UID, no bind to privileged ports. An image that ran fine on a laptop as root on port 80 hits those controls and crashes. That is useful feedback before the workload reaches production with more privilege than it needs.
Most incidents start from weak defaults, not glamorous exploits. Running non-root on high ports shrinks what an attacker can do after a compromise and makes it easier to show auditors that workloads follow least privilege.
What does it solve
Common and guardrail-less assumptions break on a shared cluster.
-
"Root is fine"
-
"Port 80 is normal"
-
"Directories are owned by root"
-
"Any public image is good enough"
OpenShift forces an explicit choice. Either rebuild for random UIDs and high ports, or document a temporary exception. This module shows both outcomes: Docker Hub httpd fails; UBI httpd succeeds without special SCCs.
Your Mission
A teammate (or a new hire following a blog post) wants to stand up a quick web service. They grab the public Docker Hub httpd image—it worked on their laptop as root on port 80—and deploy it to the shared cluster. Your job is to show why that fails under OpenShift defaults, then restore a hardened path that still serves traffic without widening privilege.
Click each step only if you need a hint.
A developer deploys a public root-friendly image
Create a project and deploy the public Docker Hub httpd image the way many people do on day one. Admission may accept it, but OpenShift’s defaults should stop it from running with root-like power.
oc new-project 101-01-httpd-demo
oc create deployment web --image=httpd
Wait a few seconds, then check whether the deployment actually stayed healthy:
oc get pods -l app=web
oc get pods -l app=web -o wide
The pod should land in CrashLoopBackOff/Error with a rising restart count—the cluster refused to run this image with the privileges it assumed on a laptop.
|
Investigate why the deployment failed
Collect evidence before anyone asks for anyuid. Logs and events are your first proof that the fail was intentional hardening, not a flaky image pull.
oc logs -l app=web --tail=50 --all-containers=true
| Expected failure resembles the following. |
AH00558: httpd: Could not reliably determine the server's fully qualified domain name...
(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
Confirm with events and pod detail:
oc get events --sort-by='.lastTimestamp' | tail -20
oc describe pod -l app=web | tail -30
Privileged port 80, root-owned log paths, and an image that assumed UID 0 all defeated by the OpenShift platform. OpenShift injected a random non-root UID, so the process could neither bind nor write where it wanted.
Confirm the UID and port the image assumed
oc create deployment does not declare a containerPort, so do not look for port 80 in the pod spec. Prove the mismatch from the container security context (non-root UID) and the logs (process tried to bind port 80).
oc get pod -l app=web -o jsonpath='Pod SC: {.items[0].spec.securityContext}{"\n"}'
oc get pod -l app=web -o jsonpath='Container SC: {.items[0].spec.containers[0].securityContext}{"\n"}'
oc get pod -l app=web -o jsonpath='runAsUser: {.items[0].spec.containers[0].securityContext.runAsUser}{"\n"}'
oc logs -l app=web --tail=50 --all-containers=true | grep -E 'bind to address|:80|Permission denied' || true
|
Container |
Counter with a hardened UBI image
Replace the public Docker Hub image with a Red Hat UBI image built for non-root on port 8080. No SCC escalation. You restore service while keeping the platform defaults intact.
| Defensive choice | Why it fits OpenShift defaults |
|---|---|
Non-root user |
Survives OpenShift’s random UID; shrinks post-compromise power |
High port (8080) |
Removes the need for privileged binds and caps |
Curated & supported |
Known provenance instead of an unvetted public tag |
oc set image deployment/web *=registry.access.redhat.com/ubi9/httpd-24
oc rollout status deployment/web --timeout=120s
Verify the hardened workload is actually up:
oc get pods -l app=web
oc logs -l app=web --tail=15
Prove non-root still holds, then expose safely
Before publishing the app, confirm the running process is still not UID 0. Then expose it with a Route so users get service—without relaxing the UID guard that just blocked the laptop-style image.
oc exec deploy/web -- id
oc exec deploy/web -- id -u
Expect a numeric UID such as 1000690000 (not 0). Expose a Service, then a Route, and note the hostname:
oc expose deployment creates a Service only. You need a second oc expose service step to create the publically available OpenShiftRoute.
|
oc expose deployment web --port=8080
oc expose service/web
Verify the service answers
Hit the Route and confirm the hardened image serves traffic. Success means the business still works while the privilege assumptions from the public image remain closed.
First, get the route and store in variable:
oc get route web
ROUTE=$(oc get route web -o jsonpath='{.spec.host}')
echo "http://$ROUTE"
Then, test the service with curl commands:
curl -s -o /dev/null -w "%{http_code}" "http://$ROUTE/" && echo " OK"
curl -s "http://$ROUTE/" | head -5
| The service does not have a certificate, so https traffic will fail. Use http instead. |
Debrief
You just proved why “it works on my laptop” is a weak security standard—and how OpenShift defaults force a safer image instead. A root-friendly image on port 80 failed; a UBI image on a high port under a random non-root UID restored service without widening privilege.
What breaks without this:
-
Root in the container → easier host breakout after compromise
-
Privileged port 80 → pressure for elevation or
anyuid -
Fixed UID ownership → teams reach for SCC exceptions instead of fixing the image
-
Unvetted public images → unknown CVEs and supply-chain risk
| Restricted SCC defaults, UBI (or equivalent) bases, high ports, scanning/signing, digest pins, and tracked exceptions only when you must run as root. |
local Docker often allows root and port 80—OpenShift does not. Prefer fixing the image; treat anyuid as temporary debt with an owner and expiry.
|
