Credential-Portknocking, Teil 1: Implementierung in der Linux-Kernelfirewall

Mittels Portknocking lassen sich für Angreifer besonders interessante Dienste verbergen. Dieser erste Teil eines mehrteiligen Tutorials zeigt, welche Varianten es gibt, und wie einfach man Credential-Portknocking mittels nftables oder iptables direkt in der Linux-Kernelfirewall implementiert.

· 17 Minuten zu lesen
Credential-Portknocking, Teil 1: Implementierung in der Linux-Kernelfirewall
🇬🇧
This article is also available in English.
ℹ️
Dieser Artikel ist Teil eines mehrteiligen Tutorials:
Teil 1: Implementierung in der Linux-Kernelfirewall
Teil 2: Integration in SSH-Clients
Teil 3: Noch nicht veröffentlicht.

Es gibt viele Gründe, Netzwerkdienste vor Dritten zu verbergen oder vor direkten Zugriffen zu schützen, insbesondere administrative oder anderweitig hoch privilegierte Dienste wie SSH.
Motive können beispielsweise sein, Internet-Kartierungsdiensten wie SHODAN einen Riegel vorzuschieben, oder auch einfach nur, um Alarmen durch ungültige Login-Versuche bspw. aufgrund von Bruteforce-Angriffen vorzubeugen. Vielleicht vertraut man aber auch der Sicherheit des Dienstes allein nicht (mehr), bei OpenSSH trotz allgemein hoher Code-Qualität bspw. aufgrund einer schweren Sicherheitslücke wie der im Juni 2024 durch das TRU-Team von Qualys (wieder)entdeckten, fast 20 Jahre alten Zombie-Sicherheitslücke CVE-2024-6387 aka regreSSHion, oder des im März 2024 von Andres Freund während Microbenchmarkings an PostgreSQL recht zufällig und gerade noch rechtzeitig aufgedeckten, aufwändig gestalteten Supply-Chain-Angriffs zur Implementierung einer Backdoor in OpenSSH über systemd, versteckt in liblzma – Ereignisse, welche die Grenzen dessen, was man als gesundes Maß an Paranoia auffassen sollte, für viele Menschen deutlich verschoben haben.

Portknocking – es muss nicht immer ein VPN sein

Um den direkten Zugriff auf administrative Dienste auf den von ihnen verantworteten Hosts zu beschränken, greifen viele Admins zu VPNs, Bastionshosts oder Proxydiensten – aber auch diese sind oft vergleichsweise komplex, sei es bezogen auf Einrichtung und Betrieb oder das zugrundeliegende Protokoll, das wiederum mit entsprechenden Risiken zum Internet exponiert werden muss. Oft lohnt sich ihr Betrieb auch erst ab einer bestimmten Anzahl verwalteter Hosts.

Sind die zu verbergenden Dienste bereits stark authentifiziert und verschlüsselt, wie dies bei SSH der Fall ist, ist ggf. Portknocking eine leichtgewichtige und zuverlässige Alternative, also das zeitweise Freischalten von Zugriffen auf administrative Dienste für die IP-Adresse eines berechtigten Clients in der Firewall.

Häufig kommen dafür spezielle Dienste zum Einsatz, die zur Laufzeit die Firewall des Hosts manipulieren. Doch je nach Methode ist dies auch gänzlich ohne Dienste möglich, indem man das komplette Portknocking direkt in der Kernel-Firewall des Systems implementiert.

Verschiedene Portknocking-Methoden

