I love nftables. I’ve personally found it to be both more simple and featureful compared to other mainstream firewall services.
It can be configured from a file with a simple yet powerful syntax, allowing you to create simple port-blocking rules, but also precisely classify and act on traffic.
Plus, it can be used side by side with other firewall-like services running on your Linux machine, allowing it to be deployed nearly everywhere without disturbing existing applications.
In this article, I will explain how I configure nftables as my firewall on my servers.
How to configure it ?
On most distributions, nftables can be configured using the /etc/nftables.conf file.
Here is such an example:
#!/usr/sbin/nft -f
flush ruleset
define wan = "eth0" # Interface used to access Internet
# Table and chain can be named anything ! I like to use non-default names
table inet itroozserver {
chain prerouting {
type nat hook prerouting priority 0; # This line decides when (on which hook) the chain will be executed
iifname $wan udp dport 1194 redirect to :51820 # Redirect incoming traffic to Wireguard, to handle networks where only the IPsec destination port is allowed
}
chain postrouting {
type nat hook postrouting priority 0;
# Allow my Wireguard clients to forward requests to Internet (masquerade)
ip saddr 10.8.8.5 return # ..except this one
ip saddr 10.8.8.0/24 oif $wan masquerade
}
chain input {
type filter hook input priority filter; policy drop; # set hook + default policy (deny traffic)
iif lo accept # Accept localhost
ct state established accept # Accept established connections
tcp dport 22 accept # Accept SSH
tcp dport 51820 accept # Accept Wireguard
# Per policy, everything else is denied
}
chain forward {
type filter hook forward priority filter;
}
chain output {
type filter hook output priority filter;
}
}
Once the configuration is set, you will need to restart the service, with systemctl restart nftables, and consult the currently applied firewall rules with nft list ruleset.
Note that this configuration will interfere with other services setting firewall rules ! For a setup that take it into account, see below.
More complicated example: act based on source
Sometimes, you may want to act on traffic based on its source. For example, you may only allow SSH from 192.168.1.0/24, or from your public IP.firewalld allows you to do that with its zones feature, but with a drawback (or at least, I didn’t find how to avoid that): rules only apply to a single zone. If you want a rule to apply to all zones (e.g. allow port 80 for HTTP), you will have to duplicate that rule in all zones.
Here is how I would do that with nftables:
#!/usr/sbin/nft -f
flush ruleset
table inet itroozserver {
set server_ips {
type ipv4_addr
flags interval
elements = {198.51.100.0/24}
}
set internal_ips {
type ipv4_addr
flags interval
elements = {198.51.100.0/24, 203.0.113.0/24}
}
chain input {
type filter hook input priority filter; policy drop;
iif lo accept
ct state established,related accept
# We could also make all traffic go there regardless of source interface
iifname "eno1" jump unsecured-input
}
# Input coming from a public interface (so untrusted)
chain unsecured-input {
jump public-input
ip saddr @internal_ips jump internal-input
ip saddr @server_ips jump server-internal-input
}
# For everyone
chain public-input {
tcp dport {80, 443} accept # HTTP/HTTPS
udp dport 51820 accept # Wireguard
}
# For specially allowed IPs
chain internal-input {
tcp dport 22 accept # SSH
tcp dport 6443 accept # kubernetes API
}
# Internal communication for my Kubernetes cluster
chain server-internal-input {
tcp dport {2379, 2380, 2381} accept
}
}
Using nftables in conjunction with other firewalls / Docker
The above examples work in isolation, but will cause problems on systems where (rootful) Docker is installed. This is because Docker modifies firewall rules for container networking, e.g. to allow containers to connect to Internet (masquerade).
nftables, on the other hand, will reset the whole firewall state every time it is applied, flushing Docker rules, and destroying container networking.
To fix this, you need to change your nftables config files like this:
#!/usr/sbin/nft -f
define wan = "eth0"
destroy table inet itroozserver
table inet itroozserver {
chain prerouting {
type nat hook prerouting priority 0;
iifname $wan udp dport 1194 redirect to :51820
}
chain postrouting {
type nat hook postrouting priority 0;
ip saddr 10.8.8.5 return
ip saddr 10.8.8.0/24 oif $wan masquerade
}
chain input {
type filter hook input priority filter; policy drop;
iif lo accept
ct state established accept
tcp dport 22 accept
tcp dport 51820 accept
}
}
Note the change: instead of doing flush ruleset to reset the firewall at the beginning, we did destroy table inet itroozserver, which will only delete our own table, and we re-apply its rules. This lets the rest of the firewall state (e.g. docker rules) unchanged.
⚠️ Doing this is not enough. By default, the nftables systemd service will flush the firewall state when restarting. Change this by editing the service: systemctl edit nftables. Notice the ExecStop directive: it flushes the firewall on stop (which is done on restart). You can remove it by adding the following patch:
[Service]
ExecStop=
You can then restart the service with systemctl restart nftables and ensure your docker/other rules are still present: nft list ruleset.
If you need to disable your firewall, you can do so by deleting your custom table with nft destroy table inet itroozserver.
⚠️ Beware that Docker may still bypass your firewall (at the time of writing, port forwarding is implemented by DNAT’ing in prerouting, which bypasses your input hook).
As such, it is recommended to forward ports by using -p 127.0.0.1:8080:8080 for ports not meant to be exposed to external networks.