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

290 lines
9.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "nftables hardening rules and good practices"
date: 2020-03-20 11:30
last_modified_at: 2023-01-19
url: nftables-hardening-rules-and-good-practices
layout: post
category: Security
image: /img/blog/nftables-hardening-rules-and-good-practices.png
description: "A not-so-complete nftables hardening guide"
---
[![A missing blog post image](/img/blog/nftables-hardening-rules-and-good-practices.png)](/img/blog/nftables-hardening-rules-and-good-practices.png)
### Introduction
From the [official documentation website](https://wiki.nftables.org/wiki-nftables/index.php/What_is_nftables%3F), 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](https://en.wikipedia.org/wiki/Netfilter#nftables) :
> 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](https://wiki.nftables.org/wiki-nftables/index.php/Scripting#File_formats), 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](https://github.com/HorlogeSkynet/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 :
{% 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](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 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](https://commons.wikimedia.org/wiki/File:Netfilter-packet-flow.svg) 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 :roll_eyes:).
> Note on "Bogons" : If you got an IPv6 stack, you _might_ be interested in the [IPv6 Full Bogons](https://www.team-cymru.org/Services/Bogons/fullbogons-ipv6.txt) list.
### One more hardening rule with conntrack
A regular anti-DDoS rule is to [block new packets that are not `SYN`](https://javapipe.com/blog/iptables-ddos-protection/#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](https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks#Priority_within_hook).
{% 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 :wink:
> 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 :ok_hand:
### Sources
* [DDoS Protection With IPtables: The Ultimate Guide](https://javapipe.com/blog/iptables-ddos-protection/)
* [How to drop 10 million packets per second](https://blog.cloudflare.com/how-to-drop-10-million-packets/)
* [IPv6 Martian and Bogon Filters](https://6session.wordpress.com/2009/04/08/ipv6-martian-and-bogon-filters/)
* [Setting up a server firewall with nftables that support WireGuard VPN](https://xdeb.org/post/2019/09/26/setting-up-a-server-firewall-with-nftables-that-support-wireguard-vpn/)
* [How to: Linux Iptables block common attacks](https://www.cyberciti.biz/tips/linux-iptables-10-how-to-block-common-attack.html)
* [Drop fragmented packets in nftables](https://serverfault.com/a/814329)
* [paulgorman.org/technical &mdash; nftables](https://paulgorman.org/technical/linux-nftables.txt.html)
* [Netfilters connection tracking system](https://people.netfilter.org/pablo/docs/login.pdf)
### Acknowledgments
* Thanks to [Timo](#isso-56) for their improvement of `conntrack`-based hardening rules
* Thanks to Thomas for digging up the [MSS "off-by-one" error](#isso-70) and [fixing the TCP XMAS detection rule](#isso-71)