Möglicherweise wird nft bereits als Backend verwendet, obwohl die Regeln über iptables erstellt wurden. Einige Distributionen, zum Beispiel das aktuelle Debian 10 “Buster”, nutzen anstelle von iptables iptables-nft, welches sich dem Benutzer gegenüber wie iptables verhält, im Hintergrund aber Regeln mit nft erstellt. Wenn dies der Fall ist, lassen sich die generierten Regeln per nft list ruleset anzeigen, sofern nftables ebenfalls installiert ist. Dies ermöglichst auch einen Migrationspfad. In diesem Artikel wollen wir allerdings eine neue Konfiguration von Grund auf erstellen.

Vorteile und Installation

Wenn nftables im Hintergrund bereits verwendet wird, warum sollte man dann auf das neue Frontend wechseln? Dafür gibt es mehrere gute Gründe: Mit nftables wird es möglich, Filterregeln zu schreiben, welche für IPv4 und IPv6 gleichermaßen gelten. Bei iptables musste dafür jede Regel ebenfalls mit ip6tables hinzugefügt werden. Eine Regel, welche einen TCP-Port sowohl für IPv4 als auch für IPv6 freigibt, benötigt eine nftables-Regel, aber zwei iptables-Regeln. Wird iptables-nft verwendet, werden zwei nftables-Regeln erstellt.

Die durch die Übersetzungsschicht erzeugten Regeln sind komplexer als handgeschriebene und aktivieren für jede Regel einen Treffer-Zähler, welcher im Regelfall nicht benötigt wird.

Ein weiterer Vorteil von nftables ist, dass alle Regeln gleichzeitig geladen werden können, während bei iptables die Regeln nacheinander eingefügt werden. Das heißt, es werden entweder im Erfolgsfall die neuen oder im Fehlerfall die alten Regeln verwendet. Fehler durch Zwischenstände können nicht mehr auftreten.

Für große Regelwerke könnte die Kernel-interne Virtuelle Maschine (BPF – Berkeley Packet Filter) zusammen mit dem Compiler zu einer besseren Performance führen.

Bevor wir anfangen können, müssen die Administrationswerkzeuge installiert werden. Auf einem System mit Debian 10 “Buster” geschieht das mit dem Befehl apt-get install nftables. Unter Debian 11 “Bullseye”, welches voraussichtlich Mitte des Jahres erscheinen wird, wird nftables anstelle von iptables bereits vorinstalliert sein.

In diesem Artikel soll die Erstellung eines Regelwerkes anhand von zwei Beispielen gezeigt werden: Ein Regelwerk für einen Webserver und eins für einen einfachen Router. Das Regelwerk wird in die Datei /etc/etc/nftables.conf gespeichert. Anschließend kann es mit dem Befehl nft -f /etc/nftables.conf geladen werden. Damit es beim Systemstart automatisch geladen wird, gibt es eine Systemd-Unit nftables.service, welche aber standardmäßig deaktiviert ist. Sie kann mit systemctl enable nftables.service aktiviert werden.

Absicherung eines Webservers

Im ersten Beispiel soll ein Webserver abgesichert werden. Dieser Webserver ist sowohl per IPv4 als auch per IPv6 erreichbar. Es werden die Standard-Ports für HTTP und HTTPS verwendet, für Wartungszwecke steht ein SSH-Zugang zur Verfügung. Weiterhin soll er von einem Monitoring-System per NRPE überwacht werden. Alle anderen Ports sollen nicht erreichbar sein. Ausgehender Datenverkehr wird nicht gefiltert. Netzwerkdiagnose-Werkzeuge sollen weiterhin funktionieren.

Fangen wir an, unser Regelwerk aufzubauen:

#!/usr/sbin/nft -f
flush ruleset

Die erste Zeile definiert, dass es sich um ein nftables-Regelwerk handelt; die zweite Zeile löscht alle bereits vorhandenen Tabellen, Ketten und Regeln.

table inet filter {…}

