Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Sicherheit & Datenschutz 05.11.2026 · 11 min Lesezeit

Firewall für den Docker-Host: nftables, SSH auf eigenem Port und Teergrube

Teil 2 der Reihe Server im Netz: Eine eigene nftables-Tabelle schließt den VPS bis auf die gewollten Ports, SSH zieht auf einen eigenen Port, endlessh hält Bots auf Port 22 fest. Mit Scan vorher und nachher und drei Fallen auf Ubuntu 24.04 mit Docker.

Geprüft am 05.11.2026 · für nftables 1.0.9, endlessh-go 2026.0730.0, OpenSSH 9.6p1

Mit KI erstellt – redaktionelle Prüfung ausstehend

WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Symbolbild: Server mit leuchtendem Kabel zur Weltkarte, Titel Der Schutzwall für den Server

Teil 1 hat gezeigt, wie schnell ein neuer Server abgeklopft wird: SSH ohne Pause, dazu Anfragen an Telnet, Remote Desktop, Datenbanken und viele andere Ports. Eine Firewall lässt nur durch, was wirklich offen sein soll. Diese Anleitung baut sie mit nftables auf einem Server mit Docker, legt SSH auf einen eigenen Port und stellt auf Port 22 eine Teergrube auf, die Bots minutenlang festhält. Dabei zeigen sich drei Fallen, die auf Ubuntu 24.04 mit Docker jeden treffen können: eine Standarddatei, die alle Docker-Regeln löscht, ein SSH-Dienst, der den Port nicht wechselt, und ein Container, der sich unbemerkt Port 22 nimmt.

Dies ist Teil 2 der Reihe „Server im Netz“. Als Beispiel dient ein netcup-VPS mit Ubuntu 24.04 und Docker, auf dem die Dienste der Reihe im Host-Netz laufen.

Voraussetzungen

  • Ein VPS mit Ubuntu 24.04 und Docker, Zugang per SSH mit Schlüssel
  • Eine zweite Sitzung oder die Konsole des Anbieters, falls Sie sich aussperren
  • Eine Liste der Dienste, die von außen erreichbar sein müssen
  • Ein Rechner außerhalb des Servers mit nmap für die Kontrolle

Schritt 1: Was die Firewall des Anbieters macht

Viele Anbieter haben eine eigene Firewall vor dem Server. Bei netcup sitzt sie im Server Control Panel unter Firewall. Auf dem Testserver war sie aktiv, mit zwei Regelsätzen ab Werk:

Firewall im SCP von netcup mit dem Regelsatz Mail block
Firewall im Server Control Panel von netcup: Der Regelsatz netcup Mail block (1) sperrt ausgehend die Mailports 25, 465 und 587 (2). Daten anonymisiert.

„netcup Mail block“ sperrt ausgehende Verbindungen auf den Mailports 25, 465 und 587, „netcup Ping allow“ erlaubt ICMP. Danach folgen implizite Regeln, die alles erlauben. Eingehend war also nichts gesperrt. Für den Mailserver in den Teilen 11 und 12 haben wir die Firewall im SCP abgeschaltet, danach waren die Mailports nach etwa 20 Sekunden offen. Die Firewall auf dem Server selbst ersetzt das nicht: Sie sieht jede Verbindung, kennt die Dienste und lässt sich mit dem Server zusammen sichern.

Verifizieren: nc -vz -w5 <Mailserver> 25 auf dem Server zeigt, ob ausgehende Verbindungen zu einem fremden Mailserver durchkommen. Mit aktivem Regelsatz kam im Test keine Verbindung zustande.

Schritt 2: Vorher messen

Von einem Rechner außerhalb prüfen Sie, welche Ports offen sind. Im Test lief nmap auf einem Rechner im Heimnetz:

nmap -Pn -sT -p- -T4 --min-rate 1500 --open sedv-lab.de

Offen waren 22, 25, 80, 443, 853 und 7000, wie erwartet. Dazu kamen zwei Ports im Bereich über 39000. Sie gehörten dem Forgejo-Runner aus Teil 15, der dort einen Cache für Actions anbietet, auf allen Adressen und bei jedem Start auf anderen Ports. Solche Überraschungen findet nur ein Scan von außen.

Verifizieren: ss -tlnp auf dem Server zeigt zu jedem offenen Port den Prozess.

