Managing Custom iptables Rules on a Debian Docker Host

When I purchased my first VPS, I had to learn how setup a firewall to secure my server. Instead of relying on a management tool like ufw, I opted to just write individual iptables rules directly, and thus have complete control over my firewall.

I came up with a base set of rules for all of my servers. To automatically apply the rules at startup, I placed a simple loader script inside of /etc/network/if-up.d/ which would essentially flush all the iptables chains and then restore my custom rules. This was achieved through the iptables-restore command. The loader script would then run systemctl restart fail2ban to restore fail2ban's chains and rules, which had been removed by the iptables-restore command.

This method of managing my firewalls was working fine until I started using Docker. I learned that Docker relies on its own iptables chains and forwarding/NAT rules for container networking to work properly. My firewall loader script would break Docker networking every time iptables-restore was called, because it completely wiped out existing Docker rules. An easy, but bad solution would be to just call systemctl restart docker to restore the rules, but this would also restart any running Docker containers.

So I did some research and came up with a better (IMO) way to load my rules. I created two systemd unit files.

At system startup, this service calls /etc/network/firewall/init.shafter fail2ban starts, but before Docker starts. This allows us to place our custom rules right where we want them. Keep in mind that at startup, iptables will be empty.

Add a RETURN rule to the end of INPUT-CUSTOM to jump back to the INPUT chain if no rules in our custom chain are matched

Add a RETURN rule to the end of DOCKER-USER to jump back to the FORWARD chain if no rules in the DOCKER-USER chain are matched

The nice part about this method is that it doesn't touch any fail2ban or Docker rules. It creates a custom chain for our rules and only flushes that chain. Any time you modify rules in rules.sh, all you have to do is run systemctl restart firewall to apply them. No restart of fail2ban or Docker will be necessary.

The unit files are flexible in that they will work just fine installed on a system that will not be used as a Docker host. For non-Docker hosts, you would just remove any references to the DOCKER-USER chain from rules.sh. I currently implement firewalls on all my servers using this method. Not having to wait for fail2ban to restart every time I reload firewall rules is great!

Practical Example

So now that you have an overview of how to load your custom rules, here is an example of how to setup rules for Docker containers.

As you see, I like to explicitly define container networks and IP addresses. In this example, all our containers are part of the compose network: 192.168.240.0/24. This makes it easier to create firewall rules.

By default, if we were to run docker-compose up -d and start these containers, anybody would be able to access the exposed ports. Even though our INPUT chain has a default policy of DROP, people will still be able to access our containers. The reason for this is that the containers are on a different network than the host, and are using NAT for port forwarding. So our Docker host is essentially a router at this point.

Any traffic for our Docker containers goes through the FORWARD chain. Even though we've set the default policy on the FORWARD chain to DROP, Docker inserts its own ACCEPT rules after the DOCKER-USER chain. So to firewall our containers, all our rules need to be added to the DOCKER-USER chain.

In the above example, you'll see that we have a container running Nagios (192.168.240.2) on port 80, and a UniFi controller (192.168.240.3) on ports 8443, 8080, and 3478/udp.

Here are the example firewall rules we want to implement:

Allow all established/related traffic to reach Docker containers

Only allow access to Nagios port 80 from the subnet 10.10.10.0/24.

Only allow access to the UniFi web interface on port 8443 from the subnet 10.10.11.0/24.

Only allow access to UniFi ports 8080 and 3478/udp from the subnet 10.10.12.0/24