Hiermit legen wir eine Tabelle mit dem Namen filter und dem Type inet an. Tabellen sind Container, welche Ketten und andere Objekte enthalten. Tabellen haben einen Typ, in diesem Fall inet. Der Vorteil des Types inet ist, dass die darin enthaltenen Regeln sowohl für das neue IPv6 als auch das veraltete IPv4 gelten, sofern die Regel nicht anhand von IP-Adressen filtert. Dies ermöglicht das Schreiben kompakter Regeln. Die Typen ip und ip6 gelten jeweils nur für das eine oder das andere Protokoll.

chain input {…}

Innerhalb dieser Tabelle legen wir eine Kette (chain) an. Diese nennen wir input, da sie eingehenden Datenverkehr regeln soll – dieser Name ist jedoch frei wählbar.

type filter hook input priority 0; policy drop;

Die erste Zeile beschreibt die Kette näher: Es soll eingehender (hook input) Datenverkehr gefiltert (type filter) werden und alle Pakete, für welche keine Entscheidung getroffen wurde, werden verworfen (policy drop;). Die Standard-Policy ist accept.

Nach dieser Zeile folgen die Regeln. In jeder Zeile wird exakt eine Regel angegeben. Die erste passende Regel wird verwendet.

iif lo accept

Als erstes soll der Host-interne Datenverkehr zugelassen werden. Mit iif wird das Netzwerk-Interface angegeben, auf welchem der Datenverkehr ankommt, lo ist das Loopback-Device. Es hat immer die IP-Adresse 127.0.0.1 und alle dorthin gesendeten Pakete kommen zum Host zurück. Es wird daher zur Kommunikation zwischen verschiedenen Prozessen genutzt und ist zum Funktionieren eines Linux-Systems erforderlich.

ct state { established, related } accept

Mit dieser Regel werden alle Pakete erlaubt, welche zu bestehenden Verbindungen gehören. ct steht für Connection Tracking. Diese Regel ermöglicht, dass Antwort-Pakete zu ausgehenden Paketen durchgelassen werden. Ohne diese Regel wären beispielsweise keine Updates des Betriebssystems möglich.

icmp type echo-request accept

Diese Regel erlaubt, dass Pings an den Host gesendet werden. Um andere ICMP-Pakete wie “Destination Unreachable” brauchen wir uns nicht zu kümmern, da diese immer zu einer bestehenden Verbindung gehören und somit von der zweiten Regel bereits erlaubt wurden.