In der Regel wird zwischen den folgenden Portknocking-Methoden unterschieden:

  1. Klassisches Portknocking: Das System öffnet den Dienst, wenn bestimmte Ports in einer bestimmten Reihenfolge angesprochen werden.
    Vorteile: Sowohl als Dienst als auch direkt per statischer Kernel-Firewall implementierbar, oft auch auf Client-Seite mit Bordmitteln des Betriebssystems implementierbar.
    Nachteile: Je nach Länge der Authentifizierungssequenz entweder langsam oder sehr simpel, wird möglicherweise schon durch einen Portscan ausgelöst, Klopf-Sequenz wird mit hoher Wahrscheinlichkeit in Verbindungsdaten-Protokollen bspw. durch Firewalls oder Router vollständig aufgezeichnet, statisches Geheimnis.
  2. Credential-Portknocking: Das System öffnet den Dienst, wenn ein bestimmtes Geheimnis empfangen wird.
    Vorteile: Quasi verzögerungsfrei verwendbar, sowohl als Dienst als auch direkt per statischer Kernel-Firewall implementierbar, oft auch auf Client-Seite mit Bordmitteln des Betriebssystems implementierbar, Geheimnis je nach Methode oft beliebig komplex wählbar, ggf. kann das Credential unauffällig in anderen Protokollen versteckt werden.
    Nachteile: Statisches Geheimnis.
  3. Challenge-Response-Portknocking: Das System öffnet den Dienst, wenn der Client eine nur durch Kenntnis eines Geheimnisses korrekt beantwortbare Aufgabe löst.
    Vorteile: Höhere Sicherheit, da das Geheimnis den Client nicht verlässt.
    Nachteile: Höhere Komplexität, erfordert bidirektionale Kommunikation, nur als Dienst sinnvoll implementierbar, benötigt spezielle Software auf dem Client.
  4. OTP-Portknocking: Das System öffnet den Dienst, wenn der Client eine Freigabe-Anforderung in Form von nur einmalig verwendbarer Codes oder einer gültigen zeit- oder ereignisbasierten Geheimnis-Ableitung liefert.
    Vorteile: Höhere Sicherheit, da das Geheimnis nur einmalig nutzbar bzw. den Client nicht verlässt.
    Nachteile: Höhere Komplexität, ohne spezielles Kernelmodul nur als Dienst implementierbar, abhängig von korrekter Systemzeit sowohl auf Server als auch Client, benötigt ggf. spezielle Software auf dem Client.

Methode der Wahl: Credential-Portknocking

Im ersten Teil dieses Tutorial betrachten wir eine direkte Variante des Credential-Portknockings, da es mit Ausnahme des statischen Geheimnisses alle Nachteile des klassischen Portknockings ausgleicht. Das Credential wird dabei einfach als UDP-Datagramm an den Server übertragen.
In einem späteren Teil folgt die Implementierung von Varianten, die das Credential in Netzwerkpaketen anderer Protokolle verstecken.

Da es uns darum geht, Dienste zu verbergen, und nicht Diensten mit bereits ausgefeilter Authentifizierung (wie SSH) eine zusätzliche Schicht komplexer Authentifizierung hinzuzufügen, geben wir uns mit einem statischen Geheimnis zufrieden. Im Bedarfsfall lassen sich aber bspw. für verschiedene Benutzer des Systems auch mehrere statische Geheimnisse zur Freischaltung des Dienstes einrichten.

Anders als Challenge-Response- und OTP-Portknocking lässt sich das Credential-Portknocking vollständig in einer statischen Kernel-Firewall implementieren, und ist somit unabhängig vom Zustand eines lokalen Dienstes zu betreiben, der zudem noch dynamisch in unseren Firewallregeln herumfummeln müsste, was für viele Systembetreuer nicht in Frage kommt.

Für die clientseitige Implementierung kommt man im Regelfall sogar ohne spezielle Software aus.

Das Szenario

Nachfolgend implementieren wir ein Credential-Portknocking, das sich unter Linux in einer statischen Kernel-Firewall mit dem aktuellen nftables oder dessen Vorgänger iptables umsetzen lässt.

Als Credential wird durchgängig die folgende Zeichenkette bestehend aus 32 ASCII-Zeichen aus dem Zeichenvorrat [A-Za-z0-9] verwendet:

$ pwgen -s 32 1
lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9

Credential für die Portknocking-Beispiele

Die Länge der Credential-Zeichenkette ist absichtlich vergleichsweise groß gewählt, um daran später größenbedingte Einschränkungen des Matchings in der Firewall zu zeigen, und wie man damit umgeht.

Im Beispiel verbergen wir den SSH-Dienst auf Port 22/tcp des Servers server.schoen-technisch.de, sowohl für IPv4 als auch IPv6. Nach erfolgreichem Anklopfen soll die Freigabe der anklopfenden IP-Adresse für Verbindungen zum Dienst für 10 Sekunden erfolgen.

Als SSH-Client kommt in den Beispielen dieses Teil des Tutorials OpenSSH unter Linux zum Einsatz.

In einem folgenden Teil erarbeiten wir dann, wie das Portknocking transparent in den Login-Prozess von OpenSSH und PuTTY sowohl unter Linux (und anderen Unix- und unixartigen OS) sowie unter Windows integriert wird.

Die Implementierung

Das Senden eines Credentials als Payload eines Datagramm an einen bestimmten, selbstgewählten UDP-Port ist die einfachste und zugleich flexibelste Variante.

