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.

$ 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 iptables true state (not shocking, but still).
  • Never use the -p option (or -P) in Docker for something you don’t want to be public.
  • Only bind on either the loopback interface or an internal IP.

Update

@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 8.8.8.8 --dns 8.8.4.4 --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.