icmpv6 type { echo-request, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept

Diese Regel erlaubt neben Pings per IPv6 noch drei weitere Arten von ICMPv6-Paketen, welche zum Funktionieren von IPv6 zwingend erforderlich sind. Mit Router Advertisements (nd-router-advert) teilt der Router dem Netzwerk das Präfix und seine Adresse mit, es ist das IPv6-Gegenstück zu DHCP. Router Solicitations (nd-router-solicit) müssen nicht erlaubt werden, da dieser Host kein Router ist. Über Neighbor Solicitations (nd-neighbor-solicit) und Neighbor Advertisements (nd-neighbor-advert) teilen sich die Hosts in einem Netzwerk mit, dass sie da sind. Es ist das Gegenstück zu ARP bei IPv4. ARP müssen wir nicht behandeln, da es von inet nicht erfasst wird. Dafür gäbe es die Tabelle arp. Wenn es für ein Protokoll keine Filter-Tabelle gibt, ist die Standard-Aktion accept.

Nachdem die grundlegenden Einstellungen vorgenommen wurden, können die Freigaben für die einzelnen Dienste vorgenommen werden.

tcp dport ssh accept

Eingehende TCP-Verbindungen mit dem Zielport 22 werden erlaubt. Der Port kann entweder eine Nummer oder wie im Beispiel ein Name sein. Zur Übersetzung vom Namen in die Nummer konsultiert nftables die Datei /etc/services.

tcp dport { http, https } accept

Es können auch mehrere Ports in einer Zeile freigegeben werden, hier 80 und 443. Der Webserver ist jetzt erreichbar.

ip saddr 203.0.113.2 tcp dport nrpe accept

Der Host mit der IP-Adresse 203.0.113.2, auf welchem in unserem Beispiel das Monitoring-System läuft, darf auf den NRPE-Port zugreifen – sonst niemand. Im Gegensatz zu den vorherigen Regeln gilt diese Regel nur für IPv4.

ip6 saddr 2001:db8:1814:517::2 tcp dport nrpe accept

Das Monitoring-System hat auch eine IPv6-Adresse, diese wird mit dieser Regel freigegeben. Analog zur vorherigen Regel gilt sie nur für IPv6.

udp dport 33434-33523 reject

Zum Schluss werden noch alle UDP-Pakete an alle Ports zwischen 33434 und 33523 verworfen und mit der ICMP-Nachricht “Destination port unreachable” abgelehnt, das entspricht einem nicht gefilterten Port, auf welchem kein Dienst läuft. Diese Regel dient einerseits als Beispiel für UDP, einen Port-Range und Reject und sorgt andererseits dafür, dass das Werkzeug traceroute funktioniert.

Und hier noch einmal das komplette Beispiel:

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
        chain input {
                type filter hook input priority 0; policy drop;
                iif lo accept
                ct state { established, related } accept
                icmp type echo-request accept
                icmpv6 type { echo-request, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
                tcp dport ssh accept
                tcp dport { http, https } accept
                ip saddr 203.0.113.2 tcp dport nrpe accept
                ip6 saddr 2001:db8:1814:517::2 tcp dport nrpe accept
                udp dport 33434-33523 reject
        }
}

Eine Netzwerk-Firewall mit nftables

Das zweite Beispiel soll ein Router mit drei Netwerk-Interfaces sein: eth0 ist das externe Netz, eth1 das interne Netz und eth2 die DMZ. In der DMZ steht ein von außen erreichbarer Server. Per IPv6 ist er direkt erreichbar, für IPv4 werden zwei Portweiterleitungen eingerichtet.

Das fertige Regelwerk sieht wie folgt aus:

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
        chain input {
                type filter hook input priority 0; policy drop;
                iif lo accept
                ct state { established, related } accept
                icmp type echo-request accept
                icmpv6 type { echo-request, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
                iifname "eth0" drop
                udp dport domain accept
                tcp dport domain accept
                iifname "eth1" tcp dport ssh accept
        }

        chain forward {
                type filter hook forward priority 0; policy drop;
                ct state { established, related } accept
                iifname "eth1" oifname "eth0" accept
                iifname "eth1" oifname "eth2" accept
                iifname "eth2" oifname "eth0" accept
                iifname "eth0" oifname "eth2" ip daddr 198.51.100.17 tcp dport { http, https } accept
                iifname "eth0" oifname "eth2" ip6 daddr 2001:db8:1814:517::3 tcp dport { http, https } accept
        }
}

table ip nat {
        chain prerouting {
                type nat hook prerouting priority -100; policy accept;
                iifname "eth0" tcp dport { 80, 443 } dnat 198.51.100.17
        }

        chain postrouting {
                type nat hook postrouting priority 100; policy accept;
                oifname "eth0" ip saddr { 192.0.2.0/24, 198.51.100.0/24 } masquerade
        }
}

Der erste Teil der input-Kette ist weitgehend identisch mit dem Webserver-Beispiel. Man beachte aber, dass nun auch Router Solicitations (nd-router-solicit) erlaubt sind, da dies ein Router ist. Da der Router gleichzeitig der DNS-Server für das Netzwerk ist, werden eingehende DNS-Anfragen erlaubt (udp/tcp port domain accept). Diese DNS-Anfragen und weitere, später hinzukommende interne Dienste sollen von außen nicht erreichbar sein. Deshalb wurde vor den Accept-Regeln die Drop-Regel (iifname “eth0” drop) eingefügt, welche eingehende Pakete explizit verwirft. Die Regeln werden von oben nach unten abgearbeitet und die erste zutreffende Regel verwendet. Wenn eine Regel zutrifft, werden die folgenden nicht mehr beachtet. Damit gelten die DNS-Regeln nur für eth1 und eth2. Alternativ hätte man die erlaubten Interfaces auch explizit mit in die Accept-Regeln integrieren können. (iifname { "eth1", "eth2" } udp dport domain accept)

Neu hinzugekommen sind eine Kette für weiterzuleitende Pakete (chain forward) innerhalb der Tabelle filter sowie eine Tabelle für NAT (table ip nat) mit den zwei Ketten prerouting und postrouting. Da NAT nur für IPv4 relevant ist, ist diese Tabelle vom Typ ip. Die Namen der Tabellen und Ketten sind wieder beliebig.

Die forward-Kette funktioniert zunächst wie die input-Kette. Da die Pakete weitergeleitet werden, gibt es jedoch die Möglichkeit, eingehendes (iifname) und ausgehendes (oifname) Interface bei den Regeln zu verwenden. Die drei ersten Regeln legen fest, welche unserer drei Zonen Extern, Intern und DMZ auf welche andere zugreifen darf. Intern (eth1) darf nach Extern (eth0) und auf die DMZ (eth2) zugreifen, die DMZ nur nach Extern. Von Extern ist kein grundsätzlicher Zugriff vorgesehen, es darf aber auf jeweils eine IPv4- und IPv6-Adresse in der DMZ auf zwei Dienste (HTTP, HTTPS) zugegriffen werden. Zugriffe von Extern nach Intern sind nicht zugelassen, sie werden durch die Drop-Policy verworfen.

In der NAT-Tabelle gibt es zwei Ketten vom Typ nat. In der Kette prerouting werden die Port-Weiterleitungen (Destination NAT) definiert. Dies muss passieren, bevor das System eine Routing-Entscheidung trifft (daher “Pre”-Routing). Die Kette hat die Priorität -100, wird also vor den Filter-Regeln (Priorität 0) ausgeführt. Die Policy ist accept, es wird an dieser Stelle nichts gefiltert, dies passiert erst zu einem späteren Zeitpunkt in der bereits definierten Kette forward. Die Regel legt fest, dass wenn auf dem externen Interface “eth0” ein Paket auf Port 80 oder 443 eingeht, die Ziel-Adresse auf 198.51.100.17 geändert wird, die Adresse unseres Webservers. Der Kernel wird das Paket also nicht selbst bearbeiten, sondern an den neuen Zielhost weiterleiten. Bei zugehörigen Antwort-Paketen wird die Quell-Adresse entsprechend zurückgeändert.

In der Kette postrouting werden die internen IP-Adressen der Hosts durch die öffentliche IP-Adresse des Routers ersetzt (Source NAT, häufig nur als NAT bezeichnet). Dies passiert, nachdem das System die Routing-Entscheidung getroffen hat (daher “Post”-Routing). Die Kette hat die Priorität 100, wird also nach den Filter-Regeln (Priorität 0) ausgeführt. Die Policy ist auch hier wieder accept, unerwünschte Pakete kommen in dieser Kette nicht an, da sie durch die Kette forward bereits verworfen wurden. In der Regel wird mit oifname das ausgehende Interface festgelegt. Das Schlüsselwort masquerade setzt die Quell-Adresse auf die aktuelle IP-Adresse des Interfaces, auf welchem das Paket den Host verlässt. Wenn das System über eine statische, öffentliche IP-Adresse verfügt, kann diese auch mit snat 203.0.113.1 angegeben werden.

Fazit

Ob Server oder Router, ob IPv4 oder IPv6, nftables überzeugt durch gut lesbare Regelwerke und die Möglichkeit atomarer Änderungen. Es erleichtert die Erstellung von Regeln für Dual-Stack-Umgebungen und macht die Verwendung von iptables-Frontends wie Shorewall oder UFW überflüssig. In Zukunft wird nftables der Standard sein, aufgrund der Vorteile lohnt sich die Verwendung bereits jetzt.

Jens Meißner
Jens Meißner arbeitet seit 2018 als Linux-Consultant bei B1 Systems und beschäftigt sich mit den Themen Netzwerk, Virtualisierung, Automatisierung und Monitoring. Am liebsten benutzt er Debian, interessiert sich neben Linux aber auch für FreeBSD und OpenBSD.