Schritt 3: Die Falle mit flush ruleset

Ubuntu bringt eine /etc/nftables.conf mit, und der Dienst nftables lädt sie beim Start, wenn er aktiviert ist. Die Datei beginnt mit flush ruleset. Das löscht alle Regeln, auch die, die Docker für seine Netze angelegt hat. Im Test haben wir die Datei einmal von Hand geladen:

nft list tables
nft -f /etc/nftables.conf
nft list tables
docker run --rm alpine:3.22 wget -q -O /dev/null https://example.org

Vorher gab es fünf Tabellen von Docker, darunter ip filter und ip nat. Danach war nur noch inet filter aus der Datei übrig, und ein Container scheiterte mit wget: bad address 'example.org', weil ohne die NAT-Regeln von Docker keine Antwort mehr zum Container zurückkam und schon die Namensauflösung scheiterte. Dienste im Host-Netz wie Caddy liefen weiter. Ein systemctl restart docker legte die Regeln in 6 Sekunden neu an, dabei starteten aber alle Container neu.

Daraus folgt die Regel für diese Anleitung: Lassen Sie nftables.service ausgeschaltet und arbeiten Sie mit einer eigenen Tabelle, die nur sich selbst ersetzt.

Verifizieren: systemctl is-enabled nftables meldet disabled.

Schritt 4: SSH auf einen eigenen Port

Bevor Port 22 frei wird, bekommt SSH einen zweiten Port. Die Einstellungen kommen in eine eigene Datei:

# /etc/ssh/sshd_config.d/10-sedv.conf
Port 22
Port 22022
PermitRootLogin prohibit-password
PasswordAuthentication no
KbdInteractiveAuthentication no
MaxAuthTries 3

Nach sshd -t und systemctl reload ssh zeigte sshd -T zwar beide Ports, gelauscht wurde aber weiter nur auf 22. Eine Verbindung auf 22022 endete mit Connection refused. Der Grund: Ubuntu 24.04 startet SSH über ssh.socket. Die Ports stehen in der Socket-Einheit, die ein Generator beim Neuladen von systemd aus der Konfiguration erzeugt:

systemctl daemon-reload
systemctl restart ssh.socket
ss -tlnp | grep sshd

Danach lauschte SSH auf beiden Ports. Testen Sie die Anmeldung auf dem neuen Port in einer zweiten Sitzung, bevor Sie Port 22 aus der Datei streichen. Dann folgen noch einmal daemon-reload und der Neustart von ssh.socket.

Verifizieren: ssh -p 22022 root@IHR-SERVER funktioniert, und sshd -T | grep -E "^port|permitrootlogin" zeigt port 22022 und permitrootlogin without-password, so nennt sshd die Einstellung prohibit-password intern.

Schritt 5: Die eigene Tabelle

#!/usr/sbin/nft -f
# Eigene Tabelle, ohne "flush ruleset": Docker-Regeln bleiben unberührt
table inet sedv
delete table inet sedv
table inet sedv {
	counter verworfen { }
	set offen_tcp {
		type inet_service
		elements = { 2223, 22022, 25, 80, 443, 853, 7000 }
	}
	set offen_udp {
		type inet_service
		elements = { 443, 853 }
	}
	chain input {
		type filter hook input priority filter - 10; policy drop;
		ct state established,related accept
		ct state invalid drop
		iifname "lo" accept
		iifname { "docker0", "br-*", "docker_gwbridge" } accept
		ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded } limit rate 10/second accept
		ip6 nexthdr icmpv6 icmpv6 type { echo-request, destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
		tcp dport @offen_tcp accept
		udp dport @offen_udp accept
		counter name verworfen drop
	}
	chain prerouting {
		type nat hook prerouting priority dstnat; policy accept;
		tcp dport 22 redirect to :2223
	}
}

Die ersten drei Zeilen machen die Datei wiederholbar: Die Tabelle wird angelegt, falls es sie nicht gibt, dann gelöscht und neu aufgebaut. Andere Tabellen bleiben unberührt. Die Docker-Bridges sind erlaubt, sonst erreichen Container wie die Jobs des Forgejo-Runners die eigenen Dienste nicht. Für IPv6 sind die ICMP-Typen der Nachbarerkennung Pflicht, ohne sie bricht IPv6 nach kurzer Zeit ab. UDP 443 ist für HTTP/3, UDP 853 für DNS-over-QUIC aus Teil 9. Die Umleitung von Port 22 auf 2223 gehört zur Teergrube aus Schritt 7.

