IT security, FreeBSD, Linux, mail server hardening, post-quantum crypto, DNS, retro computing & hands-on hardware hacks. Privater Tech-Blog seit 2003.

Kategorie: Linux & BSD (Seite 1 von 8)

Anleitungen und Erfahrungsberichte rund um Linux-Distributionen und FreeBSD — vom Desktop bis zum Server.

NEXT Biometrics NB-2033-U: Reverse Engineering eines Fingerabdrucklesers für Linux

Illustration eines USB-Fingerabdrucklesers mit Linux-Tux und USB-Protokollanalyse (Reverse Engineering NB-2033-U)

Im letzten Beitrag zum Thema hatte ich angekündigt, dass ich mir auch den NB-2033-U vornehmen will. Der steckt in einem zweiten Fujitsu Notebook hier, dem von meiner Tochter Maja. Gleicher Hersteller, gleiche Sensorfamilie, sollte ähnlich laufen wie beim NB-2020-U. Dachte ich.

Falsch gedacht.

Hersteller sagt: geht nicht

Ich hatte bei NEXT Biometrics nach Protokolldokumentation oder einem SDK für den NB-2033-U gefragt. Kevin Hung, Director FAE, antwortete freundlich aber eindeutig:

„Both 2020-U and 2033-U have different firmware and USB stack. The code flow (libusb) related to 2033-U and 2020-U is different. This could be the reason for 2033-U failure/unsupported in linux. As of now, it is not supported.“

Kein SDK, keine Doku, kein Support. Und 74 Einträge auf linux-hardware.org mit Status „failed“ für die USB ID 298d:2033. Weltweit kein Linux-Support für dieses Gerät.

Gut. Dann eben Reverse Engineering.

Erster Versuch: Windows-Treiber belauschen

Plan A war klassisch: Windows-Treiber in einer VM laufen lassen, USB-Traffic mitschneiden. VirtualBox installiert, USB-Passthrough konfiguriert, Windows gestartet. Der Fingerabdruckleser tauchte im Gerätemanager auf. Mit Code 31. Treiber konnte das Gerät nicht starten. Secure Boot hatte VirtualBox den Kernel-Treiber nicht signiert, und der USB-Passthrough war damit unbrauchbar.

Plan A verworfen.

Plan B: Das SDK direkt auf Linux

Das SDK von NEXT Biometrics (libNBBiometrics.so) unterstützt den NB-2033-U intern. Es kommuniziert direkt über libusb, ohne Kernel-Treiber. Das heißt: ich kann das SDK-Sample direkt auf dem Linux-Notebook laufen lassen und gleichzeitig den USB-Traffic mit usbmon mitschneiden.

Dafür musste Secure Boot deaktiviert werden. usbmon ist ein Kernel-Modul, und lockdown=integrity (von Secure Boot gesetzt) blockiert es auch für root. Secure Boot im BIOS aus, lockdown=none in GRUB, Neustart. Danach:

modprobe usbmon
cat /sys/kernel/debug/usb/usbmon/3u > /tmp/capture.txt &
./NBBSample

7654 Zeilen USB-Traffic. Das komplette Protokoll des NB-2033-U, aufgezeichnet während einer Enrollment-Session.

Was dabei rauskam

Das Protokoll ist komplett anders als beim NB-1010-U/NB-2020-U. Kevins Aussage stimmte. Hier die wesentlichen Unterschiede:

EigenschaftNB-1010-U / NB-2020-UNB-2033-U
Bulk IN EndpointEP 3 (0x83)EP 1 (0x81)
Kommandoformat[0x80][CMD][SEQ][0x00]...[CMD][0x00][LEN_LO][LEN_HI][PAYLOAD] (TLV)
Finger-ErkennungEinzelnes 0x38Zwei 0x0D Config + 0x38
Bildübertragung90 Chunks à 540 Bytes180 Chunks à 268 Bytes
InitEinmal 0x07Zweimal 0x07 nötig

Gleicher Sensor-Die (256×180 Pixel, 385 DPI, aktiv thermisch), aber ein komplett anderer USB-Stack. Der NB-2033-U nutzt ein TLV-Format (Type-Length-Value) statt des festen Kommandoschemas vom NB-1010-U. Jedes Kommando hat eine eigene Längenangabe, und die Antworten sind anders strukturiert.

Die Kommandos im Detail

Aus dem USB-Capture konnte ich sechs Kommandos identifizieren:

  • 0x07 — Init/Wake. Muss zweimal gesendet werden, sonst reagiert der Sensor nicht.
  • 0x0D — Sensor-Konfiguration. Wird zweimal vor jeder Finger-Erkennung gebraucht, um den „Enhanced“ Modus zu aktivieren.
  • 0x38 — Finger-Erkennung. Byte 4 der Antwort ist der Detect-Level. Schwellwert 40.
  • 0x12 — Capture starten. Liefert 180 Zeilen à 256 Pixel, 8-Bit Graustufen.
  • 0x13 — Geräteinformationen (Hersteller, Modell, Seriennummer).
  • 0xF7 — Firmware-Version.

Thermischer Sensor: Eigenheiten

Der Sensor misst Temperaturänderungen, nicht statischen Kontakt. Das klingt nach einem Detail, ist aber für die Treiber-Implementierung entscheidend. Finger auflegen erzeugt einen kurzen Spike im Detect-Wert (10 bis 50+). Finger bleibt liegen, und der Wert fällt zurück auf Basisniveau. Der Treiber muss also den Spike erkennen, nicht einen dauerhaften Zustand.

Dazu kommt: Nach dem Init gibt es transiente Spikes, die ungefähr 1,5 Sekunden brauchen, bis sie abklingen. Ohne Settle-Pause nach dem Init erkennt der Treiber Phantom-Finger.

Der Treiber

Rausgekommen ist nb2033.c, ein eigenständiger libfprint-Treiber mit rund 350 Zeilen. Kein proprietärer Code, keine SDK-Abhängigkeit. Das SDK diente nur als Referenz für die Capture-Analyse, der Treiber ist sauber von Grund auf geschrieben. Lizenz: LGPL 2.1+ wie alle libfprint-Treiber.

Die State Machine:

  1. Init (0x07 × 2) mit 1,5 Sekunden Settle-Pause
  2. Finger-Detect-Polling (0x0D + 0x0D + 0x38, Schwellwert 40)
  3. Pre-Capture Config (0x0D)
  4. Capture (0x12) mit 150 ms Pause, dann 180 Zeilen lesen
  5. Bild an libfprint übergeben

Test

Getestet auf Majas Fujitsu Notebook mit Linux Mint 22.3:

$ fprintd-enroll
Using device /net/reactivated/Fprint/Device/0
Enrolling right-index-finger finger.
Enroll result: enroll-stage-passed
[... 5/5 Stages ...]
Enroll result: enroll-completed
$ fprintd-verify
Using device /net/reactivated/Fprint/Device/0
Listing enrolled fingers:
 - #0: right-index-finger
Verify result: verify-match (true)

Richtiger Finger: Match. Falscher Finger: No Match. Enrollment sauber, Verifikation zuverlässig.

Upstream

Der Merge Request ist eingereicht: MR !574 bei libfprint. Fünf Dateien: der neue Treiber, meson.build, autosuspend.hwdb und die Allowlist. CI läuft durch. Der verwandte MR !569 für den NB-2020-U ist noch in Review.

Für die Wiki-Aktualisierung (das Gerät von der „unsupported“ Liste nehmen) gibt es Issue #134.

Fazit

Der Hersteller sagt „not supported“, 74 Linux-User melden „failed“, und trotzdem war das an einem Nachmittag erledigt. SDK auf Linux ausführen, USB-Traffic mitschneiden, Protokoll rekonstruieren, Treiber schreiben, testen, upstream einreichen. Alles mit Open-Source-Tools: usbmon, libusb, libfprint.

Das Ergebnis: Majas Notebook hat jetzt einen funktionierenden Fingerabdruckleser unter Linux. Und sobald der Merge Request durch ist, haben ihn alle anderen auch.

Wie immer: Bei Fragen, fragen.

Raspberry Pi als serieller Konsolenserver

Wir haben 2026. Alles wandert in die Cloud. Trotzdem will ich heute über serielle Konsolen schreiben. Klingt retro, ist es aber nicht. Wenn ein Switch sich verkonfiguriert hat und das Netzwerk weg ist, hilft kein Ansible und kein Dashboard in der Cloud. Dann hilft nur noch der serielle Konsolenport. Out-of-Band Management ist nicht tot. Es wurde nur teuer verpackt.

Kommerzielle Konsolenserver kosten gerne vierstellig. Oder man nimmt einen Raspberry Pi der noch herum liegt und auf eine neue Aufgabe wartet (ich habe hier ein paar Pi1 oder 2 herum liegen). Zusammen mit zwei USB Serial Adaptern hat man für unter 50 Euro einen Konsolenserver mit acht Ports. Das reicht für die meisten Setups locker aus.

Raspberry Pi als DIY-Konsolenserver mit USB-Serial-Adaptern zur Verwaltung serieller Konsolen von Netzwerkgeräten über SSH und ser2net

Wofür ein Konsolenserver

Der klassische Fall: Ein paar Switches im Rack, jedes Gerät hat einen seriellen Konsolenport. Im Normalbetrieb konfiguriert man über das Netzwerk. Aber wenn mal eine falsche Route das Management Interface unerreichbar macht oder ein VLAN Umbau schiefgeht, steht man vor dem Gerät und steckt ein Kabel rein. Wenn das im DC in Frankfurt ist, oder vielleicht irgendwo in China, dann kann das spannend werden.

Oder man hat vorgebaut.

Ein Konsolenserver hängt permanent an den seriellen Ports der Netzwerkgeräte. Man kommt per SSH auf den Konsolenserver und von dort auf die serielle Konsole des Zielgeräts. Ob das Netzwerk funktioniert oder nicht, spielt keine Rolle mehr. Öhm also ja, so grob. Der Pi sollte dann ja schon noch erreichbar sein. Aber man hat ja in einem entfernten DC auch eine Dailin Line oder ähnliches, richtig? Richtig?

Meme mit Anakin und Padmé: „Der Konsolenserver hängt an allen Switches – wir kommen immer auf die Konsole – der Raspi ist erreichbar über … die gleiche Strecke.“

Hardware

Ein Raspberry Pi. Es muss kein aktuelles Modell sein. Selbst ein alter Pi 2 reicht völlig aus. Das Ding muss ser2net laufen lassen und ein paar serielle Ports bedienen, dafür braucht man keinen Quad Core mit 8 GB RAM. Der Pi aus der Schublade bekommt endlich eine sinnvolle Aufgabe.

FTDI Quad Port USB Serial Adapter (Vendor 0403, Product 6011). Pro Adapter bekommt man vier serielle Ports. Mit zwei Adaptern hat man acht Ports. Die Dinger gibt es für kleines Geld.

RS232 Kabel zu den Console Ports der Netzwerkgeräte. Welcher Stecker passt, hängt vom Hersteller ab. RJ45 auf DB9, DB9 auf DB9, die üblichen Verdächtigen. Da muss man schauen was die eigenen Geräte mitbringen.

Stabile Gerätenamen mit udev

Das erste Problem nach dem Einstecken der USB Adapter: Linux vergibt die /dev/ttyUSBx Nummern nach Lust und Laune. Nach einem Reboot kann ttyUSB0 plötzlich ttyUSB4 sein. Wenn man wissen will welcher Port an welchem Gerät hängt, ist das unpraktisch.

Die Lösung sind udev Regeln. Jeder FTDI Adapter hat eine eigene Seriennummer. Die findet man so:

udevadm info -a -n /dev/ttyUSB0 | grep serial

Damit baut man sich Regeln die stabile Symlinks erzeugen. Datei /etc/udev/rules.d/99-serial-consoles.rules:

SUBSYSTEMS=="usb", ENV{.LOCAL_ifNum}="$attr{bInterfaceNumber}"
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6011", ATTRS{serial}=="FT000001", SYMLINK+="quad0-%E{.LOCAL_ifNum}"
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6011", ATTRS{serial}=="FT000002", SYMLINK+="quad1-%E{.LOCAL_ifNum}"

FT000001 und FT000002 ersetzt man durch die echten Seriennummern der eigenen Adapter. Das Ergebnis sind stabile Symlinks: /dev/quad0-00 bis /dev/quad0-03 für den ersten Adapter, /dev/quad1-00 bis /dev/quad1-03 für den zweiten. Acht Ports, immer gleich benannt. Egal wie oft man den Pi neustartet.

ser2net

ser2net bildet die seriellen Ports auf TCP Ports ab. Man kann dann per Telnet auf einen bestimmten Port zugreifen und landet direkt auf der seriellen Konsole des zugehörigen Geräts. Installieren mit apt install ser2net, dann die Konfiguration in /etc/ser2net.conf:

localhost,2001:telnet:600:/dev/quad0-00:9600 8DATABITS NONE 1STOPBIT banner
localhost,2002:telnet:600:/dev/quad0-01:9600 8DATABITS NONE 1STOPBIT banner
localhost,2003:telnet:600:/dev/quad0-02:9600 8DATABITS NONE 1STOPBIT banner
localhost,2004:telnet:600:/dev/quad0-03:9600 8DATABITS NONE 1STOPBIT banner
localhost,2005:telnet:600:/dev/quad1-00:9600 8DATABITS NONE 1STOPBIT banner
localhost,2006:telnet:600:/dev/quad1-01:9600 8DATABITS NONE 1STOPBIT banner
localhost,2007:telnet:600:/dev/quad1-02:9600 8DATABITS NONE 1STOPBIT banner
localhost,2008:telnet:600:/dev/quad1-03:9600 8DATABITS NONE 1STOPBIT banner

9600 8N1 ist der Standard bei den meisten Netzwerkgeräten. Falls ein Gerät eine andere Baudrate braucht, passt man die entsprechende Zeile an. Der Timeout von 600 Sekunden trennt die Verbindung nach zehn Minuten Inaktivität. Das verhindert dass ein vergessenes Telnet die Konsole dauerhaft blockiert.

Direkter Zugriff mit minicom

Wer ser2net nicht nutzen will oder schnell direkt auf einen Port muss, nimmt minicom:

minicom -D /dev/quad0-00 -b 9600

minicom ist gut für schnelle Tests und Debugging. Für den Dauerbetrieb mit mehreren Ports gleichzeitig ist ser2net die bessere Wahl.

Warum localhost

ser2net ist im gezeigten Setup bewusst auf localhost gebunden. Man muss sich erst per SSH auf den Pi einloggen und dann telnet 127.0.0.1 200x aufrufen. Das ist Absicht.

Man könnte ser2net auch auf 0.0.0.0 binden und die Ports direkt aus dem Netz erreichen. Davon rate ich ab. Telnet ist unverschlüsselt. Auch in einem Management VLAN hat das nichts verloren.

Bessere Alternativen wenn man ohne SSH auf den Pi will:

  • ser2net ab Version 4.x unterstützt SSL/TLS. Damit hat man verschlüsselte Verbindungen direkt zu den Console Ports.
  • stunnel vor ser2net schalten. stunnel terminiert TLS und reicht die Verbindung an den lokalen ser2net weiter.
  • Wer nativen SSH Zugriff direkt auf die seriellen Ports braucht, sollte sich conserver anschauen. ser2net kann kein SSH.

Für die meisten Setups ist SSH auf den Pi und dann Telnet auf localhost der einfachste und sicherste Weg.

Absichern

Ein paar Dinge die man auf dem Pi noch machen sollte:

Den Default Benutzer pi löschen. Einen eigenen Benutzer anlegen. SSH Key Authentifizierung einrichten und Login per Passwort deaktivieren. Das ist nicht optional.

NTP konfigurieren. Timestamps in Logs sind nutzlos wenn die Uhrzeit nicht stimmt.

Syslog an einen zentralen Logserver weiterleiten. Wenn man serielle Konsolen mitschneidet, will man die Logs nicht nur lokal auf dem Pi haben.

Workflow

Der Alltag sieht dann so aus:

  1. SSH auf den Pi: ssh admin@10.0.0.50
  2. Telnet auf den gewünschten Port: telnet 127.0.0.1 2003
  3. Man landet auf der seriellen Konsole von Switch 3

Alternativ direkt mit minicom: minicom -D /dev/quad0-02 -b 9600

Zum Trennen: Ctrl-] und dann quit bei Telnet. Ctrl-A gefolgt von X bei minicom.

Fazit

Ein alter Raspberry Pi, zwei USB Adapter, ein paar Kabel. Mehr braucht man nicht für einen funktionierenden Konsolenserver mit acht Ports. Die Einrichtung dauert vielleicht eine Stunde. Danach läuft das Ding und man muss nie wieder ein Konsolenkabel quer durch den Serverraum schleppen.

Und der alte Pi aus der Schublade hat endlich wieder eine Aufgabe.

Ihr habt Fragen, Anmerkungen oder baut das Setup selbst nach? Meldet euch gerne über die Kontaktseite oder direkt per E-Mail.

Siehe auch: DHT22 am Raspberry Pi

Post-Quantum TLS für Nginx — X25519MLKEM768 auf FreeBSD 15 konfigurieren

Nachdem ich zuerst OpenSSH und dann Postfix und Dovecot mit Post-Quantum-Kryptografie ausgestattet habe, kamen einige Rückfragen: Wie sieht das eigentlich für Nginx aus? Kann man das auf dem Webserver genauso einfach aktivieren? Kurze Antwort: Ja. Noch kürzer sogar als bei E-Mail.

Nginx TLS-Konfiguration mit Post-Quantum-Key-Exchange X25519MLKEM768 für HTTPS.

Spannend finde ich dabei, dass ausgerechnet der Webserver die meisten Nachfragen erzeugt hat. SSH und E-Mail laufen hier längst mit X25519MLKEM768, aber die Leser wollten vor allem wissen, wie das für HTTPS geht. Vermutlich weil jeder eine Webseite hat, aber nicht jeder seinen eigenen Mailserver betreibt?! Es kommt ja immer darauf an, was man macht und welche Daten man übermittelt. Aber SSH oder E-Mail würde mich selbst nervöser machen als normales surfen. Wobei…. Ich melde mich ja auch an, hm. OK, immer MFA. Na, eine normale E-Mail ist ja schon immer eine Postkarte, wenn man keine zusätzliche Verschlüsselung nutzt (was kaum jemand macht).

Worum geht es?

Wer die Vorgeschichte noch nicht kennt: X25519MLKEM768 ist ein hybrider Schlüsselaustausch, der klassisches X25519 (Curve25519 ECDH) mit dem Post-Quantum-Algorithmus ML-KEM-768 kombiniert. Standardisiert vom NIST als FIPS 203. Der Vorteil des hybriden Ansatzes: Selbst wenn sich ML-KEM irgendwann als unsicher herausstellt, bleibt X25519 als Absicherung. Und andersherum genauso.

Das „Store now, decrypt later“ Szenario kennt ihr vielleicht schon aus den anderen Beiträgen. Jemand schneidet heute euren TLS-verschlüsselten Datenverkehr mit und entschlüsselt ihn in ein paar Jahren mit einem Quantencomputer. Bei HTTPS betrifft das alles, was über die Leitung geht: Formulardaten, Login-Credentials, API-Aufrufe, Session-Cookies. Ob das in der Praxis relevant ist? Kommt auf euer Bedrohungsmodell an. Aber der Aufwand für die Absicherung ist so gering, dass es keinen Grund gibt, es nicht zu tun.

Es hängt an OpenSSL, nicht an Nginx

Das ist eigentlich die zentrale Erkenntnis aus allen drei Beiträgen: Ob PQC funktioniert oder nicht, entscheidet fast ausschließlich die OpenSSL-Version. Nginx, Postfix, Dovecot, OpenSSH, sie alle delegieren den Schlüsselaustausch an OpenSSL (oder LibreSSL, BoringSSL, je nach System). Die Anwendung selbst muss lediglich die gewünschte Gruppe konfigurieren können. Und das können alle genannten Programme seit Jahren.

Konkret braucht ihr OpenSSL 3.5 oder neuer. Erst ab dieser Version ist ML-KEM nativ im Default-Provider enthalten, ohne externen OQS-Provider, ohne liboqs, ohne selbst kompilieren. FreeBSD 15 liefert das von Haus aus. Bei den meisten Linux-Distributionen sieht es Stand heute leider noch anders aus. Ubuntu 24.04 hat OpenSSL 3.0, Debian 12 hat 3.0, RHEL 9 hat 3.0. Für ein aktuelles OpenSSL müsst ihr dort entweder selbst bauen oder auf neuere Releases warten.

Voraussetzungen prüfen

Auf meinem FreeBSD 15:

$ openssl version
OpenSSL 3.5.4 30 Sep 2025 (Library: OpenSSL 3.5.4 30 Sep 2025)

Nginx muss natürlich gegen dieses OpenSSL gelinkt sein. Das prüft ihr so:

$ nginx -V 2>&1 | grep -oE 'OpenSSL [0-9]+\.[0-9]+\.[0-9]+'
OpenSSL 3.5.4

Und dann die entscheidende Frage: Kennt OpenSSL die Gruppe X25519MLKEM768?

$ openssl list -tls-groups | grep -i mlkem
  X25519MLKEM768
  SecP256r1MLKEM768
  SecP384r1MLKEM1024

Wenn X25519MLKEM768 in der Liste auftaucht, kann es losgehen.

Nginx konfigurieren

In Nginx heißt die relevante Direktive ssl_ecdh_curve. Der Name ist etwas irreführend, denn sie steuert nicht nur ECDH-Kurven, sondern alle Key-Exchange-Gruppen die OpenSSL kennt. Also auch hybride PQC-Gruppen.

Meine Konfiguration in der TLS-Defaults-Datei, die per include in alle vHosts eingebunden wird:

ssl_ecdh_curve  X25519MLKEM768:X25519:secp384r1:prime256v1;

Das war’s. Eine Zeile. X25519MLKEM768 steht als bevorzugte Gruppe ganz vorne. Dahinter folgen die klassischen Kurven als Fallback für Clients, die noch kein ML-KEM sprechen. Die Reihenfolge ist die Präferenz.

Wer die Direktive lieber pro vHost setzen möchte, statt global, kann das natürlich auch tun. Ich bevorzuge eine zentrale TLS-Datei, weil ich sonst bei jedem TLS-Update zwanzig Configs anfassen müsste.

Zusätzlich habe ich die TLS 1.3 Cipher Suites explizit gesetzt:

ssl_conf_command  Ciphersuites TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256;
ssl_conf_command  Options PrioritizeChaCha;

ChaCha20 als erste Wahl, weil es auf Clients ohne AES-NI (ältere Smartphones, ARM-Geräte) deutlich schneller ist. Auf Servern mit AES-NI ist der Unterschied minimal. PrioritizeChaCha sorgt dafür, dass der Server ChaCha20 bevorzugt, wenn der Client es an erster Stelle anbietet.

Die komplette TLS-Konfiguration sieht bei mir so aus:

# Protokolle
ssl_protocols              TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers  on;

# Key-Exchange-Gruppen (Reihenfolge = Präferenz)
ssl_ecdh_curve             X25519MLKEM768:X25519:secp384r1:prime256v1;

# TLS 1.3 Cipher Suites
ssl_conf_command           Ciphersuites TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256;
ssl_conf_command           Options PrioritizeChaCha;

# TLS 1.2 Cipher Suites (nur ECDSA, kein RSA)
ssl_ciphers                ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256;

