nftables ist seit Debian 10 und RHEL 8 der Standard-Paketfilter unter Linux und löst iptables ab. Die Umstellung lohnt sich: eine Syntax statt drei Werkzeuge (iptables, ip6tables, arptables), echte Mengen (Sets und Maps), und Regeln, die sich in einem Durchlauf auf den Zustand beziehen.

Warum nftables statt iptables

iptables                            nftables
--------------------------------------------------------------
Ein Befehl je Adressfamilie          eine Familie: ip, ip6, inet
Regeln werden nacheinander          Regeln in Ketten, Sprung
  geprueft (je Kette ein Durchlauf)     ueber Ausdruecke
Kein Mengentyp                       Sets und Maps (Netze, Adressen,
                                       Ports) - extrem effizient
ipset als Zusatz noetig              Mengen eingebaut
"save/restore" zum Sichern           Regeln als eine Textdatei
Atomarer Wechsel kaum moeglich       atomarer Wechsel per "nft -f"

Beim Umstieg: iptables-nft ist ein Uebersetzungswerkzeug.
  Bestehende iptables-Regeln laufen also weiter, werden aber
  intern als nftables-Regeln gefuehrt. So kann man schrittweise
  umstellen.

Grundlagen: Tabellen, Ketten, Regeln

# Die drei wichtigen Tabellen
#   filter   Standard fuer Ein-/Aus-/Weiterleitung
#   nat      Adressumsetzung
#   mangle   Paketkopfzeilen veraendern (selten)
#
# Die Standardketten in filter:
#   input     an den eigenen Rechner
#   forward   durch den Rechner hindurch (Routing!)
#   output    von der eigenen Anwendung nach draussen
#
# Die Grundregel: Eine Kette entscheidet ueber "accept", "drop"
# oder "jump" in eine andere Kette. Ein "drop" verwirft ohne
# Nachricht, ein "reject" schickt eine Ablehnung.

# Was ist geladen und aktiv?
nft list ruleset

# Regeln in einer Tabelle
nft list table inet firewall

# Zaehler zuruecksetzen (nuetzlich bei der Fehlersuche)
nft reset counters

# Alles loeschen (Vorsicht - erst nach Test der neuen Regeln!)
nft flush ruleset

Eine vollständige Konfiguration

# /etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

# Benannte Mengen: einmal definieren, mehrfach nutzen
define aussen = { eth0 }
define intern_netze = { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 }
define admin_hosts  = { 10.0.0.5, 10.0.0.6, 10.0.0.7 }
define dienst_ports = { 22, 80, 443, 8443 }
define web_ports    = { 80, 443 }
define mail_ports   = { 25, 465, 587, 993 }

table inet firewall {

    # ---- Eingang -------------------------------------------------
    chain input {
        type filter hook input priority 0; policy drop;

        # Bereits bestehende Verbindungen weiter durchlassen
        #   Ohne diese Zeile bricht jede Antwort ab.
        ct state established,related accept

        # Ungueltige Pakete verwerfen
        ct state invalid drop

        # Loopback immer erlauben
        iif lo accept

        # ICMPv6 nicht komplett blockieren - sonst bricht IPv6
        #   (Nachbarschaftserkennung, Pfad-MTU). Nur begrenzen.
        ip6 nexthdr icmpv6 icmpv6 type { nd-neighbor-solicit,
            nd-neighbor-advert, nd-router-advert, packet-too-big } accept
        ip protocol icmp icmp type { echo-request, echo-reply,
            destination-unreachable, time-exceeded } limit rate 10/second accept

        # Verfolgte Protokolle (FTP, SIP) - nur falls gebraucht
        #   ct helper "ftp" ...

        # Dienstports oeffentlich
        tcp dport $web_ports ct state new accept
        tcp dport 22 ct state new ip saddr $admin_hosts accept

        # Mailports nur auf dem Mailserver
        tcp dport $mail_ports ct state new accept

        # Alles andere protokollieren und verwerfen
        limit rate 5/minute log prefix "nft-drop-input: " level warn
        counter drop
    }

    # ---- Weiterleitung (Router, Container) -----------------------
    chain forward {
        type filter hook forward priority 0; policy drop;
        ct state established,related accept
        ct state invalid drop

        # Nur ausgewaehlte Netze duerfen weiterleiten
        ip saddr 10.10.0.0/24 ip daddr 10.20.0.0/24 accept
        ip saddr 10.10.0.0/24 oifname "eth0" accept

        log prefix "nft-drop-forward: " level warn limit rate 5/minute
        counter drop
    }

    # ---- Ausgang -------------------------------------------------
    chain output {
        type filter hook output priority 0; policy accept;

        # Ausgangskontrolle: Nur wenn verlangt!
        #   Ein "policy drop" hier ist riskant - erst protokollieren.
        # tcp dport { 25, 465, 587 } ip saddr != 10.0.0.20 drop

        # DNS nur zum eigenen Resolver
        # udp dport 53 ip daddr != 10.0.0.53 drop
    }
}

