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.
Let's see how to set this up!
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"
}
}
root@host:~# nft -f /etc/nftables
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.
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
#!/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"
}
}
root@host:~# nft -f /etc/nftables
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.
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.
#!/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"
}
}
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.
Contact :