The dangers of UFW + Docker
In recent years, I’ve transitioned over to using Ubuntu’s UFW. In most cases, it gets the job done and it is easy to manage via provisioning tools like Ansible.
As turns out however, using UFW together with Docker can be very dangerous as I will show below.
Let’s start with an Ubuntu 14.04 server. It has UFW and Docker installed already, so let’s start by configuring UFW to block everything but SSH.
$ ufw allow ssh [...] $ ufw default deny incoming [...] $ ufw enable [...] $ sudo ufw status Status: active To Action From -- ------ ---- 22 ALLOW Anywhere 22 (v6) ALLOW Anywhere (v6)
That looks good. The default policy is set to deny all incoming traffic, and we only poke hole on port 22.
Let’s move on to Docker. For this example, we’ll use the latest version as of this writing.
$ docker version Client version: 1.3.1 Client API version: 1.15 Go version (client): go1.3.3 Git commit (client): 4e9bbfa OS/Arch (client): linux/amd64 Server version: 1.3.1 Server API version: 1.15 Go version (server): go1.3.3 Git commit (server): 4e9bbfa
Let’s now spin up a MongoDB server to listen on 0.0.0.0:27017. While a bad security practice, the firewall should block all external connections to it.
$ docker run -d -p 27017:27017 --name mongodb dockerfile/mongodb [...] $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES f07c734038c5 dockerfile/mongodb:latest "mongod" 5 seconds ago Up 4 seconds 28017/tcp, 0.0.0.0:27017->27017/tcp mongodb
With the server up and running, we can see that it is listening as expected.
I will now try to connect to my MongoDB server from my laptop on the public interface:
$ mongo --host a.b.c.d MongoDB shell version: 2.6.5 connecting to: a.b.c.d:27017/test Welcome to the MongoDB shell. For interactive help, type "help". For more comprehensive documentation, see http://docs.mongodb.org/ Questions? Try the support group http://groups.google.com/group/mongodb-user
Wait, what?! That shouldn’t be possible!
Let’s take another look at UFW.
$ ufw status Status: active To Action From -- ------ ---- 22 ALLOW Anywhere 22 (v6) ALLOW Anywhere (v6)
That still looks fine, so how did this happen?
As it turns out, Docker tampers directly with
$ iptables -L | grep 27017 ACCEPT tcp -- anywhere 172.17.0.2 tcp dpt:27017
It is expected that Docker tampers with the firewall rules to some extent. It is after all what enable Docker containers to bind on a port. Yet, this behavior is not what I would have expected.
So what’s the moral of the story here?
- UFW doesn’t tell you
iptablestrue state (not shocking, but still).
- Never use the
-P) in Docker for something you don’t want to be public.
- Only bind on either the loopback interface or an internal IP.
@karl_grz correctly pointed out that it is possible to override this behavior by adding
--iptables=false to to the Docker daemon. This is also described here. It still beats me why this isn’t the default configuration.
On Ubuntu, you can edit
/etc/default/docker and uncomment the DOCKER_OPTS line:
DOCKER_OPTS="--dns 18.104.22.168 --dns 22.214.171.124 --iptables=false"
After doing so, you need to restart Docker with
service restart docker.
I also tested this and can confirm that I was able to connect to MongoDB on the host, but not from my laptop on the public interface.
Thanks for pointing that out, Karl.