blog/_posts/2020-03-20-nftables-hardeni...

9.7 KiB
Raw Permalink Blame History

title date last_modified_at url layout category image description
nftables hardening rules and good practices 2020-03-20 11:30 2023-01-19 nftables-hardening-rules-and-good-practices post Security /img/blog/nftables-hardening-rules-and-good-practices.png A not-so-complete nftables hardening guide

A missing blog post image

Introduction

From the official documentation website, nftables is :

...

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

{% highlight nftables %} #!/usr/sbin/nft -f {% endhighlight %}

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 😨), so let's clean it up in an nft fashion :

{% highlight nftables %} flush ruleset {% endhighlight %}

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 :

{% highlight bash %} iptables -P INPUT DROP iptables -P FORWARD DROP iptables -P OUTPUT DROP

ip6tables -P INPUT DROP ip6tables -P FORWARD DROP ip6tables -P OUTPUT DROP {% endhighlight %}

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

{% highlight 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;

	# ...
}

} {% endhighlight %}

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

{% highlight nftables %} 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
}

} {% endhighlight %}

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 🙄).

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.

{% highlight nftables %} 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
}

} {% endhighlight %}

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 :

{% highlight nftables %} #!/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
}

} {% endhighlight %}

You "only" have to complete it with your own rules now 😉

Note : If you are interested in a migration from iptables, you might wanna read [this]({% post_url 2020-01-17-from-stretch-to-buster-how-to-migrate-from-iptables-to-nftables %}).

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

Sources

Acknowledgments