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:
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.