Die Hauptvorteile dieser Variante sind, dass man sowohl in der Länge und damit Komplexität des Credentials als auch bei der Auswahl des Ziel-Ports frei ist, und UDP problemlos per Port-Forwarding weitergereicht werden kann. Somit ist diese Variante insbesondere für Situationen geeignet, in denen NAT zum Einsatz kommt, um verschiedene Ziel-Hosts hinter einer gemeinsamen öffentlichen IP-Adresse zugänglich zu machen. So ist es sowohl möglich, das Portknocking zentral auf dem NAT-Router/der Firewall zu implementieren, oder auch auf den Ziel-Hosts selbst, indem man zu jedem dieser Hosts einen eigenen UDP-Port für das Portknocking weiterleitet.

Wir wählen für das Anklopfen den Port 34567/udp.

Serverseitige Implementierung

Ausgehend von einer restriktiven Stateful Firewall, die keine Zugriffe auf den zu verbergenden Dienst zulässt, besteht ein Firewall-basiertes Credential-Portknocking aus drei Komponenten je Adressfamilie (IPv4 und IPv6):

  1. Eine Liste für Einträge von IP-Adressen, die für eine bestimmte Zeit gültig bleiben.
  2. Eine Firewallregel, welche das Credential matcht und daraufhin die IP-Adresse des Urhebers des Datagramms der o.g. Liste hinzufügt.
  3. Eine Firewallregel, die Zugriffe auf den durch das Portknocking verborgenen Dienst für die in der o.g. Liste enthaltenen Quell-IP-Adressen zulässt.

Listen zur Speicherung authentifizierter Adressen

nftables

Da in nftables keine Vorgaben zu Aufbau und Namen der Firewall-Tabellen und -Ketten macht und mit einer Tabelle des Typs inet sogar sowohl IPv4 als auch IPv6 in einer gemeinsamen Filterkette abarbeiten kann, legen wir für jede der Adressfamilien eine Liste mit eigenem Namen im Kontext der jeweiligen Tabelle an. Im Falle einer gemeinsamen Tabelle für beide Adressfamilien (Typ inet) kommen also beide dieser Listen in die selbe Tabelle, anderenfalls (Typ ip + Typ ip6) in den Kontext der jeweiligen Adressfamilien-Tabelle:

  set knocked_v4 {
    type ipv4_addr
    flags timeout
  }

  set knocked_v6 {
    type ipv6_addr
    flags timeout
  }

nftables: Definition der Listen für erfolgreich anklopfende IPv4- und IPv6-Adressen.

iptables

In iptables gibt es hingegen Vorgaben, sowohl für die Namen der Tabellen als auch der Standardketten, und das Ganze strikt getrennt nach Adressfamilie; iptables für IPv4- und ip6tables für die Verarbeitung von IPv6-Paketen.

Die IP-Adresslisten für authentifizierte Clients werden in iptables durch die Erweiterung recent verwaltet. Dies geschieht implizit, d.h. anders als bei nftables müssen die Listen nicht explizit definiert werden.

Anklopf-Pakete analysieren und Listen mit authentifizierten IP-Adressen befüllen

nftables

In nftables kommt für die Analyse des Paketinhalts ein raw payload matching mit der Syntax @<Referenz>,<Offset in bit>,<Länge in bit> zum Einsatz. Leider ist raw payload matching nur in der Lage, 128 bit auf einmal zu matchen – eine Zeichenkette mit 32 ASCII-Zeichen ist jedoch 32 Byte × 8 bit/Byte = 256 bit lang. Zudem muss die Zeichenkette in Dezimal- oder mit 0x präfigierter Hexadezimalnotation vorliegen. Wir erzeugen daher aus der 256 bit langen ASCII-Zeichenkette zwei 128 bit lange Hexadezimal-Zeichenketten, die mit 0x präfigiert sind:

$ echo -n "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" | hexdump -ve '/1 "%02x"' \
  | sed -e 's/\(.\{32\}\)/0x\1 /g'; echo
0x6c6475695379765377595a7a7770394f 0x6650694d745652596a396e3652376539 

Umwandlung des 256 bit langen ASCII-Credentials in zwei 128 bit lange Hexadezimalketten.

Als Referenz für den Match bietet sich der Beginn des Payloads des UDP-Datagramms an, den man mit @ih (inner header) erhält, sodass sich das Credential an Position 0 befinden würde, zusammengefasst also @ih,0,128 für den Match über die ersten 128 bit des Credentials, und @ih,128,128 für die zweiten 128 bit.

