Why you should use iptables along with Docker

The command iptables is the historic userland access to Linux networking stack. It allows so set rules for packets filtering and to build firewalls. To route network traffic from hosts to container withing a network bridge, Docker users iptables to insert its rules. This article provides iptables basics to understand what is going on, and solve issues you can have.

Iptables is weird, this is why ufw exists

Some topics in software engineering have always been thorny for me. Iptables is one of these. This is why I prefer ufw. Ufw also allows to build packet filtering rules with a nice syntax and ensure that these rules persist at system startup. I believe this is ideal for my laptop, because I can setup default policies to deny all incoming traffic. I feel safer and I cannot remain locked out of my machine. However it does not play well on a server using Docker to publish services.

When ufw leads to more problems than solutions

In my current job, we are using Keycloak for identity management. Keycloak use an internal LDAP to check users' passwords. Keycloak and the LDAP are deployed on the same host as Docker containers along with a HAProxy to handle incoming traffic. To access LDAP, Keycloak use a private domain name that points to the HAProxy which routes the requests to the LDAP.

Without any firewall rule, Keycloak connects to the LDAP.

With ufw enabled, it does not. I see blocked connections from docker bridge toward the host using LDAP port in /var/log/ufw.log. My attempt to setup ufw to allow the trafic failed. I decided to give up and directly use iptables instead.

What is iptables and how to use it

Iptables and ip6tables are userland commands set up, maintain, and inspect the tables of IPv4 and IPv6 packet filter rules in the Linux kernel within a framework called Netfilter. That's the man page. In shorter form: it allows to configure a firewall.

Iptables consists in tables that correspond to what we want to do with ip traffic. The interesting tables when you setup a firewall and work with docker are the filter table and the nat table. Each tables contains rules organized in chains. The filter table contains chains INPUT to allow or deny incoming traffic, OUTPUT to allow or deny outgoing traffic and FORWARD to allow or deny forwarded traffic, there the server act as an intermediate. The important chains in the nat table are PREROUTING POSTROUTING.

You can create subchain of rules within a chain. This is what tools like Docker and ufw use to interact with iptables.

In each chains, each rule's condition is tested. If a packet match the condition, the rule is applied. It can be a non terminating rule (like LOG to append a log line) or a terminating rule to stop testing. ACCEPT accept the packet, DROP reject the packat and RETURN stops current chain or subchain execution.

You can append, insert and delete rules using commands iptables -A, iptables -I or iptables -D. If you want to block any network access to your server, just type:

iptables -I INPUT 1 -j DROP

Thank me later.

Use logging to help you to build your rule set

When you are debugging your firewall rules, it's a good idea to log blocked traffic in /var/log/syslog. To do so, append a LOG action before the DROP.

  iptables -A INPUT -j LOG --log-prefix "Some log prefix "
  

What is Docker doing with iptables

When you create a Docker network and a container within this network, Docker creates two sets of rules:

  • PREROUTING and POSTROUTING rules in the nat table
  • FORWARD rules in the filter table
You can list them with the commands
sudo iptables -t nat -L -v 
sudo iptables -L -v 

Option -v is important here as it will show you which network interfaces are concerned by the rules. Docker creates one interface for each network. They are named docker0 for default bridge dans are prefixed by br for user created networks.

Here is an example of PREROUTING rule set that Docker creates:

Chain DOCKER (2 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 RETURN     all  --  docker0 any     anywhere             anywhere
    0     0 RETURN     all  --  br-something any     anywhere             anywhere
  14M  845M DNAT       tcp  --  !br-something any     anywhere             server-name       tcp dpt:http-alt to:10.10.11.2:8080 

And the associated POSTROUTING rule set:

 Chain POSTROUTING (policy ACCEPT 14M packets, 848M bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MASQUERADE  all  --  any    !docker0  172.17.0.0/16        anywhere
  41M 2445M MASQUERADE  all  --  any    !br-something  10.10.11.0/24        anywhere
    0     0 MASQUERADE  tcp  --  any    any     10.10.11.2           10.10.11.2           tcp dpt:http-alt 

What happen then?

As you can see on this netfilter flowchart, PREROUTING matches are used to make a routing decision. Packets pass by INPUT and OUTPUT rules OR by FORWARD and POSTROUTING rules. It means that when you create a network and containers with published ports in this network, the traffic that goes to the containers is not tested through INPUT and OUTPUT rules in the filter table.

By the way, it's a good idea to precise the IP address when you expose your container ports, as by default it will setup your netfilter rules to accept the traffic from everywhere to any IP address (0.0.0.0).

So, what happened to me?

If you decrypt the nat PREROUTING rules, you'll see that the traffic that is not coming from interface br-something get NATed. Unfortunately, when Keycloak calls the LDAP, the requests are seen as coming from interface br-something, do not trigger DNAT rule and get blocked by INPUT rules from the filter table.

In spite of my efforts, I have not been able to configure ufw to allow this traffic. I directly used iptables instead, and resulting INPUT rule set is straightforward:

iptables -F INPUT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp -i br-something --dport 389 -j ACCEPT
iptables -A INPUT -p tcp -i lo --dport 53 -j ACCEPT
iptables -A INPUT -p udp -i lo --dport 53 -j ACCEPT
iptables -A INPUT -j DROP 

It flushes the filter table, accept already established connections, accept ssh and ping and traffic from Docker network interface to port 389 (default LDAP port). Default policy is to accept all, so I won't be kept outside of my server in case of reboot.

Iptables is for boomer anyway, it has been replaced by nft

Now I know iptables and it feels great! However, I also learned that the utility is deprecated and that nft should now be used to interact with netfilter.

I have been thinking that iptables was crytptic, but it was because I did not know nft complicated syntax.

I guess I still have a lot to learn!

Posted on 2025-04-21 at 09:00

Previous Back