
Linux Firewall mit nftables 2026 – Modernes Setup
Linux Firewall mit nftables 2026: Modernes Setup mit Sets, Maps, NAT und Docker-Integration – inkl. Migration von iptables, Performance-Werten und Praxis-Befehl
Die Zahl automatisierter SSH- und WordPress-Login-Versuche ist in den letzten Jahren nicht gesunken, sondern explodiert. Ein frisch aufgesetzter Root-Server mit öffentlicher IPv4 bekommt im Schnitt innerhalb von 8 bis 20 Minuten die ersten Login-Versuche – bei einem Test von hostazar auf einem Hetzner CX22 (3,79 €/Monat) waren es 47 Versuche in der ersten Stunde, verteilt auf 19 verschiedene IPs. Wer nur auf Passwort-Auth verzichtet und Port 22 verlegt, reduziert das Volumen, aber nicht die grundsätzliche Angriffsfläche.
Fail2ban bleibt 2026 das pragmatischste Werkzeug gegen diese Klasse von Angriffen: Es liest Logdateien, erkennt Muster per Regex und sperrt die Quell-IP temporär auf Firewall-Ebene. Kein Agent auf dem Client, kein Cloud-Abo, keine Telemetrie. Version 1.1.x ist die aktuelle stabile Reihe, sie bringt native nftables-Unterstützung, einen überarbeiteten systemd-Backend und deutlich bessere Python-3.12-Kompatibilität als die 0.11er-Generation.
Der entscheidende Vorteil gegenüber Cloud-WAF-Lösungen: Fail2ban arbeitet auf jedem Dienst, der irgendwo eine Logzeile schreibt. SSH, Postfix, Dovecot, Nextcloud, Vaultwarden, ein selbstgeschriebenes Python-Backend – wenn es loggt, kann Fail2ban es bannen. Genau das macht es für Selfhoster und kleine Agenturen weiterhin attraktiv.
Die Grenzen sollte man kennen: Fail2ban ist reaktiv, nicht präventiv. Es schützt nicht vor Zero-Day-Exploits, nicht vor langsamen verteilten Angriffen (Low-and-Slow über hunderte IPs) und nicht vor Angriffen, die aus dem eigenen Rechenzentrum kommen. Es ist ein Baustein, kein Ersatz für Key-Auth, Updates und ein restriktives Firewall-Konzept.
Auf Debian 12/13 und Ubuntu 24.04/26.04 ist Fail2ban in den Repos und in wenigen Sekunden installiert. Wichtig: Der Dienst startet beim ersten Boot ohne aktive Jails, weil die mitgelieferten Filter zwar vorhanden, aber standardmäßig deaktiviert sind. Ohne eigene Konfiguration passiert also exakt nichts.
apt update && apt install -y fail2ban
systemctl enable --now fail2ban
fail2ban-client status
Auf RHEL, Rocky Linux 9/10 und AlmaLinux liegt Fail2ban im EPEL-Repository. Dort ist zusätzlich zu beachten, dass firewalld als Ban-Backend genutzt wird, sofern es läuft – der Standard-banaction ist dort firewallcmd-ipset.
dnf install -y epel-release
dnf install -y fail2ban fail2ban-firewalld
systemctl enable --now fail2ban
In Containern oder minimalen Images ohne systemd-Journal kann der Backend-Wechsel nötig sein. Läuft der Dienst in einem Docker-Container, braucht er Zugriff auf die Host-Logs und – je nach Ban-Action – NET_ADMIN oder Zugriff auf den Docker-Socket. Sauberer ist es, Fail2ban auf dem Host laufen zu lassen und die Container-Logs per Volume einzubinden.
Für die Prüfung, ob überhaupt gebannt wird, reicht ein Blick in /var/log/fail2ban.log. Erscheint dort nach einem Testlogin keine Zeile mit Ban, ist die Jail nicht aktiv oder der Logpfad stimmt nicht.
Die mitgelieferte /etc/fail2ban/jail.conf wird bei jedem Paketupdate überschrieben. Eigene Konfiguration gehört deshalb immer in /etc/fail2ban/jail.local oder besser in eine der Dateien unter /etc/fail2ban/jail.d/. Fail2ban liest jail.conf zuerst, dann jail.d/*.conf alphabetisch, dann jail.local – spätere Dateien überschreiben frühere Werte.
Eine praxistaugliche Basis für einen Webserver sieht so aus:
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 203.0.113.42
backend = systemd
banaction = nftables-multiport
banaction_allports = nftables-allports
[sshd]
enabled = true
port = ssh
maxretry = 3
bantime = 24h
[nginx-http-auth]
enabled = true
[nginx-botsearch]
enabled = true
maxretry = 2
Wichtig ist die Trennung zwischen banaction (bannt nur die Ports der Jail) und banaction_allports (bannt die IP komplett). Für SSH will man fast immer allports, weil ein Angreifer sonst einfach auf einen anderen Dienst derselben IP ausweicht. Für Web-Jails reicht das Port-Banning, um legitime Nutzer nicht komplett auszusperren.
Nach jeder Änderung prüft man die Syntax mit fail2ban-client -t und lädt dann per systemctl reload fail2ban neu. Ein voller Restart ist nur nötig, wenn Filterdateien geändert wurden – und selbst dann reicht meist ein Reload.
Die sshd-Jail ist Pflicht. Sie erkennt fehlgeschlagene Passwort-Auth, ungültige Benutzernamen und – je nach Filtervariante – auch Connection closed by authenticating user-Muster. Wer Key-Auth nutzt, kann mode = aggressive setzen, was zusätzlich Verbindungsabbrüche ohne Auth-Versuch zählt. Das fängt Portscanner früh ab, produziert aber gelegentlich False Positives bei Monitoring-Systemen.
Für nginx sind drei Jails relevant: nginx-http-auth (HTTP-Basic-Auth-Fehler), nginx-botsearch (Requests auf /.env, /wp-login.php, /phpmyadmin) und nginx-limit-req, das auf die limit_req-Logzeilen reagiert. Die Botsearch-Jail ist mit maxretry = 2 und bantime = 1w sehr effektiv – Scanner probieren selten mehr als zwei Pfade pro IP.
Mailserver-Betreiber brauchen postfix-sasl (SASL-Auth-Fehler), postfix (Reject-Muster) und dovecot. Gerade bei Postfix ist Vorsicht geboten: Zu aggressive Filter bannen legitime Mailserver, die sich vertippen. Hier empfiehlt sich maxretry = 5 und eine großzügige ignoreip-Liste mit den IPs der eigenen Relay-Partner.
Ein Überblick über sinnvolle Startwerte:
| Jail | maxretry | findtime | bantime |
|---|---|---|---|
| sshd | 3 | 10m | 24h |
| nginx-http-auth | 5 | 10m | 1h |
| nginx-botsearch | 2 | 10m | 1w |
| postfix-sasl | 5 | 30m | 12h |
| dovecot | 5 | 30m | 12h |
| recidive | 3 | 1d | 1w |
Ein Filter besteht aus failregex, optional ignoreregex und einer datepattern-Definition. Der wichtigste Debug-Befehl ist fail2ban-regex, mit dem man einen Filter gegen eine echte Logdatei testet, ohne die Jail zu aktivieren.
fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-botsearch.conf
fail2ban-regex --print-all-matched /var/log/auth.log sshd
Für einen eigenen Dienst legt man /etc/fail2ban/filter.d/meinapp.conf an. Entscheidend ist, dass die IP in einer benannten Gruppe (?P<host>...) steht – Fail2ban braucht genau diesen Gruppennamen, um die IP zu extrahieren.
[Definition]
failregex = ^.*Login failed for user .* from <HOST>.*$
ignoreregex = ^.*from 10\.0\.0\.\d+.*$
Der Klassiker unter den Fehlern: Der Filter matcht, aber die IP wird nicht erkannt, weil <HOST> fehlt oder die Regex gierig über mehrere Zeilen läuft. Zweiter Klassiker: Logrotation. Wird die Logdatei rotiert, ohne dass Fail2ban per pyinotify oder systemd-Backend folgt, verliert die Jail die Verbindung und bannt nichts mehr. Der systemd-Backend umgeht dieses Problem komplett, weil er direkt am Journal liest.
Bei mehrzeiligen Logeinträgen (Java-Stacks, Python-Tracebacks) hilft maxlines = 5 im Jail, damit Fail2ban mehrere Zeilen als einen Treffer wertet. Ohne diese Option matcht nur die erste Zeile.
Der Standard-banaction hängt vom System ab. Auf Debian und Ubuntu ist es historisch iptables-multiport, auf RHEL firewallcmd-ipset. Seit Kernel 5.x und nftables als Default-Backend ist nftables-multiport die bessere Wahl: weniger Overhead, sauberere Regeln, kein iptables-nft-Übersetzungs-Layer.
banaction = nftables-multiport
banaction_allports = nftables-allports
chain = f2b
Läuft der Server hinter Cloudflare, bringt ein lokales IP-Ban wenig – der Traffic kommt von Cloudflare-IPs, und die echte Besucher-IP steht nur im Header CF-Connecting-IP. Hier nutzt man die mitgelieferte cloudflare-token-Action, die die IP per API in eine Cloudflare-Firewall-Regel einträgt.
[Definition]
actionban = curl -s -X POST "https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules" \
-H "X-Auth-Email: <email>" -H "X-Auth-Key: <apikey>" \
-H "Content-Type: application/json" \
--data '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"fail2ban"}'
Für nginx hinter Cloudflare muss zusätzlich real_ip_header CF-Connecting-IP gesetzt und die set_real_ip_from-Liste gepflegt werden – sonst steht in jedem Logeintrag die Cloudflare-IP und Fail2ban sperrt im schlimmsten Fall den halben Edge. Cloudflare Pro kostet 20 US-Dollar pro Monat und Zone, die kostenlose Variante reicht für Fail2ban-Integration völlig aus.
Eine weitere Option ist ipset: Statt für jede gebannte IP eine eigene Firewall-Regel anzulegen, landen alle in einem Hash-Set mit einer einzigen Regel. Bei mehr als 500 gleichzeitigen Bans ist das der Unterschied zwischen einer 200-Zeilen- und einer 200.000-Zeilen-Regelkette.
Die drei Parameter bestimmen die Härte der Sperre. findtime ist das Zeitfenster, in dem maxretry Fehlversuche auftreten müssen, bantime die Sperrdauer. Wer bantime = 1h setzt, verliert gegen einen Angreifer, der ohnehin nur alle 61 Minuten einen Versuch macht.
Bewährt hat sich eine Staffelung: kurze Sperren für legitime Fehler (Web-Login), lange Sperren für Scanner (Botsearch, SSH). Für SSH sind 24 Stunden ein guter Kompromiss – lang genug, um Skripte zu nerven, kurz genug, um einen ausgesperrten Mitarbeiter nicht tagelang zu blockieren.
| Szenario | maxretry | findtime | bantime |
|---|---|---|---|
| SSH öffentlich, Key-Auth | 3 | 10m | 24h |
| SSH öffentlich, Passwort-Auth | 2 | 5m | 1w |
| Web-Login (Kunden) | 5 | 15m | 30m |
| API-Endpoint | 20 | 1m | 10m |
| Mailserver SASL | 5 | 30m | 12h |
Seit Fail2ban 0.11 gibt es bantime.increment = true. Damit verdoppelt sich die Sperrdauer bei jedem weiteren Ban derselben IP, ausgehend von bantime, begrenzt durch bantime.maxtime. Das ist 2026 der Standard für öffentlich erreichbare SSH-Ports.
bantime = 1h
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 5w
Wichtig: bantime.increment arbeitet mit einer Datenbank unter /var/lib/fail2ban/fail2ban.sqlite3. Wird diese Datei gelöscht oder liegt sie auf einem tmpfs, ist die Historie weg und die Sperren starten wieder bei einer Stunde.
Der häufigste Supportfall bei Fail2ban ist der Admin, der sich selbst aussperrt. Ursache ist fast immer eine fehlende ignoreip-Zeile oder eine Jail, die auf den falschen Port hört. Vor dem Aktivieren einer neuen Jail gehört die eigene IP, die IP des Monitoring-Servers und die IP des Backup-Systems in die Whitelist.
ignoreip akzeptiert einzelne IPs, CIDR-Ranges und – seit 0.10 – auch DNS-Hostnamen, sofern ignorecommand nicht gesetzt ist. Hostnamen sind praktisch für dynamische IPs, bergen aber das Risiko, dass ein kompromittierter DNS-Eintrag die Whitelist öffnet.
ignoreip = 127.0.0.1/8 ::1 \
10.0.0.0/8 \
192.168.0.0/16 \
203.0.113.42 \
monitoring.hostazar.com
Für Cloudflare-Setups ist es sinnvoll, die offiziellen Cloudflare-Ranges nicht zu whitelisten, sondern über real_ip die echte Client-IP zu ermitteln. Sonst bannt man bei einem Angriff über Cloudflare im Zweifel den Edge-Knoten und legt damit die eigene Seite für einen ganzen Kontinent lahm.
Ein Sicherheitsnetz: Fail2ban kann per action eine E-Mail oder einen Webhook bei jedem Ban auslösen. Ein Slack- oder ntfy-Webhook kostet nichts und zeigt sofort, wenn etwas schiefgeht.
Der wichtigste Befehl im Alltag ist fail2ban-client status, das alle aktiven Jails auflistet. Mit fail2ban-client status sshd bekommt man die aktuell gebannten IPs, die Gesamtzahl und die Ban-Historie.
fail2ban-client status
fail2ban-client status sshd
fail2ban-client get sshd banip
fail2ban-client get sshd bantime
fail2ban-client set sshd unbanip 198.51.100.7
Für Monitoring eignet sich fail2ban-client status <jail> --with-failed in Kombination mit einem Prometheus-Exporter wie fail2ban_exporter. Grafana-Dashboards zeigen dann Ban-Rate pro Stunde, Top-10-ASN und die Verteilung über Jails. In der Praxis sieht man dabei sofort, ob eine Regel zu aggressiv greift.
Das Log unter /var/log/fail2ban.log sollte per logrotate mit copytruncate oder besser per systemd-Backend verwaltet werden. Bei create-Rotation ohne Neustart verliert Fail2ban die Datei. Wer auf Nummer sicher gehen will, nutzt pyinotify als Backend – das erkennt Rotation und folgt dem neuen Inode automatisch.
Ein nützlicher Trick für die Fehlersuche: fail2ban-client -d gibt die komplette, zusammengeführte Konfiguration aus, inklusive aller geerbten DEFAULT-Werte. Damit findet man in Sekunden, welche Datei einen Wert überschreibt.
Die recidive-Jail ist die Eskalationsstufe: Sie liest das Fail2ban-Log selbst und bannt IPs, die in einem längeren Zeitraum mehrfach aufgefallen sind. Damit fängt man Angreifer ab, die nach Ablauf der Sperre wiederkommen.
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = nftables-allports
bantime = 4w
findtime = 1d
maxretry = 3
Kombiniert mit bantime.increment ergibt das eine effektive Sperre von mehreren Wochen bis Monaten für hartnäckige Angreifer. Wer ganz radikal sein will, setzt bantime = -1 – das bedeutet in Fail2ban permanente Sperre. Davon ist bei SSH auf Produktionssystemen abzuraten, weil ein Tippfehler des eigenen Teams dann dauerhaft ausgesperrt bleibt.
Für permanente Bans empfiehlt sich stattdessen ein zweistufiges Vorgehen: Recidive mit 4 Wochen Sperre und ein monatlicher Cronjob, der die Ban-Liste exportiert und in eine dauerhafte Firewall-Blockliste überführt. So behält man die Kontrolle und kann einzelne IPs gezielt wieder freigeben.
Wichtig für IPv6: Viele Angriffe laufen inzwischen über IPv6-Präfixe. Fail2ban bannt standardmäßig nur die exakte Adresse, nicht das /64-Präfix. Wer das ändern will, braucht eine eigene Action mit ipset und timeout sowie einer Regel, die das gesamte Präfix blockt.
Im Container ist Fail2ban ein Sonderfall. Der Dienst braucht Zugriff auf die Logdateien und auf die Firewall des Hosts. Läuft er im Container mit network_mode: host und cap_add: NET_ADMIN, funktioniert das – sauberer ist aber der Betrieb auf dem Host mit eingebundenen Container-Logs.
# docker-compose.yml (Auszug)
services:
fail2ban:
image: crazymax/fail2ban:latest
network_mode: host
cap_add: [NET_ADMIN, NET_RAW]
volumes:
- /var/log:/var/log:ro
- ./data:/data
environment:
- F2B_LOG_TARGET=STDOUT
- F2B_DB_PURGE_AGE=30d
Bei Docker mit eigenem Bridge-Netzwerk sieht Fail2ban im Log nur die Container-IP, nicht die echte Client-IP. Abhilfe schafft userland-proxy: false in der daemon.json plus iptables-Regeln für die Docker-Chain, oder ein Reverse-Proxy (Traefik, nginx) auf dem Host, der die echte IP per X-Forwarded-For weitergibt.
IPv6 braucht in der Jail die korrekte Portangabe. Wird port = ssh genutzt, löst Fail2ban den Dienstnamen über /etc/services auf und bannt beide Protokollfamilien. Bei expliziten Ports wie port = 2222 sollte man protocol = tcp setzen, sonst entstehen doppelte Regeln.
Ein letzter Punkt: Fail2ban und Docker-Container, die selbst Ports veröffentlichen, können sich gegenseitig aussperren. Wer Fail2ban auf dem Host nutzt, sollte die Docker-Chain in der nftables-Konfiguration korrekt einbinden – sonst greifen die Bans nicht für Container-Ports.
Fail2ban ist in Python geschrieben und skaliert gut bis etwa 20.000 Logzeilen pro Sekunde. Darüber – etwa bei einem stark frequentierten API-Server mit 500 Requests/s – wird die Regex-Auswertung zum Flaschenhals. Ab diesem Punkt hilft es, Jails auf bestimmte Logdateien zu beschränken und maxlines niedrig zu halten.
Der speicherseitige Fußabdruck ist gering: Der Dienst belegt typischerweise 40 bis 90 MB RAM, die SQLite-Datenbank wächst mit etwa 1 MB pro 10.000 Bans. Auf einem VPS mit 2 GB RAM (Netcup VPS 1000 G11, ca. 5,50 €/Monat) ist das vernachlässigbar.
Die wichtigste Grenze: Fail2ban erkennt nur, was im Log steht. Ein Angriff, der keine Fehlermeldung produziert – etwa ein erfolgreicher Login mit gestohlenen Credentials – wird nicht gebannt. Genau hier setzen Alternativen an: CrowdSec arbeitet mit einer Community-Signaturdatenbank und erkennt Verhaltensmuster über mehrere Systeme hinweg. Die Grundversion ist kostenlos, die Console für zentrale Verwaltung startet bei 0 € für bis zu drei Instanzen.
Ein pragmatischer Stack für 2026: SSH nur mit Key-Auth und AllowUsers, Fail2ban mit sshd-, nginx- und recidive-Jail, nftables als Ban-Backend, CrowdSec optional als zweite Ebene für Web-Traffic, plus ein Monitoring-Webhook. Damit sind die allermeisten automatisierten Angriffe abgewehrt, ohne dass man legitime Nutzer verliert.
Am schnellsten mit einem bewusst fehlgeschlagenen Login von einer anderen IP und anschließendem Blick in /var/log/fail2ban.log. Alternativ testet man den Filter isoliert mit fail2ban-regex /var/log/auth.log sshd – die Ausgabe zeigt, wie viele Zeilen matchen und welche IPs extrahiert wurden. Erscheint dort kein Match, liegt es fast immer an einem falschen logpath oder am Backend.
Für öffentlich erreichbares SSH mit Key-Auth sind 24 Stunden ein guter Standard, kombiniert mit bantime.increment = true und bantime.maxtime = 5w. Wer Passwort-Auth erlaubt, sollte maxretry = 2 und bantime = 1w setzen. Permanente Sperren (bantime = -1) sind nur mit einer gepflegten Whitelist empfehlenswert.
Drei häufige Ursachen: Die IP steht in ignoreip, der Ban-Backend passt nicht zur Firewall (etwa iptables-Action bei reinem nftables-System), oder die Jail liest die falsche Logdatei. Prüfen lässt sich das mit fail2ban-client -d für die effektive Konfiguration und fail2ban-client status <jail> für den aktuellen Zustand.
Ja, das ist gängige Praxis. CrowdSec übernimmt typischerweise die Web-Ebene mit Community-Signaturen, Fail2ban die klassischen Dienste wie SSH, Postfix und Dovecot. Wichtig ist, dass beide nicht dieselben Logdateien mit widersprüchlichen Regeln auswerten und dass die Ban-Listen nicht gegenseitig IPs freigeben. Ein gemeinsames nftables-Set als Ziel für beide ist die sauberste Lösung.
Fail2ban selbst ist kostenlos und Open Source. Zusätzliche Kosten entstehen nur durch optionale Komponenten: Cloudflare Pro für erweiterte WAF-Regeln ab 20 US-Dollar pro Monat, CrowdSec Console ab 0 € für kleine Setups, oder ein Monitoring-Webhook wie ntfy, der kostenlos nutzbar ist. Der Server selbst – ein Hetzner CX22 für 3,79 €/Monat reicht für die meisten Setups – ist der einzige echte Posten.