# Session Handling
ssl_session_cache          shared:SSL:50m;
ssl_session_timeout        1d;
ssl_session_tickets        off;

# OCSP Stapling
ssl_stapling               on;
ssl_stapling_verify        on;

# HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

Danach die Konfiguration testen und Nginx neu laden:

# nginx -t
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful
# service nginx reload

Überprüfen

Funktioniert es? Mit openssl s_client lässt sich das schnell prüfen:

$ openssl s_client -connect www.kernel-error.de:443 \
    -groups X25519MLKEM768 -brief </dev/null 2>&1 | grep -E 'Protocol|group|Cipher'
Protocol version: TLSv1.3
Ciphersuite: TLS_CHACHA20_POLY1305_SHA256
Negotiated TLS1.3 group: X25519MLKEM768

TLSv1.3 mit X25519MLKEM768. Läuft. Der hybride Post-Quantum-Schlüsselaustausch ist aktiv.

Und was passiert, wenn ein Client kein ML-KEM kann?

$ openssl s_client -connect www.kernel-error.de:443 \
    -groups X25519 -brief </dev/null 2>&1 | grep -E 'Protocol|group|Cipher'
Protocol version: TLSv1.3
Ciphersuite: TLS_CHACHA20_POLY1305_SHA256

Sauberer Fallback auf X25519. Kein Fehler, keine Unterbrechung. Der Client bekommt einfach die stärkste Gruppe, die beide Seiten gemeinsam haben.

Browser-Support

Das Schöne bei HTTPS im Vergleich zu E-Mail oder SSH: Die Browser haben Post-Quantum-Kryptografie teilweise schon vor den Servern implementiert. Chrome verwendet seit Version 124 (April 2024) X25519MLKEM768 als bevorzugte Gruppe. Firefox seit Version 124 ebenfalls. Safari seit macOS 15 Sequoia und iOS 18. Edge zieht mit Chrome mit.

Wenn ihr also Nginx mit X25519MLKEM768 konfiguriert, profitieren die meisten eurer Besucher sofort davon. Ohne dass sie irgendetwas tun müssen. Das ist bei SSH und E-Mail anders, da sind die Clients noch nicht so weit. Womit sich auch erklärt, warum das Thema bei Webservern mehr Aufmerksamkeit bekommt.

HTTP/3 und QUIC

Wer wie ich HTTP/3 (QUIC) aktiviert hat, bekommt PQC dort automatisch mit. QUIC verwendet TLS 1.3 als Transportverschlüsselung und nutzt dieselbe ssl_ecdh_curve Konfiguration. Kein zusätzlicher Konfigurationsaufwand.

listen [::]:443 quic reuseport;
listen 443 quic reuseport;

Wer QUIC noch nicht nutzt: Der Overhead ist minimal, der Vorteil bei mobilen Verbindungen deutlich spürbar. Mehr dazu in meinem HTTP/3 Beitrag.

Davon abgesehen, wie ist das eigentlich mit Microsoft? Cloudflare macht ja schon knapp zu 66% PQC im HTTP Traffic. Es ist 2026, fragen wir doch mal eine AI: claude „Sag mal, wie ist das eigentlich bei Microsoft Produkten, wie Exchange oder IIS im Bezug auf PQC? Wird das schon unterstützt oder kommt das bald?“

