A missing blog post image

Introduction

From the official documentation website, nftables is :

[…] the new packet classification framework that intends to replaces the existing {ip,ip6,arp,eb}_tables infrastructure.

I won’t be listing all the pros and cons of using nftables over iptables, but simply citing the dedicated section of the Netfilter Wikipedia page :

The main advantages over iptables are simplification of the Linux kernel ABI (Application Binary Interface, ed.), reduction of code duplication, improved error reporting, and more efficient execution, storage, and incremental changes of filtering rules.

Wow ! What an introduction.

As it should be considered as the-way-of-managing-Netfilter since 2016, I was pretty frustrated not to find any “hardening” guide for it on the Web, so here is one !

Note : I’ll be using the declarative nftables scripting format, much more clear IMHO.

Everything starts with a shebang

#!/usr/sbin/nft -f

This will allow you to run a regular chmod +x on your rules definition file, and if you’re editing it with Sublime Text, the Nftables syntax definition will be automatically set (that was the moment of self-promotion, which doesn’t happen very often).

Let’s clean up this mess

I don’t know what your current ruleset looks like (and maybe you don’t know too :fearful:), so let’s clean it up in an nft fashion :

flush ruleset

Note : By not specifying any network family type, all existing tables will be removed.

A very old rule of thumb

“Anything that is not explicitly permitted is prohibited.”
— M. S.

With iptables, you would have (and I hope you did) set DROP policies on each default FILTER chains with :

iptables -P INPUT   DROP
iptables -P FORWARD DROP
iptables -P OUTPUT  DROP

ip6tables -P INPUT   DROP
ip6tables -P FORWARD DROP
ip6tables -P OUTPUT  DROP

We will drop any thoughts we may have about that, and simply look at how we can reproduce the same behavior with nftables :

table inet filter {
	chain input {
		type filter hook input priority 0; policy drop;

		# ...
	}

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

		# ...
	}

	chain output {
		type filter hook output priority 0; policy drop;

		# ...
	}
}

Describing an inet table allows us to handle any IPv4 (ip) and IPv6 (ip6) packets at the very same location (you know DRY and so on).
With each chain bound to its respective hook, and policies set to drop, we can be sure that our default skeleton will, at this step, reject any packet.

Note : If you (accidentally) forgot how Netfilter handles packet flow, here is a[n] (almost-complete) reminder.

Mitigate DDoS attacks and script kiddies exploration

table netdev filter {
	chain ingress {
		type filter hook ingress device eth0 priority -500;

		# IP FRAGMENTS
		ip frag-off & 0x1fff != 0 counter drop

		# IP BOGONS
		# From <https://www.team-cymru.com/bogon-reference.html>.
		ip saddr { \
				0.0.0.0/8, \
				10.0.0.0/8, \
				100.64.0.0/10, \
				127.0.0.0/8, \
				169.254.0.0/16, \
				172.16.0.0/12, \
				192.0.0.0/24, \
				192.0.2.0/24, \
				192.168.0.0/16, \
				198.18.0.0/15, \
				198.51.100.0/24, \
				203.0.113.0/24, \
				224.0.0.0/3 \
			} \
			counter drop

		# TCP XMAS
		tcp flags & (fin|psh|urg) == fin|psh|urg counter drop

		# TCP NULL
		tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 counter drop

		# TCP MSS
		tcp flags syn \
			tcp option maxseg size 1-535 \
			counter drop
	}
}

Here, the table has been declared with a netdev network family type. It means that any incoming packet from layer 2 would go through the created chain, as an ingress hook has been set.

You may also have noticed the -500 priority. By setting it lower than NF_IP_PRI_CONNTRACK_DEFRAG (= -400), we are sure that our chain will be evaluated before any other one registered on the ingress hook. This makes it the perfect place to set our DDoS counter-measures, as we would “spare” a few CPU cycles per packet.

About the rules themselves, there are two kind of statements (decisions) : those that are terminal, and those which are not. For instance, drop is terminal (a verdict), whereas counter is not.
Thus, we may specify counter drop, to make Netfilter count the number of packets matching the rule, and drop them at the same time (very useful for debugging purposes).
No need to duplicate weird iptables calls anymore (calls that were duplicating Netfilter registered rules by the way :roll_eyes:).

Note on “Bogons” : If you got an IPv6 stack, you might be interested in the IPv6 Full Bogons list.

One more hardening rule with conntrack

A regular anti-DDoS rule is to block new packets that are not SYN.

Why didn’t you add such a rule to the previous code snippet then ?

Well, in order to match “new” packets, we need the help of the conntrack Netfilter module.
The problem : It’s not available within a chain registered with the ingress hook, that’s why we gotta use it elsewhere.
Let’s then take the firstly encountered other “location” on the Netfilter flow : the PREROUTING chain of the filter table, at the mangle (-150) priority.

table inet mangle {
	chain prerouting {
		type filter hook prerouting priority -150;

		# CT INVALID
		ct state invalid counter drop

		# TCP SYN (CT NEW)
		tcp flags & (fin|syn|rst|ack) != syn \
			ct state new \
			counter drop
	}
}

The first rule would drop any packet flagged as invalid by the conntrack module.
The second would do the same for any new packet, presenting any other TCP flag beside SYN.

Conclusion

Here is the final skeleton detailed above :

#!/usr/sbin/nft -f


flush ruleset


table netdev filter {
	chain ingress {
		type filter hook ingress device eth0 priority -500;

		# IP FRAGMENTS
		ip frag-off & 0x1fff != 0 counter drop

		# IP BOGONS
		# From <https://www.team-cymru.com/bogon-reference.html>.
		ip saddr { \
				0.0.0.0/8, \
				10.0.0.0/8, \
				100.64.0.0/10, \
				127.0.0.0/8, \
				169.254.0.0/16, \
				172.16.0.0/12, \
				192.0.0.0/24, \
				192.0.2.0/24, \
				192.168.0.0/16, \
				198.18.0.0/15, \
				198.51.100.0/24, \
				203.0.113.0/24, \
				224.0.0.0/3 \
			} \
			counter drop

		# TCP XMAS
		tcp flags & (fin|psh|urg) == fin|psh|urg counter drop

		# TCP NULL
		tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 counter drop

		# TCP MSS
		tcp flags syn \
			tcp option maxseg size 1-535 \
			counter drop
	}
}

table inet filter {
	chain input {
		type filter hook input priority 0; policy drop;

		# ...
	}

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

		# ...
	}

	chain output {
		type filter hook output priority 0; policy drop;

		# ...
	}
}

table inet mangle {
	chain prerouting {
		type filter hook prerouting priority -150;

		# CT INVALID
		ct state invalid counter drop

		# TCP SYN (CT NEW)
		tcp flags & (fin|syn|rst|ack) != syn \
			ct state new \
			counter drop
	}
}

You “only” have to complete it with your own rules now :wink:

Note : If you are interested in a migration from iptables, you might wanna read this.

If you think that something is definitely missing (or wrong !), please feel free to leave a comment below, as usual :ok_hand:

Sources

Acknowledgments