Leider ist die Implementierung von @ih noch vergleichsweise jung und nur in Linux-Kerneln ab Version 5.16 und nftables ab Version 1.0.1 enthalten. Einige Distributionen wie bspw. RedHat Enterprise Linux 9 und kompatible EL9-Distros haben das Feature in ihre älteren Kernel rückportiert und liefern eine passende Version von nftables aus. Doch mit vielen älteren LTS-Distributionen guckt man in die Röhre.

Um auch zu älteren Linux-Distributionen kompatibel zu bleiben, verwenden wir daher die Referenz @th (transport header), welche sich auf den Beginn des verwendeten Transportprotokolls bezieht, in diesem Fall UDP, das eine feste Header-Länge von 8 Byte × 8 bit/Byte = 64 bit besitzt. Der Match ändert sich somit in @th,64,128 für die ersten 128 bit des Credentials, und @th,192,128 für die restlichen 128 bit des Credentials.

Bevor wir aber mittels raw payload matching den Paketinhalt betrachten, matchen wir zuerst das Protokoll und den Zielport sowie die Größe des UDP-Datagramms; wir wollen den Payload des Pakets nur untersuchen, wenn es sich um ein UDP-Datagramm handelt, das an den richtigen Port 34567/udp gerichtet ist (udp dport 34567), und genau 8 Byte UDP-Header + 32 Byte Payload (Credential) = 40 Byte lang ist (udp length 40).

Trifft dies alles zu und das Credential wurde gematcht, wird die Quell-IP-Adresse des Pakets plus die gewünschte Öffnungsdauer für das Portknocking der zur Adressfamilie passenden Liste hinzugefügt (add @knocked_v4 { ip saddr timeout 10s } für IPv4 bzw. add @knocked_v6 { ip6 saddr timeout 10s } für IPv6), zudem wünschen wir einen Paket- und Bytecounter für erfolgreiche Matches (counter) und wollen das erfolgreiche Anklopfen mit einem passenden Präfix versehen in den Systemprotokollen vermerken (log prefix "Knocked: ").

# IPv4: Matche UDP-Pakete an Zielport 34567 mit der Länge 40 Bytes und dem Payload
# "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" und füge die Quell-Adresse sowie ein Timeout
# von 10 Sekunden dem Set knocked_v4 hinzu, zähle das Paket und logge es mit dem
# Präfix "Knocked: ":
udp dport 34567 udp length 40 @th,64,128 0x6c6475695379765377595a7a7770394f \
  @th,192,128 0x6650694d745652596a396e3652376539 \
  add @knocked_v4 { ip  saddr timeout 10s } counter log prefix "Knocked: "

# IPv6: Matche UDP-Pakete an Zielport 34567 mit der Länge 40 Bytes und dem Payload
# "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" und füge die Quell-Adresse sowie ein Timeout
# von 10 Sekunden dem Set knocked_v6 hinzu, zähle das Paket und logge es mit dem
# Präfix "Knocked: ":
udp dport 34567 udp length 40 @th,64,128 0x6c6475695379765377595a7a7770394f \
  @th,192,128 0x6650694d745652596a396e3652376539 \
  add @knocked_v6 { ip6 saddr timeout 10s } counter log prefix "Knocked: "

nftables: IPv4- und IPv6-Regeln zum Credential-Matching mit Befüllung der IP-Listen.

Es ist wichtig, dass diese Regeln in der Kette für eingehende Pakete vor jeglichen anderen eventuell auf die Anklopf-Pakete zutreffende Regeln mit finalem Urteil (accept, reject oder drop) positioniert werden. Das gilt ebenfalls für Absprünge in andere Kette ohne Rückkehr oder mit ihrerseits möglicherweise zutreffenden Regeln mit finalem Urteil.

iptables

Die Credential-Matching-Regeln für iptables lassen sich nach dem selben Schema aufbauen, es gibt jedoch ein paar Unterschiede:

  • Die length-Erweiterung von iptables kann nicht die Länge des UDP-Teils im IP-Payload matchen, sondern bezieht sich immer auf die gesamte Größe des IP-Pakets. Zu den bei nftables gematchten 40 Bytes müssen daher bei iptables noch 20 Bytes IPv4- bzw. 40 Bytes IPv6-Header addiert werden.
  • iptables besitzt mit der string-Erweiterung die Möglichkeit, Pakete mit verschiedenen Algorithmen nach Zeichenketten zu durchsuchen.
  • Da die Regelsätze für IPv4 und IPv6 in iptables strikt getrennt sind, kann die IP-Adressliste in beiden Regelketten den gleichen Namen tragen. Wir verwenden knocked.
  • Die Festlegung der Gültigkeitszeit durch die recent-Erweiterung erfolgt nicht beim Credential-Matching, sondern erst beim Zulassen der Verbindung im nächsten Abschnitt.
  • iptables-Regeln können sich in den mit iptables-restore in den Kernel zu ladenden Regelsatz-Dateien nicht zur besseren Übersicht durch Trennung mit \ über mehrere Zeilen erstrecken.