# ---- NAT (nur auf Routern) ---------------------------------------
table ip nat {
    chain prerouting {
        type nat hook prerouting priority -100; policy accept;

        # Portweiterleitung nach innen
        #   Port 8080 von aussen -> 10.0.0.11:80 innen
        iifname "eth0" tcp dport 8080 dnat to 10.0.0.11:80

        # Zieladresse in Abhaengigkeit vom Port (Map!)
        #   Die Map ersetzt lange if-Ketten.
        # dnat to tcp dport map {
        #   80   : 10.0.0.11:80,
        #   443  : 10.0.0.11:443,
        #   8080 : 10.0.0.12:8080,
        # }
    }

    chain postrouting {
        type nat hook postrouting priority 100; policy accept;

        # Quelladressumsetzung fuer ausgehenden Verkehr
        oifname "eth0" masquerade

        # Alternative fuer feste Adresse (schneller als masquerade)
        # oifname "eth0" ip saddr 10.0.0.0/24 snat to 203.0.113.7
    }
}

# ---- Mengen mit Zeitablauf (praktisch fuer Sperrlisten) ---------
table ip sperren {
    set zu_oft {
        type ipv4_addr
        flags dynamic, timeout        # dynamisch befuellbar, laeuft ab
        timeout 1h
    }

    chain eingang {
        type filter hook input priority -10; policy accept;

        # Adressen in der Menge werden verworfen
        ip saddr @zu_oft drop

        # Ratenbegrenzung: ueber 100 neue Verbindungen pro
        #   Minute -> in die Menge aufnehmen
        tcp dport { 22, 80, 443 } ct state new \
            meter ratenlimit { ip saddr limit rate over 100/minute } \
            add @zu_oft { ip saddr }
    }
}

Regeln setzen und prüfen

# Konfiguration pruefen und laden - ATOMAR
nft -c -f /etc/nftables.conf          # -c = Syntaxpruefung, nichts anwenden
nft -f /etc/nftables.conf             # anwenden (ersetzt alles)

# WICHTIG: Bei "policy drop" fuer input niemals aus einer
#   SSH-Sitzung heraus arbeiten, ohne eine Notfallregel
#   bereitzuhalten. Zweite Sitzung offen halten!
#
#   Notfall (falls man sich ausgesperrt hat):
#     nft insert rule inet firewall input tcp dport 22 accept
#     nft flush chain inet firewall input

# Einzelne Regel anhaengen
nft add rule inet firewall input tcp dport 8443 ct state new accept

# Regel mit Handle anzeigen und loeschen
nft -a list chain inet firewall input
nft delete rule inet firewall input handle 12

# Zaehler auswerten: Welche Regel greift am haeufigsten?
nft list ruleset | grep -A2 counter

# Logik testen
nft monitor trace                     # Regelverfolgung (dann Regeln mit "trace")
journalctl -k -f                      # Kernelmeldungen der Regeln
nft list ruleset > /etc/nftables.conf # aktuelle Regeln sichern

# Automatisch laden beim Start
systemctl enable nftables
systemctl reload nftables

Prüfliste

  1. Ist ct state established,related accept vorhanden (ohne das bricht die Antwort)?
  2. Sind ICMPv4/ICMPv6 nicht komplett blockiert (Path-MTU, IPv6-Nachbarschaft)?
  3. Ist die Grundregel drop statt reject, wo keine Auskunft nötig ist?
  4. Ist die Protokollierung mit limit rate begrenzt (sonst flutet ein Angriff die Protokolle)?
  5. Sind Zähler aktiv, um zu sehen, welche Regeln greifen?
  6. Ist ein Testpfad für den Aussperrfall dokumentiert und erreichbar?
  7. Ist nft -c -f in der Änderungsschleife?
  8. Sind die Regeln in /etc/nftables.conf gespeichert (sonst nach dem Neustart verloren)?
  9. Sind die Mengen mit timeout versehen, damit die Tabellen nicht unbegrenzt wachsen?
  10. Ist die Weiterleitungskette korrekt (policy drop), wenn der Rechner als Router dient?

Häufige Fehler

  1. Ausgesperrt. Regeln mit policy drop für input ohne Ausweichpfad laden. Immer eine zweite Sitzung offen halten und Regeln in einer zeitlich begrenzten Warteschleife laden (nft -f regeln && sleep 60 && ...).
  2. established vergessen. Alles eingehend wird verworfen, auch Antworten auf eigene Anfragen.
  3. ICMP gesperrt. Path-MTU-Erkennung bricht, IPv6 funktioniert gar nicht. Verbindungen hängen bei großen Übertragungen.
  4. Regeln nur im Arbeitsspeicher. Nach dem Neustart ist alles weg. /etc/nftables.conf schreiben und den Dienst aktivieren.
  5. Protokollierung ohne Limit. Ein Port-Scan füllt die Festplatte in Minuten.
  6. Forward-Kette nicht beachtet. Bei Containern und Routern wird forward benötigt – input allein reicht nicht.
  7. NAT und Filter vermischt. NAT wird vor dem Filter ausgewertet (Priorität -100). Wer im Filter Zieladressen nach NAT prüft, prüft die falschen Werte (dafür ct original nutzen).
  8. Endlos wachsende Mengen. Ohne timeout füllt eine Sperrliste den Speicher.

Verwandte Grundlagen: Firewall, NAT und Portweiterleitung, Fail2ban, SSH.