debian, docker and nftables

Debian Buster ships with nf_tables as the  firewall backend. So when installing it on one of my machines, I wanted to use the associated nftables frontend with it as well, instead of relying on the iptables-nft compatibility layer as a default.
After all, when would be a better time for learning it than now?

On the other hand I use docker quite extensively for my development environments, and docker uses iptables to set up the correct packet forwarding rules between containers and the host. Without it the containers would not be able to access each other or the internet.

So what should I/you do in this case?

This post will shine some light on the difficulties I faced, some facts I gathered and a solution. If you are not too interested in the how and why but just want to get things working simply copy the following configuration into /etc/nftables.conf

#!/usr/sbin/nft -f

flush ruleset

# IPv4
table ip filter {
        chain INPUT {
                 type filter hook input priority 0; policy drop;

                 # accept any localhost traffic
                 iif lo accept

                 # accept traffic originated from us
                 ct state established,related accept
        }

	chain FORWARD {
                 type filter hook forward priority 0; policy drop;
	}

	chain OUTPUT {
		# allow all outwards traffic
        	type filter hook output priority 0; policy accept;
        }
}

# IPv6
table ip6 filter {
        chain INPUT {
                type filter hook input priority 0; policy drop;

                # accept any localhost traffic
                iif lo accept

                # accept traffic originated from us
                ct state established,related accept

                # accept neighbour discovery otherwise connectivity breaks
                icmpv6 type { nd-neighbor-solicit, echo-request, nd-router-advert, nd-neighbor-advert } accept
        }
	
	chain FORWARD {
                type filter hook forward priority 0; policy drop;
	}

	chain OUTPUT {
		# allow all outwards traffic
        	type filter hook output priority 0; policy accept;
        }

}

install nftables with apt install nftables and enable and start it with systemctl enable nftables && systemctl start nftables. This assumes a fresh install without any other firewall. If another one is active make sure to remove it first.

If you want some more information about why this works and whether or not it is safe, enjoy the test of the article.

Nftables & iptables-nft together

The first thing I wanted to investigate was how nftables and iptables-nft work together when both are installed and active. Will rules from iptables overwrite my configuration in nftables?

To answer this I read the wiki and did some experiments. First of all I found:

**Verdict statements
**The verdict statement alters control flow in the ruleset and issues policy decisions for packets. The valid verdict statements are:

accept: Accept the packet and stop the remain rules evaluation.
drop: Drop the packet and stop the remain rules evaluation.
queue: Queue the packet to userspace and stop the remain rules evaluation.
continue: Continue the ruleset evaluation with the next rule.
return: Return from the current chain and continue at the next rule of the last chain. In a base chain it is equivalent to accept
jump : Continue at the first rule of . It will continue at the next rule after a return statement is issued
goto : Similar to jump, but after the new chain  the evaluation will continue at the last chain instead of the one  containing the goto statement

https://wiki.nftables.org/wiki-nftables/index.php/Quick_reference-nftables_in_10_minutes#Statements

This already tells me that my rules will take effect if nftables rules are the first ones to be checked.

To test that, I used the the configuration from the start of the article and simply ran iptables -A OUTPUT -o lo -j ACCEPT, which allows all outgoing traffic from loopback interfaces.
Listing my new rules with nft list ruleset gives the follwoing output

  1 table ip filter {
  2         chain INPUT {
  3                 type filter hook input priority 0; policy drop;
  4                 iif "lo" accept
  5                 ct state established,related accept
  6         }
  7
  8         chain FORWARD {
  9                 type filter hook forward priority 0; policy drop;
 10         }
 11
 12         chain OUTPUT {
 13                 type filter hook output priority 0; policy accept;
 14                 oifname "lo" counter packets 0 bytes 0 accept
 15         }
 16 }
 17 table ip6 filter {
 18         chain INPUT {
 19                 type filter hook input priority 0; policy drop;
 20                 iif "lo" accept
 21                 ct state established,related accept
 22                 icmpv6 type { echo-request, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
 23         }
 24
 25         chain FORWARD {
 26                 type filter hook forward priority 0; policy drop;
 27         }
 28
 29         chain OUTPUT {
 30                 type filter hook output priority 0; policy accept;
 31         }
 32 }

On line 15 the new rule was appended to my existing rules. Thats great, it wont affect any of my rules that would match a packet first.

Now I will try overwrite an existing rule with another one that contradicts it. I chose to try and overwrite line 5 with iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j DROP, which should basically break my internet, since no packages would be allowed in. First, the internet still worked, secondly listing the rules again shows:

  2         chain INPUT {
  3                 type filter hook input priority 0; policy drop;
  4                 iif "lo" accept
  5                 ct state established,related accept
  6                 ct state related,established counter packets 0 bytes 0 accept
  7                 ct state related,established counter packets 0 bytes 0 drop
  8         }

The new rule was added on line 8 but does not overwrite the statement on line **7.  **

I am now quite confident to have both tools enabled side by side, since I have nothing set up that would execute iptable statements before systemd starts my nftables firewall.

But why do I even need both?

Docker and iptables

Behind the scenes docker is using iptables and enables ip forwarding. When the docker daemon starts it will set up the necessary kernel settings and iptable rules.
So in order to have docker keep doing all the work for us we need to have its dependencies running on the system.
Another solution would be to write all the necessary nftables rules into our own configuration.  It would then be necessary to keep them up to date on each change in docker though, which to me is cumbersome and will probably lead to confusion down the road.

So I instead opted for making docker write its necessary rules onto my ruleset by itself, similar to the iptable commands that I used in my experiments, using its own tools.
Because docker does not know about the different firewall that is used we need to adhere to a few things, to make our ruleset backwards compatible to tools using iptables:

  • use an ip and ipv6 table instead of inet
  • name all chains exactly as in iptables: INPUT, OUTPUT & FORWARD

Any change in this will cause errors, since the rules from docker might end up in the wrong place.

Conclusion

Switch to nftables. Writing the /etc/nftables.conf was way quicker and better to understand than a block of iptable rules, and this is the main benefit. Your firewall becomes easier to maintain and extend.
iptables should still be installed for now, as a fallback for applications that still use its interface.

Making docker work with this was simple if the 2 rules I mentioned are followed for legacy reasons and there is no better time to get used to nftables than now, when a stable debian release pretty much makes it the default of the future.

Back to overview