rss logo

How to Configure nftables Rules from Scratch on GNU/Linux

nftables Logo

Intro

In this article, I'll detail a method I use to configure a set of nftables rules on GNU/Linux computers. The aim is to have the most restrictive and precise set of rules possible. This method is suitable for all types of configuration, such as servers, workstations, routers etc. It consists of tracing potentially blocked traffic using a “classic” set of rules, then adding new rules as and when we observe them in the logs.

  • This is done in three steps:
    • Add a basic configuration and activate tracing.
    • Analize the logs and add new rules if legitimate traffic is not already explicitly authorized.
    • Once all legitimate traffic has been authorized, activate default blocking.

Let's see how to set this up!

Configure default and known rules

  • Here, we're simply configuring a classic set of rules (⚠️remember to replace enp2s0 with the name of your network interface!⚠️) :
    • rules for the local interface
    • to allow ICMP
    • and to accept web traffic

This is a typical profile for most PCs today. Note the presence of lines with “log prefix”. These “accept” rules authorize (for the moment) traffic while collecting all information from every frame passing through them. This will enable us to trace traffic that would have been blocked had the default blocking been enabled.

#!/usr/bin/nft -f flush ruleset # We declare an interface variable associated with our network card so that we can use $interface in our rules instead of "enp2s0". define interface = { enp2s0 } # ----- IPv4 and IPv6 ----- table inet filter { chain INPUT { type filter hook input priority 0; policy drop; # Local interface rules ct state invalid counter drop comment "Drop invalid connections" iif != lo ip daddr 127.0.0.1/8 counter drop comment "drop connections to loopback not coming from loopback" iif != lo ip6 daddr ::1/128 counter drop comment "drop connections to loopback not coming from loopback" iif lo accept comment "Accept any localhost traffic" # ICMP traffic ip protocol icmp counter accept comment "accept all ICMP types" meta l4proto ipv6-icmp counter accept comment "accept all ICMPv6 types" # Accept web traffic (ALL to PC : if related or established) : HTTP + HTTPS + HTTP/3 QUIC + DNS + NTP iif $interface tcp sport { http, https, domain, ntp } ct state { related, established } counter accept comment "Accept Web Traffic TCP" iif $interface udp sport { https, domain } ct state { related, established } counter accept comment "Accept Web Traffic UDP" # Accept but trace all other INPUT traffic log prefix "INPUT : " accept comment "log INPUT" } chain FORWARD { type filter hook forward priority 0; policy drop; # FORWARD traffic is dropped and logged. There should be no network frames if the Linux PC is not in router mode. log prefix "FORWARD : " drop comment "log FORWARD" } chain OUTPUT { type filter hook output priority 0; policy drop; # Local interface rules oif lo accept comment "Accept any localhost traffic" # ICMP traffic ip protocol icmp counter accept comment "accept all ICMP types" meta l4proto ipv6-icmp counter accept comment "accept all ICMPv6 types" # Accept web traffic (PC to ALL : new, related and established) : HTTP + HTTPS + HTTP/3 QUIC + DNS + NTP oif $interface tcp dport { http, https, domain, ntp } ct state { new, related, established } counter accept comment "Accept Web Traffic" oif $interface udp dport { https, domain } ct state { new, related, established } counter accept comment "Accept Web Traffic" # Accept but trace all other OUTPUT traffic log prefix "OUTPUT : " accept comment "log out of rules traffic" } }
  • Apply the nftables configuration file:
root@host:~# nft -f /etc/nftables

Log analysis

  • Here we use the journalctl command to display frames passing through our “log prefix” rules. To do this, we use the “grep” option to filter lines containing the words INPUT, OUTPUT or FORWARD and concerning the kernel (-k option).:
root@host:~# journalctl -k --grep="INPUT|OUTPUT|FORWARD" Dec 14 15:57:09 WK-STD kernel: INPUT : IN=ens33 OUT= MAC=00:50:56:80:4f:3d:34:56:dc:b7:ec:ec:08:00 SRC=192.168.1.90 DST=192.168.1.88 LEN=88 TOS=0x00 PREC=0x00 TTL=62 ID=59819 DF PROTO=TCP SPT=58616 DPT=22 WINDOW=1091 RES=0x00 ACK PSH URGP=0 Dec 14 15:57:09 WK-STD kernel: OUTPUT : IN= OUT=ens33 SRC=192.168.1.88 DST=192.168.1.90 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=10678 DF PROTO=TCP SPT=22 DPT=58616 WINDOW=524 RES=0x00 ACK URGP=0 Dec 14 15:57:10 WK-STD kernel: OUTPUT : IN= OUT=ens33 SRC=192.168.1.88 DST=192.168.1.90 LEN=592 TOS=0x10 PREC=0x00 TTL=64 ID=10679 DF PROTO=TCP SPT=22 DPT=58616 WINDOW=524 RES=0x00 ACK PSH URGP=0 Dec 14 15:57:10 WK-STD kernel: OUTPUT : IN= OUT=ens33 SRC=192.168.1.88 DST=192.168.1.90 LEN=1208 TOS=0x10 PREC=0x00 TTL=64 ID=10680 DF PROTO=TCP SPT=22 DPT=58616 WINDOW=524 RES=0x00 ACK PSH URGP=0 Dec 14 15:57:10 WK-STD kernel: INPUT : IN=ens33 OUT= MAC=00:50:56:80:4f:3d:34:56:dc:b7:ec:ec:08:00 SRC=192.168.1.90 DST=192.168.1.88 LEN=52 TOS=0x00 PREC=0x00 TTL=62 ID=59820 DF PROTO=TCP SPT=58616 DPT=22 WINDOW=1078 RES=0x00 ACK URGP=0 Dec 14 15:57:10 WK-STD kernel: OUTPUT : IN= OUT=ens33 SRC=192.168.1.88 DST=192.168.1.90 LEN=336 TOS=0x10 PREC=0x00 TTL=64 ID=10681 DF PROTO=TCP SPT=22 DPT=58616 WINDOW=524 RES=0x00 ACK PSH URGP=0 Dec 14 15:57:10 WK-STD kernel: OUTPUT : IN= OUT=ens33 SRC=192.168.1.88 DST=192.168.1.90 LEN=632 TOS=0x10 PREC=0x00 TTL=64 ID=10682 DF PROTO=TCP SPT=22 DPT=58616 WINDOW=524 RES=0x00 ACK PSH URGP=0 Dec 14 15:57:10 WK-STD kernel: INPUT : IN=ens33 OUT= MAC=00:50:56:80:4f:3d:34:56:dc:b7:ec:ec:08:00 SRC=192.168.1.90 DST=192.168.1.88 LEN=52 TOS=0x00 PREC=0x00 TTL=62 ID=59821 DF PROTO=TCP SPT=58616 DPT=22 WINDOW=1072 RES=0x00 ACK URGP=0 Dec 14 15:57:11 WK-STD kernel: OUTPUT : IN= OUT=ens33 SRC=192.168.1.88 DST=192.168.1.90 LEN=880 TOS=0x10 PREC=0x00 TTL=64 ID=10683 DF PROTO=TCP SPT=22 DPT=58616 WINDOW=524 RES=0x00 ACK PSH URGP=0 Dec 14 15:57:11 WK-STD kernel: INPUT : IN=ens33 OUT= MAC=00:50:56:80:4f:3d:34:56:dc:b7:ec:ec:08:00 SRC=192.168.1.90 DST=192.168.1.88 LEN=52 TOS=0x00 PREC=0x00 TTL=62 ID=59822 DF PROTO=TCP SPT=58616 DPT=22 WINDOW=1066 RES=0x00 ACK URGP=0 Dec 14 15:57:11 WK-STD kernel: OUTPUT : IN= OUT=ens33 SRC=192.168.1.88 DST=192.168.1.90 LEN=632 TOS=0x10 PREC=0x00 TTL=64 ID=10684 DF PROTO=TCP SPT=22 DPT=58616 WINDOW=524 RES=0x00 ACK PSH URGP=0 Dec 14 15:57:11 WK-STD kernel: INPUT : IN=ens33 OUT= MAC=00:50:56:80:4f:3d:34:56:dc:b7:ec:ec:08:00 SRC=192.168.1.90 DST=192.168.1.88 LEN=52 TOS=0x00 PREC=0x00 TTL=62 ID=59823 DF PROTO=TCP SPT=58616 DPT=22 WINDOW=1062 RES=0x00 ACK URGP=0

