Friends don't let friends run containers as root

Introduction

In my daily work, I encounter different container engines tools during the development process. For the development of the container image, I use a local container engine like Docker for Desktop or Podman. After developing on my local machine, the image is staged from the development environment, over test stages to production. In those environments, Kubernetes or Red Hat Openshift is the container platform of choice.

Unfortunately, the platforms use different approaches with which user ID the container process is started. This can be incredibly frustrating if you start with an official Docker image that just won’t run in Openshift due to stricter security policies.

Let’s look at what rules you should follow for each platform and what I do to consider the quirks of all container engines.

Docker for Desktop / Podman

The desktop tools Docker for Desktop and Podman have pretty loose rules when you run a containerized application. For example, if you don’t do anything, your application is executed as root.

And of course, running an application as root is not recommended, and consequently, many projects use the following approach:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FROM alpine:3.14

RUN addgroup -S -g 3000 groupFoo
# Setup application user
RUN adduser -S -u 3000 -G groupFoo johndoe

# Copy your app to the image and change file owner to johndoe
COPY --chown 3000:3000 app/foo /app/foo
# Switch to executing user.
USER 3000
ENTRYPOINT [ "/app/foo/docker-entrypoint.sh" ] 

The advantage of this approach is that you cannot forget to switch to a non-root user when you start the container.

The obvious drawback is that you hardcoded the user id and group id into the image.

For example Grafana, Prometheus, and Cassandra use this approach.

Kubernetes

Kubernetes extends the capabilities of a container runtime like Docker with additional orchestration capabilities. But it does not add any security policies. Consequently, you could build an image with the previous approach and start a pod using that image.

But Kubernetes introduces the concept of PodSecurityContext. With PodSecurityContext, you can configure the user who runs the container process.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-context
  labels:
    app: user-context
spec:
  replicas: 1
  selector:
    matchLabels:
      app: user-context
  template:
    metadata:
      labels:
        app: user-context
    spec:
      securityContext:
        runAsUser: 3000
        runAsGroup: 3000
      containers:
      #...

This Deployment tells Kubernetes to run all containers in this pod as user ID 3000 and group ID 3000. (SecurityContext allows to do the same thing but applies to a single container only.)

If we combine, building the user id and group id into the container image and PodSecurityContext, we need to ensure that both configure the same user ID and group ID. If we don’t do that, the file permissions do not allow us to access the application files.

Openshift

Openshift adds two additional security layers to what we have seen so far.

Firstly, Openshift runs all pods using an arbitrarily assigned user ID. The reason behind this is additional security against processes escaping the container due to a container engine vulnerability and thereby achieving escalated permissions on the host node. Please refer to section Support arbitrary user IDs of the Openshift documentation. The randomization of user IDs renders the first approach building the user ID and group ID into the image useless.

You might think that you can use PodSecurityContext to tell Openshift which user ID to take. And yes, this is working to some degree. But now comes the second security layer. Openshift restricts with Security Context Constraints the range of allowed user IDs. And this range can be different for every environment. Hence, we have to assume the user ID is random.

Luckily, the randomization is limited to the user ID. This means that whatever Openshifts selects as user ID, the user is always a member of the root group (group ID 0). So if we make sure that all files are accessible by the root group, Openshift can run the container process without any problems.

Conclusion

I found the following points very helpful to ensure that a workload never runs as root:

  • Docker / Podman
    • Set the user and group ID of the user running the container process with the USER instruction. E.g., USER 3000:0. The user ID you select can be any number equal or greater than 1000. This is needed to ensure that the container is not accidentally run as root or use a well-known user ID.
    • Do not use user ID 65535, which is the nobody user. This user is reserved for nfs.
    • Do not use adduser to create the user in the container OS. adduser might not be available in the image. Further on, the USER directive is more evident than reading the content of all RUN directives.
    • Make sure that all relevant files are accessible by group ID 0.
  • Kubernetes
    • For running a plain container in Kubernetes, you don’t have to do anything. Kubernetes starts the container with the user ID set by the USER instruction in the Dockerfile.
  • Helm Charts
    • If you need to set the PodSecurityContext, make this value configurable.
  • Openshift
    • After following the previous steps, the container is up and running without any issues.