Zum Inhalt springen
myvpsguide

Firewall auf dem VPS: nftables und die vergessene Richtung

Eine nftables-Konfiguration zum Verstehen: eingehend dichtmachen, ausgehend begrenzen, IPv6 mitnehmen und warum Port 25 gesperrt gehört.

veröffentlicht 29.07.2026
4 min lesezeit
fortgeschritten

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.

Links eingehende Verbindungen, die teils erlaubt und teils verworfen werden, rechts ausgehende Verbindungen. Ausgehend Port 25 ist gesperrt, ausgehend alles andere meist offen.
Eingehend regelt fast jeder. Was ein übernommener Server tut, passiert auf der rechten Seite.

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:

ufwnftables direkt
Einstiegdrei Befehle, fertigeine Datei, die man lesen muss
Regeln nachvollziehenufw status zeigt eine Listedie Datei ist die Liste
Komplexe Regelnwird schnell umständlichdafür gebaut
Versionierbarnein, Zustand liegt verteiltja, 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
Vor dem Laden

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
Vom eigenen Server aus zählt nicht

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#

  1. Eingehend: policy drop, dann gezielt öffnen. ICMP nicht komplett blocken.
  2. Ausgehend: begrenzen, mindestens Port 25 sperren und protokollieren.
  3. IPv6 mit derselben Sorgfalt wie IPv4, eine inet-Tabelle deckt beides ab.
  4. Dienste, die lokal bleiben sollen, auf 127.0.0.1 binden statt sie wegzufiltern.
  5. Vor jeder Änderung einen Rückfall-Timer. Immer.
MRMedia

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.