Rootless Docker on Linux and Security Benefits

Background

Docker is an amazing tool/ecosystem/development experience. However, it is not entirely obvious how to appropriately secure it in a Linux environment. When running Docker on Windows or Mac, you are actually experiencing Docker through a virtualized platform that is transparently installed on the OS (Docker Desktop). This results in some performance considerations, but is ultimately a good thing as it minimizes some of the attack surfaces to the host OS. However, on Linux you don’t have a “desktop” option, and you are left with installing the full Docker Engine as a “server” platform. While this option will give you the best performance by far, there are significant security considerations to be aware of.

RootFULL Docker

By default, when you install Docker for Ubuntu (and most other Linux OS’s) you are creating a daemon that runs in a very privileged way (as root).

aro@rootfuldocker:~$ ps -aux | grep docker
root        1802  0.0  2.1 946760 86456 ?        Ssl  13:25   0:00 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

This is necessary to support all of the features that Docker has, including bind mounts and publishing ports to name a few. This allows you to run commands like the following:

docker run -d -p 80:80 nginx

This command will actually expose port 80 of the host and forward to port 80 of the nginx container which would not be possible without this daemon running as root (or other host networking modifications). As another example, the following command would mount /etc of the host into /etc of the container:

aro@rootfuldocker:~$ docker run -it --rm -v /etc:/etc ubuntu

What’s the problem?

There’s nothing inherently wrong with how Docker works in a rootfull context, however, many Linux desktop users will often follow advice to allow their user account to run Docker commands without sudo. This can be seen on the Docker website and even on popular plugins like the Docker VSCode extension: Docker Post-Install Docker VSCode Extension To Docker’s credit, they do warn that this results in an insecure configuration, but from what I’ve seen this is a very common setup for Linux users. Since Docker is often used as a development tool, people will generally not want to type their sudo password every time they want to run a Docker command. To help illustrate why this could be problematic, consider the following scenario with bind mounts:

First, create a file on the host:

aro@rootfuldocker:~$ echo "test file!" > test.txt
aro@rootfuldocker:~$ ls -l
total 4
-rw-rw-r-- 1 aro aro 11 Mar 27 14:40 test.txt

Now, we’ll mount my home directory into /app of a busybox container image:

aro@rootfuldocker:~$ docker run -it --rm -v ~/:/app busybox
/ # cd /app/
/app # ls -l
total 4
-rw-rw-r--    1 1000     1000            11 Mar 27 14:40 test.txt
/app # cat test.txt
test file!

We can see that the file is visible inside of the container image, and it is owned by UID and GID 1000. However, the user inside of the busybox container image is root, and it can also create a file in the directory:

/app # whoami
root
/app # echo "I AM ROOT!" > containertest.txt

Once you exit the container, you can see that the resulting file on the host is actually owned by root!

aro@rootfuldocker:~$ ls -l
total 8
-rw-r--r-- 1 root root 11 Mar 27 14:46 containertest.txt
-rw-rw-r-- 1 aro  aro  11 Mar 27 14:40 test.txt

What would happen if the command was modified to do something like this?

docker run -it --rm -v /:/hostos busybox

This would result in the entire filesystem of the host OS to be mounted inside of the container, allowing you to make any modifications you’d like, all originating from a single Docker command. This is especially concerning with the previously mentioned configuration that allows any of these Docker commands to be run without sudo. The ability to run Docker commands is effectively the same as having full root access on a machine. In a world where so much tooling is installed with curl http://somewebsite.com | bash a malicious actor could check for Docker and easily gain access to a host machine without sudo or an exploit.

Enter rootless Docker

How do you use easily use Docker as part of your development workflow without exposing yourself to the risks mentioned above? One answer is using rootless Docker! Luckily, Docker makes this very easy to install using the directions on their website.

The short version (on Ubuntu) is:

sudo apt install -y uidmap
/usr/bin/dockerd-rootless-setuptool.sh install
echo "export PATH=/home/aro/bin:$PATH" >> ~/.bashrc
echo "export DOCKER_HOST=unix:///run/user/1000/docker.sock" >> ~/.bashrc
source ~/.bashrc

This results in now having two different Docker daemons running - one as root, and one as your user!