Wer eine Firewall per SSH lädt, kann sich aussperren. Ein Timer, der die Tabelle nach fünf Minuten wieder entfernt, ist das Sicherheitsnetz:

cp sedv-firewall.nft /etc/sedv-firewall.nft
systemd-run --unit nft-notaus --on-active=300 /usr/sbin/nft delete table inet sedv
nft -f /etc/sedv-firewall.nft
# in einer zweiten Sitzung testen, dann:
systemctl stop nft-notaus.timer

Verifizieren: nft list table inet sedv zeigt die Regeln, und nft list counter inet sedv verworfen zählt nach kurzer Zeit die ersten verworfenen Pakete.

Schritt 6: Beim Start laden

Eine kleine Einheit lädt die Tabelle beim Start, ohne flush ruleset:

# /etc/systemd/system/sedv-firewall.service
[Unit]
Description=Eigene nftables-Tabelle inet sedv (ohne flush ruleset)
After=network-pre.target docker.service
Wants=network-pre.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/nft -f /etc/sedv-firewall.nft
ExecStop=/usr/sbin/nft delete table inet sedv

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now sedv-firewall.service

Im Test ging der Server danach mehrmals aus und wieder an. Jedes Mal waren die eigene Tabelle, die Tabellen von Docker, SSH auf 22022 und alle Container ohne Zutun wieder da.

Verifizieren: Nach einem Neustart zeigt nft list tables neben den Tabellen von Docker auch inet sedv.

Schritt 7: Die Teergrube auf Port 22

endlessh-go nimmt Verbindungen an und schickt alle paar Sekunden eine zufällige Zeile, aber nie das Banner, mit dem eine SSH-Verbindung beginnt. Bots warten oft lange, bevor sie aufgeben. Wir lassen es auf Port 2223 laufen, die Umleitung aus Schritt 5 schickt Port 22 dorthin:

services:
  endlessh:
    image: shizunge/endlessh-go:2026.0730.0
    container_name: endlessh
    restart: unless-stopped
    network_mode: host
    command:
      - -interval_ms=10000
      - -logtostderr
      - -v=1
      - -port=2223
      - -enable_prometheus
      - -prometheus_host=127.0.0.1
      - -prometheus_port=2112
      - -healthcheck_enabled

Warum nicht direkt Port 22? Das haben wir zuerst versucht. Der Container läuft nicht als root und darf Ports unter 1024 nicht öffnen, das Log meldete listen tcp 0.0.0.0:22: bind: permission denied. Die Umleitung per nftables ist sauberer, als dem Container mehr Rechte zu geben. -healthcheck_enabled braucht der eingebaute Healthcheck des Images, ohne den Schalter stand der Container nach kurzer Zeit auf „unhealthy“.

Beim ersten Versuch antwortete auf Port 22 übrigens nicht endlessh, sondern ein SSH-Server mit fremdem Schlüssel. Es war der OpenSSH-Dienst im Image von Forgejo aus Teil 15, der im Host-Netz Port 22 übernommen hatte, sobald der Server-SSH ihn freigegeben hatte. Teil 15 zeigt inzwischen, wie man ihn abschaltet. Prüfen Sie nach jedem Umbau mit ss -tlnp, welcher Prozess wirklich hinter einem Port steht.

Verifizieren: nc IHR-SERVER 22 liefert alle 10 Sekunden eine Zeile Zeichensalat statt eines SSH-Banners.

Schritt 8: Nachher messen

Grafik: offene Ports vor und nach der Firewall
Eigene Messung: offene TCP-Ports von außen vor und nach der Firewall. Der Cache des Forgejo-Runners ist nachher gefiltert, SSH liegt auf 22022.

Ein schneller Vollscan aller 65535 Ports fiel in die Zeit, in der wir die Firewall für die Tests mehrmals neu luden und den Server neu starteten, und war deshalb nicht aussagekräftig. Ein gezielter Scan der bekannten Ports war eindeutig: offen nur noch 22 als Teergrube, 25, 80, 443, 853, 7000 und 22022. Dazu kommt 2223, der eigentliche Port der Teergrube: Nach der Umleitung sieht die Input-Kette Port 2223, deshalb steht er in offen_tcp, und Bots können ihn auch direkt erreichen. Alle internen Ports und der Cache des Runners waren gefiltert. Die Websites, DNS-over-HTTPS und ein Lauf von Forgejo Actions funktionierten weiter.

