Kleines Update zur Mini-Saga rund um Post-Quantum-TLS auf Postfix. Zur Erinnerung: Im Original-Beitrag vom Februar stand tls_eecdh_auto_curves noch global in der main.cf, mit X25519MLKEM768 an erster Stelle. Im Nachtrag vom 1. April im selben Beitrag kam dann die Trennung in master.cf, weil die globale Variante einen ClientHello jenseits 1400 Bytes erzeugt und damit gegen manche Zielserver auf die Nase faellt. Dank smtp_tls_security_level = may faellt Postfix in dem Fall stillschweigend auf Plaintext zurueck, eure Mail geht raus, aber unverschluesselt. Klingt akademisch, ist es nicht.
Mit dem letzten regulaeren pkg upgrade auf FreeBSD 15.0-RELEASE-p7 ist Postfix 3.11.1 eingezogen. Damit erledigen sich beide Workarounds. Der Built-in-Default ist jetzt korrekt, und der ganze master.cf-Override-Block aus dem April darf ersatzlos raus. Wer beide Vorgaengerbeitraege nachgebaut hat, darf jetzt rueckwaerts wieder aufraeumen — mit zwei postfix reload ist man durch.
Was Postfix 3.11.1 mitbringt
Postfix 3.11.1 setzt den Built-in-Default fuer tls_eecdh_auto_curves auf:
tls_eecdh_auto_curves = ?X25519MLKEM768:DEFAULT
Das Fragezeichen vor dem Gruppennamen ist OpenSSL-3.5+ Syntax fuer Delayed Key-Share, Postfix reicht das 1:1 durch. Bedeutung: X25519MLKEM768 wird in der TLS-Extension supported_groups annonciert, der eigentliche KeyShare aber NICHT vorab im ClientHello generiert. Der KeyShare wird erst materialisiert, wenn der Server per HelloRetryRequest gezielt danach fragt. ClientHello bleibt damit klein, MLKEM wird trotzdem ausgehandelt sobald die Gegenstelle es unterstuetzt — und faellt sauber auf eine klassische Kurve zurueck wenn nicht.
Damit erledigt sich die Begruendung fuer die Client/Server-Trennung aus dem April-Update. Inbound und Outbound koennen denselben Default nutzen — kein Bloat in eine Richtung, kein Verzicht auf PQC in die andere.
master.cf aufraeumen
Aus master.cf werden alle -o tls_eecdh_auto_curves=... Overrides ersatzlos entfernt. Im konkreten Setup hier waren das fuenf Stellen: smtp/inet, submission/inet, der eigene 2525/inet-Listener, smtps/inet und smtp/unix fuer Outbound. Diese Zeilen alle raus:
smtp inet n - n - - smtpd -o tls_eecdh_auto_curves=X25519MLKEM768,X25519,prime256v1,secp384r1 # raus submission inet n - n - - smtpd -o tls_eecdh_auto_curves=X25519MLKEM768,X25519,prime256v1,secp384r1 # raus smtps inet n - n - - smtpd -o tls_eecdh_auto_curves=X25519MLKEM768,X25519,prime256v1,secp384r1 # raus smtp unix - - n - - smtp -o tls_eecdh_auto_curves=X25519,X25519MLKEM768,prime256v1,secp384r1 # raus
In der main.cf wird ebenfalls nichts mehr explizit gesetzt — der Built-in-Default greift. Verifikation:
# postconf tls_eecdh_auto_curves tls_eecdh_auto_curves = ?X25519MLKEM768:DEFAULT # postconf -P | grep tls_eecdh_auto_curves (leer -- kein Override aktiv)
Anschliessend postfix reload. Kein Restart, kein Service-Ausfall, keine offenen Verbindungen verloren. Dovecot bleibt unveraendert, dort sieht es weiter so aus:
ssl_curve_list = X25519MLKEM768:X25519:prime256v1:secp384r1
Dovecot ist nur Server, kein Outbound-Client. Da gibt es kein ClientHello-Bloat-Problem.
Was DEFAULT konkret enthaelt
Hinter dem schlichten Wort DEFAULT versteckt OpenSSL 3.5.4 eine konkrete Liste. Ich habe sie aus dem extension_type=supported_groups-Block eines Test-Handshakes herausgezogen:
X25519MLKEM768 (4588) # mit ?-Prefix vom davorgestellten Eintrag, dedupliziert X25519 (29) secp256r1 (23) # prime256v1 X448 (30) secp384r1 (24) secp521r1 (25) ffdhe2048 (256) ffdhe3072 (257)
Damit ist klar warum man den Default in der Regel ungeaendert laesst — er enthaelt MLKEM768 hybrid an erster Stelle, alle gaengigen klassischen Kurven dahinter und ein paar FFDHE-Gruppen als Backup. Wer es minimaler will und auf FFDHE verzichten kann, setzt explizit ?X25519MLKEM768:X25519:prime256v1:secp384r1. Notwendig ist das nicht.
Verifikation #1: ClientHello-Groessen mit openssl s_client
Um das Delayed-Key-Share-Verhalten ohne Glaubensfrage zu pruefen, vier verschiedene Group-Listen gegen einen MLKEM-faehigen MX gefahren — Gmail tut sich da als Test-Target ganz gut. Die Schleife direkt zum Nachstellen:
for groups in
'?X25519MLKEM768:DEFAULT'
'X25519MLKEM768:X25519:prime256v1:secp384r1'
'X25519:X25519MLKEM768:prime256v1:secp384r1'
'X25519'; do
echo "=== groups=$groups ==="
openssl s_client -connect gmail-smtp-in.l.google.com:25 -starttls smtp
-groups "$groups" -msg </dev/null 2>&1
| grep 'Handshake.*ClientHello'
done
In der -msg-Ausgabe steht pro Handshake-Record eine Zeile wie
>>> TLS 1.3, Handshake [length 014e], ClientHello
Die hex-Length ist die Groesse des TLS-Records. Tabellarisch:
| Group-Liste | Initial ClientHello | Nach HRR |
|---|---|---|
?X25519MLKEM768:DEFAULT (Postfix 3.11 Default) | 334 Bytes (0x014e) | 1518 Bytes (0x05ee) |
X25519MLKEM768:X25519:prime256v1:secp384r1 (alt-Inbound) | 1510 Bytes (0x05e6) | — |
X25519:X25519MLKEM768:prime256v1:secp384r1 (alt-Outbound) | 326 Bytes (0x0146) | 1510 Bytes (0x05e6) |
X25519 (klassisch ohne PQC) | 320 Bytes (0x0140) | — |
Interpretation: Der neue Default in Zeile 1 verhaelt sich praktisch identisch zum alten Outbound-Workaround in Zeile 3 — kleines Initial-ClientHello mit nur klassischen KeyShares, MLKEM wird erst ueber HelloRetryRequest aktiviert. Der alte Inbound-Style in Zeile 2 hingegen pusht den rund 1184 Byte grossen ML-KEM-768-PublicKey schon im ersten ClientHello mit und sprengt damit die 1400-Byte-Schwelle, an der einige Mail-Frontends regelmaessig stoppen. Genau das Verhalten, das im Original-Beitrag und im April-Update als Problem identifiziert wurde — jetzt nativ vermieden. Ich muss ja grade etwas grinsen…. Meine Frau ist immer mal wieder so cool und liest mir meine neuen Beiträge vor. So höre ich sie noch mal und das Feedback meine Frau ist mich auch wichtig. Sie versteht aber im Grunde kein Wort und ersetzt immer jegliche Codeblöcke oder zu technische Specs gegen „bla / irgendwas“. Ich glaube dieser Beitrag wird einer der schwierigen, dennoch wird sie es für mich lesen und das ist toll 😀 Danke!
Verifikation #2: tcpdump auf dem Wire
Wer dem OpenSSL-Output nicht glaubt, kann sich das ClientHello auch direkt vom Draht holen. Damit der Capture funktioniert, sind zwei Stolpersteine zu beachten:
- tcpdump muss auf dem Host laufen, nicht in der Jail. In FreeBSD-Jails ist der direkte BPF-Zugriff aufs Interface defaultmaessig abgeschaltet (
Packet capture is not supported on that device). tcpdump auf der Host-Seite sieht den Jail-Verkehr aber sauber ueber das geteilte Interface. - openssl s_client greift per Default zu IPv6, wenn der Zielhost AAAA-Records hat. Ein reiner IPv4-Filter bleibt dann leer (
0 packets captured / 689 packets received by filter— ich gestehe, der Moment war kurz verwirrend). Loesung: openssl mit-4zwingen oder den Filter dual-stack auslegen. Ja klar, ist ja eine selbstverständliche Kleinigkeit. Sehe ich auch so, nur warum habe ich dann 5 Minuten lang, mit einem komplett leeren Blick, wie ein Eichhörnchen auf meinen Monitor geschaut und nicht verstanden, warum ich keine packets sehe? Bis zu dem Moment habe ich mich ja für einen kurzem Moment cool gefühlt, das alles so weit verstanden zu haben. Tja und dann? Dann 5 Minuten lang: „kein Anschluss unter dieser Nummer“…
Konkret im Test gelaufen (smtp-Jail mit IPv4 148.251.30.205, IPv6 2a01:4f8:262:4716::25, Interface igb0):
tcpdump -i igb0 -n -s 0 -c 8 -w /tmp/ch.pcap 'dst port 25 and (host 148.251.30.205 or host 2a01:4f8:262:4716::25)' & jexec smtp openssl s_client -4 -connect gmail-smtp-in.l.google.com:25 -starttls smtp -groups '?X25519MLKEM768:DEFAULT' </dev/null
Anschliessend pcap auslesen:
tcpdump -nn -tttt -r /tmp/ch.pcap # Paketsequenz tcpdump -nn -r /tmp/ch.pcap -X # Hex-Dump fuer TLS-Record-Inspektion
Gekuerzte Sequenz:
18:35:59.443 IP 148.251.30.205.10420 > 142.251.127.27.25: Flags [S], length 0 18:35:59.448 IP Flags [.], length 0 18:35:59.525 IP Flags [P.], length 23: SMTP: EHLO ... 18:35:59.541 IP Flags [P.], length 10: SMTP: STARTTLS 18:35:59.554 IP Flags [P.], length 339: SMTP
Das letzte Paket mit 339 Byte Payload enthaelt das ClientHello. Hex-Auszug aus tcpdump -X:
0x0030: ...1603 0101 4e 01 0001 4a 03 0354 0x0040: 045a 83dd 0481 ee15 c537 75f2 6b38 f360 ...
Aufgeschluesselt:
0x16 -- TLS Record Type Handshake 0x0301 -- TLS Version (legacy in TLS 1.3 ClientHello) 0x014e -- TLS Record Length 334 Bytes 0x01 -- Handshake Type ClientHello 0x00014a -- ClientHello-Body 330 Bytes
339 Byte TCP-Payload teilen sich auf in 5 Byte TLS-Record-Header und 334 Byte Body. Das passt locker in ein einziges TCP-Segment, weit unter der 1400-Byte-Schwelle. Damit ist das ?-Praefix-Verhalten auch auf dem Wire bestaetigt.
Verifikation #3: eigene Inbound-Seite von extern
Zum Abschluss von einem zweiten FreeBSD-Host aus — also nicht aus der smtp-Jail selbst, sondern als komplett externer Client — gegen alle relevanten Ports getestet:
for entry in 25:smtp 465: 587:smtp 993: 4190:sieve; do
port=${entry%:*}; starttls=${entry#*:}
host=smtp.kernel-error.de
case $port in 993|4190) host=imap.kernel-error.de ;; esac
if [ -z "$starttls" ]; then args="-connect $host:$port"
else args="-connect $host:$port -starttls $starttls"; fi
echo "--- Port $port ---"
echo QUIT | openssl s_client $args -brief 2>&1 | grep -E 'Negotiated|Verification:'
done
Ergebnis fuer jeden der fuenf Ports identisch:
Verification: OK Negotiated TLS1.3 group: X25519MLKEM768
Postfix-Inbound auf 25/465/587 und Dovecot auf 993/4190 verhandeln durchweg X25519MLKEM768. Der Default greift fuer smtpd genauso wie fuer smtp outbound, ohne dass irgendwo noch eine Trennung haendisch erzwungen werden muss.
Fazit
Mit Postfix 3.11 und OpenSSL 3.5 ist sowohl die main.cf-globale Variante aus dem Original-Beitrag als auch die master.cf-Trennung aus dem April-Update Geschichte. Der Built-in-Default ?X25519MLKEM768:DEFAULT liefert genau das Verhalten, das ich vorher manuell aufgebaut hatte: kleiner ClientHello outbound, MLKEM via HelloRetryRequest, klassischer Fallback wo noetig.
Konkret: wer den Original-Beitrag oder den April-Update nachgebaut hat, kann nach dem pkg-Upgrade auf Postfix 3.11.x die tls_eecdh_auto_curves-Eintraege ersatzlos rausnehmen — aus main.cf und/oder master.cf — und mit postfix reload aktivieren. openssl s_client -msg und ein kurzer tcpdump bestaetigen anschliessend, dass das initiale ClientHello tatsaechlich klein bleibt und MLKEM ueber HRR aushandelt.
Drei Iterationen, ein Default, der am Ende einfach passt. So darf das gerne haeufiger laufen.
Siehe auch: Post-Quantum TLS fuer E-Mail (Original-Beitrag mit April-Nachtrag), Post-Quantum TLS fuer Nginx, Post-Quantum TLS auf Nginx: 15 Tage $ssl_curve ausgewertet und der Mailinglisten-Thread auf postfix-users, der die ganze Saga ueberhaupt erst angestossen hat.
Wie immer: bei Fragen, fragen.









