Die für iptables angepasste Regel für die filter-Tabelle des IPv4-Regelsatzes sieht somit so aus:

Hänge der Kette INPUT eine Regel an (-A INPUT), die auf UDP-Datagramme (-p udp) zutrifft, die an den Zielport 34567 gerichtet sind (--dport 34567) und genau 20 Byte IPv4-Header + 8 Byte UDP-Header + 32 Byte Payload (Credential) = 60 Bytes lang sind (-m length --length 60) sowie als Inhalt unser Credential tragen (-m string --algo bm --string "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9"). Wenn dies zutrifft, merke die Quell-IP unter dem Namen knocked (-m recent --set --name knocked --rsource) und protokolliere das erfolgreiche Anklopfen mit dem Präfix Knocked: in den Systemprotokollen (-j LOG --log-prefix "Knocked: ").

# IPv4: Matche UDP-Pakete an Zielport 34567 mit der Paketgröße 60 Bytes und dem
# Payload "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" und merke die Quell-Adresse unter
# dem Namen "knocked", dann logge es mit dem Präfix "Knocked: ":
-A INPUT -p udp --dport 34567 -m length --length 60 -m string --algo bm --string "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" -m recent --set --name knocked --rsource -j LOG --log-prefix "Knocked: "

iptables: IPv4-Regel zum Credential-Matching mit Merken der Quell-IP-Adresse.

Für den IPv6-Regelsatz ist die Regel identisch, außer dass die Paketlänge genau 40 Byte IPv6-Header + 8 Byte UDP-Header + 32 Byte Payload (Credential) = 80 Byte betragen soll (-m length --length 80).

# IPv6: Matche UDP-Pakete an Zielport 34567 mit der Paketgröße 80 Bytes und dem
# Payload "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" und merke die Quell-Adresse unter
# dem Namen "knocked", dann logge es mit dem Präfix "Knocked: ":
-A INPUT -p udp --dport 34567 -m length --length 80 -m string --algo bm --string "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" -m recent --set --name knocked --rsource -j LOG --log-prefix "Knocked: "

iptables: IPv6-Regel zum Credential-Matching mit Merken der Quell-IP-Adresse.

Bei der Positionierung der Regeln sollte identisch zu nftables vorgegangen werden.

SSH-Verbindungen von authentifizierten IP-Adressen zulassen

nftables

Da wir nun für beide Adressfamilien die Listen für erfolgreich anklopfende IP-Adressen und die Matches für das Credential-Portknocking aufgesetzt haben, bleiben nur noch Regeln, die für den zu verbergenden SSH-Dienst (tcp dport 22) eingehende neue Verbindungen (ct state new) für alle in den Listen erfolgreicher Portknocks befindlichen IPv4- (ip saddr @knocked_v4) bzw. IPv6-Quelladressen (ip6 saddr @knocked_v6) zählen (counter) und zulassen (accept).

# IPv4: TCP-Pakete an Port 22 (SSH) mit neuen Verbindungen, deren Quell-IP-Adresse
# im Set knocked_v4 enthalten ist, zählen und erlauben:
tcp dport 22 ct state new ip  saddr @knocked_v4 counter accept

# IPv6: TCP-Pakete an Port 22 (SSH) mit neuen Verbindungen, deren Quell-IP-Adresse
# im Set knocked_v6 enthalten ist, zählen und erlauben:
tcp dport 22 ct state new ip6 saddr @knocked_v6 counter accept

nftables: Zulassen neuer SSH-Verbindungen von Quell-IPs in den IPv4- und IPv6-Listen.

Auch hier gilt, dass diese Regeln in der Kette für eingehende Pakete so positioniert werden müssen, dass eingehende neue Verbindungen nicht von einer davor liegenden Regel bereits abgelehnt oder zugelassen werden.

iptables

Die Regel zum Zulassen von Verbindungen durch erfolgreich anklopfende Clients ist bei iptables für die IPv4- und IPv6-Regelsätze identisch, und baut sich wie folgt auf:

Hänge der INPUT-Kette der filter-Tabelle eine Regel an (-A INPUT), die auf an den TCP-Zielport 22 (-p tcp --dport 22) gerichtete neue Verbindungen zutrifft (-m state --state NEW), wenn die Quell-IP-Adresse innerhalb der vergangenen 10 Sekunden unter dem Namen knocked erfasst wurde (-m recent --rcheck --seconds 10 --name knocked --rsource), und dieses Paket dann akzeptiert (-j ACCEPT).