Darstellung: Teergrube endlessh und ihre Kennzahlen
Ergebnisse aus dem Test: Die Teergrube schickt statt eines SSH-Banners Zeichensalat, die Kennzahlen zeigen gefangene Verbindungen und Zeit.

Nach gut zehn Minuten zählte endlessh 34 Verbindungen, die zusammen 3990 Sekunden festhingen. Die Hälfte der beendeten Verbindungen gab nach etwa 150 Sekunden auf, die längste beendete hielt 230 Sekunden aus. Unter den Gefangenen waren Adressen aus den beiden Netzen, die in Teil 1 die meisten SSH-Versuche geschickt hatten. Und auf dem neuen Port 22022 kam in gut 20 Minuten Beobachtung kein einziger fremder Anmeldeversuch an.

Verifizieren: curl -s 127.0.0.1:2112/metrics | grep endlessh_client_open_count_total zeigt die Zahl der gefangenen Verbindungen.

Typische Fehler

Meldung oder BeobachtungUrsache und Lösung
Container haben kein Netz, wget: bad addressflush ruleset hat die Regeln von Docker gelöscht. systemctl restart docker, nftables.service ausschalten.
Neuer SSH-Port: Connection refusedUbuntu 24.04 nutzt ssh.socket. systemctl daemon-reload und systemctl restart ssh.socket.
REMOTE HOST IDENTIFICATION HAS CHANGED auf Port 22Ein anderer Dienst hat Port 22 übernommen, etwa ein Container im Host-Netz. Mit ss -tlnp prüfen.
listen tcp 0.0.0.0:22: bind: permission deniedContainer ohne root darf Port 22 nicht öffnen. Hohen Port nehmen und per nftables umleiten.
endlessh „unhealthy“Healthcheck ohne Endpunkt. -healthcheck_enabled ergänzen.
Ausgesperrt nach dem Laden der RegelnSSH-Port fehlt in offen_tcp. Notaus-Timer abwarten oder über die Konsole des Anbieters korrigieren.

Häufige Fragen

Warum nicht UFW?

UFW ist bequemer, arbeitet aber mit eigenen Ketten, die sich mit den Regeln von Docker überschneiden. Veröffentlichte Ports von Containern umgehen UFW sogar. Mit einer eigenen nftables-Tabelle sehen Sie genau, was gilt. Die Grundlagen zu UFW stehen in der Anleitung zu UFW und Fail2ban.

Schützt die Firewall auch Ports, die Docker veröffentlicht?

Nein. Ports, die Docker mit ports: veröffentlicht, laufen über Weiterleitung und NAT, die Input-Kette sieht sie gar nicht. In dieser Reihe laufen die Dienste im Host-Netz oder lauschen nur auf 127.0.0.1, deshalb greift die Tabelle. Bei veröffentlichten Ports gehören Regeln in die Kette DOCKER-USER des iptables-Backends von Docker.

Bringt die Teergrube Sicherheit?

Sie hält Bots auf und hält das echte SSH-Log sauber, ersetzt aber keine Anmeldung mit Schlüssel. Sie kostet kaum Ressourcen, jede gefangene Verbindung belegt nur einen Socket und etwas Speicher.

Fazit

Eine eigene nftables-Tabelle reicht, um einen Docker-Host von außen auf die gewollten Ports zu beschränken, ohne Docker ins Gehege zu kommen. Die Fallen lagen im Detail: die Standarddatei mit flush ruleset, der Socket von SSH unter Ubuntu 24.04 und ein Container, der sich Port 22 nahm. Der Scan von außen davor und danach lohnt sich, im Test fand er einen offenen Cache-Port, den niemand geplant hatte. Die Teergrube auf Port 22 ist ein netter Nebeneffekt: Die Bots beschäftigen sich mit ihr, nicht mit dem echten SSH.

Weiterführende Anleitungen und Quellen

Server im NetznftablesFirewallDockerSSHendlesshUbuntu 24.04