Quick Links

Processes in a Docker container should not be run as root. It's safer to run your applications as a non-root user which you specify as part of your Dockerfile or when using docker run. This minimizes risk by presenting a reduced attack surface to any threats in your container.

In this article, you'll learn about the dangers of running containerized applications as root. You'll also see how to create a non-root user and set up namespacing in situations where this isn't possible.

Why Is Running as Root Dangerous?

Containers are run as root by default. The Docker daemon executes as root on your host and running containers will be root too.

Although it can seem like root inside the container is an independent user, it's actually the same as the root account on your host. Separation's only provided by Docker's container isolation mechanisms. There's no strong physical boundary; your container's another process run by the root user on your host's kernel. This means a vulnerability in your application, the Docker runtime, or the Linux kernel could allow attackers to break out of the container and perform root-privileged operations on your machine.

There are some built-in protections that lessen the risk of this happening. Root inside the container is unprivileged and has restricted capabilities. This prevents the container from using system administration commands unless you manually add capabilities or use privileged mode when you start your containers.

Despite this mitigation, allowing applications to run as root remains a hazard. Just like you'd restrict use of root in a traditional environment, it's unwise to unnecessarily use it within your containers. You're providing an over-privileged environment that gives attackers more of a foothold in the event a breach occurs.

Running Containerized Applications as a Non-Root User

It's best practice for containerized applications to run as a regular user. Most software doesn't need root access so changing the user provides an immediate layer of defense against container breakout.

You should create a new user account as one of the final stages in your Dockerfile. You can achieve this with the USER instruction:

FROM base-image:latest

RUN apt install demo-package

USER demo-user:demo-group

ENTRYPOINT ["demo-binary"]

Containers started from this image will run as demo-user. The user will be a member of the demo-group group. You can omit the group name if you don't need the user to be in a group:

USER demo-user

You may specify a user ID (UID) and group ID (GID) instead of names:

USER 950:950

Allocating a known UID and GID is usually the safest way to proceed. It prevents the user in the container from being mapped to an over-privileged host account.

USER is often specified as the penultimate stage in a Dockerfile. This means you can still run operations that require root earlier in the image build. The apt install instruction in the example above has a legitimate need for root. If the USER instruction was placed above it, apt would be run as demo-user which would lack the necessary permissions. As Dockerfile instructions only apply to image builds, not running containers, it's safe to leave changing the user until later in your Dockerfile.

Changing the user your container runs as might require you to update the permissions on the files and folders it accesses. Set the ownership on any paths that will be used by your application:

COPY initial-config.yaml /data/config.yaml

USER demo-user:demo-group

RUN chown demo-user:demo-group /data

In this example the /data directory needs to be owned by demo-user so the application can make changes to its config file. The earlier COPY statement will have copied the file in as root. A shorthand is available by using the --chown flag with copy:

COPY --chown=demo-user:demo-group initial-config.yaml /data/config.yaml

Changing the User When Starting a Container

While you can easily change the user in your own Dockerfiles, many third-party applications continue to run as root. You can reduce the risk associated with using these by setting the --user flag each time you call docker run. This overrides the user set in the image's Dockerfile.

$ docker run -d --user demo-user:demo-group demo-image:latest

$ docker run -d --user demo-user demo-image:latest

$ docker run -d --user 950:950 demo-image:latest

The --user flag runs the container's process as the specified user. It's less safe than the Dockerfile USER instruction because you have to apply it individually to every docker run command. A better option for regularly used images is to create your own derivative image that can set a new user account:

FROM image-that-runs-as-root:latest

USER demo-user

$ docker build . -t image-that-now-runs-as-non-root:latest

Changing the user of a third-party image can cause problems: if the container expects to be run as root, or needs to access filesystem paths owned by root, you'll see errors as you use the application. You could try manually changing the permissions on the paths that cause problems. Alternatively, check whether the vendor has a supported method for running the application with a non-privileged user account.

Handling Applications That Have to Run as Root

User namespacing is a technique for dealing with applications that need some root privileges. It lets you map root inside a container to a non-root user on your host. The simulated root inside the container has the privileges it needs but a breakout won't provide root access to the host.

Namespace remapping is activated by adding a userns-remap field to your /etc/docker/daemon.json file:

{

"userns-remap": "default"

}

Using default as the value for userns-remap instructs Docker to automatically create a new user on your host called dockremap. Root within containers will map back to dockremap on your host. You can optionally specify an existing user and group instead, using a UID/GID or username/group name combination:

{

"userns-remap": "demo-user"

}

Restart the Docker daemon after applying your change:

$ sudo service docker restart

If you're using nsuser-remap: default, the dockremap user should now exist on your host:

$ id dockremap

uid=140(dockremap) gid=119(dockremap) groups=119(dockremap)

The user should also appear in the /etc/subuid and /etc/subgid subordinate ID files:

$ dockremap:231500:65535

The user has been allocated a range of 65,535 subordinate IDs starting from 231500. Within the user namespace, ID 231500 is mapped to 0, making it the root user in your containers. Being a high-numbered UID, 231500 has no privileges on the host so container breakout attacks won't be able to inflict so much damage.

All the containers you start will run using the remapped user namespace unless you opt out with docker run --userns=host. The mechanism works by creating namespaced directories inside /var/lib/docker that are owned by the subordinate UID and GID of the namespaced user:

$ sudo ls -l /var/lib/docker/231500.231500

total 14

drwx------ 5 231500 231500 13 Jul 22 19:00 aufs

drwx------ 3 231500 231500 13 Jul 22 19:00 containers

...

User namespacing is an effective way to increase container isolation, avoid breakouts, and preserve compatibility with applications that need root privileges. There are some tradeoffs though: the feature works best on a fresh Docker instance, volumes mounted from the host must have their permissions adjusted, and some external storage drivers don't support user mapping at all. You should review the documentation before adopting this option.

Summary

Running containerized applications as root is a security risk. Although easy to overlook, the isolation provided by containers is not strong enough to fully separate kernel users from container users. Root in the container is the same as root on your host so a successful compromise could provide control of your machine.

As an image author, you should include the USER instruction in your Dockerfile so your application runs without root. Image users can override this with docker run --user to assign a specific UID and GID. This helps mitigate cases where the image normally uses root.

You can further tighten security by dropping all capabilities from the container using --cap-drop=ALL, then whitelisting those that are required with --cap-add flags. Combining these techniques will run your application as a non-root user with the minimum set of privileges it needs, improving your security posture.