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.