● Gute Frage — Microsoft ist da tatsächlich in einer interessanten Position:                                                                                                                                                                                                                                                  
                 
  Windows / Schannel (TLS-Stack)                                                                                                                                                                                                                                                                                              
                  
  - Windows 11 24H2 / Windows Server 2025 haben experimentelle Unterstützung für ML-KEM (X25519MLKEM768) in TLS 1.3 bekommen — allerdings zunächst nur als Preview/Feature-Flag, nicht standardmäßig aktiv.                                                                                                                   
  - Der zugrunde liegende TLS-Stack (Schannel) muss das können, damit IIS und Exchange davon profitieren.                                                                                                                                                                                                                     

  IIS

  - IIS nutzt Schannel direkt. Sobald Schannel auf dem jeweiligen Windows Server PQC-Key-Exchange unterstützt und es aktiviert ist, kann IIS das theoretisch sofort.
  - Stand meines Wissens (Mai 2025): Noch kein produktiver PQC-Support in IIS. Die Preview in Windows Server 2025 ist eher für Tests gedacht.

  Exchange

  - Exchange Online (Microsoft 365): Microsoft hat angekündigt, PQC schrittweise in ihre Cloud-Dienste zu integrieren. Für TLS-Verbindungen zwischen Microsoft-Datacentern wurde bereits mit PQC experimentiert.
  - Exchange On-Premises: Hängt komplett am darunterliegenden Windows Server / Schannel. Solange Schannel kein produktives PQC kann, kann Exchange es auch nicht.

  Microsofts Roadmap

  - Microsoft hat ein "Quantum-Safe Program" und arbeitet aktiv an PQC-Integration in Windows, Azure und M365.
  - Die Priorität liegt auf TLS 1.3 mit ML-KEM (hybrid, also X25519MLKEM768.
  - Einen festen Zeitplan für "PQC by default" in Exchange/IIS gibt es meines Wissens noch nicht.

  Fazit

  Microsoft ist hintendran verglichen mit dem Open-Source-Stack. OpenSSL 3.5+ / LibreSSL / BoringSSL haben PQC-Support bereits produktionsreif. Bei Microsoft wartet man darauf, dass Schannel das nativ und stabil liefert — und bis das
  in Exchange On-Prem ankommt, dauert es erfahrungsgemäß nochmal länger.

  Für Exchange Online dürfte Microsoft das irgendwann einfach serverseitig einschalten, ohne dass Admins etwas tun müssen. Für On-Prem heißt es: warten auf den nächsten Windows Server Release, der PQC in Schannel GA (General Availability) hat.

  Caveat: Mein Wissensstand ist Mai 2025 — es kann sein, dass Microsoft seitdem konkretere Termine genannt hat. Wäre ggf. einen Blick in den Microsoft Security Blog wert.

Klingt gut. Ich muss ja zugeben, dass ich, seit ich nicht mehr mit Martin Peuker bei Items arbeite, irgendwie komplett von der Microsoft-Welt abgekoppelt bin. Oh, ist das jetzt korrekt von der AI? Keine Ahnung, „klingt“ richtig.

Was das nicht leistet

Den Absatz kennt ihr inzwischen aus den anderen Beiträgen, aber er gehört dazu: Wir sichern hier den Schlüsselaustausch ab, nicht die Authentifizierung. Die TLS-Zertifikate nutzen weiterhin klassische Algorithmen (in meinem Fall ECDSA P-384). Für Post-Quantum-Signaturen bräuchte man ML-DSA (ehemals CRYSTALS-Dilithium) in den Zertifikaten, aber keine öffentliche CA stellt solche Zertifikate aus. Das wird kommen, aber noch nicht heute.

In der Praxis heißt das: Ein Angreifer mit Quantencomputer könnte die Serverauthentifizierung angreifen, müsste das aber in Echtzeit tun. „Store now, decrypt later“ greift dort nicht. Der Schlüsselaustausch und damit die Vertraulichkeit eurer Daten ist durch X25519MLKEM768 geschützt. Auch in Zukunft.

Fazit

Eine Zeile in der Nginx-Konfiguration, ein Reload, fertig. Euer Webserver verhandelt danach mit jedem modernen Browser einen quantensicheren Schlüsselaustausch. Vollständig abwärtskompatibel für ältere Clients. Kein Risiko, kein Aufwand, kein Nachteil.

Die eigentliche Hürde ist nicht Nginx, sondern die OpenSSL-Version auf eurem System. Wer FreeBSD 15 oder ein System mit OpenSSL 3.5+ hat, kann sofort loslegen. Alle anderen müssen auf ihre Distribution warten oder selbst bauen.

Damit habe ich jetzt SSH, E-Mail und Webserver mit Post-Quantum-Kryptografie abgedeckt. Fehlt eigentlich nur noch DNS. Aber DoH und DoT laufen ja auch über TLS … *grübel*

Update: Inzwischen habe ich HTTPS RR und SVCB Records für alle Dienste deployt. Damit wissen Clients schon beim DNS-Lookup, dass HTTP/3 und QUIC verfügbar sind.

Update 2: Wer wissen will, ob die eigenen Besucher tatsächlich PQC nutzen: Nginx kann die ausgehandelte Key-Exchange-Gruppe loggen. Die Variable $ssl_curve zeigt pro Verbindung, ob X25519MLKEM768, klassisches X25519 oder etwas anderes verhandelt wurde. Einfach ins bestehende Log-Format einfügen, irgendwo nach $ssl_cipher:

log_format combined_pqc
  '$remote_addr - $remote_user [$time_local] '
  '"$request" $status $body_bytes_sent '
  '"$http_referer" "$http_user_agent" '
  '$ssl_protocol $ssl_cipher $ssl_curve';

Im Log sieht das dann so aus. Ein Chrome mit PQC-Support:

203.0.113.42 - - [07/Apr/2026:13:22:36 +0200] "GET / HTTP/2.0" 200 56687 "-" "Mozilla/5.0 [...] Chrome/146.0.0.0" TLSv1.3 TLS_CHACHA20_POLY1305_SHA256 X25519MLKEM768

Und ein älterer Client ohne PQC:

198.51.100.7 - - [07/Apr/2026:14:05:11 +0200] "GET / HTTP/2.0" 200 56687 "-" "Mozilla/5.0 [...] Chrome/109.0.0.0" TLSv1.3 TLS_AES_256_GCM_SHA384 X25519

Der Unterschied steht ganz am Ende: X25519MLKEM768 vs. X25519. Ein Reload, ein paar Wochen sammeln, und ihr habt echte Zahlen statt Theorie. Ich werde die Ergebnisse in einem eigenen Beitrag auswerten, sobald genug Daten zusammengekommen sind.

Wie immer: Bei Fragen, fragen.

NEXT Biometrics NB-2020-U Fingerabdruckleser unter Linux zum Laufen gebracht

In meinem Fujitsu Notebook steckt ein Fingerabdruckleser. Ein NEXT Biometrics NB-2020-U, USB ID 298d:2020. Unter Windows funktioniert er, unter Linux nicht. Kein Treiber, kein Support, nichts. Das Gerät taucht in lsusb auf, wird aber von keinem Treiber erkannt. Im libfprint Wiki steht es auf der Liste der nicht unterstützten Geräte. Dort steht es schon eine Weile.

Das hat mich gestört.

Picture of NB-2020-U

libfprint kennt den NB-1010-U. Das ist ein externer USB Fingerabdruckleser von NEXT Biometrics, der seit einiger Zeit einen funktionierenden Treiber hat. Der NB-2020-U ist die eingebettete Variante desselben Sensors, gedacht für den Einbau in Notebooks. Wenn man sich Teardown Reports ansieht, etwa von System Plus Consulting oder Yole Group, dann stellt man fest: Beide Geräte verwenden den identischen Sensor Die. Gleiche Technik, anderes Gehäuse.

Das war der erste Anhaltspunkt. Wenn die Hardware gleich ist, sollte auch das USB Protokoll gleich sein. Und wenn das Protokoll gleich ist, sollte der vorhandene Treiber funktionieren.

Bevor ich aber einfach auf Verdacht losprogrammiert habe, wollte ich es absichern. Ich habe NEXT Biometrics direkt angeschrieben. Kevin Hung, Director FAE bei NEXT Biometrics, hatte mir bereits 2022 auf eine Anfrage zu Linux Treibern geantwortet. Damals war sein Vorschlag, über Fujitsu zu gehen. Das führte ins Leere. Diesmal habe ich konkret angeboten, selbst einen libfprint Treiber zu schreiben, und um das SDK gebeten.

Kevin hat mir daraufhin das NBBiometrics ANF SDK 3.0.0.1384 zugeschickt. Ein komplettes SDK mit Headern, Bibliotheken, Beispielcode und Dokumentation. Das war sehr hilfreich, denn die Header bestätigen einiges. Das SDK nutzt eine einzige Shared Library libNBBiometrics.so für alle Gerätetypen. Der NB-1010-U hat den internen Gerätetyp 200, der NB-2020-U den Typ 202. Beide verwenden dasselbe Scanformat: 180×256 Pixel bei 385 DPI. Die USB Vendor ID ist bei beiden 0x298d, nur die Product ID unterscheidet sich: 0x1010 beim einen, 0x2020 beim anderen.

Wichtig: Das SDK ist proprietär. Für den eigentlichen Treiber habe ich keinen Code daraus verwendet. libfprint akzeptiert nur sauberen, eigenständig entwickelten Code. Das SDK diente ausschließlich als Referenz, um die Protokollkompatibilität zu bestätigen.

Also habe ich es einfach ausprobiert. Den bestehenden nb1010.c Treiber genommen, die USB Product ID 0x2020 zur id_table hinzugefügt und gebaut. Dann auf dem Fujitsu Notebook getestet.

Es funktionierte sofort.

Geräteerkennung, USB Interface Claim, die State Machine für die Fingererkennung, alles lief auf Anhieb. fprintd-enroll hat Fingerabdrücke aufgenommen, fprintd-verify hat sie korrekt verifiziert. Der bestehende Treibercode brauchte keinerlei Anpassungen. Null. Nur die PID in der Tabelle und den Gerätenamen.

Ein Blick auf die USB Deskriptoren bestätigt das Bild. Der NB-2020-U hat exakt dasselbe Endpoint Layout wie der NB-1010-U: Bulk OUT auf Endpoint 0x02, Bulk IN auf Endpoint 0x83. Dazu kommt ein Interrupt Endpoint auf 0x81, den der Treiber nicht verwendet. Die Kommunikation läuft identisch ab.

Der Patch selbst ist entsprechend klein. Drei Dateien, drei Zeilen rein, drei Zeilen raus:

  1. libfprint/drivers/nb1010.c: Die neue PID 0x2020 wird in die id_table eingetragen und der full_name auf "NextBiometrics NB-1010-U/NB-2020-U" erweitert.
  2. data/autosuspend.hwdb: Der Eintrag 298d:2020 wird von der Liste der nicht unterstützten Geräte in die Sektion des nb1010 Treibers verschoben.
  3. libfprint/fprint-list-udev-hwdb.c: Der Eintrag wird aus der Allowlist der nicht unterstützten Geräte entfernt, da er jetzt vom Treiber abgedeckt wird.

Den Merge Request habe ich bei libfprint upstream eingereicht: MR !569. Die CI Pipeline läuft durch, alle 124 Tests bestehen. Jetzt heißt es warten auf das Review durch die Maintainer.

Für alle, die denselben Fingerabdruckleser in ihrem Notebook haben: Sobald der Patch gemergt und in einer neuen libfprint Version enthalten ist, funktioniert der Sensor out of the box. Enrollment und Verifikation über fprintd laufen sauber. Wer nicht warten möchte, kann den Patch auch jetzt schon selbst auf ein aktuelles libfprint anwenden.

Im selben Fujitsu Notebook meiner Tochter steckt ein NB-2033-U, ein weiterer Fingerabdruckleser aus der gleichen Familie. Der verwendet allerdings ein komplett anderes Protokoll und ließ sich nicht einfach mit dem nb1010 Treiber ansprechen. Den habe ich per Reverse Engineering geknackt.

Post-Quantum TLS für E-Mail — Postfix und Dovecot mit X25519MLKEM768 auf FreeBSD 15

Visualisierung hybrider Post-Quantum-TLS-Verschlüsselung für E-Mail mit X25519MLKEM768 (ML-KEM-768 + X25519) auf Postfix und Dovecot unter FreeBSD 15

Siehe auch: Post-Quantum TLS für Nginx — X25519MLKEM768 auf FreeBSD 15 konfigurieren

Nachdem ich im letzten Beitrag OpenSSH mit hybriden Post-Quantum-Algorithmen abgesichert habe, lag die Frage nahe: Was ist eigentlich mit E-Mail? Mein FreeBSD 15 liefert Postfix 3.10.6, Dovecot 2.3.21.1 und OpenSSL 3.5.4 – und genau diese Kombination bringt alles mit, was man für quantensichere Verschlüsselung im Mailverkehr braucht. Ohne zusätzliche Pakete, ohne Patches, ohne Gefrickel.

Warum überhaupt PQC für E-Mail?

Das „Store now, decrypt later„-Szenario, das ich beim SSH-Beitrag angesprochen habe, trifft auf E-Mail mindestens genauso zu. E-Mails werden über SMTP zwischen Servern transportiert – und dieser Transport ist grundsätzlich abfangbar. Wer heute TLS-verschlüsselten Mailverkehr mitschneidet und archiviert, könnte diesen in einigen Jahren mit einem ausreichend leistungsfähigen Quantencomputer entschlüsseln. Zumindest theoretisch.

Heißt das, morgen liest jemand eure Mails? Nein. Aber wenn ihr vertrauliche Kommunikation betreibt und die heute eingesetzte Kryptografie in zehn Jahren noch standhalten soll, ist jetzt der richtige Zeitpunkt zum Handeln. Zumal der Aufwand (wie ihr gleich seht) überschaubar ist.

Was steckt hinter X25519MLKEM768?

Kurz zur Einordnung: ML-KEM (ehemals CRYSTALS-Kyber) ist der vom NIST im August 2024 standardisierte Post-Quantum-Algorithmus für den Schlüsselaustausch (FIPS 203). X25519MLKEM768 ist ein sogenannter Hybrid-Algorithmus – er kombiniert das klassische X25519 (Curve25519 ECDH) mit ML-KEM-768 zu einem gemeinsamen Schlüssel.

Der Clou dabei: Selbst wenn ML-KEM irgendwann gebrochen werden sollte, bleibt die klassische X25519-Komponente intakt. Und umgekehrt. Man muss also nicht darauf vertrauen, dass der neue Algorithmus auch wirklich hält – man bekommt das Beste aus beiden Welten.

Wer Firefox nutzt, hat das übrigens vermutlich schon in Aktion gesehen: Seit Firefox 124 wird bei TLS 1.3 standardmäßig X25519MLKEM768 für den Schlüsselaustausch verwendet. Schaut mal in die Verbindungsdetails einer HTTPS-Seite – die Chancen stehen gut, dass dort bereits ein hybrider PQC-Schlüsselaustausch stattfindet. Also, wenn der Server das anbietet, wie dieser hier *zwinker*.

Voraussetzungen prüfen

Bevor ihr konfiguriert, solltet ihr sicherstellen, dass euer OpenSSL ML-KEM überhaupt kann. Auf meinem FreeBSD 15:

$ openssl version
OpenSSL 3.5.4 30 Sep 2025 (Library: OpenSSL 3.5.4 30 Sep 2025)

Und dann die entscheidende Frage – kennt OpenSSL die benötigten KEM-Algorithmen?

$ openssl list -kem-algorithms | grep -i mlkem
  ML-KEM-512
  ML-KEM-768
  ML-KEM-1024
  X25519MLKEM768
  SecP256r1MLKEM768

Wenn X25519MLKEM768 in der Liste auftaucht, seid ihr startklar. Das ist bei OpenSSL ab Version 3.5 der Fall – der ML-KEM-Support ist im Default-Provider enthalten, es wird kein zusätzlicher OQS-Provider und kein liboqs benötigt.

Noch ein Check – sind die Algorithmen auch als TLS-Gruppen verfügbar?

$ openssl list -tls-groups | grep -i mlkem
  X25519MLKEM768
  SecP256r1MLKEM768
  SecP384r1MLKEM1024

Perfekt. Weiter geht’s.

Postfix konfigurieren

Postfix steuert die verwendeten TLS-Gruppen für den Schlüsselaustausch über den Parameter tls_eecdh_auto_curves. Dieser gilt sowohl für eingehende (smtpd) als auch für ausgehende (smtp) Verbindungen.

Vorher:

tls_eecdh_auto_curves = X25519, prime256v1, secp384r1

Nachher:

tls_eecdh_auto_curves = X25519MLKEM768, X25519, prime256v1, secp384r1

Das war’s. Eine Zeile. X25519MLKEM768 wird als bevorzugte Gruppe an den Anfang gestellt, die klassischen Kurven bleiben als Fallback erhalten. Clients die kein ML-KEM beherrschen, verhandeln einfach X25519 oder prime256v1 – die Abwärtskompatibilität bleibt also vollständig gewahrt.

Die Änderung setzt ihr entweder direkt in /usr/local/etc/postfix/main.cf oder über:

# postconf "tls_eecdh_auto_curves = X25519MLKEM768, X25519, prime256v1, secp384r1"
# postfix reload

Wichtig: Dieser Parameter beeinflusst alle Postfix-Dienste – SMTP (Port 25), Submission (Port 587) und SMTPS (Port 465). Ihr müsst also nicht jeden Port einzeln konfigurieren.

Update 01.04.2026: Die oben gezeigte globale Methode über main.cf hat einen Haken, den ich erst später auf der Postfix-Users Mailingliste realisiert habe. Die korrigierte Konfiguration mit getrennten Einstellungen für Inbound und Outbound findet ihr weiter unten im Nachtrag.

Dovecot konfigurieren

Dovecot verwendet den Parameter ssl_curve_list um die TLS-Gruppen für IMAP-Verbindungen festzulegen. Standardmäßig ist dieser leer, was bedeutet, dass OpenSSL seine eigenen Defaults verwendet. Das kann funktionieren, muss aber nicht.

In /usr/local/etc/dovecot/conf.d/10-ssl.conf:

ssl_curve_list = X25519MLKEM768:X25519:prime256v1:secp384r1

Achtung: Dovecot verwendet Doppelpunkte als Trennzeichen (OpenSSL-Syntax), Postfix verwendet Kommas. Nicht verwechseln. Ja, passiert mir oft.

Danach:

# doveadm reload

Überprüfen

Jetzt wird’s spannend. Funktioniert es tatsächlich? Zum Testen verwende ich openssl s_client direkt auf dem Server; denn euer lokales Linux oder macOS hat möglicherweise noch kein OpenSSL 3.5 mit ML-KEM-Support. Mein Linux Mint 22.3 hat es leider noch nicht *schnief*

SMTP (Port 25, STARTTLS):

$ openssl s_client -connect smtp.kernel-error.de:25 -starttls smtp \
    -groups X25519MLKEM768 -brief </dev/null 2>&1 | grep -E 'Protocol|group'
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768

SMTPS (Port 465):

$ openssl s_client -connect smtp.kernel-error.de:465 \
    -groups X25519MLKEM768 -brief </dev/null 2>&1 | grep -E 'Protocol|group'
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768

Submission (Port 587, STARTTLS):

$ openssl s_client -connect smtp.kernel-error.de:587 -starttls smtp \
    -groups X25519MLKEM768 -brief </dev/null 2>&1 | grep -E 'Protocol|group'
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768

IMAPS (Port 993):

$ openssl s_client -connect imap.kernel-error.de:993 \
    -groups X25519MLKEM768 -brief </dev/null 2>&1 | grep -E 'Protocol|group'
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768

Alle vier Ports verhandeln TLSv1.3 mit X25519MLKEM768. Die hybride Post-Quantum-Verschlüsselung ist aktiv.

Wenn ihr testen wollt, was passiert wenn ein Client kein ML-KEM unterstützt:

$ openssl s_client -connect imap.kernel-error.de:465 \
    -groups X25519 -brief </dev/null 2>&1 | grep -E 'Protocol|group'
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519

Fallback auf X25519 – funktioniert sauber.

Was das nicht leistet

Wie schon beim SSH-Beitrag muss ich auch hier einschränken: Wir sichern damit den Schlüsselaustausch ab, nicht die Authentifizierung. Die TLS-Zertifikate verwenden weiterhin klassische Algorithmen (RSA, ECDSA). Für Post-Quantum-Signaturen in Zertifikaten bräuchte man ML-DSA (ehemals CRYSTALS-Dilithium) – und obwohl OpenSSL 3.5 das theoretisch unterstützt, gibt es Stand heute keine öffentliche Zertifizierungsstelle, die ML-DSA-Zertifikate ausstellt. Das wird kommen, ist aber noch Zukunftsmusik. Hey, wie ECDSA bei S/MIME (oder ist das schon anders?).

Für die Praxis bedeutet das: Ein Angreifer mit einem Quantencomputer könnte theoretisch die Serverauthentifizierung angreifen (ECDSA/RSA brechen), müsste das aber in Echtzeit tun – hier greift „store now, decrypt later“ nicht, weil eine gefälschte Authentifizierung nur im Moment der Verbindung nützt. Der Schlüsselaustausch hingegen – und damit die eigentliche Vertraulichkeit der transportierten E-Mails – ist durch X25519MLKEM768 auch gegen zukünftige Quantenangriffe geschützt.

Nachtrag (01.04.2026): Inbound und Outbound trennen

Ich muss die Postfix-Konfiguration oben korrigieren. Der Parameter tls_eecdh_auto_curves gilt für SMTP-Client und SMTP-Server gleichzeitig. Steht X25519MLKEM768 an erster Stelle, wird der PQC Key-Share direkt im initialen ClientHello mitgeschickt. Das bläht den ClientHello von rund 400 auf über 1400 Bytes auf.

Klingt erstmal harmlos. Ist es aber nicht. Manche Zielserver kommen mit dem übergroßen ClientHello nicht klar, der TLS-Handshake scheitert. Bei smtp_tls_security_level = may fällt Postfix dann stillschweigend auf Plaintext zurück. Dasselbe passiert bei dane ohne TLSA-Records, also bei der Mehrheit aller Domains da draußen. Eure Mails gehen raus, aber unverschlüsselt. Super.

Das Problem ist, dass hier zwei unterschiedliche Policies in einem Parameter stecken. Inbound will man PQC bevorzugen, weil man selbst kontrolliert was der Server akzeptiert. Outbound will man Kompatibilität priorisieren, weil man nicht weiß was auf der anderen Seite steht. Die gehören nicht in einen globalen Parameter.

Die Lösung: tls_eecdh_auto_curves nicht mehr global in main.cf setzen, sondern per master.cf pro Dienst überschreiben.

Server-Seite (Inbound) – PQC bevorzugen, an allen smtpd-Listenern:

smtp      inet  n       -       n       -       -       smtpd
  -o tls_eecdh_auto_curves=X25519MLKEM768,X25519,prime256v1,secp384r1

submission inet n       -       n       -       -       smtpd
  -o tls_eecdh_auto_curves=X25519MLKEM768,X25519,prime256v1,secp384r1

smtps     inet  n       -       n       -       -       smtpd
  -o tls_eecdh_auto_curves=X25519MLKEM768,X25519,prime256v1,secp384r1

Client-Seite (Outbound) – kleiner ClientHello, PQC nur via HelloRetryRequest:

smtp      unix  -       -       n       -       -       smtp
  -o tls_eecdh_auto_curves=X25519,X25519MLKEM768,prime256v1,secp384r1

Der Trick: Outbound steht X25519 an erster Stelle. Der initiale ClientHello bleibt damit klein. X25519MLKEM768 steht trotzdem in den supported_groups und wird verhandelt, wenn der Zielserver per HelloRetryRequest nachzieht. Inbound bekommen moderne Clients dagegen sofort PQC.

Dovecot ist davon nicht betroffen. Da gibt es nur die Server-Seite, ssl_curve_list bleibt wie oben beschrieben.

Noch ein Wort zum Postfix-Default: postconf -d zeigt auf meinem 3.10.6 kein X25519MLKEM768 im Default. Die postconf(5)-Doku beschreibt zwar für neuere Builds ein Delayed-Key-Share-Verhalten, aber was die Doku beschreibt und was ein konkreter Build tut, können zwei verschiedene Dinge sein. Deshalb die explizite Trennung per master.cf. Danke an die Postfix-Users Mailingliste für die Diskussion, die mich auf dieses Problem aufmerksam gemacht hat und selbstverständlich an den Kommentierenden!

Zwei Zeilen Konfiguration, ein Reload pro Dienst – und euer Mailserver verhandelt quantensichere Verschlüsselung. Okay, es sind jetzt ein paar mehr Zeilen als ursprünglich versprochen. Aber die Trennung zwischen Inbound und Outbound ist es wert, denn blind auf Kompatibilität aller Zielserver zu hoffen ist keine Strategie.

Viel Spaß beim Nachbauen – und wie immer: bei Fragen, fragen.

BIND auf FreeBSD: DoT & DoH einrichten mit Views, IP‑Trennung und Testplan für IPv4/IPv6.

Wofür braucht man noch gleich DoT oder DoH?

Nun, wenn du eine Internetadresse eingibst, muss dein Gerät zuerst herausfinden, zu welchem Server diese Adresse gehört. Diese Nachfragen heißen DNS. Lange Zeit liefen sie unverschlüsselt durchs Netz, vergleichbar mit einer Postkarte. Jeder, der den Datenverkehr sehen konnte, wusste dadurch sehr genau, welche Webseiten aufgerufen werden, und konnte die Antworten sogar manipulieren.

Beitragsgrafik zu BIND 9.20 auf FreeBSD 15: schematische Trennung von autoritativem DNS und rekursivem Resolver. Links ein Authoritative-DNS-Server mit deaktivierter Rekursion und blockiertem UDP/53, rechts ein Resolver, der ausschließlich DNS over TLS (Port 853) und DNS over HTTPS (Port 443) anbietet. In der Mitte ein Schild mit DoT/DoH-Symbolen, Pfeile zeigen verschlüsselten DNS-Verkehr. Fokus auf Sicherheits- und Rollen-Trennung.

DoT und DoH lösen genau dieses Problem. Beide sorgen dafür, dass diese DNS-Nachfragen verschlüsselt übertragen werden. Bei DNS over TLS, kurz DoT, wird die Anfrage in eine eigene sichere Verbindung gepackt. Außenstehende sehen noch, dass eine DNS-Anfrage stattfindet, aber nicht mehr, welche Webseite gemeint ist. Bei DNS over HTTPS, kurz DoH, wird dieselbe Anfrage zusätzlich im normalen Webseitenverkehr versteckt. Von außen sieht sie aus wie ein ganz gewöhnlicher Zugriff auf eine Website.

Der Zweck von beiden ist also derselbe: Schutz der Privatsphäre und Schutz vor Manipulation. Der Unterschied liegt darin, wie sichtbar diese Nachfragen noch sind. DoT ist transparent und gut kontrollierbar, DoH ist unauffälliger, kann dafür aber lokale Regeln und Schutzmechanismen umgehen.

Mal angenommen, du möchtest eine gewisse Webseite aufrufen. Dann geht der Client los und holt über einen DNS-Server die IP-Adressen vom Server. Dies kann man mitlesen und ggf. verändern. Mitlesen sagt dem Mitlesenden, wo du dich so im Internet herumtreibst. Verändern könnte man als Angriff nutzen, indem man dir einfach eine andere Webseite vorsetzt, während du versuchst, dich in deinen Mailaccount einzuloggen. Beides wird durch DoH und DoT deutlich erschwert.

Dann soll es ja Netzwerke geben, in welchen dir ein bestimmter DNS-Server aufgezwungen wird, weil dieser DNS-Server nach Werbung oder ungewollten Inhalten filtert. Damit dies nun ebenfalls nicht einfach umgangen werden kann, blockt man den Zugriff aus dem Netzwerk einfach auf die Ports, welche sonst für eine DNS-Abfrage benutzt werden (TCP/53, UDP/53, TCP/853). Da kommt nun DoH ins Spiel, denn das läuft auf dem ganz normalen HTTPS-Port TCP/443. Blockt man den, kann keiner mehr auf Webseiten zugreifen (ok, unverschlüsselt, aber hey, das macht doch keiner mehr, oder?).

Die Zeit ging weiter – BIND auch.
Meine älteren Artikel zu DoT/DoH waren für ihren Zeitpunkt korrekt, aber inzwischen hat sich an zwei Stellen richtig was getan:

  1. BIND spricht DoT/DoH nativ (kein Stunnel-/Proxy-Zirkus mehr nötig – außer du willst bewusst terminieren/filtern).
  2. „Authoritative + Public Resolver auf derselben Kiste“ ist ohne klare Trennung schnell ein Sicherheitsproblem (Open-Resolver/Reflection-Missbrauch lässt grüßen).

Darum gibt’s hier das Update:

  • ns1.kernel-error.de: nur autoritativ auf UDP/TCP 53 (Zonen, DNSSEC wie gehabt)
  • dns.kernel-error.de: Public Resolver nur auf DoT 853/TCP und DoH 443/TCP (rekursiv, DNSSEC-validierend)
  • Trennung über zusätzliche IPs + Views. Ergebnis: Authoritative bleibt „stumm rekursiv“, Resolver ist nur über TLS/HTTPS erreichbar.

Zielbild

Uff, ich muss zugeben, diesen Beitrag schon VIEL zu lange als Draft zu haben. Es ist einfach viel zu schreiben, bschreiben und mir fehlte die Zeit. Aber das kennt ihr ja. OK… das Zielbild, was soll es werden?

Was soll am Ende gelten:

  • Port 53 auf Authoritative-IP(s):
    • beantwortet nur meine autoritativen Zonen
    • keine Rekursion → REFUSED bei google.com
  • DoT/DoH auf separaten Resolver-IP(s):
    • rekursiv für „das ganze Internet“
    • DNSSEC-Validation aktiv
    • kein offenes UDP/53 → weniger Angriffsfläche für Reflection/Amplification

Warum das wichtig ist:
Ein „Public Resolver“ ist per Definition attraktiv für Missbrauch. Der Klassiker ist DNS-Amplification über UDP/53. Wenn man Rekursion auf 53 offen hat, ist man sehr schnell Teil fremder Probleme. DoT/DoH sind TCP-basiert – das ist schon mal deutlich unattraktiver für Reflection. (Nicht „unmöglich“, aber praktisch viel weniger lohnend.)

Warum „Views“ – und warum zusätzliche IPs?

1) Views – weil Policy pro Anfrage gelten muss

