Do Not Run Dockerized Applications as Root
There is no doubt that the inception of Docker changed the way we deploy and run services in production.
Before Docker’s rise in popularity, services would be installed on a host. Whether it was a physical or virtual host was more often than not a capacity question.
When virtual machines hit the streets, they promised the security benefit of “isolation”. This made it cost effective to run a single service per virtual machine, with many virtual machines per physical host.
Before virtual machines, it was common to run as many services on the same host as we could fit. This cut down the cost of buying and running a new physical host for each service. Having many services running on the same host, however, created security holes. It created situations where a vulnerability in one service could allow access to another service on the same host. By isolating services onto a single virtual machine, we could eliminate those scenarios.
Eventually, however, there was a realization. While a virtual machine does offer some isolation, it still needs to be secured like a physical host. An attacker gaining privileged access to a virtual machine was just as bad as accessing a physical host, even if services were isolated within their own virtual machines.
Like virtualization, Docker brings a similar layer of isolation, allowing you to deploy an application within a very small OS runtime—a runtime smaller than a virtual host. This reduces the exposure of the runtime environment, which lends itself to the security principle of “only what is needed to run.”
However, as with the isolation provided by virtualization, that doesn’t mean we can ignore security best practices. In this article we are going to talk about one of the most well-understood security practices. Specifically, we will discuss running applications as a non-privileged user, but within a container.
It’s easy to forget
Running applications or services as a non-privileged user is commonplace. It has been practiced
for years. When the runtime for an application is a full OS installation, it is easy to
understand the risks of running as root
.
With Docker, however, it’s not as straightforward to understand or even see because Docker abstracts away
much of the runtime environment from end users. Thus it is easy to forget basic security concepts,
such as ensuring the runtime user has the fewest privileges possible.
To better understand this, let’s take a look at the following Dockerfile
.
FROM ubuntu:latest
RUN apt-get update --fix-missing && \
apt-get install -y redis-server && \
rm -rf /var/lib/apt/lists/*
EXPOSE 6379
CMD redis-server
This produces a bare-bones Redis container. Let’s run this container overriding the CMD
instruction with the whoami
command.
$ docker run --rm example whoami
root
When executed, the whoami
command will return the user executing it. In the example
above, it returned root
. This is because within our Dockerfile
we never specified a “user” to
run as. This means that our service ran as the current user for the Ubuntu image. That user
happens to be root
.
As with the Ubuntu base image, most base images have the current user set to root
. The reason for
this is simple: base images are usually used as a “base” from which to build other Docker containers.
As in our Redis example, most users take these base images and then install packages on top of them.
This installation step requires root
privileges, which is why most base images default to root
.
Unless we are specifically thinking about the runtime user, it is very easy for a service to
inadvertently run as root.
The security implications of this are as serious as a root
user-owned service running on a full
OS.
If the container is isolated, why does it matter?
Remote command execution is a common method used by attackers. When an attacker can use this
against a service running as root
, it means any execution of code will also be root
.
Many times when we think of a host being “rooted,” we think about other services on that host being exposed. This is something that container isolation has certainly made more secure. Since our service is isolated within a container, it becomes more difficult to attack other services on the same host.
I say “more difficult” because it is still possible for an attacker to exploit other services on the same
host. Isolation prevents the attacker from using root
privileges to exploit those services
directly, but it does not prevent the attacker from exploiting those services via the network.
An exploited service can be an entry point for an attacker, whether that service is running as
root
or not. However, a root
owned service makes it easier for an attacker to explore and
identify other services.
If an attacker were to install a command such as nmap
(not typically installed by default), they
could use this tool to find other vulnerable services.
Nmap is a common security tool that is used to “explore” networks. A powerful scan option used with
the nmap
command is -sS
. This option tells nmap
to use SYN
packets to scan network targets.
To generate these SYN
packets, nmap
must be running as root
. Without root
privileges, the
nmap
command is limited to less informative scanning techniques.
Let’s see this in action by running the nmap
command with the container we created above.
$ docker run --rm example nmap -sS -p 6379 localhost
Starting Nmap 7.01 ( https://nmap.org ) at 2018-01-01 19:01 UTC
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000089s latency).
Other addresses for localhost (not scanned): ::1
PORT STATE SERVICE
6379/tcp closed unknown
Nmap done: 1 IP address (1 host up) scanned in 0.39 seconds
Now what happens when that same command is executed but we tell Docker to run it as an unprivileged
user using the -u
flag?
$ docker run --rm -u 9000 example nmap -sS -p 6379 localhost
You requested a scan type which requires root privileges.
QUITTING!
This is a very simple but very real example of why running as root
can create vulnerabilities.
If an attacker were able to use root
access to install a tool such as nmap
and execute a
SYN
scan, this information could be used to attack other services.
While using the vulnerable service as an entry point for attack is possible with or without root
,
running a service as root
makes it easier for attackers to make those attacks more effective.
How to reduce privileges in a container
Docker provides a few methods of downgrading user privileges within a running container.
We used one method above—simply using the -u
flag to specify the user at runtime.
$ docker run --rm -u 9000 example
In the command above, we specified the user as 9000
, which isn’t a username, but rather a user ID.
With Docker’s -u
flag, you can specify either a username or a user ID. Note that when specifying a user ID,
a corresponding user does not have to exist.
The user ID used in our example, 9000
, is an arbitrary user ID that has no
special privileges. In accordance with Linux standards, user IDs between 0
and 499
tend to be
reserved. It therefore is advisable to use a user ID with a value greater than 499
to avoid unintentionally running
as a default system user.
The -u
flag is a nice simple method for downgrading user privileges; however, this only applies
when executing the docker run
command. What happens if the flag is forgotten? We are right back
to running the service as root
.
Using the USER instruction
Another method of downgrading user privileges is to use the USER
instruction. We can use this
instruction within our Dockerfile
to change the runtime user during image build.
Let’s see this in action.
Earlier in the article we used the following Dockerfile
to create a Redis container.
FROM ubuntu:latest
RUN apt-get update --fix-missing && \
apt-get install -y redis-server && \
rm -rf /var/lib/apt/lists/*
EXPOSE 6379
CMD redis-server
We need to be root
in order to execute the apt-get
commands on line 2
. We do this by
adding the USER
instruction immediately following the RUN
instruction.
FROM ubuntu:latest
RUN apt-get update --fix-missing && \
apt-get install -y redis-server && \
rm -rf /var/lib/apt/lists/*
USER 9000
EXPOSE 6379
CMD redis-server
This will change the runtime user to user ID 9000
, which means any action after the USER
instruction, including CMD
, will be executed as that user.
Let’s see how this changes our previous whoami
example.
$ docker run --rm example whoami
whoami: cannot find name for user ID 9000
As you can see, the whoami
command was executed as user ID 9000
, which of course doesn’t
really exist.
This method of downgrading user privileges is a bit better as it adds consistency. Unless the -u
flag is used, the resulting container from this Dockerfile
will run as user ID 9000
every time.
Summary
In this article we explored how easy it is to create a Docker container that runs its services as
the root
user. We also explored how doing so can lead to serious security vulnerabilities.
Luckily, the solution is as simple as adding a single line to our Dockerfile
.
USER 9000
This article focused on the Docker practice of specifying a runtime user. Docker security does not stop there, however. There are many more practices for securing Docker containers that are just as important. A good place to start learning more about Docker security is Docker’s own documentation.
Important Notice: Opinions expressed here are the author’s alone. While we're proud of our engineers and employee bloggers, they are not your engineers, and you should independently verify and rely on your own judgment, not ours. All article content is made available AS IS without any warranties. Third parties and any of their content linked or mentioned in this article are not affiliated with, sponsored by or endorsed by American Express, unless otherwise explicitly noted. All trademarks and other intellectual property used or displayed remain their respective owners'. This article is © 2018 American Express Company. All Rights Reserved.