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 runAsUser is a large non-zero UID (for example 1001210000) with runAsNonRoot: true, and the logs show a failed bind to port 80. That mismatch is the guardrail. Do not "fix" this by granting extra SCC power—that trains teams to bypass defaults instead of shipping an OpenShift-ready image.

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.
giphy

Cleanup

Before moving to the next module, run the lab cleanup script to reset transient resources from this module.

cd ~/openshift-security-roadshow
bash setup/lab-cleanup.sh --module 101-01