Wir wollen auf derselben named-Instanz zwei sehr unterschiedliche Rollen:

  • Authoritative: recursion no;
  • Resolver: recursion yes; + Root-Hints/Cache

Das muss pro eingehender Anfrage entschieden werden. Dafür sind Views da.

2) Also: Trennung über Ziel-IP (match-destinations)

Wenn wir DoH/DoT auf andere IPs legen, kann die View anhand der Zieladresse entscheiden:

  • Anfrage geht an 93.177.67.26 / 2a03:4000:38:20e::53auth-View
  • Anfrage geht an 37.120.183.220 / 2a03:4000:38:20e::853resolver-View

Und genau deshalb brauchen wir:

  • zusätzliche IPs (damit die Rollen sauber getrennt sind)
  • separaten FQDN dns.kernel-error.de (damit Clients überhaupt sinnvoll DoT/DoH nutzen können – und für TLS/SNI/Cert-Match)

Wenn du also grade ein ripe from ausfüllst und angeben musst, warum da eine weitere IPv4 Adresse „verbrannt“ werden soll, hast du nun eine gute Antwort.

BIND-Config

Ich beschreibe hier nur die Teile, die für das Rollen-Split relevant sind. Die Zonendateien/Slaves bleiben wie sie sind.

1) /usr/local/etc/namedb/named.conf – Views