We can see traffic between the addresses 192.168.1.90 (source) and 192.168.1.88 (destination) on the SSH port (22 TCP). In my case, this is perfectly normal, as I'm connected to machine 192.168.1.88 from machine 192.168.1.90 via this protocol. So let's take a look at what needs to be done to authorize this traffic by adding specific rules.

  • If we examine the first line in detail, we can see all the information we need to add our nftables rule:
    • Concerns the INPUT chain.
    • Source IP address is 192.168.1.88.
    • Destination IP address 192.168.1.90.
    • Protocol is TCP.
    • Destination port is 22.
Dec 14 15:57:09 WK-STD kernel: INPUT : IN=ens33 OUT= MAC=00:50:56:80:4f:3d:34:56:dc:b7:ec:ec:08:00 SRC=192.168.1.90 DST=192.168.1.88 LEN=88 TOS=0x00 PREC=0x00 TTL=62 ID=59819 DF PROTO=TCP SPT=58616 DPT=22 WINDOW=1091 RES=0x00 ACK PSH URGP=0
  • We can do the same with the second line, which concerns the OUTPUT chain:
    • Concern the OUTPUT chain.
    • Source IP address is 192.168.1.90
    • Destination IP address is 192.168.1.88
    • Protocol is TCP.
    • Source port is 22.
Dec 14 15:57:09 WK-STD kernel: OUTPUT : IN= OUT=ens33 SRC=192.168.1.88 DST=192.168.1.90 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=10678 DF PROTO=TCP SPT=22 DPT=58616 WINDOW=524 RES=0x00 ACK URGP=0

Adding the rules

  • Once the analysis is complete, we now need to edit our to edit our /etc/nftables.conf configuration file to add our two new rules (one in INPUT and the other in OUTPUT) and place them just before our “log prefix” rules:
#!/usr/bin/nft -f flush ruleset # We declare an interface variable associated with our network card so that we can use $interface in our rules instead of "enp2s0". define interface = { enp2s0 } # ----- IPv4 and IPv6 ----- table inet filter { chain INPUT { type filter hook input priority 0; policy drop; # Local interface rules ct state invalid counter drop comment "Drop invalid connections" iif != lo ip daddr 127.0.0.1/8 counter drop comment "drop connections to loopback not coming from loopback" iif != lo ip6 daddr ::1/128 counter drop comment "drop connections to loopback not coming from loopback" iif lo accept comment "Accept any localhost traffic" # ICMP traffic ip protocol icmp counter accept comment "accept all ICMP types" meta l4proto ipv6-icmp counter accept comment "accept all ICMPv6 types" # Accept web traffic (ALL to PC : if related or established) : HTTP + HTTPS + HTTP/3 QUIC + DNS + NTP iif $interface tcp sport { http, https, domain, ntp } ct state { related, established } counter accept comment "Accept Web Traffic TCP" iif $interface udp sport { https, domain } ct state { related, established } counter accept comment "Accept Web Traffic UDP" # NEW RULE iif $interface ip saddr 192.168.1.88 ip daddr 192.168.1.90 tcp dport { 22 } accept comment "INPUT SSH" # Accept but trace all other INPUT traffic log prefix "INPUT : " accept comment "log INPUT" } chain FORWARD { type filter hook forward priority 0; policy drop; # FORWARD traffic is dropped and logged. There should be no network frames if the Linux PC is not in router mode. log prefix "FORWARD : " drop comment "log FORWARD" } chain OUTPUT { type filter hook output priority 0; policy drop; # Local interface rules oif lo accept comment "Accept any localhost traffic" # ICMP traffic ip protocol icmp counter accept comment "accept all ICMP types" meta l4proto ipv6-icmp counter accept comment "accept all ICMPv6 types" # Accept web traffic (PC to ALL : new, related and established) : HTTP + HTTPS + HTTP/3 QUIC + DNS + NTP oif $interface tcp dport { http, https, domain, ntp } ct state { new, related, established } counter accept comment "Accept Web Traffic" oif $interface udp dport { https, domain } ct state { new, related, established } counter accept comment "Accept Web Traffic" # NEW RULE oif $interface ip saddr 192.168.1.90 tcp sport { 22 } ip daddr 192.168.1.88 accept comment "OUTPUT SSH" # Accept but trace all other OUTPUT traffic log prefix "OUTPUT : " accept comment "log out of rules traffic" } }
  • We then apply the new configuration:
