Securing your server with nftables

Securing your server as a Linux administrator has been a bit of a hassle in past years. You had to get familiar with a variety of tools, most notably the iptables and it’s derivatives ip6tables, ebtables and arptables. Keeping them in sync requires you to either manually update all of them with every change or create a custom script to do it for you.

Nftables is developed by the netfilter project, which stands behind iptables as well. It’s meant to directly address the most pressing problems and inconveniences. While nftables has been a part of linux kernel since 2014 only very recently it has become a recommended solution for network traffic filtering.

Nftables vs iptables

Quick refresher on terminology:

  • table refers to a container of chains with no specific semantics.
  • chain within a table refers to a container of rules.
  • rule refers to an action to be configured within a chain.

What stays the same

Nftables keeps the best from its predecessor. It reuses the existing hook infrastructure, Connection Tracking System, NAT engine, logging infrastructure, userspace queueing and so on.

nftables are a perfect example of changing the promblematic parts, while keeping what was already perfected.

Simplified syntax

As you will see in the following examples nftables provides very readable syntax, which makes it easy to get into. Data structures set, map and dictionary all contribute to less repetition during configuration.

If you’re no stranger to network analyzers, nftables atomic commands may seem familiar to you. The syntax has been inspired by tcpdump.

Netfilter provided us with an iptables-translate tool to ease the transition from iptables. Interestingly enough some linux distributions use it by default when you try to configure iptables. You might actually be using nftables already without even knowing it!

Many of the following examples are taken directly from nftables documentation wiki with some slight modifications I thought would make it more readable.

Ingress hooks

New ingress hook allows you to filter L2 traffic. It comes before prerouting, after the packet is passed up from the NIC driver. This means you can enforce very early filtering policies. When adding a chain on ingress hook, it is mandatory to specify the device where the chain will be attached

nft add chain netdev foo dev0filter { type filter hook ingress device eth0 priority 0 \; }

All the hooks used by netfilter are shown in the following diagram.

Generic set

nftables comes with a built-in generic set infrastructure which allows you to create both named and anonymous sets. For example allowing IPv6 packet on different ports

nft add rule ip6 firewall input tcp dport {telnet, http, https} accept

is a simple rule that makes use of an anonymous set.

If you expect to update your sets over time you can use a named set, which can be easily edited whenever you like. Keep in mind that they are always associated with one table.

nft add set firewall spammers { type ipv4_address\;}
nft add element firewall spammers { 192.168.1.1, 192.168.1.3 }
nft add rule ip firewall filter ip saddr @spammers drop
//when later .2 starts spamming as well
nft add element firewall spammers { 192.168.1.2 }

Map

Map is an extension to a generic set that stores key-value pairs. The following example shows how the destination TCP port selects the destination IP address to DNAT the packet:

nft add rule ip nat prerouting dnat tcp dport map {\
80 : 192.168.1.100, 8888 : 192.168.1.101 }

If the TCP destination port is 80, then the packet is DNAT’ed to 192.168.1.100. If the TCP destination port is 8888, then the packet is DNAT’ed to 192.168.1.101.

Note: You must first define table nat and chain prerouting.

Dictionary

Also known as verdict maps, dictionaries allow you to attach an action to an element. Since the dictionary also has a generic set under the hood, the syntax is very similar to maps. You can create a simple ruleset very quickly. As it’s a named dictionary, you can add or remove element-action pairs whenever you like.

nft add map firewall mydict { type ipv4_addr : verdict\; }
nft add element firewall mydict { 192.168.0.10 : drop, 192.168.0.11 : accept }
nft add rule firewall input ip saddr vmap @mydict

Great example to show how dictionaries (and other structures) can help to reduce the number of traversed chains is attachment of a particular chain to chosen layer 4 protocol.

nft add rule ip firewall input ip protocol vmap {\ 
tcp : jump tcp-chain, udp : jump udp-chain , icmp : jump icmp-chain }

This example above assumes that you’ve already created the tcp-chain, udp-chain and icmp-chain custom chains.

Concatenations

Nftables provides concatenations to allow you to put more selectors together using all the structures mentioned above. Using them you can easily match ip address AND port, source address AND destination address and pretty much any arbitrary combination of elements.

nft add rule ip firewall input ip saddr . ip daddr . ip protocol {\
1.1.1.1 . 2.2.2.2 . tcp, 1.1.1.1 . 3.3.3.3 . udp} accept

If the packet’s source IP address AND destination IP address AND level 4 protocol match any rules from the set, nftables accepts the packet.

This practice makes it easy to create complex rules which are internally hashed and performed nearly O(1) according to documentation.

Sample configuration

Let’s have a look at the simplest webserver firewall configuration. It only accepts ssh connections, IPv4 ping and HTTP(s) trafic.

nft add table inet firewall
nft add chain inet firewall input { type filter hook input priority 0 \; policy drop\; } 
# Allow Established/Related
nft add rule inet firewall input ct state established,related accept
# Drop Invalid Connections
nft add rule inet firewall input ct state invalid drop
# allow ping
nft add rule inet firewall input ip protocol icmp limit rate 4/second accept
# allow ssh and http(s)
nft add rule inet firewall input tcp dport { 22, http, https }
nft add rule inet firewall input udp dport { 22, http, https }
# don't forward anything, you're not a router!
nft add chain inet firewall forward { type filter hook forward priority 0 \; policy drop\; } 
# allow all outgoing traffic
nft add chain inet firewall output { type filter hook output priority 0 \; policy accept\; }

You can also forgo atomic rule use completely and create the whole ruleset in a text file, which you can easily load. Simply use nft -f <filepath>. Your file is in the same format as nft list ruleset which means that nft list ruleset > file is a perfectly fine way to backup your setup.

flush ruleset

table inet firewall {
    chain inbound {
    type filter hook input priority 0; policy drop;
        tcp dport 22 accept

        tcp dport { http, https } accept
        udp dport { http, https } accept
    }

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

    chain outbound {
        type filter hook output priority 0; policy accept;
    }
}

You can also export your rules to xml or json.

Common Mistakes

Not escaping semicolon

This one is obligatory. Most of you know, but it never hurts to be reminded. You must escape semicolon in bash!

In some examples you can see alternative approach using nft ‘<commands>’

nft add chain inet my_table my_input { type filter hook input priority 0 \; policy drop \; }
nft add chain inet my_table my_input '{ type filter hook input priority 0 ; policy drop ; }'
nft 'add chain inet my_table my_input { type filter hook input priority 0 ; policy drop ; }'

All of the examples above are equivalent. Just use the one you’re the most comfortable with.

Family is important

Nftables defaults to the ip(v4) protocol family when not told otherwise. While it’s not a bad design you may end up defining rules you don’t really want.

Splitting IPv4 and IPv6

New inet family exists specifically so you don’t have to duplicate your rules for both protocols.

Loading before flushing

In the sample configuration you can see that we first flush ruleset before adding the rules. It’s a recommended practice to prevent duplicate rules.

Resources