Wichtig: Sobald wir view {} nutzen, müssen alle Zonen in Views liegen, sonst bricht named-checkconf ab. Das ist kein „Feature“, das ist BIND. Leicht nervig, vor allem wenn man nun viel in seinem Setup umschreiben muss. Aber ich eigentlich schon mal erwähnt, dass ich auf der Arbeit mal einen, nennen wir es mal View Ersatz, für powerdns gesehen habe? Da hat tatsächlich jemand mit einer Cisco ASA in die DNS Pakete geschaut und je nachdem welche quelle angefragt hat, wurde dann durch die ASA eine neue Adresse in die DNS Pakete geschrieben. Furchtbar! Richtig schlimm. Bis man so etwas findet, wenn man es nicht weiß. DNSsec geht kaputt und aaahhhhhhaaaaaahhhhh. Egal, mein PTBS kickt da grade. Öhm wo waren wir? Genau…

Beispiel:

include "/usr/local/etc/namedb/named.conf.options";

view "auth" {
    match-clients { any; };
    match-destinations { 93.177.67.26; 2a03:4000:38:20e::53; };

    recursion no;
    allow-recursion { none; };
    allow-query-cache { none; };
    allow-query { any; };

    include "/usr/local/etc/namedb/named.conf.default-zones";
    include "/usr/local/etc/namedb/named.conf.master";
    include "/usr/local/etc/namedb/named.conf.slave";
};

view "resolver" {
    match-clients { any; };
    match-destinations { 37.120.183.220; 2a03:4000:38:20e::853; 127.0.0.1; ::1; };

    recursion yes;
    allow-recursion { any; };
    allow-query-cache { any; };
    allow-query { any; };

    zone "." { type hint; file "/usr/local/etc/namedb/named.root"; };
};

Warum Root-Hints nur im Resolver-View?
Weil nur dieser View rekursiv arbeiten soll. Ohne Root-Hints ist Rekursion tot; dat wolln wa so!

2) /usr/local/etc/namedb/named.conf.options – Listener-Trennung + DoH/DoT

Der „Aha-Moment“ hier: Wir trennen nicht nur per View, sondern auch per listen-on.
Damit bindet named die Ports wirklich nur auf den gewünschten IPs.

Authoritative (nur 53):

listen-on { 93.177.67.26; 127.0.0.1; };
listen-on-v6 { 2a03:4000:38:20e::53; ::1; };

DoT auf Resolver-IPs (+ Loopback für lokale Tests):

listen-on port 853 tls local-tls { 37.120.183.220; 127.0.0.1; };
listen-on-v6 port 853 tls local-tls { 2a03:4000:38:20e::853; ::1; };

DoH auf Resolver-IPs (+ Loopback):
BIND 9.18+ kann DoH nativ, Endpoint typischerweise /dns-query

http doh-local {
    endpoints { "/dns-query"; };
    listener-clients 1000;
    streams-per-connection 256;
};

listen-on port 443 tls local-tls http doh-local { 37.120.183.220; 127.0.0.1; };
listen-on-v6 port 443 tls local-tls http doh-local { 2a03:4000:38:20e::853; ::1; };

TLS-Block (DoT/DoH):

tls local-tls {
    cert-file "/usr/local/etc/nginx/ssl/wild.kernel-error.de/2025/ecp/chain.crt";
    key-file "/usr/local/etc/nginx/ssl/wild.kernel-error.de/2025/ecp/http.key";
    protocols { TLSv1.2; TLSv1.3; };
    ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256";
    cipher-suites "TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256";
    prefer-server-ciphers yes;
    session-tickets no;
};

„Ich schalte nginx davor – muss BIND TLS können?“
Wenn nginx wirklich TLS terminiert, kann BIND auch ohne TLS dahinter laufen – dann sprichst du intern HTTP/2 cleartext oder HTTP/1.1, je nach Setup. Das habe ich ebenfalls so umgesetzt, es hängt immer etwas davon ab, was man so will und wie groß das Setup wird. Ich lasse es in diesem Beitrag aber mal weg, so läuft alles nur mit bind. Ob BIND dafür „tls none“/HTTP-Listener sauber unterstützt, hängt an der BIND-DoH-Implementierung – hier ist die BIND/ARM-Doku die Wahrheit. bind9.readthedocs.io+1

Testplan – Linux-CLI – bewusst IPv4 und IPv6

Wir wollen natürlich einmal reproduzierbar testen. Also: jede Stufe zweimal. Einmal -4, einmal -6. Also ob es bei IPv4 und bei IPv6 jeweils korrekt ist. Ihr könnt euch nicht vorstellen, wie oft ich fest davon überzeugt bin, es für beide Adressfamilien korrekt konfiguriert zu haben, dann aber noch ein unterschied zwischen v4 und v6 ist. Daher testen wir das.

Voraussetzungen auf Linux

which dig kdig curl openssl

Schritt 1 – DoT-TLS-Handshake prüfen (IPv4/IPv6)

IPv4

openssl s_client \
  -connect 37.120.183.220:853 \
  -servername dns.kernel-error.de \
  -alpn dot

Erwartung:

  • Zertifikat passt auf dns.kernel-error.de (SAN / Wildcard ok)
  • ALPN protocol: dot
  • Verify return code: 0 (ok)

IPv6

openssl s_client \
  -connect '[2a03:4000:38:20e::853]:853' \
  -servername dns.kernel-error.de \
  -alpn dot

Wenn das passt, ist TLS-Transport ok. Also nur die TLS Terminierung für IPv4 und IPv6, da war noch keine DNS Abfrage enthalten.

Schritt 2 – DoT-Query (kdig) – IPv4/IPv6

IPv4

kdig +tls @37.120.183.220 google.com A

Erwartung:

  • status: NOERROR
  • Flags: rd ra (Recursion Desired/Available)
  • eine A-Antwort

IPv6

kdig +tls @[2a03:4000:38:20e::853] google.com A

Gleiche Erwartungshaltung wie bei IPv4.

Schritt 3 – Sicherstellen: kein Resolver auf UDP/TCP 53

Resolver-IPs dürfen auf 53 nicht antworten

dig -4 @37.120.183.220 google.com A
dig -6 @2a03:4000:38:20e::853 google.com A

Erwartung:

  • Timeout / no servers reached
    Genau das wollen wir ja: kein UDP/53 auf den Resolver-IPs.

Authoritative-IPs dürfen nicht rekursiv sein

dig -4 @93.177.67.26 google.com A
dig -6 @2a03:4000:38:20e::53 google.com A

Erwartung:

  • status: REFUSED
  • idealerweise EDE: (recursion disabled)
    Das ist genau die „nicht missbrauchbar als Open-Resolver“-Bremse.

Und unser positiver Check:

dig -4 @93.177.67.26 kernel-error.de A
dig -6 @2a03:4000:38:20e::53 kernel-error.de A

Erwartung:

  • aa gesetzt (authoritative answer)
  • Antwort aus meiner Zone

Schritt 4 – DoH GET (Base64url) – IPv4/IPv6

4.1 Query bauen (DNS-Wireformat → base64url)

Beispiel google.com A:

echo -n -e '\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x06google\x03com\x00\x00\x01\x00\x01' \
| base64 -w0 | tr '+/' '-_' | tr -d '='

Das Ergebnis ist mein dns= Parameter (base64url ohne = padding). Das ist DoH-Standard nach RFC 8484.

4.2 DoH GET erzwingen – IPv4

curl -4 --http2 -s \
'https://dns.kernel-error.de/dns-query?dns=<DEIN_DNS_PARAM>' \
| hexdump -C

IPv6

curl -6 --http2 -s \
'https://dns.kernel-error.de/dns-query?dns=<DEIN_DNS_PARAM>' \
| hexdump -C

Erwartung:

  • HTTP/2 200
  • content-type: application/dns-message
  • Im Hexdump siehst du eine valide DNS-Response.

Schritt 5 – DoH POST (application/dns-message) – IPv4/IPv6

Das ist der „richtige“ DoH-Weg für Tools/Clients.

IPv4

printf '\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x06google\x03com\x00\x00\x01\x00\x01' \
| curl -4 --http2 -s \
  -H 'content-type: application/dns-message' \
  --data-binary @- \
  https://dns.kernel-error.de/dns-query \
| hexdump -C

IPv6

printf '\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x06google\x03com\x00\x00\x01\x00\x01' \
| curl -6 --http2 -s \
  -H 'content-type: application/dns-message' \
  --data-binary @- \
  https://dns.kernel-error.de/dns-query \
| hexdump -C

Erwartung:

  • DNS-Response im Wireformat
  • keine HTML-Antwort, kein Redirect-Quatsch

Was wir damit jetzt sicher(er) gelöst haben:

  • Kein Open-Resolver auf UDP/53 → massiver Gewinn gegen DNS-Amplification.
  • Authoritative bleibt Authoritative → Zonen-Betrieb unverändert stabil.
  • Resolver nur über DoT/DoH → TCP/TLS-Transport, weniger Missbrauchsfläche.
  • Saubere technische Trennung → Views per Ziel-IP sind simpel, robust, nachvollziehbar.

Und ja: „Public Resolver“ heißt trotzdem Monitoring/Rate-Limiting/Abuse-Handling.
Das Feintuning (RRL, QPS-Limits, minimal-responses, Response-Policy, ggf. ECS-Handling, Logging, Fail2ban-Signale) ist das nächste Kapitel. Wobei, wenn ich grade auf die TLS Parameter schaue, sollte ich da vielleicht noch mal nacharbeiten, hm?

Wenn ihr noch eine kleine liste von erreichbaren Servern sucht: GitHub-curl-wiki

Alles hilft natürlich nicht, wenn man am Ende doch komplett IP- oder Hostnamebasiert geblockt wird. In China ist da nicht viel zu holen und auch hier gibt es immer mal wieder etwas.


Japp… TLS geht besser. Im Beitrag habe ich es oben schon angepasst, es war:

tls local-tls {
    cert-file "/pfad/chain.crt";
    key-file  "/pfad/http.key";
    dhparam-file "/pfad/dhparam.pem";
    protocols { TLSv1.2; TLSv1.3; };
    ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256";
    prefer-server-ciphers yes;
    session-tickets no;
};
  • dhparam-file ist komplett raus weil, ja weil es nicht benutzt wird ich mach ja kein DHE sondern ECDHE
  • cipher-suites für TLS1.3 waren nicht gesetzt.
  • Dann konnten auch gleich die Cipher aufgeräumt werden.

Hey, da hat es sich doch gelohnt, das mal runter zu schreiben. So habe ich es direkt gefunden und nicht erst, weil mich jemand von euch darauf hinweist (macht das aber bitte immer wenn ich hier Mist schreibe) oder es beim nächsten eigenen Audit auffällt.

Siehe auch: HTTPS RR und SVCB Records — die passenden DNS-Records, damit Clients dieses DoH/DoT-Setup automatisch entdecken können (RFC 9461).

Quantensichere Kryptografie mit OpenSSH auf FreeBSD 15 richtig konfigurieren

Siehe auch: Post-Quantum TLS für E-Mail — Postfix und Dovecot mit X25519MLKEM768 auf FreeBSD 15, Post-Quantum TLS für Nginx — X25519MLKEM768 auf FreeBSD 15 konfigurieren

Mein FreeBSD 15 kommt mit OpenSSH 10.0p2 und OpenSSL 3.5.4.
Beide bringen inzwischen das mit, was man aktuell als quantensichere Kryptografie bezeichnet. Oder genauer gesagt das, was wir Stand heute für ausreichend robust gegen zukünftige Quantenangriffe halten.

