The Surprise Every Developer Hits
You write a Dockerfile. You follow best practices. You set a non-root user:
|
|
You build, push, deploy to OpenShift. Then you oc exec into the pod and run id:
uid=1000620000(1000620000) gid=0(root) groups=0(root),1000620000
What happened to USER 1000?
You remove the USER directive, rebuild, redeploy. Same result — still 1000620000, and the GID is still 0 (root). Even weirder.
This is one of those moments where the platform is doing something deliberate, and understanding it saves you hours of futile Dockerfile tweaking.
Why OpenShift Does This: Security by Default
Most containers in the wild still run as root. That violates two principles that matter in production: least privilege and need to know.
If a container runs as root and an attacker compromises it, they inherit a powerful foothold: install tooling, probe the network, pivot laterally, download malware, and turn the pod into a zombie inside your cluster. If the process can only do what the application needs — read its own files, bind to its port, talk to its dependencies — the blast radius shrinks dramatically.
OpenShift enforces this at the platform level. Instead of trusting whatever USER line you put in a Dockerfile, it assigns each project a dedicated UID range and runs your pods with UIDs from that range. The USER directive in your image is effectively overridden unless you explicitly opt out through Security Context Constraints (SCCs).
What Happens Under the Hood
Project UID ranges
When OpenShift creates a project (namespace), it assigns three things:
- A UID range for
runAsUser - A supplemental GID range for shared storage access (NFS, GlusterFS)
- Unique SELinux MCS labels for multi-tenancy isolation
These ranges are unique per project — no overlap with other namespaces. You can inspect them with:
|
|
Look for annotations like:
openshift.io/sa.scc.uid-range: 1000620000/10000
openshift.io/sa.scc.supplemental-groups: 1000620000/10000
In this example, the first UID in the range is 1000620000. By default, every pod in this project starts with that UID unless the pod spec requests a different one within the assigned range.
Service accounts and SCCs
Every namespace ships with default service accounts (default, builder, deployer). When you deploy a pod without specifying a service account, OpenShift uses default, which is bound to the restricted SCC.
The restricted SCC enforces:
- Pods must run as a non-root UID from the namespace range
- Arbitrary UIDs outside the range are rejected
- Capabilities are dropped
OpenShift also provides SCCs like nonroot and anyuid for workloads that need more flexibility. Granting anyuid to a service account allows runAsUser: 0 or any custom UID — but that is a conscious security decision, not the default.
|
|
The GID=0 Detail That Breaks File Permissions
Here is the part that trips people up even after they accept the random-looking UID.
Inside the container, the effective user is the namespace UID (e.g. 1000620000), but the primary group is always GID 0 (root). OpenShift 4.x also appends the effective UID to /etc/passwd automatically, so whoami returns the numeric UID rather than a name you defined in the image.
From the Red Hat guide on OpenShift and UIDs:
The Container user is always a member of the root group, so it can read or write files accessible by GID=0.
This means your application runs as an unprivileged UID paired with the root group. It cannot escalate to host root, but it can read and write anything the root group can access inside the container filesystem.
Design implication
Directories and files that processes need to write must be:
- Owned by group
root(GID 0), or at least group-writable by GID 0 - Readable (and executable, for binaries) by the root group
Files owned exclusively by USER 1000 with mode 700 will fail silently or loudly depending on what your app tries to do.
Making Your Image OpenShift-Ready
Add permission setup to your Dockerfile so the runtime UID can actually use the filesystem:
|
|
What each line does:
| Command | Purpose |
|---|---|
chgrp -R 0 /app |
Set group ownership to root (GID 0) |
chmod -R g=u /app |
Grant the group the same permissions as the owner |
chmod g+s /app |
Set the SGID bit so new files and directories inherit GID 0 |
This pattern works for most modern applications: stateless APIs, workers, and microservices that only need to write logs, temp files, or a local cache under /app.
Two Use Cases: Native vs Legacy Workloads
Not every application fits the “arbitrary UID + GID 0” model. Here is how I think about the split.
OpenShift-native applications
These are apps you control end to end. They do not hardcode a UID, do not assume they own files by user ID, and listen on ports above 1024 (or use a Route/Ingress in front).
What to do:
- Drop the
USERdirective from the Dockerfile - Apply the
chgrp/chmodpattern above - Deploy with the default service account and
restrictedSCC - Let OpenShift assign the namespace UID
This is the path of least resistance and the most secure default.
Non-OpenShift-native applications
Legacy apps often assume a specific UID (e.g. 1000), require files owned by that exact user, or bind to privileged ports below 1024. Third-party images you cannot rebuild fall into this bucket too.
What to do depends on the constraint:
| Constraint | Approach |
|---|---|
| App requires a specific UID | Create a dedicated service account, grant the nonroot or anyuid SCC, set securityContext.runAsUser in the pod spec |
| App needs files owned by the runtime UID | Use an entrypoint wrapper that chowns directories to the allocated UID at startup |
| App binds to port < 1024 | Run as UID 0 via anyuid SCC (SELinux still isolates the container), or reconfigure the app to listen on a higher port |
| Third-party image runs as root | Avoid if possible; if unavoidable, use anyuid with a dedicated service account and document the risk |
Example pod spec for a workload that must run as UID 1000:
|
|
And the service account setup:
|
|
Hardcoding UIDs is a last resort. It creates collision risk across namespaces and fights the platform model. Refactor toward GID-0 group permissions whenever you can.
Quick Debugging Checklist
When a pod fails with permission errors in OpenShift, I check these in order:
- What UID is the process actually running as? —
oc exec <pod> -- id - What is the namespace UID range? —
oc describe project <project> - Which SCC is the service account using? —
oc describe sa defaultandoc get pod <pod> -o yamlundersecurityContext - Who owns the files the app needs? —
oc exec <pod> -- ls -la <path> - Is the Dockerfile setting
USERunnecessarily? — Remove it for OpenShift-native apps
Key Takeaways
- OpenShift assigns a per-project UID range at namespace creation; your Dockerfile
USERdirective is overridden by default. - Pods run as an unprivileged UID from that range, but with GID 0 (root group) — design file permissions accordingly.
- The
restrictedSCC and default service account enforce this without extra configuration. - Use
chgrp -R 0andchmod -R g=uin your Dockerfile for OpenShift-native apps. - Legacy apps that need a fixed UID require explicit
runAsUser, a custom service account, and thenonrootoranyuidSCC.
Understanding this behavior is not academic. It is the difference between a deployment that works on the first try and one that sends you chasing phantom Dockerfile bugs for a week.