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

Schlagwort: Encryption (Seite 1 von 8)

ts3level: TeamSpeak-Identity-Level auf der GPU rechnen, mit automatischem .ini-Patch

Beitragsbild zu ts3level: Eine TeamSpeak-Identity-INI-Datei wird per GPU von Security-Level 45 auf 55 hochgerechnet und automatisch gepatcht.

Wer eine TeamSpeak-3-Identity mit einem höheren Security Level haben möchte als das, was der offizielle Client innerhalb eines Menschenlebens rausrechnet, kommt um GPU-Hilfe nicht herum. Es gibt da seit Jahren eine Handvoll Drittanbieter-Tools. Was bisher fehlte, war der unspektakuläre letzte Schritt: das gefundene Ergebnis wieder in die .ini zu schreiben. Genau diese Lücke schließt ts3level, ein neues Open-Source-Tool, an dem ich die letzten Wochen gesessen habe.

ts3level GUI Hauptfenster mit aktivem Hash-Run, Identity-Details links, GPU-Stats und Auslastungsgraph rechts
Das Hauptfenster während eines laufenden Hash-Runs. Links die geparsten Identity-Details, rechts Live-Progress, Hashrate, ETA und ein kleiner Cairo-Graph mit 60 Sekunden GPU-Auslastung.

Das Repo liegt auf GitHub unter Kernel-Error/ts-identities-security-level (MIT-Lizenz). Release v0.1.0 inklusive prebuilt Tarball für x86_64 Linux mit glibc ≥ 2.39 gibt’s hier. Der Tarball ist rund 2 MB groß, mehr braucht es nicht.

Das Problem: Single-Thread-CPU vs. exponentielles Wachstum

Das „Security Level“ einer TS3-Identity ist nichts anderes als ein Proof-of-Work. Man variiert einen uint64-Counter und hashed solange SHA1(pubkey_b64 || decimal(counter)) bis vorne genug Null-Bits stehen. Pro Level verdoppelt sich der durchschnittliche Aufwand. Der offizielle TS3-Client rechnet das in einem einzelnen CPU-Thread. Ab ungefähr Level 50 wird das schlicht unbrauchbar.

Es gibt seit Jahren GPU-beschleunigte Drittanbieter-Hasher. landave/TSIdentityTool ist die CPU-Referenz in C, mittlerweile dormant. landave/TeamSpeakHasher bringt das auf OpenCL, der CUDA-Fork von thissepic ist aktuell das aktivste Tool und schafft auf einer RTX 4070 Ti rund 20 GH/s. bratkartoffel/ts3idtools macht das in C mit pthreads auf der CPU.

Allen gemeinsam ist die UX-Lücke: keines schreibt das Ergebnis zurück. Der typische Workflow ist Pubkey aus der .ini extrahieren, Hasher füttern, Counter aufschreiben, Counter manuell in die .ini patchen, Datei re-importieren. landave begründet das bewusst mit Privatekey-Schutz, der Hasher soll den Private Key gar nicht erst zu sehen kriegen. Eine sehr saubere Trennung, aber für den Endanwender eine ziemlich friktionsreiche Angelegenheit.

Genau dort setzt ts3level an. Man zeigt auf seine .ini, der Rest passiert automatisch. Atomarer Write, einmaliges .bak, fertig. Drumherum eine GTK-GUI, NVML-Telemetrie, Übersetzungen auf en/de/es/fr, eine ehrliche ETA-Anzeige und ein Identity-Details-Panel, damit man nicht jedes Mal die Datei von Hand parsen muss.

Wie funktioniert „Security Level“ überhaupt?

Die Kernformel ist erfrischend einfach:

level = leading_zero_bits( SHA1( pubkey_b64 || decimal(counter) ) )

pubkey_b64 ist die base64 der public-key-only ASN.1 DER eines ECDSA-Keys auf der Kurve secp256r1 (NIST P-256). Es ist genau der String, den der TS3-Client im Identity-Dialog unter „Public Key“ anzeigt. decimal(counter) ist die ganz normale Dezimaldarstellung des uint64 ohne Padding, also "42" und nicht "0042". Konkatenation passiert auf ASCII-Stringebene, nicht auf irgendwelchen rohen Bytes. Klingt banal, ist aber tatsächlich die häufigste Fehlerquelle bei Re-Implementierungen.

Das Zählen der führenden Null-Bits läuft byte-0-zuerst und innerhalb jedes Bytes von LSB nach MSB. Genau die hashcat-Konvention. In Python wäre das:

zero_bytes = 0
while zero_bytes < 20 and hash[zero_bytes] == 0:
    zero_bytes += 1
zero_bits = 0
if zero_bytes < 20:
    b = hash[zero_bytes]
    while not (b & 1):
        zero_bits += 1
        b >>= 1
level = 8 * zero_bytes + zero_bits

Die Wahrscheinlichkeit dass ein einzelner Hash mindestens Level L erreicht ist 1 / 2^L. Im Mittel braucht es also 2^L Versuche pro angestrebtes Level. Auf meiner Testkarte, einer RTX 4060 Ti bei rund 2,4 GH/s, sieht das so aus:

Ziel-LevelMittlere Wartezeit
40~7 Minuten
45~4 Stunden
50~5 Tage
55~5 Monate
60~14 Jahre

Diese Werte sind statistische Mittelwerte. Die echte Wartezeit ist exponentialverteilt, man kann also mit Faktor 2 bis 3 schneller oder langsamer Glück haben. Genau deshalb zeigt das Tool die ETA auch als das was sie ist (ein Mittelwert), und nicht als seriös wirkenden Countdown.

Was im .ini-File steckt

Eine exportierte TS3-Identity sieht ungefähr so aus:

[Identity]
id=Standard
identity="<counter>V<base64_obfuscated_blob>"
nickname=Kernel-Error
phonetic_nickname=

Der Teil hinter dem V ist doppelt base64-codiert, mit einer XOR-Schicht dazwischen. Das hat landave per Black-Box-Analyse aus dem Client-Verhalten rekonstruiert. Schematisch:

data = base64_decode(blob_b64)        # outer base64
hash_part = data[20:]                  # everything past the first 20 bytes
sha = sha1(hash_part)
data[0:20] ^= sha                      # undo SHA-1 mask
data[0:min(100, len)] ^= TSKEY         # undo static-key XOR
inner_ascii = data                     # ASCII-base64 of the ASN.1 DER
asn1_der = base64_decode(inner_ascii)  # inner base64

Der amüsante Stolperstein dabei: TSKEY ist im C-Original definiert als const char *TSKEY = "b9dfaa7bee...";. Das sind nicht die 64 Hex-Bytes, sondern die 128 ASCII-Zeichen der Hex-Darstellung selbst. Eine Stunde Debugging, bis ich gemerkt habe woran es liegt. Ist halt einer dieser Punkte, bei denen die Spec ohne Quelltext nicht reichen würde.

Im inneren DER liegt der libtomcrypt-formatierte Keypair:

SEQUENCE {
  BIT STRING       flags     -- 1 bit; 0 = public-only, 1 = with private key
  SHORT INTEGER    keysize   -- 32 for P-256
  INTEGER          x
  INTEGER          y
  [INTEGER         k]        -- only when flags = 1
}

Für den Hash-Input brauchen wir nur die public-only Variante dieses DER-Blobs: gleiche Struktur, flags=0, X, Y, und der private Skalar k fällt einfach raus. Der Hasher bekommt damit nie den privaten Schlüssel zu sehen. Genau diese Trennung, die landave aus gutem Grund eingeführt hat, bleibt erhalten. Das Auto-Patchen der .ini passiert in einem separaten Modul, das den Counter zurückschreibt, ohne den Private Key überhaupt anzufassen.

Warum eine GPU

SHA-1 ist ein dankbarer Kandidat für massiv-paralleles Hashing. Es gibt keine Datenabhängigkeiten zwischen verschiedenen Counter-Werten, jede der zigtausend GPU-Threads kann einen anderen Counter testen und das Ergebnis unabhängig vergleichen. Ein moderner Desktop-Kern auf der CPU kommt vielleicht in den niedrigen dreistelligen MH/s-Bereich für SHA-1, eine RTX 4060 Ti im Tool rund 2,4 GH/s. Das reicht um aus Level 50 statt „mehreren Jahren“ so etwas wie „mehreren Tagen“ zu machen, also einen Bereich, in dem ein Run überhaupt erst Sinn ergibt.

Das Tool macht nichts Heldenhaftes mit dem Kernel: simples SHA-1 in CUDA C, ein Counter pro Thread, atomarer Compare-and-Swap auf der „bester gefundener Level“-Variable im global memory. Hochoptimierte Hasher wie der von thissepic kommen mit Midstate-Precompute und LOP3.LUT-Tricks auf ungefähr das fünffache. Dieser Teil steht auf der Roadmap, ist aber für den ersten Release bewusst nicht angegangen worden. Der eigentliche Mehrwert von ts3level ist UX und Auto-Patch, nicht der letzte ausgequetschte GH/s.

Was das Tool macht: zwei Binaries, ein Source-Tree

Aus dem Workspace fallen zwei Binaries:

  • ts3level: headless CLI mit Progress-Spinner, gut für Server, tmux, SSH-Sessions
  • ts3level-gui: GTK4 + libadwaita Desktop-Anwendung, übersetzt in en/de/es/fr

Konkrete Use-Cases, die im Auge waren beim Entwurf:

  • Die eigene TS-Identity von einem aktuellen Level (z. B. 45) auf ein Wunsch-Level (z. B. 55) hochrechnen, ohne sich mit base64-Blobs und Counter-Werten herumzuschlagen.
  • Headless auf einem Server mit einer NVIDIA-Karte über Nacht laufen lassen, morgens fertige .ini aus dem Backup-Verzeichnis ziehen.
  • Endless-Mode: das Tool läuft permanent, jedes Mal wenn ein höheres Level gefunden wird, wird die Datei sofort aktualisiert. Wer kein konkretes Ziel hat und einfach gucken will, was an einem Wochenende rauskommt, fährt damit ganz gut.
  • Identity-Details ablesen, ohne die .ini per Hand zu parsen: Nickname, Fingerprint (also die TS3-„Unique ID“), Public Key, Current Level, Current Counter.

Beim Start auf eine frisch generierte Demo-Identity sieht der CLI-Output so aus:

Using device: [CUDA:0] NVIDIA GeForce RTX 4060 Ti (cc 8.9, 34 SMs, 15.6 GiB)

Nickname: DemoUser
Local ID: DemoStandard
Fingerprint: 3cltecu4AFn+OK7Lx3Ish6wZN+Y=
Current level: 25  (counter: 7131210)

note: target 1 <= 25 (current level): raising to 26
Target: 26

Und während der Run läuft sieht die Statuszeile so aus:

⠹ level: 50  counter: 1964603892897982  2.45 GH/s  ETA→+1: 0.4s  ETA→target: 10.3d  GPU: 100% 76°C 165W  (10s)

In der GUI ist genau dieselbe Information da, plus ein 60-Sekunden-Ringbuffer-Graph der GPU-Auslastung. Praktisch wenn man parallel zockt und sehen will, ob der Hash-Run noch genug Luft hat oder die Karte sich gegen das Spiel durchsetzen muss.

Architektur (kurz)

Komplett Rust, fünf Crates in einem Workspace:

crates/ts3level-core/    parser, obfuscation, pubkey extraction, CPU SHA-1, atomic writer
crates/ts3level-engine/  HashEngine trait, preflight, driver loop, NVML telemetry
crates/ts3level-cuda/    SHA-1 kernel in CUDA C, bound via cudarc
crates/ts3level-cli/     clap + indicatif + gettext
crates/ts3level-gui/     gtk4-rs + libadwaita + Cairo graph

Beim Build ruft build.rs einmal nvcc --fatbin auf und erzeugt eine Fatbin mit echtem SASS für sm_70, sm_75, sm_80, sm_86, sm_89, sm_90, plus PTX-Fallback für künftige Architekturen. Diese Fatbin wird per include_bytes! direkt in das Rust-Binary einkompiliert. Der Endanwender braucht damit nur den NVIDIA-Treiber, kein CUDA-Toolkit. libcuda.so.1 und libnvidia-ml.so.1 werden zur Laufzeit per dlopen gezogen, das funktioniert ab Treiber 525 ohne Klimmzüge.

Das Zurückschreiben in die .ini macht das Tool über flock(LOCK_EX) auf die Datei, einmaliges .bak wenn keines existiert, neue Bytes in eine tempfile::NamedTempFile im selben Verzeichnis, fsync, anschließend rename(2) drüber. POSIX-atomar. Bei Stromausfall mittendrin gibt es entweder die alte oder die neue Datei, nie ein halbgares Gemisch.

Vor jedem Run prüft ein Preflight neun Bedingungen: libcuda ladbar, CUDA-Devices vorhanden, Zugriffsrechte auf /dev/nvidia*, .ini lesbar, schreibbar, Parent-Dir schreibbar, Datei strukturell gültig, kein konkurrierender Lock, genug Platz für eine .bak. Fehler werden als strukturierte Codes ausgegeben, dokumentiert in der README. Backtraces gibt es nicht, weil sie für Endanwender nur Lärm sind.

Tests stehen bei 68 verteilt über alle Crates: 40 in ts3level-core (Parser, Obfuscation, Pubkey, Level-Berechnung, Writer, Sign-Prefix-Edge-Cases), 18 in ts3level-engine (Preflight, Driver, ETA, GpuStats), 3 in ts3level-cuda (eine Million Counter Parity GPU vs. CPU, Hashrate-Probe), 7 in ts3level-cli (Smoke-Tests, End-to-End gegen eine echte GPU, i18n).

Validierung gegen den echten TS3-Client