# TCP-Pakete an Port 22 (SSH) mit neuen Verbindungen erlauben, deren Quell-IP-
# Adresse vor weniger als 10 Sekunden unter dem Namen knocked gemerkt wurden:
-A INPUT -p tcp --dport 22 -m state --state NEW -m recent --rcheck --seconds 10 --name knocked --rsource -j ACCEPT

iptables: Zulassen neuer SSH-Verbindungen von in knocked gemerkten Quell-IPs.

Auch diese Regel muss, analog zur nftables-Regel, im Regelsatz so positioniert werden, dass eingehende neue Verbindungen nicht von einer davor liegenden Regel bereits abgelehnt oder zugelassen werden.

Inbetriebnahme mit Test der Anklopf-Funktion

Für die Inbetriebnahme ist es empfehlenswert zur Verifizierung der Funktion eine SSH-Verbindungen allgemein zulassende Regel (bspw. tcp dport 22 ct state new counter accept für nftables, -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT für iptables) unmittelbar nach den oben konstruierten Regeln zu platzieren, bzw. die erfolgreich anklopfende Clients freigebenden Regeln unmittelbar vor bereits vorhandenen, den SSH-Dienst allgemein zulassenden Regeln zu positionieren.

Anschließend lässt sich nach einem SSH-Verbindungsaufbau per Portknock mittels nft list ruleset bzw. iptables -L -vn und ip6tables -L -vn prüfen, ob der Verbindungsaufbau durch die Portknocking- oder die allgemeine Regel zugelassen wurde. Dazu werden die Paket-Zähler betrachtet um zu sehen, bei welcher der Regeln sich diese durch das Zutreffen der Regel beim Verbindungsaufbau erhöht haben. Ist sichergestellt, dass das Portknocking funktioniert, kann die allgemein zulassende Regel aus dem Regelsatz entfernt oder auskommentiert werden.

Kompletter Beispiel-Regelsatz

Die nachfolgenden Beispiele kombinieren alle zuvor beschriebenen Elemente zu funktionsfähigen restriktiven Regelsätzen mit Stateful Firewalling und Portknocking für SSH – einmal ein kombinierter nftables-Regelsatz für IPv4 und IPv6, ein iptables-Regelsatz für IPv4, und ein iptables-Regelsatz für IPv6 (ip6tables).

Die Beispiel-Regelsätze sollen nicht eine bestehende Firewall einfach ersetzen, sondern verdeutlichen, welche Elemente vorhanden sein sollten und wie diese in einem Regelsatz positioniert werden müssen. Alternativ kann das Beispiel auch als Grundlage für den Aufbau eines neuen, auf das jeweilige Zielsystem zugeschnittenen Firewall-Regelsatzes verwendet werden.

nftables
flush ruleset

table inet filter {

  set knocked_v4 {
    type ipv4_addr
    flags timeout
  }

  set knocked_v6 {
    type ipv6_addr
    flags timeout
  }

  chain input {
    type filter hook input priority filter; policy drop;
  
    # Stateful Firewalling:
    ct state related,established counter accept
    ct state invalid counter drop
  
    # IPv6 NDP zulassen:
    icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } \
      counter accept

    # Loopback-Traffic zulassen:
    iif lo counter accept

    # IPv4: Matche UDP-Pakete an Zielport 34567 mit der Länge 40 Bytes und dem Payload
    # "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" und füge die Quell-Adresse sowie ein Timeout
    # von 10 Sekunden dem Set knocked_v4 hinzu, zähle das Paket und logge es mit dem
    # Präfix "Knocked: ":
    udp dport 34567 udp length 40 @th,64,128 0x6c6475695379765377595a7a7770394f \
      @th,192,128 0x6650694d745652596a396e3652376539 \
      add @knocked_v4 { ip  saddr timeout 10s } counter log prefix "Knocked: "

    # IPv6: Matche UDP-Pakete an Zielport 34567 mit der Länge 40 Bytes und dem Payload
    # "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" und füge die Quell-Adresse sowie ein Timeout
    # von 10 Sekunden dem Set knocked_v6 hinzu, zähle das Paket und logge es mit dem
    # Präfix "Knocked: ":
    udp dport 34567 udp length 40 @th,64,128 0x6c6475695379765377595a7a7770394f \
      @th,192,128 0x6650694d745652596a396e3652376539 \
      add @knocked_v6 { ip6 saddr timeout 10s } counter log prefix "Knocked: "

    # IPv4: TCP-Pakete an Port 22 (SSH) mit neuen Verbindungen, deren Quell-IP-Adresse
    # im Set knocked_v4 enthalten ist, zählen und erlauben:
    tcp dport 22 ct state new ip  saddr @knocked_v4 counter accept

    # IPv6: TCP-Pakete an Port 22 (SSH) mit neuen Verbindungen, deren Quell-IP-Adresse
    # im Set knocked_v6 enthalten ist, zählen und erlauben:
    tcp dport 22 ct state new ip6 saddr @knocked_v6 counter accept

    # Test-Regel zum Test bei Inbetriebnahme und gegen versehentliches Aussperren:
    #tcp dport 22 ct state new counter accept

    # Regeln zur Freigabe anderer (bspw. allgemeinzugänglicher) Dienste hier
    # platzieren.

    # Pings zulassen, aber bei eingeschränkter Rate:
    icmp   type echo-request limit rate 5/second counter accept
    icmpv6 type echo-request limit rate 5/second counter accept

    # Alles andere zurückweisen (ergänzend zur drop-Policy der Kette):
    counter reject with icmpx type admin-prohibited
  }

}