Illustration zu quantensicherer Kryptografie mit OpenSSH auf FreeBSD 15. Dargestellt sind ein Quantenchip, kryptografische Symbole, ein Server, ein SSH Schlüssel sowie der FreeBSD Daemon als Sinnbild für post-quantum Key Exchange und sichere Serverkommunikation.

Quantensicher? Nein, das hat nichts mit Füßen zu tun, sondern tatsächlich mit den Quanten aus der Physik. Quantencomputer sind eine grundlegend andere Art von Rechnern. Googles aktueller Quantenchip war in diesem Jahr bei bestimmten Physiksimulationen rund 13.000-mal schneller als der derzeit leistungsstärkste klassische Supercomputer. Der chinesische Quantencomputer Jiuzhang wurde bei speziellen Aufgaben sogar als 100 Billionen Mal schneller eingestuft.

Kurz gesagt: Quantencomputer sind bei bestimmten Berechnungen extrem viel schneller als heutige klassische Rechner. Und genau das ist für Kryptografie ein Problem.

Als Vergleich aus der klassischen Welt: Moderne Grafikkarten haben die Zeit zum Knacken von Passwörtern in den letzten Jahren drastisch verkürzt.

  • Nur Zahlen: Ein 12-stelliges Passwort wird praktisch sofort geknackt.
  • Nur Kleinbuchstaben: wenige Wochen bis Monate.
  • Groß- und Kleinschreibung plus Zahlen: etwa 100 bis 300 Jahre.
  • Zusätzlich Sonderzeichen: 2025 noch als sehr sicher einzustufen mit geschätzten 226 bis 3.000 Jahren.

Quantencomputer nutzen spezielle Algorithmen wie den Grover-Algorithmus, der die effektive Sicherheit symmetrischer Verfahren halbiert. Ein ausreichend leistungsfähiger Quantencomputer könnte damit die benötigte Zeit drastisch reduzieren. Was heute Jahrhunderte dauert, könnte theoretisch auf Tage oder Stunden schrumpfen.

Stand 2025 sind solche Systeme zwar real und in der Forschung extrem leistungsfähig, werden aber noch nicht flächendeckend zum Brechen realer Kryptosysteme eingesetzt.

Heißt das also alles entspannt bleiben? Jein.

Verschlüsselte Datenträger lassen sich kopieren und für später weglegen. Gleiches gilt für aufgezeichneten verschlüsselten Netzwerkverkehr. Heute kommt man nicht an die Daten heran, aber es ist absehbar, dass das in Zukunft möglich sein könnte. Genau hier setzt quantensichere Kryptografie an. Ziel ist es, auch aufgezeichnete Daten dauerhaft vertraulich zu halten.

Ein praktisches Beispiel ist der Schlüsselaustausch mlkem768x25519. Wenn ihr diese Seite nicht gerade über Tor lest, ist die Wahrscheinlichkeit hoch, dass euer Browser bereits eine solche hybride, post-quantum-fähige Verbindung nutzt. Im Firefox lässt sich das einfach prüfen über F12, Network, eine Verbindung anklicken, dann Security und dort die Key Exchange Group. Taucht dort mlkem768x25519 auf, ist die Verbindung entsprechend abgesichert. Richtig, auf dem Screenhot seht ihr auch HTTP/3.

Image of mlkem768+x25519 in firefox.

Für diese Webseite ist das nicht zwingend nötig. Für SSH-Verbindungen zu Servern aber unter Umständen schon eher. Deshalb zeige ich hier, wie man einen OpenSSH-Server entsprechend konfiguriert.

Ich beziehe mich dabei bewusst nur auf die Kryptografie. Ein echtes SSH-Hardening umfasst deutlich mehr, darum geht es hier aber nicht.

Die zentrale Konfigurationsdatei ist wie üblich: /etc/ssh/sshd_config

Stand Ende 2025 kann ich folgende Konfiguration empfehlen:

KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com
HostKeyAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com

Die Zeilen werden entweder an die bestehende Konfiguration angehängt oder ersetzen vorhandene Einträge. Da wir nicht einfach blind kopieren wollen, hier kurz die Erklärung.

Schlüsselaustausch:
Bevorzugt werden hybride Verfahren wie mlkem768 kombiniert mit x25519 sowie sntrup761 kombiniert mit x25519. Diese verbinden klassische elliptische Kryptografie mit post-quantum-resistenten Algorithmen. Damit ist die Verbindung sowohl gegen heutige Angreifer als auch gegen zukünftige Store-now-decrypt-later-Szenarien abgesichert. Curve25519 dient als bewährter Fallback. Klassische Diffie-Hellman-Gruppen sind nur aus Kompatibilitätsgründen enthalten.

Verschlüsselung:
Es werden ausschließlich moderne Algorithmen eingesetzt. Primär kommen AEAD-Ciphers wie ChaCha20-Poly1305 und AES-GCM zum Einsatz, die Vertraulichkeit und Integrität gleichzeitig liefern und bekannte Schwächen älterer Modi vermeiden. Ältere Verfahren wie CBC sind bewusst ausgeschlossen.

Integrität:
Zum Einsatz kommen ausschließlich SHA-2-basierte MACs im Encrypt-then-MAC-Modus. Dadurch werden klassische Angriffe auf SSH wie Padding-Oracles und bestimmte Timing-Leaks wirksam verhindert.

Serveridentität:
Als Hostkey-Algorithmus wird Ed25519 verwendet. Optional auch mit Zertifikaten oder hardwaregestützten Security Keys. Das bietet hohe kryptografische Sicherheit bei überschaubarem Verwaltungsaufwand.

Wichtig: Das funktioniert nur, wenn Server und Client diese Algorithmen auch unterstützen. Wer bereits mit SSH-Keys arbeitet, sollte prüfen, dass es sich um Ed25519-Keys handelt. Andernfalls sperrt man sich im Zweifel selbst aus.

Auf dem Server lässt sich die aktive Konfiguration prüfen mit:

sshd -T | grep -Ei 'kexalgorithms|ciphers|macs|hostkeyalgorithms'

Auf dem Client geht es am einfachsten mit:

ssh -Q kex
ssh -Q cipher
ssh -Q mac
ssh -Q key

So sieht man schnell, welche Algorithmen tatsächlich verfügbar sind.

Zur externen Überprüfung der SSH-Konfiguration kann ich außerdem das Tool ssh-audit empfehlen. Aufruf einfach per:

ssh-audit hostname oder IP -p PORT

Das liefert eine brauchbare Einschätzung der aktiven Kryptografie und möglicher Schwachstellen. Oh, wenn ihr schon dabei seit, vergesst nicht:

Hinweis zur Einordnung der Quantensicherheit:
Die hier gezeigte Konfiguration verbessert ausschließlich den Schlüsselaustausch (Key Exchange) durch hybride post-quantum-fähige Verfahren. Hostkeys und Signaturen in OpenSSH basieren weiterhin auf klassischen Algorithmen (z. B. Ed25519 oder ECDSA); standardisierte post-quantum-Signaturalgorithmen sind in OpenSSH aktuell noch nicht implementiert. Es existieren zwar experimentelle Forks (z. B. aus dem Open-Quantum-Safe-Projekt), diese gelten jedoch ausdrücklich nicht als produktionsreif und sind nicht Bestandteil des OpenSSH-Mainlines. Die hier gezeigte Konfiguration ist daher als pragmatischer Übergangsschritt zu verstehen, um „store-now-decrypt-later“-Risiken beim Schlüsselaustausch bereits heute zu reduzieren, ohne auf instabile oder nicht standardisierte Komponenten zu setzen.
Weiterführende Informationen zum aktuellen Stand der post-quantum-Unterstützung in OpenSSH finden sich in der offiziellen Dokumentation: https://www.openssh.com/pq.html

Viel Spaß beim Nachbauen. Und wie immer: bei Fragen, fragen.

FreeBSD SSH-Server absichern: MFA mit Google Authenticator einrichten​

Siehe auch: FreeBSD OpenSSH: OS-Banner sicher entfernen, SSH-Bruteforce, DigitalOcean und AbuseIPDB – warum Blocken das Problem nicht löst

SSH-Keys sind der Standard. Aber manchmal lässt es sich nicht vermeiden, dass ein Login nur mit Benutzername und Kennwort abgesichert ist. Um das aufzuwerten, lässt sich der SSH-Server mit einem zweiten Faktor ausstatten — hier mit dem Google Authenticator (TOTP) auf FreeBSD.

Installation

pkg install pam_google_authenticator

PAM-Konfiguration

In /etc/pam.d/sshd das Google Authenticator PAM-Modul als zweiten required-Eintrag nach pam_unix einfügen:

#
# PAM configuration for the "sshd" service
#

# auth
auth		required	pam_unix.so		no_warn try_first_pass
auth		required	/usr/local/lib/pam_google_authenticator.so

# account
account		required	pam_nologin.so
account		required	pam_login_access.so
account		required	pam_unix.so

# session
session		required	pam_permit.so

# password
password	required	pam_unix.so		no_warn try_first_pass

Die Reihenfolge ist wichtig: Erst das Kennwort (pam_unix), dann der TOTP-Code. Auf dem gleichen Weg lässt sich MFA auch für su, den Konsolen-Login oder SSH-Keys einrichten — einfach das entsprechende PAM-File anpassen.

sshd_config anpassen

In /etc/ssh/sshd_config muss Challenge-Response aktiviert sein:

# Seit OpenSSH 8.7 heißt die Option KbdInteractiveAuthentication
# ChallengeResponseAuthentication ist ein Alias und funktioniert weiterhin
KbdInteractiveAuthentication yes

Danach service sshd restart — aber vorher sicherstellen, dass man noch eine offene Session hat, falls etwas nicht stimmt.

Authenticator einrichten

Auf dem Smartphone den Google Authenticator installieren (oder eine andere TOTP-App wie Aegis, 2FAS oder den Microsoft Authenticator). Dann auf dem Server mit dem gewünschten Benutzer google-authenticator aufrufen:

cd ~
google-authenticator

Das Tool zeigt einen QR-Code im Terminal, den man mit der Authenticator-App scannt:

Danach den angezeigten Code einmal eingeben — fertig. Bei jedem Kennwort-Login wird jetzt zusätzlich der aktuelle TOTP-Code abgefragt.

Wichtig: Das Tool zeigt auch Backup-Codes an. Diese unbedingt sicher aufbewahren — wenn das Smartphone verloren geht, kommt man sonst nicht mehr rein. Die Konfiguration liegt in ~/.google_authenticator und kann dort auch nachträglich eingesehen werden.

Unter Linux ist die Einrichtung sehr ähnlich — das PAM-Modul heißt dort libpam-google-authenticator. Fragen? Einfach melden.

DNSSEC und SSHFP unter Linux Mint und Ubuntu zum Laufen bringen

Heute habe ich versucht, mich von meiner neuen Linux Mint Installation aus mit einem meiner SSH-Server zu verbinden. Mein SSH-Client hat mich direkt gefragt, ob ich dem Hostkey vertrauen möchte:

ssh username@hostname.kernel-error.org
The authenticity of host 'hostname.kernel-error.org (2a01:5a8:362:4416::32)' can't be established.
ED25519 key fingerprint is SHA256:kTRGVCMRLiHfvJunW2CbW5H3NZmn3Wkx2KnHJXl3iJu.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])?

Für viele ist das normal — man tippt „yes“ und sieht die Meldung nie wieder. Aber diese Meldung hat ihren Grund. Beim ersten Verbindungsaufbau zeigt SSH den Fingerprint des Server-Hostkeys an, damit man prüfen kann, ob man wirklich mit dem richtigen Server spricht und nicht mit einem Angreifer. Wer eh immer „yes“ sagt, könnte den Check auch gleich in seiner ~/.ssh/config abschalten:

Host *
    StrictHostKeyChecking no

SSHFP — Hostkeys per DNS verifizieren