Einfacher aber wichtiger Schritt: nachdem das Tool intern korrekt aussah, habe ich eine reale Identity aus dem TS3-Client exportiert und parallel den vom Tool berechneten Fingerprint und das Security Level mit der Anzeige im Client verglichen. Beides byte-identisch. Heisst: die Algorithmus-Spec stimmt, es gibt kein Off-by-One zwischen meiner Implementierung und dem, was ein TS-Server für die Identifikation tatsächlich sieht. Wer den Counter mit ts3level hochrechnet, bekommt eine Identity, die der Client genauso akzeptiert, als hätte er den Aufwand selbst betrieben.

Hardware-Support

Out-of-the-Box im prebuilt Tarball:

GenerationBeispieleCompute Cap
VoltaTitan V, V100sm_70
TuringRTX 2060/70/80, T4sm_75
Ampere DCA100sm_80
Ampere ConsumerRTX 3060/70/80/90sm_86
AdaRTX 4060/70/80/90sm_89
HopperH100sm_90
Blackwell und künftigeRTX 5xxx, GB200sm_100+ via JIT

Pascal (GTX 10-Serie) und älter sind bewusst nicht dabei. Eine Zeile in build.rs ergänzen und neu bauen reicht, dann läuft auch das, nur ist die SHA-1-Performance auf diesen Karten ohnehin nicht konkurrenzfähig.

Installation

Auf Ubuntu, Mint oder Debian sieht das so aus:

sudo apt install nvidia-driver-550 libgtk-4-1 libadwaita-1-0 gettext-base
sudo usermod -aG render $USER   # log out and back in afterwards
curl -LO https://github.com/Kernel-Error/ts-identities-security-level/releases/download/v0.1.0/ts3level-v0.1.0-x86_64-linux.tar.gz
tar -xzf ts3level-v0.1.0-x86_64-linux.tar.gz
cd ts3level-v0.1.0-x86_64-linux
sudo install -m755 bin/ts3level     /usr/local/bin/
sudo install -m755 bin/ts3level-gui /usr/local/bin/
sudo cp -r share/locale/*           /usr/share/locale/

Verifizieren mit ts3level --list-devices. Die volle Installationsanleitung inklusive Rechte-Setup und Troubleshooting steht im Repo unter docs/installation.md.

Bekannte Grenzen und Roadmap

Ehrlich bleiben:

  • 2,4 GH/s auf der 4060 Ti sind nur ungefähr ein Achtel dessen, was thissepic mit hand-optimierten Kernels rausholt. Midstate-Precompute, In-Place-Counter-Inkrement und LOP3.LUT-PTX-Tricks würden das in den 10 GH/s-Bereich heben. Steht auf der Roadmap, aber nicht für den ersten Release.
  • v0.1 ist NVIDIA-only. Das HashEngine-Trait ist allerdings genau dafür gedacht: ein OpenCL-Backend für AMD und Intel wäre ein relativ entspannter Drop-In.
  • Kein Flatpak bisher. Wer auf älteren Distros sitzt, etwa Ubuntu 22.04 mit glibc 2.35, muss aus den Quellen bauen. Lässt sich machen, ist aber nicht das Plug-and-Play das man von einem Tarball erwartet.
  • Pascal und älter brauchen den oben erwähnten Einzeiler in build.rs.

Die volle Roadmap mit Effort/Impact-Einschätzung liegt im Repo unter docs/roadmap.md.

Lizenz, Repo, Disclaimer

MIT-Lizenz, Repo unter github.com/Kernel-Error/ts-identities-security-level. Releases inklusive prebuilt Tarball werden auf der Releases-Seite veröffentlicht.

Dieses Tool ist nicht mit der TeamSpeak Systems GmbH affiliiert, von ihr endorsed oder gesponsort. „TeamSpeak“ ist eine Wortmarke der TeamSpeak Systems GmbH und wird hier ausschließlich beschreibend verwendet.

Das Tool kommuniziert nicht mit TS-Servern, umgeht keine Zugangsbeschränkungen, rechnet ausschließlich offline auf einer Datei, die dem Anwender selbst gehört. Algorithmisch dasselbe was der offizielle Client auch tut, nur eben auf der GPU. §69d UrhG (Beobachtung, Untersuchung und Testen von Software) deckt die Black-Box-Analyse ab, aus der die Algorithmus-Specs in Tools wie landave/TSIdentityTool über Jahre öffentlich entstanden sind. §202c StGB greift nicht: kein Bypass einer Zugangskontrolle, keine fremden Systeme, eigene Daten. Mein Versuch einer rechtliche Einordnung steht im Repo unter docs/legal.md.

Siehe auch: Voltcraft CM 2016 mit GTK4-GUI unter Linux (anderes GTK4-Projekt, andere Hardware), peon-ping Sound-Benachrichtigungen (kleines Rust-Tool fuer den Desktop).

Fragen, Kommentare oder Verbesserungsvorschläge gerne über die fragen-Seite oder als Issue im Repo.

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.

Quantis USB – Alter Quantenzufall aus der Schublade

Ich hatte noch einen Quantis USB in der Schublade liegen. Einen Hardware-Quantenzufallsgenerator von ID Quantique aus Genf. Ein Gerät, das echten Zufall erzeugt. Nicht pseudo, nicht algorithmisch, nicht „irgendwie aus Interrupts zusammengewürfelt“, sondern auf Basis von Quantenphysik. Fundamental unvorhersagbar.

Image of quantis usb

Nachdem ich in den letzten Beiträgen OpenSSH und Postfix/Dovecot mit Post-Quantum-Kryptografie abgesichert habe, fiel mir wieder ein: PQC schützt die Algorithmen vor Quantencomputern. Schön und gut. Aber was ist eigentlich mit der Zufallsquelle, die diese Algorithmen füttert? Zeit, das Teil mal wieder anzuschließen und zu schauen, was es taugt.

Was steckt in dem Gerät?

Der Quantis USB von ID Quantique ist ein sogenannter Quantum Random Number Generator, kurz QRNG. Das Prinzip dahinter: Ein Photonendetektor misst quantenoptisches Vakuumrauschen. Das sind Fluktuationen im elektromagnetischen Feld, die nach den Gesetzen der Quantenmechanik fundamental zufällig sind. Nicht „fast zufällig“ oder „praktisch zufällig“, sondern physikalisch beweisbar unvorhersagbar. Das ist ein wichtiger Unterschied zu allem, was ein Algorithmus je leisten kann. Dazu gleich mehr.

Das Gerät selbst ist fast schon enttäuschend simpel. USB 2.0 High-Speed, ein einziger Bulk-IN-Endpoint (0x86), 512 Bytes pro Read, rund 4 Mbit/s Durchsatz. Flashbare Firmware gibt es nicht. Die „Intelligenz“ steckt in der Optik und einem FPGA, nicht in Software. Das Ding macht genau eine Sache, und die macht es gut.

Mein Testgerät hat die Seriennummer 132244A410. Der Quantis USB ist inzwischen ein Legacy-Produkt, ID Quantique hat einen Nachfolger mit höherem Durchsatz im Programm. Einen öffentlichen Preis hatte das Gerät nie. „Request a Quote“, wie das bei Nischenprodukten mit Zertifizierungsanforderungen so üblich ist. Das Gerät ist METAS-zertifiziert und war für Kunden gedacht, die Common-Criteria-Anforderungen erfüllen müssen. Vergleichbare QRNGs bewegen sich im Bereich von 900 bis 2.000 Euro. Nicht gerade ein Impulskauf.

Einrichten unter Linux

Angeschlossen an mein Linux Mint 22.3 (Ubuntu 24.04 Basis) meldet sich das Gerät sofort im Kernel-Log:

$ dmesg | tail
usb 1-1: New USB device found, idVendor=0aba, idProduct=0102
usb 1-1: Product: Quantis USB
usb 1-1: Manufacturer: id Quantique
usb 1-1: SerialNumber: 132244A410

Kein spezieller Treiber nötig. Das ist ein generisches USB-Bulk-Device, der Kernel erkennt es und das war’s. Die proprietäre libquantis von ID Quantique kann man sich komplett sparen. Man kann direkt mit pyusb auf den Endpoint zugreifen. So mag ich das.

Damit das auch ohne Root funktioniert, legt man eine udev-Regel an:

# /etc/udev/rules.d/99-quantis.rules
SUBSYSTEM=="usb", ATTR{idVendor}=="0aba", ATTR{idProduct}=="0102", MODE="0666", GROUP="plugdev", TAG+="uaccess"

Danach:

$ sudo udevadm control --reload-rules && sudo udevadm trigger

Gerät abstecken, wieder anstecken, fertig. Ab jetzt kann jeder Benutzer in der Gruppe plugdev auf das Gerät zugreifen.

Daten lesen mit Python

Zum Auslesen reicht das Paket python3-usb (pyusb). Installieren via apt install python3-usb, falls nicht vorhanden. Dann braucht man erstaunlich wenig Code:

import usb.core, usb.util

dev = usb.core.find(idVendor=0x0ABA, idProduct=0x0102)
dev.set_configuration()

cfg = dev.get_active_configuration()
intf = cfg[(0, 0)]
ep = usb.util.find_descriptor(
    intf,
    custom_match=lambda e:
        usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN
)

data = ep.read(512, timeout=5000)
print(f"{len(data)} Bytes Quantenzufall gelesen")

Das ist alles. USB öffnen, Configuration setzen, den einen IN-Endpoint finden, 512 Bytes lesen. Fertig. Kein SDK, keine Bibliothek, kein Account, kein Cloud-Dienst. USB rein, Bytes raus.

Wichtig: Immer volle 512-Byte-Blöcke lesen (wMaxPacketSize). Wer weniger anfordert, bekommt USB-Overflow-Fehler. Das Gerät kennt keine halben Sachen. Es produziert kontinuierlich Zufallsdaten und schiebt sie in den USB-Puffer. Die müssen abgeholt werden, so wie sie kommen.

Für den Test habe ich das Ganze in eine Schleife gepackt und 100.000 Bytes gesammelt. Parallel dazu 100.000 Bytes aus /dev/urandom. Beide Datensätze dann durch dieselben statistischen Tests gejagt.

Der Test: Quantis vs. /dev/urandom

Jetzt wird’s spannend. Wie gut ist echter Quantenzufall im Vergleich zum Software-PRNG des Linux-Kernels?

Spoiler: Statistisch seht ihr keinen Unterschied. Und genau das ist der Punkt.

MetrikQuantis USB/dev/urandom
Shannon-Entropie7,998513 Bits/Byte7,998077 Bits/Byte
Maximum (theoretisch)8,0000008,000000
Effizienz99,9814 %99,9760 %
Chi² (Byte-Verteilung)205,8267,3
Erwartet (Chi²)~255 ± 23~255 ± 23
Bit-Balance (Anteil Einsen)49,975 %50,018 %
Serielle Korrelation+0,001230+0,003801
Längster Bit-Run (10 kB)15 Bits21 Bits
Erwarteter Run~16~16

Die Shannon-Entropie liegt bei beiden Quellen über 99,97 % des theoretischen Maximums von 8 Bit pro Byte. Das ist hervorragend. Die Chi²-Werte zeigen eine gleichmäßige Byte-Verteilung, beide liegen im erwarteten Bereich um 255. Die Bit-Balance ist nahezu perfekt bei 50/50, die serielle Korrelation praktisch null.

In den einfachen Tests schneidet der Quantis sogar minimal besser ab: niedrigere Korrelation, gleichmäßigere Verteilung, kürzerer maximaler Bit-Run. Aber ehrlich gesagt liegt das im statistischen Rauschen. Bei 100.000 Bytes Sample-Größe kann man keine belastbare Aussage über die Überlegenheit einer Quelle treffen. Man müsste Millionen oder Milliarden Bytes testen und Testsuiten wie die NIST SP 800-22 oder Dieharder durchlaufen lassen, um wirklich statistisch signifikante Unterschiede zu finden.

Heißt das, der Quantis ist überflüssig? Nein. Denn der Unterschied liegt nicht in der Statistik.

Wo liegt dann der echte Unterschied?

Die spannende Frage ist nicht, ob die Zahlen „zufälliger“ sind, sondern warum sie es sind.

/dev/urandom verwendet intern ChaCha20, einen deterministischen CSPRNG (Cryptographically Secure Pseudo-Random Number Generator). Der initiale Seed kommt aus der Kernel-Entropie: Hardware-Interrupts, Timing-Jitter, Geräte-Events, und seit einigen Jahren auch RDRAND/RDSEED aus der CPU, falls vorhanden. Das funktioniert in der Praxis hervorragend und ist extrem gut untersucht.

Aber es bleibt ein Algorithmus mit einem internen State. Wer diesen State kennt (und sei es nur theoretisch), kann alle zukünftigen Outputs berechnen. Das ist kein realistisches Angriffsszenario für euren Laptop. Aber es ist eine fundamentale Eigenschaft: Die Sicherheit von /dev/urandom basiert auf Berechnungsannahmen. Man nimmt an, dass ChaCha20 nicht effizient invertierbar ist. Stand heute stimmt das. Aber es ist eine Annahme, kein Beweis.

Der Quantis hingegen erzeugt Zufall aus Quantenvakuum-Fluktuationen. Da gibt es keinen Algorithmus, keinen State, keinen Seed. Die Unvorhersagbarkeit ist nicht durch die Komplexität eines Algorithmus geschützt, sondern durch die Gesetze der Quantenmechanik. Kein Angreifer, egal mit welcher Rechenleistung und egal mit wie viel Zeit, kann die nächsten Bits vorhersagen. Auch kein Quantencomputer. Das ist nicht berechnungstheoretisch sicher, sondern informationstheoretisch sicher. Die höchste Sicherheitskategorie, die es gibt.

Klingt akademisch? Zum Teil. Für den Alltag auf eurem Desktop oder Server reicht /dev/urandom völlig aus. Es gibt keinen bekannten praktischen Angriff darauf, und Linux‘ CSPRNG ist schnell, überall verfügbar und gut gewartet.

Aber es gibt Szenarien, in denen der Unterschied real zählt:

  • Erzeugung kryptografischer Schlüssel mit höchsten Sicherheitsanforderungen
  • Seeding von HSMs (Hardware Security Modules), die selbst keine eigene Entropiequelle haben
  • Regulatorische und Zertifizierungsanforderungen, also Common Criteria, FIPS-Validierung, BSI-Vorgaben
  • Wissenschaftliche Experimente, die physikalisch echten Zufall benötigen (z. B. Quantenoptik, Monte-Carlo-Simulationen)
  • Quantenschlüsselaustausch (QKD), ein Bereich in dem ID Quantique ebenfalls aktiv ist

Das größere Bild: QRNG und PQC

Post-Quantum Cryptography schützt kryptografische Algorithmen davor, von Quantencomputern gebrochen zu werden. ML-KEM für den Schlüsselaustausch, ML-DSA für Signaturen. Das ist die eine Seite der Medaille.

Die andere Seite ist die Zufallsquelle. Ein kryptografischer Algorithmus kann noch so quantensicher sein. Wenn der Zufall, mit dem Schlüssel erzeugt werden, vorhersagbar oder manipulierbar ist, hilft das alles nichts. Der Zufall ist das Fundament, auf dem alles andere aufbaut.

Ein QRNG schützt genau diesen Angriffsvektor. Beides zusammen, PQC-Algorithmen und eine quantenphysikalische Zufallsquelle, ergibt ein quantum-safe Gesamtsystem. Das ist heute für die meisten von uns Overkill. Aber die Bausteine existieren, sie sind verfügbar, und es schadet nicht zu wissen, wie sie funktionieren.

Übrigens: Wer jetzt denkt „dann stecke ich den Quantis in meinen Server und bin sicher“, der macht es sich zu einfach. Die Vertrauensfrage verschiebt sich nur. Woher weiß ich, dass das Gerät tatsächlich Quantenzufall liefert und nicht einfach einen internen PRNG hat? Bei einem zertifizierten Gerät wie dem Quantis gibt es dafür Prüfberichte. Aber Vertrauen in Hardware bleibt immer ein Thema. Das ist bei Intel RDRAND nicht anders.

Einbindung ins System

Für die Vollständigkeit: Der Quantis USB lässt sich über rng-tools (rngd) als zusätzliche Entropiequelle in den Kernel-Entropiepool einbinden. Für Server mit hohem Entropie-Bedarf, also TLS-Terminierung unter Last, Massenerzeugung von Schlüsseln oder VPN-Gateways, kann das sinnvoll sein.

Ich habe das auf meinem Desktop nicht gemacht. Brauche ich dort nicht. Aber die Möglichkeit steht im Raum, falls jemand von euch einen Quantis oder ein vergleichbares Gerät an einen Server hängen möchte.

Fazit

Ein alter Hardware-QRNG, ein USB-Port, ein paar Zeilen Python, und man hat echten Quantenzufall auf dem Tisch. Statistisch nicht unterscheidbar von /dev/urandom, aber fundamental anders in der Entstehung. Die Sicherheit kommt nicht aus einem Algorithmus, sondern aus der Physik. Informationstheoretisch statt berechnungstheoretisch. Ein Unterschied, der in den allermeisten Fällen keine praktische Rolle spielt. Aber ein verdammt eleganter.

Für euren Desktop braucht ihr das nicht. Aber verstehen, warum es existiert und wie es sich einordnet, gerade im Kontext von Post-Quantum-Kryptografie, das lohnt sich. Warum denke ich jetzt an CIA und MAD? ;-D

Wie haltet ihr es mit euren Zufallsquellen? Vertraut ihr blind auf /dev/urandom, oder habt ihr euch schon mal Gedanken über die Entropiequelle dahinter gemacht?

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.

Update 01.05.2026: Mit Postfix 3.11.1 (FreeBSD 15.0-RELEASE-p7) und OpenSSL 3.5 sind sowohl die main.cf-Variante als auch dieser master.cf-Workaround Geschichte – der Built-in-Default ?X25519MLKEM768:DEFAULT macht beides überflüssig. Wire-Mitschnitt, ClientHello-Größenvergleich und externe Verifikation im Folgebeitrag.

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

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

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

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

Volksverschlüsselung wird eingestellt

An mir flog gerade die Information vorbei, dass die Volksverschlüsselung zum 31.01.2026 eingestellt wird. Ich habe mir dort vor ein paar Jahren mal ein S/MIME-Zertifikat für meine E-Mails geholt. Mir hat der Ansatz gefallen, dass man sich mit seinem neuen Personalausweis und dessen Online-Funktion dort legitimieren kann und im Anschluss sein Zertifikat bekommt. Für mich hat diese Abhängigkeit absolut Sinn ergeben. Wir haben ja schon alle einen Perso mit Online-Funktion und PIN und was weiß ich alles. Also warum nicht auch einfach und schnell Zertifikate darüber erstellen?

Screenshot der Meldung zur Einstellung des Dienstes Volksverschlüsselung.

Funktioniert hat das alles wirklich gut – leider mit dem gleichen Problemchen wie auch bei cacert.org: Die Root-Zertifikate sind nicht in den Trust Stores der Betriebssysteme, Browser, Mailclients usw. Signiert man also etwas damit, wird es beim Empfänger als ungültig und unsicher angezeigt, es sei denn, dieser installiert manuell die Root-Zertifikate. Das macht natürlich niemand. Damit war die Volksverschlüsselung für mich genauso raus wie leider auch cacert.org.

Nun hing hinter der Volksverschlüsselung ebenfalls das Fraunhofer-Institut. Meine Hoffnung war, dass über diesen Weg am Ende doch mal die Root-Zertifikate in die Trust Stores kommen. Aber leider nicht.

Die Diskussion, ob man Trust Stores wirklich braucht, ob man sie vor allem vorgefüllt braucht, mache ich genauso wenig auf wie DNSSEC und TLSA, ok? Denn uns allen ist ja inzwischen klar, dass wir es „sicher“ haben könnten – wenn man nur nicht so scheiß viel Geld mit den CAs verdienen könnte. Denn die ganzen CAs zahlen ja schon ein paar Euro, um in die Trust Stores zu kommen.

Soviel dann also zur Volksverschlüsselung.

Siehe auch: S/MIME per DNS mit SMIMEA

Fragen? Einfach melden.

S/MIME-Zertifikat per DNS veröffentlichen – SMIMEA

SMIMEA — S/MIME-Zertifikat per DNS veröffentlichen

Siehe auch: Volksverschlüsselung wird eingestellt, OPENPGPKEY: GPG-Schlüssel direkt im DNS veröffentlichen, Kleiner Nachtrag zum GlobalSign S/MIME Zertifikat…

Mal wieder soweit: Mein aktuelles S/MIME-Zertifikat zum Signieren von E-Mails läuft aus. Also habe ich mir ein neues besorgt. Da GlobalSign keine Class-2-Zertifikate mehr für Privatpersonen anbietet, musste ich die CA wechseln. Durch Zufall bin ich auf SSLplus gestoßen – die haben echt gute Angebote für alle möglichen Zertifikate. Aber darum soll es in diesem Beitrag nicht gehen.

Wie immer will ich mein Zertifikat öffentlich zugänglich machen, sonst müsste jeder erst eine von mir signierte E-Mail erhalten, bevor er mein Zertifikat hat. Erst dann könnten Absender mir verschlüsselte E-Mails schicken.

Dafür gibt es ein experimentelles RFC 8162, das beschreibt, wie sich ein solches Zertifikat in einer DNSSEC-geschützten Zone veröffentlichen lässt. Natürlich gibt es im Internet wieder zig verschiedene Anleitungen und Wege, um das zu realisieren. Aber nichts wirklich Zuverlässiges, was ich finden konnte. Den DNS-Record für meine Bind9-Zone wieder manuell zu erstellen, hatte ich jedenfalls keine Lust.

Also habe ich zwei kleine Python3-Skripte geschrieben:

smimea_generate_record.py

Erstellt einen kopierbaren RR für die DNS-Zone. Kann interaktiv genutzt werden: Fragt nach E-Mail-Adresse und PEM-Zertifikat. Oder direkt mit Parametern aufgerufen werden. Prüft, ob E-Mail-Adresse und Zertifikat zusammenpassen, und gibt den fertigen Record aus.

./smimea_generate_record.py
Enter the email address: kernel-error@kernel-error.com
Enter the path to the PEM certificate: mail.pem
✅ Email 'kernel-error@kernel-error.com' matches the certificate!

🔹 **Generated BIND9 DNS Record:**

70e1c7d87e825b3aba45e2a478025ea0d91d298038436abde5a4c2d0._smimecert.kernel-error.com. 3600 IN SMIMEA 3 0 0 (
   30820714308204FCA003020102021073C13C478DA7B114B871F00737F1B0FB30
   0D06092A864886F70D01010B0500304E310B300906035504061302504C312130
   1F060355040A0C1841737365636F20446174612053797374656D7320532E412E
   [... komplettes Zertifikat in Hex ...]
   7573CA35477D59B98DE4852065F58FB60E0E620D3E2F5CAD
   )

smimea_lookup.py

Fragt den SMIMEA-Record im DNS ab, lädt das Zertifikat herunter und prüft es mit OpenSSL auf Gültigkeit. Funktioniert interaktiv oder mit übergebenen Werten.

./smimea_lookup.py
Enter the email address: kernel-error@kernel-error.com

Querying DNS for SMIMEA record:
  70e1c7d87e825b3aba45e2a478025ea0d91d298038436abde5a4c2d0._smimecert.kernel-error.com

Certificate saved as smimea_cert.der
Certificate successfully retrieved and verified:

Certificate:
    Data:
        Version: 3 (0x2)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = PL, O = Asseco Data Systems S.A., CN = Certum SMIME RSA CA
        Validity
            Not Before: Mar 13 13:41:55 2025 GMT
            Not After : Mar 13 13:41:54 2027 GMT
        Subject: SN = van de Meer, GN = Sebastian, CN = Sebastian van de Meer,
                 emailAddress = kernel-error@kernel-error.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (4096 bit)
        X509v3 Extended Key Usage:
            E-mail Protection, TLS Web Client Authentication
        X509v3 Key Usage: critical
            Digital Signature, Non Repudiation, Key Encipherment, Data Encipherment
        X509v3 Subject Alternative Name:
            email:kernel-error@kernel-error.com

Beide Skripte findet ihr auf GitHub, damit ihr sie nutzen oder verbessern könnt.

Warum viele Anleitungen falsch sind

Warum habe ich geschrieben, dass ich nichts Zuverlässiges finden konnte? Nun, oft stoße ich auf Anleitungen, die noch auf TYPE53 basieren. Das ist nötig, wenn Bind9 den eigentlichen RR-Type noch nicht kennt – also ein klares Zeichen dafür, dass es sich um eine sehr frühe Implementierung handelt.

Ein weiteres häufiges Problem: Der Hash des Local-Parts wird einfach weggelassen. Stattdessen erfolgen die Abfragen direkt auf _smimecert., was aber falsch ist. Ohne den SHA256-Hash des Local-Parts gibt es keine eindeutige Zuordnung zur jeweiligen E-Mail-Adresse.

Aufbau des SMIMEA-DNS-Records

Der erste Teil — der SHA256-Hash — sorgt dafür, dass nicht einfach jeder direkt aus der DNS-Zone die E-Mail-Adressen auslesen kann. Statt die E-Mail-Adresse im Klartext zu speichern, wird nur der SHA256-Hash des Local-Parts (also der Teil vor dem @) genutzt. Wer die genaue E-Mail-Adresse kennt, kann den passenden DNS-Eintrag finden — aber jemand, der blind durch die Zone scannt, sieht nur Hashes.

Der _smimecert-Prefix zeigt an, dass es sich um einen SMIMEA-Record handelt, ähnlich wie bei ._tcp. für SRV-Records oder _acme-challenge. für Let’s Encrypt. Und schließlich kommt die Domain, zu der die E-Mail-Adresse gehört.

Manuelle Abfrage mit dig

Möchte man die Abfrage manuell durchführen, muss man zuerst den Local-Part der E-Mail-Adresse mit SHA256 hashen. Laut RFC 8162, Abschnitt 3.1 wird der Hash auf die ersten 28 Bytes (56 Hex-Zeichen) gekürzt, um die DNS-Label-Längenbeschränkung von 63 Zeichen (RFC 1035, Abschnitt 2.3.4) einzuhalten:

echo -n "kernel-error" | sha256sum | awk '{print $1}' | cut -c1-56
70e1c7d87e825b3aba45e2a478025ea0d91d298038436abde5a4c2d0

Anschließend die dig-Abfrage:

dig +dnssec +short 70e1c7d87e825b3aba45e2a478025ea0d91d298038436abde5a4c2d0._smimecert.kernel-error.com. SMIMEA
3 0 0 30820714308204FCA003020102021073C13C478DA7B114B871F00737
F1B0FB300D06092A864886F70D01010B0500304E310B30090603550406
[... Zertifikat in Hex ...]

Was bedeuten die Felder?

  • 3 — Usage: End-Entity-Zertifikat (DANE-EE), also für die tatsächliche E-Mail-Verschlüsselung und Signatur
  • 0 — Selector: Das komplette Zertifikat wird gespeichert (alternativ: 1 für nur den Public Key)
  • 0 — Matching Type: Keine Hash-Funktion, das Zertifikat liegt im Klartext vor (alternativ: 1 für SHA-256, 2 für SHA-512)
  • Hex-Werte — Der eigentliche Zertifikatsinhalt in hexadezimaler Darstellung

Manuelle Prüfung auf der Konsole

Den kompletten DNS-Record abrufen, die SMIMEA-Parameter (3 0 0) entfernen und als Hex-Datei speichern:

dig +short 70e1c7d87e825b3aba45e2a478025ea0d91d298038436abde5a4c2d0._smimecert.kernel-error.com SMIMEA | sed 's/^3 0 0 //' | tr -d '[:space:]' > dns_cert.hex

Hex in eine binäre DER-Datei umwandeln und mit OpenSSL anzeigen:

# Hex → DER
xxd -r -p dns_cert.hex dns_cert.der

# Zertifikat anzeigen
openssl x509 -inform DER -in dns_cert.der -text -noout

Verbreitung und Ausblick

SMIMEA ist leider noch immer nicht besonders weit verbreitet. Das liegt daran, dass das RFC noch immer experimental ist, aber auch daran, dass es auf weiteren Techniken aufbaut, die ebenfalls eher selten genutzt werden. Man braucht SMIMEA nur, wenn man überhaupt ein S/MIME-Zertifikat zur Signatur und Verschlüsselung von E-Mails verwendet. Zusätzlich muss die Domain per DNSSEC geschützt sein — und dann muss auch noch der zusätzliche Mehrwert von SMIMEA verstanden werden.

Denn SMIMEA verteilt nicht nur die Zertifikate, sondern macht einen direkt initial verschlüsselt erreichbar. Wenn man der Empfänger einer solchen signierten Nachricht ist, kann man das Zertifikat zudem gegen eine vertrauenswürdige DNS-Zone halten und sich so vergewissern, dass es wirklich die Signatur des Absenders ist — ähnlich wie bei TLSA/DANE.

Die Implementierung ist aktuell sehr überschaubar. Es gibt Milter für beispielsweise Postfix oder Plugins für Thunderbird, aber vor allem im Enterprise-Umfeld ist mir momentan keine funktionierende Lösung bekannt.

Eigentlich wollte ich doch nur schnell schreiben, dass ich da zwei Python-Skripte zusammengebastelt habe — und am Ende ist es doch wieder so ein riesiges Ding geworden. Aber ich denke, vor allem der Teil mit dem gekürzten Hash des Local-Parts ist wichtig zu erklären. Das ist echt eine verrückte Konstruktion. Klar, das hat seinen Sinn, aber zumindest ich bin damals genau an diesem Punkt hängen geblieben.


Das einzig korrekt funktionierende Online-Tool, das ich finden konnte: co.tt/smimea.cgi. Alle anderen sind nicht erreichbar, halten sich nicht ans RFC oder ich war zu blöde, sie zu bedienen. Fragen? Einfach melden.

Sicherheitslücken melden: Mein Umgang mit einem Vulnerability Report

Vulnerability Report

Vor Kurzem habe ich einen Vulnerability Report erhalten. Ich freue mich über solche Hinweise. Sie helfen mir, mein Setup zu verbessern, bevor jemand eine Schwachstelle tatsächlich ausnutzt.

Der Report

Subject: Vulnerability Report: Vulnerable System Detected at openpgpkey.kernel-error.com

Hello Team,

I have identified a security issue in your system related to a vulnerability
(CVE-2023-48795) in Terrapin.

Vulnerability Details:
- CVE Identifier: CVE-2023-48795
- Vulnerability Type: javascript
- Severity: medium
- Host: openpgpkey.kernel-error.com
- Affected Port: 22

[...]

Best Regards,
Security Team

Erste Einschätzung

Terrapin hatte ich eigentlich schon überall gepatcht. Dann der Hinweis auf openpgpkey.kernel-error.com. Die Domain existiert als CNAME und gehört zur Web Key Directory (WKD), damit GPG-Keys automatisiert abgerufen werden können. Ich habe das als CNAME zu wkd.keys.openpgp.org angelegt, weil dieser Keyserver eine E-Mail-Validierung beim Hochladen durchführt.

Der betroffene SSH-Server gehört also gar nicht zu meiner Infrastruktur. Ich kann selbst nichts tun.

Vulnerability Type: JavaScript bei einem SSH-Problem auf Port 22? Der Finder hat vermutlich sein Standard-Template benutzt und nicht angepasst. Aber ich wollte trotzdem prüfen, ob seine Einschätzung zum SSH-Server zutrifft:

# ssh-audit openpgpkey.kernel-error.com (gekürzt)
(gen) banner: SSH-2.0-OpenSSH_8.4p1 Debian-5+deb11u3
(gen) software: OpenSSH 8.4p1

(cve) CVE-2021-41617  -- (CVSSv2: 7.0) privilege escalation via supplemental groups
(cve) CVE-2016-20012  -- (CVSSv2: 5.3) enumerate usernames via challenge response

(kex) ecdh-sha2-nistp256  -- [fail] suspected NSA backdoor
(kex) ecdh-sha2-nistp384  -- [fail] suspected NSA backdoor
(kex) kex-strict-s-v00@openssh.com  -- [info] Terrapin counter-measure present

(key) ssh-rsa (2048-bit)  -- [fail] broken SHA-1 hash algorithm
(key) ecdsa-sha2-nistp256 -- [fail] suspected NSA backdoor

(mac) hmac-sha1-etm@openssh.com  -- [fail] broken SHA-1 hash algorithm
(mac) hmac-sha1                  -- [fail] broken SHA-1

Sieht tatsächlich nicht optimal aus. NIST-Kurven, SHA-1, 2048-Bit RSA. Der Hinweis war also nicht unberechtigt. Ich habe dem Finder freundlich und dankbar geantwortet, aber darauf hingewiesen, dass das System nicht zu meiner Infrastruktur gehört. Die relevanten WHOIS-Informationen zur IP habe ich mitgeschickt.

Die Antwort

Thank you for your answer.

Let me know if you need anything else from myside

I hope this type of hard efforts deserves something reward

„Hard efforts“. Ich will das nicht schlechtreden. Im beruflichen Umfeld hätte ich mich vielleicht sogar für eine Kleinigkeit stark gemacht. Aber hier geht es um meine private Infrastruktur, und dann noch mit dem JavaScript-Hinweis und der Meldung zu einem fremden System. Das wirkt oberflächlich. Also habe ich ihn freundlich darauf hingewiesen.

Wie man mit Vulnerability Reports umgehen sollte

Wenn euch eine solche Nachricht erreicht: Schnell und freundlich reagieren. Den Report ernst nehmen, bewerten und eine angemessene Rückmeldung geben. Die Mühe des Finders wertschätzen. Zwei Wochen später mit „Anzeige ist raus!“ zu antworten wäre der falsche Weg. Es ist für jemanden deutlich aufwendiger, eine Meldung zu schreiben, als das Ganze in ein Darknet-Forum zu posten und dort ein paar XMR einzusammeln.

Macht es den Leuten einfach, euch zu kontaktieren. Eine security.txt oder klare Kontaktinformationen für eine Security-Mailbox helfen ungemein. Hauptsache, jemand kann seinen Report unkompliziert abgeben, und er wird von jemandem gelesen, der das bewerten kann.

Mehr zum Thema security.txt:
securitytxt.org | Wikipedia | BSI Allianz für Cybersicherheit

Zum Vergleich: Mein eigener SSH-Server

So sieht meine SSH-Konfiguration von außen aus:

# ssh-audit bsd01.kernel-error.de (gekürzt)
(gen) banner: SSH-2.0-OpenSSH_9.7 DemMeisterSeinRennAuto
(gen) software: OpenSSH 9.7

(kex) sntrup761x25519-sha512@openssh.com  -- [info] Post-Quantum Key Exchange
(kex) curve25519-sha256                   -- [info] default since OpenSSH 6.4
(kex) diffie-hellman-group16-sha512       -- [info] available since OpenSSH 7.3
(kex) kex-strict-s-v00@openssh.com        -- [info] Terrapin counter-measure

(key) ssh-ed25519                         -- [info] available since OpenSSH 6.5

(enc) aes256-gcm@openssh.com              -- [info] available since OpenSSH 6.2
(enc) aes128-gcm@openssh.com              -- [info] available since OpenSSH 6.2

(mac) hmac-sha2-256-etm@openssh.com       -- [info] available since OpenSSH 6.2
(mac) hmac-sha2-512-etm@openssh.com       -- [info] available since OpenSSH 6.2

Keine NIST-Kurven, kein SHA-1, kein RSA, Post-Quantum Key Exchange mit sntrup761. Nur Ed25519 als Host Key. Der Banner ist übrigens Absicht.

Fragen? Einfach melden.

BitLocker im Dual-Boot: Systemplatte auf Passwortschutz umstellen

Die Feiertage sind da, und ich hatte tatsächlich etwas Zeit zum Zocken. Gearbeitet wird unter Linux, gezockt unter Windows. Dafür habe ich mein Windows auf einer gesonderten SSD installiert, verschlüsselt mit BitLocker.

Illustration eines Dual-Boot-Systems mit Linux und Windows, dargestellt durch die Logos beider Betriebssysteme. Die Windows-Seite zeigt ein BitLocker-Schloss-Symbol, das auf die Verschlüsselung der Systemplatte hinweist.

Die SSD hat irgendwann aufgegeben, Windows musste neu drauf. Backup spare ich mir, ist eh nur zum Zocken. Windows 11, Treiber, Games, fertig. Aber: BitLocker fragt bei jedem Start nach dem Wiederherstellungsschlüssel, wenn ich vorher in Linux war. Das hatte ich schon mal, am gleichen Rechner. Damals nicht aufgeschrieben. Diesen Fehler mache ich nicht noch mal.

Das Problem

BitLocker mit TPM merkt, wenn sich die Boot-Kette ändert. Wechselt man über GRUB zwischen Linux und Windows, sieht das TPM eine Änderung und verlangt den Recovery Key. Die Lösung: TPM als Schlüsselschutz entfernen und stattdessen ein Passwort setzen. Dann fragt BitLocker bei jedem Start nach dem Passwort, egal ob vorher Linux oder Windows lief.

Gruppenrichtlinie anpassen

Damit Windows erlaubt, TPM vom Betriebssystemvolume zu entfernen, muss eine Gruppenrichtlinie geändert werden. Win + S, nach Gruppenrichtlinie bearbeiten suchen, dann:

ComputerkonfigurationAdministrative VorlagenBitLocker-LaufwerkverschlüsselungBetriebssystemlaufwerkeZusätzliche Authentifizierung beim Start anfordern

Editor für lokale Gruppenrichtlinien mit geöffneter Einstellung 'Zusätzliche Authentifizierung beim Start anfordern' unter 'Computerkonfiguration > Administrative Vorlagen > BitLocker-Laufwerkverschlüsselung > Betriebssystemlaufwerke'. Die Option ist aktiviert, um die BitLocker-Verschlüsselung ohne TPM zu ermöglichen.
Detailansicht der Gruppenrichtlinieneinstellung 'Zusätzliche Authentifizierung beim Start anfordern'. Die Option 'BitLocker ohne kompatibles TPM zulassen' ist aktiviert, und die Konfiguration für TPM-Start, Systemstartschlüssel und PIN wird angezeigt. Die Beschreibung auf der rechten Seite erläutert die Auswirkungen der Richtlinie.

Den Haken bei „BitLocker ohne kompatibles TPM zulassen“ setzen. Danach im Administrator-Terminal die Richtlinie anwenden:

gpupdate /force

Schlüsselschutz umstellen

Zuerst den aktuellen Status prüfen:

manage-bde -protectors -get C:

In meinem Fall waren drei Schlüsselschutzvorrichtungen konfiguriert: Numerisches Kennwort, TPM und PIN, sowie TPM. Alle müssen weg, damit nur noch das Passwort übrig bleibt.

Schritt für Schritt im Administrator-Terminal:

# 1. Alle Schutzvorrichtungen deaktivieren
manage-bde -protectors -disable C:
# 2. Vorhandene Schutzvorrichtungen anzeigen (IDs notieren)
manage-bde -protectors -get C:
# 3. Jede Schutzvorrichtung einzeln löschen (ID aus Schritt 2)
manage-bde -protectors -delete C: -id {TPM-UND-PIN-ID}
manage-bde -protectors -delete C: -id {TPM-ID}
manage-bde -protectors -delete C: -id {NUMERISCHES-KENNWORT-ID}
# 4. Prüfen, dass keine mehr da sind
manage-bde -protectors -get C:
# Erwartete Ausgabe: "Es wurden keine Schlüsselschutzvorrichtungen gefunden."
# 5. Passwort als neue Schutzvorrichtung hinzufügen
manage-bde -protectors -add C: -password
# (Passwort eingeben und bestätigen)
# 6. Schutz wieder aktivieren
manage-bde -protectors -enable C:

Ergebnis prüfen

manage-bde -status
Volume "C:" [System]
    Verschlüsselt (Prozent):      100,0 %
    Verschlüsselungsmethode:      XTS-AES 128
    Schutzstatus:                 Der Schutz ist aktiviert.
    Schlüsselschutzvorrichtungen:
        Kennwort

Nur noch Kennwort als Schutzvorrichtung. Beim nächsten Start fragt BitLocker nach dem Passwort, unabhängig davon ob vorher Linux oder Windows lief. GRUB stört nicht mehr.

Wer auf der Linux-Seite ebenfalls verschlüsselt: Bei LUKS kann ein falsches Tastaturlayout bei der Passphrase-Abfrage für Verwirrung sorgen.

Fragen? Einfach melden.

« Ältere Beiträge

© 2026 -=Kernel-Error=-RSS

Theme von Anders NorénHoch ↑