nftables: Kompletter Beispiel-Regelsatz zur Filterung von IPv4 und IPv6.

iptables für IPv4
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]

# Stateful Firewalling:
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -m state --state INVALID -j DROP

# Loopback-Traffic zulassen:
-A INPUT -i lo -j ACCEPT

# IPv4: Matche UDP-Pakete an Zielport 34567 mit der Paketgröße 60 Bytes und dem
# Payload "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" und merke die Quell-Adresse unter
# dem Namen "knocked", dann logge es mit dem Präfix "Knocked: ":
-A INPUT -p udp --dport 34567 -m length --length 60 -m string --algo bm --string "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" -m recent --set --name knocked --rsource -j LOG --log-prefix "Knocked: "

# TCP-Pakete an Port 22 (SSH) mit neuen Verbindungen erlauben, deren Quell-IP-
# Adresse vor weniger als 10 Sekunden unter dem Namen knocked gemerkt wurden:
-A INPUT -p tcp --dport 22 -m state --state NEW -m recent --rcheck --seconds 10 --name knocked --rsource -j ACCEPT

# Test-Regel zum Test bei Inbetriebnahme und gegen versehentliches Aussperren:
#-A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT

# Regeln zur Freigabe anderer (bspw. allgemeinzugänglicher) Dienste hier
# platzieren.

# Pings zulassen, aber bei eingeschränkter Rate:
-A INPUT -p icmp --icmp-type echo-request -m limit --limit 5/second -j ACCEPT 

# Alles andere zurückweisen (ergänzend zur drop-Policy der Kette):
-A INPUT -j REJECT --reject-with icmp-admin-prohibited

COMMIT

iptables: Kompletter Beispiel-Regelsatz zur Filterung von IPv4.

iptables für IPv6
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]

# Stateful Firewalling:
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -m state --state INVALID -j DROP

# IPv6 NDP zulassen:
-A INPUT -p icmpv6 --icmpv6-type neighbour-solicitation -j ACCEPT
-A INPUT -p icmpv6 --icmpv6-type router-advertisement -j ACCEPT
-A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -j ACCEPT

# Loopback-Traffic zulassen:
-A INPUT -i lo -j ACCEPT

# IPv6: Matche UDP-Pakete an Zielport 34567 mit der Paketgröße 80 Bytes und dem
# Payload "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" und merke die Quell-Adresse unter
# dem Namen "knocked", dann logge es mit dem Präfix "Knocked: ":
-A INPUT -p udp --dport 34567 -m length --length 80 -m string --algo bm --string "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" -m recent --set --name knocked --rsource -j LOG --log-prefix "Knocked: "

# TCP-Pakete an Port 22 (SSH) mit neuen Verbindungen erlauben, deren Quell-IP-
# Adresse vor weniger als 10 Sekunden unter dem Namen knocked gemerkt wurden:
-A INPUT -p tcp --dport 22 -m state --state NEW -m recent --rcheck --seconds 10 --name knocked --rsource -j ACCEPT

# Test-Regel zum Test bei Inbetriebnahme und gegen versehentliches Aussperren:
#-A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT

# Regeln zur Freigabe anderer (bspw. allgemeinzugänglicher) Dienste hier
# platzieren.

# Pings zulassen, aber bei eingeschränkter Rate:
-A INPUT -p icmpv6 --icmpv6-type echo-request -m limit --limit 5/second -j ACCEPT 