Es gibt einen besseren Weg: SSHFP-Records (RFC 4255). Man hinterlegt die Fingerprints der erwarteten Hostkeys als DNS-Einträge. Der SSH-Client prüft diese automatisch — vorausgesetzt die DNS-Antwort ist per DNSSEC abgesichert. In der ~/.ssh/config:

Host *
   VerifyHostKeyDNS yes

Meine DNS-Server unterstützen alle DNSSEC, mein lokaler Resolver auf dem Router auch, die SSH-Config stimmt — und trotzdem erscheint die Meldung. Also mit ssh -vvv debuggen:

debug1: found 2 insecure fingerprints in DNS

Insecure. SSH findet die SSHFP-Records, vertraut ihnen aber nicht, weil die DNS-Antwort nicht als DNSSEC-validiert markiert ist.

Das Problem: systemd-resolved

Schneller Test mit dig +dnssec gegen Google DNS:

dig +dnssec hostname.kernel-error.org @8.8.8.8
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

Das ad-Flag (Authenticated Data) ist gesetzt — meine DNS-Server liefern DNSSEC korrekt aus. Auch der lokale Router-Resolver liefert ad. Aber ohne expliziten @server:

dig +dnssec hostname.kernel-error.org
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

Kein ad. Was steht in /etc/resolv.conf? 127.0.0.53systemd-resolved. Der Stub-Resolver von systemd schluckt das AD-Flag.

Man könnte in /etc/systemd/resolved.conf einfach DNSSEC=yes setzen — bei mir ging danach aber gar keine DNS-Auflösung mehr. Das liegt am Stub-Resolver, den man ebenfalls umkonfigurieren müsste. Nennt mich oldschool, aber für meine Zwecke reicht der klassische Weg über die vom NetworkManager gepflegte resolv.conf.

Lösung: systemd-resolved abschalten

sudo systemctl disable systemd-resolved
sudo systemctl stop systemd-resolved
sudo rm /etc/resolv.conf

In /etc/NetworkManager/NetworkManager.conf in der [main]-Sektion:

dns=default

NetworkManager neu starten:

sudo systemctl restart NetworkManager
cat /etc/resolv.conf
# Generated by NetworkManager
search kernel-error.local
nameserver 10.10.88.1
nameserver fd00:424e:6eff:f525:454e:6eff:f525:4241

DNS-Auflösung geht. Aber SSH sagt weiterhin „insecure“. Es fehlen noch zwei Optionen in der resolv.conf.

edns0 und trust-ad

Erste Erkenntnis — edns0 muss aktiviert sein, damit DNSSEC-Daten überhaupt transportiert werden. In /etc/resolv.conf:

options edns0

Jetzt zeigt dig das ad-Flag. Aber SSH sagt immer noch „insecure“. Warum? Ein Blick in den SSH-Quellcode — die ldns-Bibliothek macht die DNSSEC-Validierung:

        /* Check for authenticated data */
        if (ldns_pkt_ad(pkt)) {
                rrset->rri_flags |= RRSET_VALIDATED;
        } else { /* AD is not set, try autonomous validation */
                ldns_rr_list * trusted_keys = ldns_rr_list_new();
                /* ... */
                if ((err = ldns_verify_trusted(ldns_res, rrdata, rrsigs,
                     trusted_keys)) == LDNS_STATUS_OK) {
                        rrset->rri_flags |= RRSET_VALIDATED;
                }
        }

ldns prüft das AD-Flag im DNS-Paket. Aber die glibc setzt das AD-Flag in der Antwort nur dann, wenn trust-ad in der resolv.conf steht — sonst wird es aus Sicherheitsgründen herausgefiltert. Die vollständige Option:

options edns0 trust-ad

Und jetzt:

ssh username@hostname.kernel-error.org -vvv
[...]
debug1: found 2 secure fingerprints in DNS
debug3: verify_host_key_dns: checking SSHFP type 4 fptype 1
debug1: verify_host_key_dns: matched SSHFP type 4 fptype 1
debug3: verify_host_key_dns: checking SSHFP type 4 fptype 2
debug1: verify_host_key_dns: matched SSHFP type 4 fptype 2
debug1: matching host key fingerprint found in DNS

secure statt insecure. SSH verifiziert den Hostkey automatisch per DNSSEC — keine manuelle Fingerprint-Prüfung mehr nötig.

Rebootfest machen

Die manuell eingetragenen Optionen in der resolv.conf überleben keinen Reboot — der NetworkManager überschreibt die Datei. Per nmcli die Optionen dauerhaft im Netzwerkprofil setzen, für IPv4 und IPv6:

nmcli conn modify DEINE-PROFIL-UUID ipv4.dns-options edns0,trust-ad
nmcli conn modify DEINE-PROFIL-UUID ipv6.dns-options edns0,trust-ad

Die UUID des aktiven Profils findet man mit nmcli conn show. Beide Zeilen sind nötig — fehlt eine, greift es nicht.


Zusammenfassung: systemd-resolved unter Linux Mint und Ubuntu filtert das DNSSEC-AD-Flag heraus. Ohne AD-Flag kann SSH die SSHFP-Records nicht als vertrauenswürdig einstufen. Lösung: systemd-resolved abschalten, NetworkManager mit dns=default nutzen, edns0,trust-ad per nmcli setzen.

Wer einen DNSSEC-validierenden Resolver sucht — dns.kernel-error.de ist ein öffentlicher DNS-Resolver mit DNSSEC, DNS over TLS und DNS over HTTPS.

Und die offene Frage: Ich bin mit meinem FreeBSD-Wissen an das Thema gegangen. Wie macht man das als Linux-User mit systemd-resolved richtig? Schreibt mir, wenn ihr es wisst.

Siehe auch: SSH Host Keys per SSHFP

DNS over TLS mit BIND, Stunnel und Android 9: Eigener DoT-Server

Die Zeit ging weiter, die Entwicklung bei BIND und DNS ebenfalls. Daher gibt es nun einen neuen Beitrag, der das aktuelle Setup mit BIND 9.20 auf FreeBSD 15 beschreibt – inklusive sauberer Trennung von authoritative DNS (Port 53) und öffentlichem Resolver (DoT/DoH) sowie reproduzierbaren CLI-Tests für IPv4 und IPv6. Bitte dort weiterlesen.

Über die Techniken DoT (DNS over TLS) habe ich bereits im Zusammenhang mit Bind 9.16 geschrieben. Ebenfalls DoH (DNS over HTTPS) gibt es einen kleinen Beitrag.

Bilder der Bind 9 TLS Konfiguration

Zu diesem Zeitpunkt bracht BIND 9 die Unterstützung für DoH und DoT noch nicht selbst mit. Daher waren zu diesem Zeitpunkt noch Umwege über stunnel oder nginx zusammen mit doh-proxy nötig.

Zum Glück kommt die letzte stable Version 9.18.0 (26. Januar 2022) mit dem nötigen Support.

named now supports securing DNS traffic using Transport Layer Security (TLS). TLS is used by both DNS over TLS (DoT) and DNS over HTTPS (DoH).

Warum möchte man noch gleich DoH oder DoT benutzen? Ganz einfach… Über diese Techniken werden DNS Abfragen verschlüsselt übertragen. Dieses ist ein weiterer Schutz davor manipulierte Antworten zu bekommen und selbstverständlich, damit die eigenen DNS Abfragen erst überhaupt nicht mitgelesen werden. Denn wenn von einem Gerät im Netzwerk die DNS Abfrage zu z.B.: www.tagesschau.de kommt, könnte man davon bereits Dinge ableiten.

Wie die meisten Bind Konfigurationen ist dieses ebenfalls straightforward. Ab Version 9.18 bringt Bind alles Nötige mit. Da wir nun TLS mit dem Bind sprechen möchten, benötigen wir natürlich ein gültiges Zertifikat, wie z.B. beim nginx für seine Webseite.

Ebenfalls sollte man ein paar frische Diffie-Hellmann Parameter generieren:

openssl dhparam -out dhparam.pem 4096

Die eigentliche bind Konfiguration kann in der named.conf.options geschehen:

options {
        [...]
        listen-on port 853 tls local-tls { 37.120.183.220; };
        listen-on-v6 port 853 tls local-tls { 2a03:4000:38:20e::853; };
        listen-on port 443 tls local-tls http default { 37.120.183.220;  };
        listen-on-v6 port 443 tls local-tls http default { 2a03:4000:38:20e::853; };
        [...]
        allow-recursion-on { 127.0.0.0/8; ::1/128; 2a03:4000:38:20e::853; 37.120.183.220; };
        [...]
};

Da der bind auf weiteren Ports lauschen soll erweitert man diese für IPv4 und IPv6. Der Default Port für DoH ist dabei 443 und der default Port für DoT ist 853, beides TCP.

listen-on sowie listen-on-v6 sind wohl selbsterklärend.
port ist der TCP Port und erklärt sich ebenfalls.
tls sagt dem Bind das wir tls sprechen möchten.
local-tls verweißt auf den gleichnamigen tls Block über welchen man seine TLS Konfiguration vornimmt.
http ist für DoH.
default gibt den eigentlichen endpoint für die DoH Abfragen an, im default ist es /dns-query

Da der Server unsere DNS Abfragen erledigen soll, müssen wir ihm dieses noch per allow-recursion-on auf den jeweiligen Adressen erlauben.

Als nächstes wird die eigentliche TLS Terminierung konfiguriert (das lässt sich ebenfalls auslagern, wenn gewünscht). Dafür wird der folgende Block, außerhalb der Options Blocks, ergänzt:

tls local-tls {
    cert-file "/usr/local/etc/ssl/wild.kernel-error.de/2022/ecp/chain.crt";
    key-file "/usr/local/etc/ssl/wild.kernel-error.de/2022/ecp/http.key";
    dhparam-file "/usr/local/etc/ssl/dhparam.pem";
    protocols { TLSv1.2; TLSv1.3; };
    ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256";
    prefer-server-ciphers yes;
    session-tickets no;
};

local-tls ist dabei der name des Blocks. Auf diesen verweisen wir oben.
cert-file ist der Pfad zum Zertifikat. Ich habe dort nicht nur das Zertifikat, sondern die gesamte Chain, also mit Intermediate und Root.
key-file ist der Pfad zum Key des Zertifikates.
dhparam-file ist der Pfad zu den Diffie-Hellman Parametern.
protocols definiert die zu verwendenden TLS Protokolle. In diesem Beispiel TLS1.2 sowie TLS1.3.
ciphers definiert die zu verwendenden cipher. Es soll ja „sicher“ bleiben.
prefer-server-ciphers übermittelt dem Client die Information, in welcher Reihenfolge protokoll/cipher Kombinationen probiert werden sollen um einen Match zu finden. Erst das vermeintlich sicherste und dann immer „schlechter“.
session-tickets regelt ob eine Wiederaufnahme von TLS Sessions erlaubt ist oder nicht. Da ich forward secrecy nutzen möchte, ist es deaktiviert.

Damit ist die Konfiguration schon abgeschlossen (Firewall ggf. nicht vergessen!). Also testen….

Ein einfaches Tool dafür ist dog, oder natürlich dig aus den bind-tools aber Version 9.18. Für bind gibt es dann die Optionen +https oder auch +tls

dig +https @dns.kernel-error.de www.kernel-error.de A
dig +tls @dns.kernel-error.de www.kernel-error.de A

Der gleiche Test mit dog, sieht wie folgt aus:

dog www.kernel-error.de --tls "@dns.kernel-error.de"
A www.kernel-error.de. 6h00m00s   148.251.40.23
dog www.kernel-error.de --https "@https://dns.kernel-error.de/dns-query"
A www.kernel-error.de. 6h00m00s   148.251.40.23

Das war es auch schon! Viele Spaß mit einem „besseren“ DNS und wenn es noch Fragen gibt, einfach fragen.

« Ältere Beiträge

© 2026 -=Kernel-Error=-RSS

Theme von Anders NorénHoch ↑