Die meisten Firewall-Anleitungen enden nach drei Zeilen: Port 22 auf, Port 80 und 443 auf, Rest zu. Das ist richtig und reicht für den Anfang. Es lässt aber die Hälfte des Problems offen.
Ein kompromittierter Server tut drei Dinge: Er lädt Schadcode nach, er sendet Spam, und er verbindet sich zu einem Steuerungsserver. Alle drei sind ausgehende Verbindungen. Eine Firewall, die nur eingehend filtert, sieht davon nichts.
ufw oder nftables?#
ufw ist ein Frontend, nftables der eigentliche Unterbau des Kernels seit Jahren. Beide führen zum Ziel:
| ufw | nftables direkt | |
|---|---|---|
| Einstieg | drei Befehle, fertig | eine Datei, die man lesen muss |
| Regeln nachvollziehen | ufw status zeigt eine Liste | die Datei ist die Liste |
| Komplexe Regeln | wird schnell umständlich | dafür gebaut |
| Versionierbar | nein, Zustand liegt verteilt | ja, eine Datei ins Repo |
Für einen einzelnen Webserver ist ufw völlig in Ordnung, die erste Stunde macht es genau so. Sobald mehrere Dienste, IPv6 und ausgehende Regeln dazukommen, ist eine nftables-Datei ehrlicher: Man sieht auf einen Blick alles, was gilt.
Ein vollständiges Regelwerk#
Diese Datei ersetzt /etc/nftables.conf. Sie deckt IPv4 und IPv6 in einer inet-Tabelle ab, das ist der Punkt, an dem klassische iptables-Anleitungen die Hälfte vergessen.
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
# Was darf raus? Bewusst eine kurze Liste.
set egress_ports {
type inet_service
elements = { 53, 80, 443, 123 }
}
chain input {
type filter hook input priority filter; policy drop;
# Bestehende Verbindungen und Loopback zuerst — spart jede
# weitere Prüfung für den Großteil der Pakete.
ct state established,related accept
iif "lo" accept
ct state invalid drop
# ICMP nicht komplett blocken: ohne Fragmentation-Needed
# bricht die Path-MTU-Erkennung, und Verbindungen hängen
# scheinbar grundlos.
ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded } accept
ip6 nexthdr icmpv6 icmpv6 type { echo-request, destination-unreachable, packet-too-big, time-exceeded, nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert } accept
tcp dport 22 ct state new limit rate 10/minute accept comment "SSH"
tcp dport { 80, 443 } accept comment "Web"
# Alles Übrige verwerfen und mitzählen, damit man später sieht,
# ob eine Regel fehlt.
counter comment "verworfen eingehend"
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy drop;
ct state established,related accept
oif "lo" accept
# DNS, HTTP(S), NTP raus — mehr braucht ein Webserver nicht.
tcp dport @egress_ports accept
udp dport @egress_ports accept
# Ausgehendes SMTP explizit sperren und protokollieren.
tcp dport { 25, 465, 587 } log prefix "SMTP-EGRESS " drop
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
counter comment "verworfen ausgehend"
}
}
Laden und dauerhaft aktivieren:
sudo nft -c -f /etc/nftables.conf # -c prüft nur, ohne zu laden
sudo systemctl enable --now nftables
sudo nft list ruleset # was tatsächlich aktiv ist
policy drop auf der Output-Chain trennt jede laufende Verbindung, deren Port nicht erlaubt ist, auch deine SSH-Sitzung, falls du SSH auf einem anderen Port betreibst und ihn hier vergessen hast. Lege dir vorher einen Rückfall-Timer an: sudo sh -c 'sleep 120 && nft flush ruleset' &. Funktioniert die Verbindung noch, brichst du ihn ab, funktioniert sie nicht, räumt er in zwei Minuten auf.
Warum ausgehend Port 25 gesperrt gehört#
Wenn dein Server keine Mail versendet und die meisten tun das nicht direkt, sondern über einen Relay-Dienst, dann ist jede ausgehende Verbindung auf Port 25 verdächtig. Genau diesen Port braucht ein Spam-Skript.
Die Sperre kostet nichts und ist die zuverlässigste Einzelmaßnahme dagegen, dass deine IP-Adresse auf einer Blockliste landet. Und wenn die Adresse einmal dort steht, dauert das Herauskommen Wochen, Zeit, in der auch legitime Mail nicht ankommt.
Wer wirklich Mail versendet, macht die Ausnahme bewusst und für den einen Dienst:
# Nur der Benutzer, unter dem der Mailserver läuft, darf auf 25 raus
meta skuid "postfix" tcp dport 25 accept
Wie ein eigener Mailserver sonst aussieht, steht in Eigener Mailserver: was wirklich dranhängt.
IPv6 nicht vergessen#
Der häufigste stille Fehler: Die IPv4-Regeln sitzen, IPv6 ist offen. Viele Dienste binden per Default auf beiden Protokollen, und wenn der Anbieter IPv6 mitliefert, ist der Dienst darüber erreichbar, ohne dass in ufw status etwas Auffälliges steht.
Prüfen, was tatsächlich lauscht:
sudo ss -tulpn | grep -E 'LISTEN'
In der Ausgabe steht 0.0.0.0:* für „alle IPv4-Adressen“ und [::]:* für „alle IPv6-Adressen“. Ein Dienst, der nur lokal erreichbar sein soll, gehört auf 127.0.0.1 bzw. [::1] gebunden, das ist wirksamer als jede Firewall-Regel, weil es gar nicht erst hört.
Von außen gegenprüfen, aus einem anderen Netz:
nmap -Pn -p- --min-rate 2000 203.0.113.10 # IPv4
nmap -6 -Pn -p- --min-rate 2000 2001:db8::1 # IPv6
Ein Scan von der Maschine auf sich selbst geht über Loopback und sagt nichts über die Erreichbarkeit von außen. Nutze einen zweiten Server oder einen der öffentlichen Portscan-Dienste und scanne nur Adressen, die dir gehören.
Protokolle lesen, sonst hat es keinen Zweck#
Regeln mit log und counter sind nur dann etwas wert, wenn jemand hinsieht:
# Was wurde ausgehend geblockt?
sudo journalctl -k --since "24 hours ago" | grep "SMTP-EGRESS"
# Trefferzähler pro Regel — findet die Regel, die nie greift,
# und die, die zu oft greift
sudo nft -a list ruleset | grep counter
Ein Zähler, der bei null steht, ist entweder eine überflüssige Regel oder ein Hinweis, dass der Verkehr woanders vorbeigeht. Beides lohnt einen Blick.
Sonderfall: Docker umgeht deine Firewall#
Docker schreibt eigene Regeln in die nat-Tabelle und veröffentlicht Ports damit an der Firewall vorbei. Ein -p 5432:5432 macht die Datenbank öffentlich erreichbar, auch wenn ufw behauptet, der Port sei zu.
Die saubere Lösung ist nicht, gegen Docker anzukämpfen, sondern gar nicht erst nach außen zu binden:
services:
db:
image: postgres:17
ports:
- "127.0.0.1:5432:5432" # nur lokal, nicht 0.0.0.0
Mehr dazu in Docker im echten Betrieb.
Kurzfassung#
- Eingehend:
policy drop, dann gezielt öffnen. ICMP nicht komplett blocken. - Ausgehend: begrenzen, mindestens Port 25 sperren und protokollieren.
- IPv6 mit derselben Sorgfalt wie IPv4, eine
inet-Tabelle deckt beides ab. - Dienste, die lokal bleiben sollen, auf
127.0.0.1binden statt sie wegzufiltern. - Vor jeder Änderung einen Rückfall-Timer. Immer.
Geschrieben aus dem laufenden Betrieb: MRMedia betreibt eigene Proxmox- Hosts, einen Backup-Server und Kundendienste in der EU. Fehler in einer Anleitung sind Fehler im eigenen Betrieb. Korrekturen bitte über Discord.