# Alles andere zurückweisen (ergänzend zur drop-Policy der Kette):
-A INPUT -j REJECT --reject-with icmp6-adm-prohibited

COMMIT

iptables: Kompletter Beispiel-Regelsatz zur Filterung von IPv6.

Clientseitige Implementierung

Um ein UDP-Datagramm mit dem Credential als Payload an den Anklopf-Port des Servers zu senden, wird ein Tool benötigt. Im einfachsten Fall ist dies die GNU Bash, die auf so ziemlich jedem Linux-Rechner bereits vorhanden sein sollte.

Spätestens wenn es an die Integration in PuTTY geht (dazu mehr im nächsten Teil), wird jedoch auch ein TCP-Proxy benötigt. Hierfür kommen bspw. OpenBSD netcat, Nmap ncat oder auch socat in Frage, die allesamt ebenfalls genutzt werden können, um unser Credential als UDP-Datagramm abzusetzen, sodass sowohl für das Anklopfen als auch als TCP-Proxy das selbe Tool verwendet werden kann. Die meisten Linux-Distributionen bieten fertige Pakete für alle drei Tools an.
Socat ist dabei jedoch aus drei Gründen nur bedingt zu empfehlen; die Befehlszeilen-Syntax ist signifikant komplexer als bei den anderen Tools, es legt nach dem Versenden eines UDP-Datagramms trotz SIGPIPE erst einmal eine Gedenkpause ein, die man durch Setzen des Timeouts auf 0 Sekunden explizit deaktivieren muss, und die Angabe von IPv6-Adressen erfordert eine Syntax, die von der Syntax zur Angabe von IPv4-Adressen oder Hostnames abweicht.

Die folgenden vier Befehle lassen sich äquivalent zum Anklopfen an unserem Server verwenden (der erste Befehl erfordert Bash als Shell):

$ echo -n "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" \
  > /dev/udp/server.schoen-technisch.de/34567

$ echo -n "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" \
  | netcat -u "server.schoen-technisch.de" "34567"

$ echo -n "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" \
  | ncat --send-only -u "server.schoen-technisch.de" "34567"

$ echo -n "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" \
  | socat -t 0 STDIO "UDP-SENDTO:server.schoen-technisch.de:34567"

Anklopfen am Server per UDP-Credential mittels Bash, netcat, ncat und socat.

SSH-Verbindungsaufbau mit Portknocking

Nach dem Anklopfen mit einer beliebigen der vorgenannten Methoden sollte jetzt die Kernelfirewall des Servers in den Systemprotokollen ein mit der Zeichenkette Knocked: präfigiertes Paket protokolliert haben, und ein Verbindungsaufbau per SSH sollte für 10 Sekunden möglich sein.

$ ssh user@server.schoen-technich.de
ssh: connect to host server.schoen-technich.de port 22: Network is unreachable

$ echo -n "lduiSyvSwYZzwp9OfPiMtVRYj9n6R7e9" > /dev/udp/server.schoen-technisch.de/34567
  
$ ssh user@server.schoen-technich.de
Last login: Thu May 30 11:52:51 2024 from 10.0.0.2
[user@server ~]$ 

Fehlgeschlagene SSH-Verbindung vor, und erfolgreiche SSH-Verbindung nach manuellem Anklopfen.

Ausblick: Integration in SSH-Clients und Protokolle

In diesem ersten Teil haben wir nun erarbeitet, mit wie wenig Aufwand sich ein Credential-Portknocking zum Schutz des SSH-Dienstes direkt und solide über die Linux-Kernelfirewall implementieren lässt.

Der zweite Teil beschäftigt sich mit diversen Arten der Integration in die SSH-Clients OpenSSH und PuTTY sowohl unter Linux (und anderen Unix(-artigen) OS) als auch unter Windows befassen, sodass das Anklopfen automatisch beim Verbindungsaufbau durchgeführt und die Nutzung des Credential-Portknockings somit für den Anwender transparent wird.

Im dritten Teil der Serie wird vorgestellt, wie sich die Übertragung des Credentials in Datagramme von Protokollen wie ICMP Echo Requests (Ping) oder SNMP-Abfragen integrieren lässt, um das Anklopfen zusätzlich zu verbergen oder einfacher vorgeschaltete Firewalls passieren zu lassen.


Nächster Teil: Credential-Portknocking, Teil 2: Integration in SSH-Clients

Portknocking-Integration in SSH-Clients
Vollautomatisches Portknocking mit OpenSSH und PuTTY unter Linux/Unix und Windows.