aro@rootlessdocker:~$ ps -aux | grep docker
root        3887  0.1  2.2 873028 89996 ?        Ssl  15:18   0:00 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
aro         5016  0.0  0.3 632588 13200 ?        Ssl  15:21   0:00 rootlesskit --net=slirp4netns --mtu=65520 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run --propagation=rslave /usr/bin/dockerd-rootless.sh
aro         5029  0.0  0.3 633660 12860 ?        Sl   15:21   0:00 /proc/self/exe --net=slirp4netns --mtu=65520 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run --propagation=rslave /usr/bin/dockerd-rootless.sh
aro         5052  0.2  2.2 946760 89480 ?        Sl   15:21   0:00 dockerd
aro         5068  0.5  1.1 898872 46900 ?        Ssl  15:21   0:00 containerd --config /run/user/1000/docker/containerd/containerd.toml --log-level info

Finally, disable the rootFULL Docker daemon:

aro@rootlessdocker:~$ sudo systemctl disable docker docker.socket --now
Synchronizing state of docker.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install disable docker
Removed /etc/systemd/system/multi-user.target.wants/docker.service.
Removed /etc/systemd/system/sockets.target.wants/docker.socket.

NOTE: Although the directions on the Docker website only mentioning disabling docker.service I noticed that if you do not also disable docker.socket, Docker will just be re-enabled if you run a Docker command with sudo.

Now that we have a rootless Docker install, lets try the same examples from the rootFULL Docker install:

aro@rootlessdocker:~$ docker run -d --rm -p 80:80 nginx
1eebda4df1fdfb5c5cca2902f83dae894198807d58c7834bdc7ec07982332e38
docker: Error response from daemon: driver failed programming external connectivity on endpoint priceless_solomon (ac637883097531b8f59ac11c09e50f8d610164d22f7a9bbed5fdc8a74f69b633): Error starting userland proxy: error while calling PortManager.AddPort(): cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or set CAP_NET_BIND_SERVICE on rootlesskit binary, or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission denied.

We can see that since this is trying to bind to a port below 1024, we get permission denied since this is just running as an unprivileged user (not root). We could instead forward port 8080 to the container like this:

docker run -d --rm -p 8080:80 nginx

Now the bind mount examples:

aro@rootlessdocker:~$ echo "test file!" > test.txt
aro@rootlessdocker:~$ ls -l
total 4
-rw-rw-r-- 1 aro aro 11 Mar 27 15:37 test.txt

We’ll do the same mount with the busybox container image:

aro@rootlessdocker:~$ docker run -it --rm -v ~/:/app busybox
/ # cd /app/
/app # ls -l
total 4
-rw-rw-r--    1 root     root            11 Mar 27 15:37 test.txt
/app # whoami
root
/app # echo "I AM ROOT!" > containertest.txt
/app # ls -l
total 8
-rw-r--r--    1 root     root            11 Mar 27 15:39 containertest.txt
-rw-rw-r--    1 root     root            11 Mar 27 15:37 test.txt

Notice the difference here is that even the test.txt file is shown as owned by root. However, once we exit the container and look at the host, we can see that both files are in fact owned by your user:

aro@rootlessdocker:~$ ls -l
total 8
-rw-r--r-- 1 aro aro 11 Mar 27 15:39 containertest.txt
-rw-rw-r-- 1 aro aro 11 Mar 27 15:37 test.txt

This means that using rootless Docker won’t give us full root access to the host OS, and instead maps the UID’s using the uidmap package installed as part of this process.

Another example showing a file on the host that is owned by root:

aro@rootlessdocker:~$ echo "actually root" > root.txt
aro@rootlessdocker:~$ sudo chown root:root root.txt
aro@rootlessdocker:~$ sudo chmod 600 root.txt
aro@rootlessdocker:~$ ls -l
total 12
-rw-r--r-- 1 aro  aro  11 Mar 27 15:39 containertest.txt
-rw------- 1 root root 14 Mar 27 15:52 root.txt
-rw-rw-r-- 1 aro  aro  11 Mar 27 15:37 test.txt
aro@rootlessdocker:~$ cat root.txt
cat: root.txt: Permission denied

We can see that even though the container thinks it is root, it still can’t read the file:

aro@rootlessdocker:~$ docker run -it --rm -v ~/:/app busybox
/ # cd /app/
/app # cat root.txt
cat: can't open 'root.txt': Permission denied
/app # whoami
root

Final Thoughts

I hope this helped show some of the benefits of using rootless Docker and the security considerations it helps with. There are also interesting projects like Podman that seem to be able to do everything shown here without a daemon at all. I hope to explore this and other projects like it soon!

Update!

I got a merged PR to the VSCode Docker extension which now suggests that Linux users use rootless Docker.

Another Update!

I updated the command in the Docker docs for disabling rootful Docker to include both docker.socket and docker.service. You can see the change in this merged PR.

comments powered by Disqus