nftables
is a powerful and precise firewall designed specifically to replace iptables
. It is designed for greater human-readability and greater scaling. This document is to show how to configure nftables
to conform to a default-deny access strategy: nothing goes through until expressly permitted.
It is possible that your system simultaneously has iptables
and nftables
installed, in a suboptimal configuration known as iptables-legacy
. Though these two firewall suites are remarkably similar in function and syntax, where possible, keep only one firewall and uninstall iptables
for both ipv4 and ipv6.
If you are familiar with functional programming languages, the syntax of nftables
may be familiar. These rules will be written as verbosely as possible to help aid learning the different syntax structure. The conventions of this document are liberal in the use of {}
braces to accentuate lists, since braces are usually only required for multiple parameters.
Just like iptables
, a packet will enter different chains, though there are no default tables and chains–the differences are drastic in terminology, but the general flow remains similar.
Though books may dedicate chapters to explaining the ways these rules work, the gist of understanding nft
is understanding each {}
is a parameter sent to just the function to its left. When all the components of a rule match, the verdict
(e.g., accept, drop) is executed.
Your installation of nftables
will already include an example firewall at the conventional location /etc/nftables.conf
. You can use this file as the basis for your ruleset, or you can create it manually.
# cat << EOF > /etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset
table inet pkt_filter {
chain inbound {
type filter hook input priority 0; policy drop;
ct state { related, established } accept
tcp dport { 22 } ct state { new } accept
}
chain outbound {
type filter hook output priority 0; policy drop;
ct state { related, established } accept
}
}
EOF
Right from the start, set the default policy to drop
–no packets should be allowed unless explicitly matched. The above rules will allow the bare minimum required to maintain an ssh
connection during configuration.
This example defines a table pkt_filter
, as well as chains inbound
and outbound
–all of which can be renamed as desired.
Now, with a minimal firewall configuration, enable nftables
and get it started:
# systemctl enable nftables
# systemctl start nftables
After any changes to the configuration, you need to reload the ruleset. nftables
will detect syntax errors upon reload and fail-safely without applying any changes: you can then make necessary corrections.
$ systemctl reload nftables
Create logging rules first. Logged packets can help construct more specific, targeted rules. Take note of the ...
which signify these rules should be the last rules (bottom) of the stanza.
chain inbound {
...
# log all remaining packets
ip protocol { tcp } counter log prefix "tcp.in.dropped: "
ip protocol { udp } counter log prefix "udp.in.dropped: "
}
chain outbound {
...
# log all remaining packets
ip protocol { tcp } counter log prefix "tcp.out.dropped: "
ip protocol { udp } counter log prefix "udp.out.dropped: "
}
All the logged traffic goes to /var/log/kern.log
and contains the prefix designated above.
Many activies a server will perform will likely require DNS lookups. DNS operates on outbound UDP port 53.
chain outbound {
...
# allow dns resolution for the host
udp dport { 53 } accept
...
}
# ping minecraft.codeemo.com
ping: minecraft.codeemo.com: Temporary failure in name resolution
# vi /etc/nftables.conf
# systemctl reload nftables
# ping minecraft.codeemo.com
PING minecraft.codeemo.com (167.71.248.91) 56(84) bytes of data.
[snip]
If you need to download an online file, it’s easy to get files via HTTP
and HTTPS
chain outbound {
...
# initiate outbound connections http/https
tcp dport { 80, 443 } accept
...
}
Let’s let ICMP
through, but also implement rate-limiting. Normal pings are about 1/second; anything hitting a threshold such as 4/second might not be friendly traffic.
chain inbound {
...
# icmp, rate limited
icmp type { echo-request } limit rate 4/second accept
icmpv6 type { echo-request } limit rate 4/second accept
...
}
chain inbound {
...
# accept localhost traffic
iif lo accept
...
}
To open up any given port, follow this template:
chain inbound {
...
# allow 8443 (mineos webui) through
tcp dport { 8443 } ct state { new } accept
# match type (tcp) +
# matching criteria
# values to match
# match type (connection tracking)
# match criteria
# values to match
# verdict
# alternate way to make this rule
# tcp dport 8443 ct state new accept
...
}
The match types are innumerable. See the nftables wiki for more information.
Let’s look at the current rules so far. We use simple command nft list ruleset
to see the firewall state, including counters–this is live, up to the second data.
# nft list ruleset
table inet pkt_filter {
chain inbound {
type filter hook input priority 0; policy drop;
ct state { established, related } accept
tcp dport { ssh } ct state { new } accept
tcp dport { 8443 } ct state { new } accept
icmp type { echo-request } limit rate 4/second accept
icmpv6 type { echo-request } limit rate 4/second accept
ip protocol { tcp } counter packets 0 bytes 0 log prefix "tcp.in.dropped: "
ip protocol { udp } counter packets 0 bytes 0 log prefix "udp.in.dropped: "
}
chain outbound {
type filter hook output priority 0; policy drop;
ct state { established, related } accept
tcp dport { http, https } accept
udp dport { domain } accept
ip protocol { tcp } counter packets 0 bytes 0 log prefix "tcp.out.dropped: "
ip protocol { udp } counter packets 77 bytes 5852 log prefix "udp.out.dropped: "
}
}
The counters of nftables
are much less tabular, but still informative. You can watch live-updated traffic with watch
:
# watch nft list ruleset
Our logging rules will produce lines that append to /var/log/kern.log
:
Jun 30 06:06:13 mineos-tkldev kernel: [ 8486.974964] tcp.in.dropped IN=eth0 OUT= MAC=00:16:3e:5e:6c:00:fe:ff:ff:ff:ff:ff:08:00 SRC=10.137.0.14 DST=10.137.0.16 LEN=52 TOS=0x00 PREC=0x00 TTL=63 ID=758 DF PROTO=TCP SPT=56296 DPT=8443 WINDOW=64240 RES=0x00 SYN URGP=0
There are resources online to help you understand each of these logged segments, but in the meantime it will suffice to be able to identify these key/value pairs:
... tcp.in.dropped IN=eth0 ... SRC=10.137.0.14 DEST=10.137.0.16 ... DPORT=8443 ...
tcp.in.dropped
tells us it’s a TCP packet inbound to 8443.
Let’s find some packets that just don’t make sense to ever honor, and drop them immediately.
chain inbound {
...
# reject trash traffic
ct state { invalid } drop
tcp flags & (fin|syn|rst|ack) != syn ct state { new } drop
...
}
We can easily remove excessive noise and get to the interesting lines using grep
.
# grep 'tcp.in.dropped' /var/log/kern.log #all tcp packets that showed up at silent ports
# grep 'udp.in.dropped' /var/log/kern.log #all udp packets that showed up at silent ports
# grep 'tcp.in' /var/log/kern.log #shorthand to see all unexpected tcp traffic (untrusted origin, unused port)
# grep 'in.dropped' /var/log/kern.log #shorthand to see unused port traffic
nftables
provides an immense amount of control of packet flow. Creating good rules from the outset will lower the effort required to maintain a secured system. There’s much more nftables
offers for the discerning sysadmin; check out the nftables
wiki for more inspiration!