When using Docker, it has added a whole bunch of firewall rules by default. These rules allow you to intelligently route the host machine's ports to the right containers, but also to allow exchanges between several networks (in a Swarm, for example). It is, however, complicated to set up our own rules when Docker issues its own.

Let's use UFW

UFW is a very simple application to avoid putting your fingers in the complex world of firewalls. With a few commands you can allow or block a port from one IP to a new one.

Now, how's it going? Any rules you put in place will pass after the rules put in place by Docker. So if you block port 80 using UFW, for example, the containers will remain accessible. By default, the policy I like to use is the following:

ufw allow ssh ufw default deny incoming ufw default allow outgoing

We block all incoming connections and allow all outgoing ones. I want to be in control of everything that goes through the server.

Execute UFW rules before those of Docker

There's a trick to it. Indeed, our objective here is to execute UFW rules before Docker's. There is a chain in IPTables called DOCKER-USER , which allows rules to be executed before generic container rules. However, UFW cannot communicate with this chain, but only with ufw-user-input (in our case). So let's start by resetting these rules each time UFW is restarted: modify the /etc/ufw/before.init file to include the lines about the firewall :

set -e box " $1 " in start ) ; ; stop ) iptables -F DOCKER- USER || true iptables -A DOCKER- USER -j RETURN || true iptables -X ufw-user-input || true ; ; status ) ; ; flush-all ) ; ; * ) echo "' $1 ' not supported" echo "Usage: before.init {start|stop|flush-all|status}" ; ;

Now you have to tell the firewall that the rules defined by UFW must be executed before those of Docker. Let's add these lines to the /etc/ufw/after.init file.

Note that the $INTERFACE variable must be replaced with the name of the primary host interface used by Docker (such as eth0 or eno1 ).

*filter :DOCKER- USER - [ 0 :0 ] :ufw-user-input - [ 0 :0 ] :ufw-after-logging-forward - [ 0 :0 ] -A DOCKER- USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A DOCKER- USER -m conntrack --ctstate INVALID -j DROP -A DOCKER- USER -i $INTERFACE -j ufw-user-input -A DOCKER- USER -i $INTERFACE -j ufw-after-logging-forward -A DOCKER- USER -i $INTERFACE -j DROP COMMIT

Before restarting UFW, you must also allow primary connections to the server, namely :

ufw allow web ufw allow proto tcp from $SERVER1_IP to any port 2377,7946 ufw allow proto udp from $SERVER1_IP to any port 4789,7946 ufw allow proto tcp from $SERVER2_IP to any port 2377,7946 ufw allow proto udp from $SERVER2_IP to any port 4789,7946

We can restart ufw to take into account all the changes we have made. Be careful not to restart ufw too soon, otherwise you won't have remote access to the server (all ports will be closed if you didn't allow SSH).

ufw reload

You can easily do a test run, start a listening container on the host (port 8000, for example) and you should not have access to the service until you allow the port using UFW. We now have complete control over our server.

Bonus: Ansible

Here are some useful rules used to implement these rules automatically using Ansible.