root@host:~# nft -f /etc/nftables
  • Use the journalctl command again to check that no other legitimate traffic is passing through our “log prefix” rules for INPUT, OUTPUT or FORWARD chains:
root@host:~# journalctl -k --grep="INPUT|OUTPUT|FORWARD"

If legitimate traffic is detected, simply add new nftables rules, as we did for the SSH service.

Finalizing nftables configuration

At this stage, we should have created all the rules we need to allow legitimate traffic. Now it's time to secure our configuration by modifying the behavior of our “log prefix” rules from accept to drop.

  • To do this, edit /etc/nftables.conf once again:
#!/usr/bin/nft -f flush ruleset # We declare an interface variable associated with our network card so that we can use $interface in our rules instead of "enp2s0". define interface = { enp2s0 } # ----- IPv4 and IPv6 ----- table inet filter { chain INPUT { type filter hook input priority 0; policy drop; # Local interface rules ct state invalid counter drop comment "Drop invalid connections" iif != lo ip daddr 127.0.0.1/8 counter drop comment "drop connections to loopback not coming from loopback" iif != lo ip6 daddr ::1/128 counter drop comment "drop connections to loopback not coming from loopback" iif lo accept comment "Accept any localhost traffic" # ICMP traffic ip protocol icmp counter accept comment "accept all ICMP types" meta l4proto ipv6-icmp counter accept comment "accept all ICMPv6 types" # Accept web traffic (ALL to PC : if related or established) : HTTP + HTTPS + HTTP/3 QUIC + DNS + NTP iif $interface tcp sport { http, https, domain, ntp } ct state { related, established } counter accept comment "Accept Web Traffic TCP" iif $interface udp sport { https, domain } ct state { related, established } counter accept comment "Accept Web Traffic UDP" # NEW RULE iif $interface ip saddr 192.168.1.88 ip daddr 192.168.1.90 tcp dport { 22 } accept comment "INPUT SSH" # Accept but trace all other INPUT traffic log prefix "INPUT : " drop comment "log INPUT" } chain FORWARD { type filter hook forward priority 0; policy drop; # FORWARD traffic is dropped and logged. There should be no network frames if the Linux PC is not in router mode. log prefix "FORWARD : " drop comment "log FORWARD" } chain OUTPUT { type filter hook output priority 0; policy drop; # Local interface rules oif lo accept comment "Accept any localhost traffic" # ICMP traffic ip protocol icmp counter accept comment "accept all ICMP types" meta l4proto ipv6-icmp counter accept comment "accept all ICMPv6 types" # Accept web traffic (PC to ALL : new, related and established) : HTTP + HTTPS + HTTP/3 QUIC + DNS + NTP oif $interface tcp dport { http, https, domain, ntp } ct state { new, related, established } counter accept comment "Accept Web Traffic" oif $interface udp dport { https, domain } ct state { new, related, established } counter accept comment "Accept Web Traffic" # NEW RULE oif $interface ip saddr 192.168.1.90 tcp sport { 22 } ip daddr 192.168.1.88 accept comment "OUTPUT SSH" # Accept but trace all other OUTPUT traffic log prefix "OUTPUT : " drop comment "log out of rules traffic" } }
  • Then reapply the rules:
root@host:~# nft -f /etc/nftables

We now have a perfectly secure and controlled configuration. And now you know what to do if certain network services don't work or can't be accessed. In this case, all you need to do is reanalyze the logs with the journalctl command and add new rules, as we did earlier for the SSH service.

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Contact :

contact mail address