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

Schlagwort: InfoSec (Seite 1 von 15)

Intel QuickAssist 8950-SCCP: Krypto-Beschleuniger von 2013 gegen eine CPU von heute

Intel-QuickAssist-8950-SCCP-Krypto-Beschleuniger neben einer modernen Server-CPU im technischen Leistungsvergleich.

Manche Bauteile überleben ihren eigenen Sinn. Diese Karte liegt seit Jahren bei mir herum, hat einmal einen echten Job gemacht, und heute ist sie ein Stück Technikgeschichte mit Kühlkörper. Also habe ich sie in meine Workstation gesteckt, einfach um zu sehen, was sie noch kann und wie sie sich gegen eine aktuelle CPU schlägt. Das Ergebnis ist interessanter geworden als erwartet, aber nicht auf die Art, die ich erwartet hatte.

Die Vorgeschichte: irgendwann lief bei mir ein FreeBSD-Server mit einer Intel-Atom-CPU, und diese CPU hatte kein AES-NI. Auf so einer Maschine verschlüsselte Platten zu betreiben ist zäh. Also kam eine Intel QuickAssist Adapter 8950-SCCP rein, eine PCIe-Steckkarte, die genau das in Hardware macht. Und sie hat ihren Job gut gemacht: weniger CPU-Last, mehr Durchsatz am Storage. Später wanderte sie in einen älteren Xeon, immer noch FreeBSD, immer noch GELI. FreeBSD 14 hat sie produktiv nie gesehen.

Intel QuickAssist Adapter 8950-SCCP mit montiertem passiven Kühlkörper, Low-Profile-Karte mit PCIe-Steckkontakten und Slotblech
So hing sie im Server: Low Profile, PCIe x8, ein gerippter Aluklotz über fast der ganzen Platine. Lüfter gibt es keinen, den soll das Rack liefern.

Warum das damals überhaupt ein Problem war

Das ist der Teil, den heute fast keiner mehr auf dem Schirm hat: AES in der CPU war jahrelang keine Selbstverständlichkeit. AES-NI kam 2010 mit Westmere, aber Intel hat den Befehlssatz danach als Segmentierungsmerkmal benutzt. Bei vielen Atom-, Celeron- und Pentium-Modellen fehlte er, und zwar genau in den stromsparenden Storage- und Firewall-Plattformen, in denen man ihn am dringendsten gebraucht hätte. Ohne AES-NI rechnet eine CPU AES über Tabellen-Lookups, also grob eine Größenordnung langsamer, und obendrein mit Cache-Timing-Seitenkanälen, die man erst mit Aufwand wieder zubekommt.

Bei ARM war es lange genauso, und da ist es sogar noch besser zu greifen. ARMv7 hatte überhaupt keine AES-Befehle. Die Cryptography Extensions von ARMv8-A sind bis heute optional, ein Chiphersteller kann sie einfach weglassen. Der Raspberry Pi ist das schönste Beispiel: alle 64-Bit-fähigen Pis bis einschließlich Modell 4 haben keine AES-Beschleunigung, erst der BCM2712 im Pi 5 bringt sie mit. Wer sich mal gefragt hat, warum verschlüsselte Backups auf einem Pi 4 so quälend langsam sind: das ist der Grund, und es ist kein Softwareproblem.

Dazu kommt der zweite Teil der Geschichte. Verschlüsselung war lange die Ausnahme, nicht die Regel. Wer 2010 eine Webseite betrieben hat, hat HTTPS für den Loginbereich angeschaltet und sonst nirgends, weil es teuer war, weil Zertifikate Geld kosteten und weil TLS auf schwacher Hardware wirklich weh tat. Erst mit Snowden und vor allem mit dem Druck, den die Browserhersteller danach aufgebaut haben, kippte das ins Gegenteil. Plötzlich sollte alles verschlüsselt sein, und zwar sofort. In dieser Phase steckten in Loadbalancern und Reverse Proxies routinemäßig Offload-Karten, für Krypto oder für Kompression, weil die Allzweck-CPU dahinter das schlicht nicht mitgemacht hat. Diese Karte kommt aus genau dieser Zeit.

Was das für eine Karte ist

Auf dem Aufkleber steht INTEL(R) QUICK ASSIST ADAPTER 8950-SCCP, Bestellnummer IQA89501G1P5, Intel MM 929848, Herstelldatum 09/2017, Made in Malaysia. Der Chip darauf ist allerdings älter als die Karte: die Generation stammt aus Q4 2013.

Rückseite des Intel QuickAssist Adapter 8950-SCCP mit Typenschild, Bestellnummer IQA89501G1P5, Intel MM 929848 und Herstelldatum 09/2017
Die Rückseite mit dem Typenschild. Der blaue Aufkleber ist ein Echtheitsmerkmal von yottamark, dazu KCC-Zulassung, UL-Nummer und das PCI-Express-Logo.

SCCP steht für Single Coleto Creek PCIe. Coleto Creek ist der interne Codename, der Beschleunigerchip heißt DH895xCC, und Intel hat ihn als Intel Communications Chipset 8955 verkauft. Die PCI-ID ist 8086:0435. Bei Intel ist die Karte längst End of Life, abgekündigt zusammen mit ihrem Nachfolger 8960.

Die Datenblattwerte, also Herstellerangaben und nichts von mir Gemessenes:

AngabeWert
Bulk-Kryptobis 50 Gbit/s
Kompressionbis 24 Gbit/s
Host-InterfacePCIe Gen3 x8
SR-IOV1 Physical Function, 32 Virtual Functions
Leistungsaufnahmemaximal etwa 40 W
Umgebungstemperatur0 bis 55 Grad Celsius

Gebaut wurde das Ding nicht für Workstations, sondern für Telco und Netzwerk: VPN-Konzentratoren, IPsec-Gateways, TLS-Terminierung, Deduplizierungs-Appliances. 50 Gbit/s Bulk-Krypto in einer Low-Profile-Karte war 2013 eine Ansage. Mein Lieblingsdetail aus dem Featureset ist aber ein anderes: die Karte kann Kasumi und Snow 3G in Hardware, also die Mobilfunkchiffren aus 3G und LTE. Das ist so wunderbar spezifisch, dass man sofort weiß, in welchem Blech sie eigentlich stecken sollte, und das ist bestimmt kein Tower unter einem Schreibtisch.

Kühlkörper ab: zwei Chips, nicht einer

Jetzt wird es hübsch. Unter dem Kühlkörper sitzen nämlich nicht ein großer Chip, sondern zwei, und dazu eine Leerstelle.

Platine des Intel QuickAssist Adapter 8950-SCCP ohne Kühlkörper, links der nackte Coleto-Creek-Die, mittig der PLX PEX 8724 als PCIe-Switch, rechts eine unbestückte Lötfläche
Links U5, der Coleto Creek als Flip-Chip ohne Heatspreader. Mittig U1, der PLX PEX 8724. Rechts neben dem Slotblech die leere Fläche, um die es weiter unten geht.

Links auf Position U5 liegt der Coleto Creek als Flip-Chip-BGA ohne Heatspreader. Das nackte Silizium liegt frei, man schaut direkt auf den Die. Das sieht man heute selten und es ist genau der Grund, warum ich den Kühlkörper überhaupt abgeschraubt habe.

Mittig auf U1 sitzt aber ein Chip, den ich dort nicht erwartet hätte: ein PLX Technology PEX8724-CA80BC G, Fertigungscode 1703, also Kalenderwoche 3 in 2017, gefertigt in Taiwan. Das ist ein PCIe-Gen3-Switch mit 24 Lanes und 6 Ports. Auf einer Karte, die nur einen einzigen Beschleunigerchip trägt, ist ein Switch zunächst mal erklärungsbedürftig.

Rechts, direkt neben dem Slotblech, liegt dann noch eine komplett unbestückte BGA-Fläche. Ein vollständiges, leeres Lötpad-Raster in Chipgröße. Da war offensichtlich mal etwas geplant.

Nahaufnahme der unbestückten BGA-Lötfläche neben dem Slotblech des Intel QuickAssist Adapter 8950-SCCP, ein komplettes leeres Pad-Raster in Chipgröße
Der leere Platz aus der Nähe. Ein Platz in Chipgröße, alles drumherum bestückt, nur hier fehlt der Chip.

Wenn man Switch und Leerstelle zusammennimmt, geht eine Rechnung verdächtig glatt auf:

PEX 8724 = 24 Lanes

  x8  Upstream zum Slot
+ x8  zum bestückten Coleto Creek (U5)
+ x8  zu einem weiteren, unbestückten Platz
= 24

Und der Kernel liefert dazu tatsächlich einen passenden Befund: der Downstream-Port c4:08.0 des Switches existiert, hat LnkCap Width x8 und ist untrainiert, also LnkSta Width x0. Da hängt nichts dran.

Und hier muss ich mich selbst bremsen, denn die Versuchung ist groß. Belegt ist nur: in der Switch-Konfiguration sind diesem Port acht Lanes zugewiesen, und es hängt nichts daran. Nicht belegt ist, dass diese acht Lanes zu dem leeren Platz führen. Das könnte genauso ein nie herausgeführter Port sein oder für etwas völlig anderes gedacht gewesen sein. Die Lötfläche macht die Deutung attraktiv, und mehr ist es nicht. Wer es wirklich wissen will, müsste die Lanes am Board durchmessen oder das EEPROM des PEX8724 auslesen und dort in die Port Configuration schauen. Beides habe ich nicht gemacht.

Genauso vorsichtig muss man mit der naheliegenden Erzählung sein, Intel habe hier dieselbe Platine für eine Dual-Chip-Variante vorgesehen. Das passt zur Namenslogik, das S in SCCP steht ja für Single, und es passt zum leeren Platz. Eine offizielle Bestätigung für ein 8950-DCCP oder Ähnliches habe ich trotzdem nicht gefunden. Also: plausible Deutung, kein Faktum.

Am Rand der Platine sitzt außerdem noch ein schwarzer, geschirmter Steckverbinder mit der Bezeichnung J8. Der macht das, was bei einer Grafikkarte der Zusatzstecker macht: Stromversorgung direkt vom Netzteil. Das passt zu den bis zu 40 Watt aus dem Datenblatt, denn ein PCIe-Steckplatz dieser Bauform liefert nach Spezifikation nur 25 Watt. Dazu mehrere Spannungsregler mit Speicherdrosseln, ein 100-MHz-Quarz und Siebdruck-Markierungen für x1, x4 und x8 am Kartenrand.

Einbau: erfreulich langweilig

Die Karte steckt jetzt in einem Slot mit PCIe 4.0 x8 an einem Xeon Gold 5315Y, unter Linux Mint mit Kernel 7.0. Und dann passiert genau das, was man sich von einem In-Kernel-Treiber wünscht: nichts Besonderes.

[   36.915329] dh895xcc 0000:c5:00.0: enabling device (0140 -> 0142)
[   37.744772] dh895xcc 0000:c5:00.0: qat_dev0 started 12 acceleration engines

Kein Paket installiert, keine Firmware nachgeladen, keine Konfigurationsdatei, kein adf_ctl. Die Module qat_dh895xcc und intel_qat laden von allein, die Firmware qat_895xcc.bin.zst lag über linux-firmware längst auf der Platte. Zwölf Acceleration Engines starten, alle Selftests bestehen, der Heartbeat meldet sich gesund. Ein Chip von 2013 auf einer Karte von 2017 in einem Board von 2021 unter einem Kernel von 2026, und es ist ein Nichtereignis. Das ist ein starkes Argument für In-Tree-Treiber, und es wird weiter unten noch bitter kontrastiert.

Was man zum Prüfen braucht:

lspci -nn | grep -i qat
dmesg | grep -i dh895
grep -c qat /proc/crypto
ls /sys/kernel/debug/qat_dh895xcc_0000:c5:00.0/

Das debugfs-Verzeichnis ist die interessanteste Fundstelle der ganzen Karte. Dort liegt in dev_cfg die komplette, vom Treiber selbst generierte Instanzkonfiguration: 16 Krypto-Instanzen, 16 Kompressions-Instanzen, je Instanz 512 gleichzeitige symmetrische oder 128 asymmetrische Requests, dazu Interrupt-Coalescing und ein Heartbeat-Timer. In fw_counters stehen Requests und Responses pro Acceleration Engine, und das ist später mein Beweis, dass die Karte wirklich rechnet und nicht die CPU heimlich einspringt. Dazu heartbeat/status, cnv_errors und 32 Ring-Banks unter transport/.

Ein PCIe-Switch, der Generationen übersetzt

Jetzt löst sich auch auf, warum der PLX-Switch da ist. So sieht der Baum aus:

c2:02.0  Root Port #17          LnkCap Gen4 x8  ->  LnkSta Gen3 x8
  c3:00.0  PLX PEX 8724 Upstream                 ->  LnkSta Gen3 x8
    c4:00.0  Downstream Port #0                  ->  LnkSta Gen2 x8
      c5:00.0  Intel DH895XCC Series QAT   [8086:0435]
    c4:08.0  Downstream Port #8                  ->  LnkSta x0   (leer)

Host-seitig läuft die Karte mit Gen3 x8, also 8 GT/s. Chip-seitig kommen aber nur Gen2 x8 an, also 5 GT/s. Der Coleto Creek ist ein Gen2-Baustein, und damit die Karte am Steckplatz trotzdem als moderne Gen3-x8-Karte auftritt, sitzt der PEX 8724 als Übersetzer dazwischen. Der QAT-Endpunkt behauptet in seiner LnkCap sogar x16, verdrahtet sind aber acht Lanes.

Das ist der erste wirklich belastbare Befund des Tages: das Nadelöhr sitzt auf der Karte, nicht im Steckplatz und nicht in der CPU. Dafür braucht man die Dual-Chip-Spekulation von oben überhaupt nicht.

Zwei weitere Kleinigkeiten aus dem laufenden System, die ich hübsch finde. Die Karte sitzt allein in ihrer IOMMU-Gruppe und unterstützt Function Level Reset, sie lässt sich also ohne ACS-Override-Gebastel per VFIO an eine VM durchreichen. Und sie meldet 32 Virtual Functions, aktiv sind davon null.

Der praktisch wichtigste Nebeneffekt des Einbaus hat aber gar nichts mit Krypto zu tun. Der PLX-Switch belegt vier Busnummern, und dadurch ist meine NVMe von c3:00.0 auf c7:00.0 gewandert. Auf der alten Adresse sitzt jetzt der Upstream-Port des Switches. Ich hatte mir ein setpci-Kommando mit der alten Adresse notiert, und das hätte nach dem Einbau munter in die Register des Switches geschrieben statt in die der SSD. Merke: PCI-Adressen sind nichts, was man sich aufschreibt. Die holt man sich jedes Mal frisch aus lspci. Ist ja jetzt nicht so, als wenn mir das schon mal untergekommen ist bzw. ich so was gefettfingert habe 😛

Die Prioritäten-Tabelle, oder: der Kernel hat schon entschieden

Der Treiber registriert zehn Algorithmen in der Crypto-API des Kernels, alle mit selftest: passed:

xts(aes)                        qat_aes_xts               skcipher   prio=100
ctr(aes)                        qat_aes_ctr               skcipher   prio=100
cbc(aes)                        qat_aes_cbc               skcipher   prio=100
authenc(hmac(sha1),cbc(aes))    qat_aes_cbc_hmac_sha1     aead       prio=100
authenc(hmac(sha256),cbc(aes))  qat_aes_cbc_hmac_sha256   aead       prio=100
authenc(hmac(sha512),cbc(aes))  qat_aes_cbc_hmac_sha512   aead       prio=100
rsa                             qat-rsa                   akcipher   prio=1000
dh                              qat-dh                    kpp        prio=1000
pkcs1(rsa,sha512)               pkcs1(qat-rsa,sha512)     sig        prio=1000
deflate                         qat_deflate               acomp      prio=4001

Diese Prioritäten sind die halbe Geschichte des Beitrags. Die Linux Crypto API wählt bei gleichem Algorithmusnamen immer die Implementierung mit der höchsten Priorität. Wenn man das mit den CPU-Implementierungen auf derselben Maschine vergleicht, wird es sehr deutlich:

AlgorithmusQATbester CPU-Wertwer gewinnt
xts(aes)100xts-aes-vaes-avx2 = 600CPU
cbc(aes)100cbc-aes-aesni höherCPU
authenc(...)100CPU-Kombis höherCPU
rsa1000rsa-generic = 100Karte
dh1000dh-generic = 100Karte
deflate4001deflate-generic = 0Karte

Übersetzt heißt das: für symmetrische Massenverschlüsselung wird diese Karte nie von allein benutzt. Für rsa, dh und deflate dagegen immer. Das ist kein Zufall und kein Konfigurationsfehler, sondern eine Aussage der Kernel-Entwickler darüber, wo Offload bei einer aktuellen CPU überhaupt noch etwas bringt. Man kann diese Tabelle lesen wie ein Urteil über die Karte, und genau so ist sie auch gemeint.

Ein Detail nur mit Vorsicht: pkcs1(qat-rsa,sha512) steht mit Priorität 1000 registriert und hat den Selftest bestanden. Daraus folgt noch nicht, dass Kernel-Modul-Signaturprüfung tatsächlich über die Karte läuft. Die Registrierung macht es möglich, die Priorität spricht dafür, aber um es zu belegen müsste man den Verifikationspfad tracen und dabei die Zähler in fw_counters beobachten. Habe ich nicht gemacht, also behaupte ich es auch nicht.

Bevor die Zahlen kommen: was meine Messung nicht zeigt

Diesen Absatz gibt es, weil die Tabellen danach sonst präziser wirken als sie sind. Wer mir eine Zahl vorhält, soll wissen, wie sie entstanden ist.

  • Alle Kryptomessungen laufen über AF_ALG aus einem Python-Skript mit einem synchronen Request-Loop. Das ist ungefähr der Worst Case für eine asynchrone Offload-Karte: pro Request wird gewartet, statt die Queue tief zu halten, für die die Hardware gebaut wurde. Die Zahlen charakterisieren also diesen Zugangsweg, nicht die Karte an sich.
  • Kein Warmup, keine Wiederholungen, keine Streuung, kein CPU-Pinning. Angaben mit einer Nachkommastelle tragen eine Genauigkeit, die statistisch nicht gedeckt ist. Für belastbare Latenzen bräuchte es Median und P95 über mehrere Läufe.
  • Die Skalierung über Worker nutzt Prozesse, nicht Threads. Kontextwechsel und Speicherkopien gehen also mit in die Zahl ein.
  • cryptsetup benchmark ist selbst nur ein Indikator. Es misst im Speicher über die Crypto-API und ist laut eigener Manpage nicht direkt auf reale Storage-Verschlüsselung übertragbar.
  • Hart sind dagegen die fw_counters. Die kommen aus der Hardware und belegen, dass und wie oft die Karte gerechnet hat. Und die Form der Ergebnisse ist robust: Latenz pro Request hoch, Skalierung über Worker fast linear, CPU bei kleinen Blöcken weit vorn. Nur die absoluten Werte sind weich.

Als Gegner steht bewusst der ungünstigste Fall für die Karte: ein Xeon Gold 5315Y mit acht Kernen und vollem VAES-Befehlssatz. Wenn eine 13 Jahre alte Karte gegen die Rechenwerke einer aktuellen CPU antritt, dann bitte richtig.

Ein Strom, synchron: die Karte verliert deutlich

Zuerst die CPU-Basislinie mit cryptsetup benchmark, damit die Größenordnung steht:

aes-cbc   128b   1168,2 MiB/s enc   3936,0 MiB/s dec
aes-cbc   256b   1004,5 MiB/s enc   3729,8 MiB/s dec
aes-xts   256b   5494,4 MiB/s enc   5510,1 MiB/s dec
aes-xts   512b   5045,2 MiB/s enc   5053,6 MiB/s dec

Und dann der direkte Vergleich über AF_ALG, ein einzelner synchroner Strom:

ImplementierungBlockDurchsatzLatenz pro Operation
xts-aes-vaes-avx24 KiB1157,2 MiB/s3,4 µs
xts-aes-vaes-avx216 KiB2681,2 MiB/s5,8 µs
xts-aes-vaes-avx264 KiB4327,2 MiB/s14,4 µs
qat_aes_xts4 KiB100,3 MiB/s38,9 µs
qat_aes_xts16 KiB244,9 MiB/s63,6 µs
qat_aes_xts64 KiB308,8 MiB/s201,6 µs
cbc-aes-aesni4 KiB583,5 MiB/s6,7 µs
qat_aes_cbc4 KiB79,8 MiB/s48,9 µs

Pro Einzelrequest ist die Karte also grob elfmal langsamer als die CPU, ungefähr 39 gegen 3,4 Mikrosekunden bei 4 KiB. Für ein Bauteil, das mal genau dafür gekauft wurde, ist das eine ernüchternde Zahl.

Nur: das ist nicht die Hardwarelatenz der Karte. Gemessen ist die Latenz dieses Pfades, und der ist lang: Python, sendmsg über AF_ALG, Socket-Puffer, Kernel-Copy, Treiber, PCIe, Karte, und alles wieder zurück, plus Scheduling und Aufwecken des wartenden Prozesses. Ein erheblicher Teil davon kann im Socket-Pfad stecken. Die CPU-Variante läuft durch denselben Overhead, profitiert aber davon, dass sie synchron im selben Kontext rechnet und überhaupt nicht schlafen muss. Belastbar ist deshalb nur die relative Aussage für diesen Zugangsweg und die Form der Kurve. Wer die reine Hardwarelatenz will, braucht ein Frontend ohne Socket-Umweg, also praktisch den Userspace-Stack, und der existiert für diesen Chip nicht mehr. Dazu unten mehr. Das ist eine Messlücke, die ich mit den vorhandenen Mitteln nicht schließen kann, und ich schreibe sie lieber hin als sie zu verstecken.

Viele Ströme: jetzt zeigt sie, wofür sie gebaut wurde

Und dann wird es doch noch spannend. Dieselbe Messung mit parallelen Workern, Blockgröße 16 KiB:

Workerqat_aes_xtsxts-aes-vaes-avx2
1210,7 MiB/s2632,6 MiB/s
2454,1 MiB/s5311,9 MiB/s
4799,3 MiB/s10777,5 MiB/s
81714,5 MiB/s22056,0 MiB/s
162926,7 MiB/s25229,7 MiB/s

Die Karte skaliert von einem auf 16 Worker um fast Faktor 14, also nahezu linear, und bei 16 Workern war sie noch nicht am Ende. Das ist exakt das Verhalten, für das so ein Baustein entworfen wurde: viele gleichzeitige, unabhängige Operationen, keine einzelne schnelle. Eine Karte in einem Loadbalancer sieht nie einen Strom, sie sieht tausende.

Nur skaliert die CPU eben auch. Und sie landet am Ende 8,6 mal höher. Das ist der ganze Punkt dieses Beitrags in einer Zahl. So fertig, feierabend, einpacken!

Dass wirklich die Karte gerechnet hat und nicht heimlich doch die CPU, zeigen die Firmware-Zähler nach dem Lauf:

QAT firmware requests in diesem Lauf: 1.340.416

AE   Requests   Responses
 0:    154181     154181
 1:     94420      94420
 2:     90590      90590
 3:    154177     154177
 ...
11:     90662      90662

Alle zwölf Acceleration Engines haben gearbeitet, Requests und Responses stimmen überall überein, kein verlorener Request. Die Verteilung ist nicht gleichmäßig, sondern fällt in Dreiergruppen von etwa 154k, 94k und 90k. Das kommt von der Zuordnung Ring-Bank zu Engine und ist so ein Detail, an dem man beim Lesen kleben bleibt.

Wo der Deckel liegen könnte, und warum ich mich da nicht festlege

Die 2926,7 MiB/s liegen auffällig nah an der Kapazität des internen Links:

2926,7 MiB/s Nutzdaten = 3069 MB/s
Jedes Byte muss zweimal über den Bus: rein zum Verschlüsseln, raus als Chiffrat.
Gen2 x8 = 5 GT/s x 8 Lanes x 8/10 = 4000 MB/s pro Richtung
3069 / 4000 = 77 % der theoretischen Richtungskapazität

77 Prozent Nutzlast auf einem PCIe-Link sind praktisch der Anschlag, mit dem TLP-Overhead bei 256 Byte MaxPayload liegt das erreichbare Maximum bei etwa 85 bis 90 Prozent. Das sieht also sehr nach einem Bus-Limit aus.

Aber: meine Messung kann das nicht trennen. Sie zeigt, dass dieser Zugangsweg in der Nähe eines Ceilings landet, und sie kann nicht sagen, ob das der PCIe-Link, der Treiber oder AF_ALG ist. Die Rechnung oben ist ein Plausibilitätsargument, keine Messung des Busses. Sauber wäre es, PCIe-Bandwidth-Events über die Uncore-Zähler mitzuschreiben. Das ist der wichtigste offene Messpunkt in diesem Beitrag, weil eine der Hauptaussagen daran hängt.

Noch eine Rechnung, die verlockend glatt aufgeht und bei der man aufpassen muss. Die gemessenen 2926,7 MiB/s sind 24,6 Gbit/s, das Datenblatt sagt 50 Gbit/s, das sind fast genau 49 Prozent. Und verdrahtet sind acht von 16 Lanes, also genau die Hälfte. Der Kurzschluss liegt auf der Zunge, und er ist falsch: „mit x16 wäre man auf dem Datenblattwert“ und „die Platine war für zwei Chips gedacht“ sind zwei verschiedene Hypothesen, nicht eine. In einer Dual-Chip-Bestückung bekäme jeder Chip x8, keiner käme je auf x16, und das Nadelöhr wäre dann der gemeinsame Gen3-x8-Upstream. Rechnerisch landet man auf beiden Wegen bei ungefähr 49 Gbit/s, aber über völlig verschiedene Mechanismen. Wer das zusammenrührt, argumentiert falsch, auch wenn das Ergebnis stimmt. Die Karte selbst hat ja auch nur einen PCIe 8x Anschluss.

Und es gibt eine zweite Lesart, an der der ganze Vergleich mit dem Marketing hängt. Wenn Intels 50 Gbit/s als Summe beider Richtungen gemeint sind, also 25 rein und 25 raus, dann reicht Gen2 x8 dafür aus, und meine gemessenen 24,6 Gbit/s pro Richtung sind aggregiert 49,2 Gbit/s. Dann lautet der richtige Satz nicht „die Karte erreicht die Hälfte“, sondern „die Karte erreicht ihre Spezifikation“. Intel dokumentiert nirgends, wie gezählt wird, also ist das aus den vorliegenden Unterlagen nicht entscheidbar. Die 24,6 Gbit/s stehen fest, nur ihre Deutung hängt an einer undokumentierten Konvention. Natürlich kann ich auch einfach dran vorbei gelesen haben oder ich messe falsch. Korrigiert mich gerne, dann lerne ich selbst etwas \o/

Und jetzt die Enttäuschung: dm-crypt will nicht

Der naheliegendste Anwendungsfall, und ausgerechnet genau der, für den die Karte in meinem FreeBSD-Server gearbeitet hat, funktioniert unter aktuellem Linux nicht:

# cryptsetup plainOpen /dev/ram0 probe --cipher capi:qat_aes_xts-plain64 --key-size 512 --key-file /dev/zero
device-mapper: reload ioctl on probe (252:3) failed: Datei oder Verzeichnis nicht gefunden

Der erste Reflex ist natürlich, dass ich den Namen falsch geschrieben habe. Also Gegenprobe mit derselben capi:-Syntax über sieben Varianten:

Angabe bei --cipherErgebnis
aes-xts-plain64funktioniert
capi:xts(aes)-plain64funktioniert
capi:xts-aes-aesni-plain64funktioniert
capi:xts-aes-vaes-avx2-plain64funktioniert
capi:xts-aes-vaes-avx512-plain64funktioniert
capi:qat_aes_xts-plain64Fehler, ENOENT
capi:qat_aes_cbc-plain64Fehler, ENOENT

dm-crypt kann also sehr wohl einen konkreten Treiber adressieren, sogar xts-aes-vaes-avx512, das mit seiner niedrigen Priorität sonst nie zum Zug käme. Nur die beiden QAT-Treiber fliegen raus. Das ist kein Tippfehler und keine Namensauflösung.

Der Beweis kommt aus dem laufenden Kernel selbst, ausgelesen über die NETLINK_CRYPTO-Schnittstelle. Das ist die belastbare Quelle, denn sie sagt, was dieser Kernel registriert hat, und nicht, was irgendeine Quelldatei im Netz behauptet:

qat_aes_xts              flags=0x00010585   ASYNC | NEED_FALLBACK | TESTED | ALLOCATES_MEMORY
qat_aes_cbc              flags=0x00010485   ASYNC | TESTED | ALLOCATES_MEMORY
qat_aes_ctr              flags=0x00010485   ASYNC | TESTED | ALLOCATES_MEMORY
qat_aes_cbc_hmac_sha256  flags=0x00010483   ASYNC | TESTED | ALLOCATES_MEMORY
qat_deflate              flags=0x0001048a   ASYNC | TESTED | ALLOCATES_MEMORY
qat-rsa                  flags=0x00000406   TESTED
xts-aes-aesni            flags=0x00000405   TESTED
xts-aes-vaes-avx2        flags=0x00000405   TESTED
cbc-aes-aesni            flags=0x00000405   TESTED
deflate-generic          flags=0x0004040a   TESTED

Der Unterschied ist genau ein Bit: 0x00010000, also CRYPTO_ALG_ALLOCATES_MEMORY. Alle symmetrischen QAT-Implementierungen haben es, keine einzige CPU-Implementierung hat es. Die niedrigen Bits sind übrigens kein Flag, sondern der Algorithmustyp.

Und dm-crypt fordert seine Transforms genau mit diesem Bit als Maske an, an drei Stellen im Code:

cc->cipher_tfm.tfms[i]      = crypto_alloc_skcipher(ciphermode, 0, CRYPTO_ALG_ALLOCATES_MEMORY);
cc->cipher_tfm.tfms_aead[0] = crypto_alloc_aead(ciphermode, 0, CRYPTO_ALG_ALLOCATES_MEMORY);
mac                         = crypto_alloc_ahash(mac_alg, 0, CRYPTO_ALG_ALLOCATES_MEMORY);

Die Suche findet damit nichts und liefert ENOENT, und das kommt oben als „Datei oder Verzeichnis nicht gefunden“ an. Kette geschlossen, keine Hypothese mehr. Dass qat-rsa das Flag nicht hat, passt genau ins Bild: RSA läuft über die Karte, weil es kein Block-I/O-Pfad ist.

Die Wendung: das ist eine Leitplanke, kein Bürokratiefehler

An dieser Stelle wollte ich anfangen zu schimpfen. Die Karte kann AES-XTS in Hardware, der Kernel registriert es, und der eine Konsument, für den es gedacht war, weigert sich. Klingt nach Linux, das sich selbst im Weg steht. Ist es aber nicht.

Die Maske stammt von Mikulas Patocka, und die Begründung im Commit ist knapp:

Don’t use crypto drivers that have the flag CRYPTO_ALG_ALLOCATES_MEMORY set. These drivers allocate memory and thus they are unsuitable for block I/O processing.

Der Grund dahinter ist ein Low-Memory-Deadlock. Stell dir vor, es wird auf ein dm-crypt-Gerät geswappt. Der Kernel will Speicher freimachen, schickt die Swap-Out-BIO durch dm-crypt, dm-crypt fragt die Crypto-API, und die fordert daraufhin Speicher an, den es gerade nicht gibt. Ende der Vorstellung.

Und mit QAT ist genau das schon einmal schiefgegangen. Im März 2022 gibt es einen Bericht auf der Kernel-Mailingliste über massive Datenkorruption mit QAT plus dm-crypt plus XFS. Die Diagnose kam von Giovanni Cabiddu, Intel:

The implementations of aead and skcipher in the QAT driver are not properly supporting requests with the CRYPTO_TFM_REQ_MAY_BACKLOG flag set. If the HW queue is full, the driver returns -EBUSY but does not enqueue the request.

Die Folge: dm-crypt wartet endlos auf die Completion eines Requests, der nie an die Hardware gegangen ist. Und das Dateisystem war hinüber. Das ist kein theoretisches Risiko, das ist ein Schadensfall mit Datum.

Die Zeitleiste danach ist lehrreich, weil sie nicht so endet, wie man denkt:

DatumWas passiert
Juli 2020Das Flag CRYPTO_ALG_ALLOCATES_MEMORY wird auf QAT gesetzt
Juli 2020dm-crypt schließt Treiber mit diesem Flag aus (Patocka)
März 2022Der Schadensfall: Datenkorruption mit QAT, dm-crypt und XFS
Mai 2022Intel liefert den eigentlichen Fix, ein Backlog-Mechanismus
Juli 2023Intel will das Flag entfernen, um QAT für dm-crypt zurückzuholen. Nie gemerged.
Juni 2025Stattdessen: Priorität von Skcipher und AEAD wird von 4001 auf 100 gesenkt
August 2026Auf meinem Kernel gemessen: Flag gesetzt, Priorität 100, dm-crypt verweigert

Die Auflösung war also nicht „dm-crypt darf QAT wieder benutzen“, sondern „QAT wird per Priorität aus dem Weg geräumt“. Und die Begründung in diesem Commit ist der stärkste Absatz, den ich zu dieser Karte gelesen habe:

Most kernel applications utilizing the crypto API operate synchronously and on small buffer sizes, therefore do not benefit from QAT acceleration. Reduce the priority of QAT implementations for both skcipher and aead algorithms, allowing more suitable alternatives to be selected by default.

„Synchron und kleine Puffer“ ist exakt das, was ich oben unabhängig gemessen habe, 39 gegen 3,4 Mikrosekunden bei 4 KiB. Der Kernel hat 2025 formal festgestellt, was meine Messung 2026 auf dieser Maschine bestätigt. Und der Patch, der Intels eigene Hardware degradiert, kommt von Intel, mit Acked-by von Eric Biggers. Wenn man es wohlwollend liest, ist das ein Hersteller, der ehrlich ist. Zur Version muss man genau sein: der Commit ging in Mainline 6.17, wurde aber wegen Cc: stable auch in ältere Stable-Zweige zurückportiert, war dort also schon vor dem 6.17-Release wirksam.

Und damit ist die Pointe eine viel bessere als „Linux ist im Weg“: was wie eine willkürliche Blockade aussieht, ist eine Leitplanke aus einem echten Schadensfall. Das erklärt übrigens auch, warum FreeBSD hier lockerer war. Dort gibt es diese Leitplanke nicht, es gibt keine Priority-Hierarchie, die den Beschleuniger aussortiert, und keine Maske, die ihn vom Blockgerät fernhält. Das heißt allerdings nicht, dass das Problem dort nicht existiert. Es heißt nur, dass niemand ein Geländer davor gebaut hat.

Ein Blick auf die FreeBSD-Seite lohnt an dieser Stelle sowieso, denn dort ist der Weg ein struktureller anderer. qat(4) ist ein Treiber für das OpenCrypto-Framework, also für crypto(9). Wer GELI benutzt, muss überhaupt nichts umkonfigurieren: GELI fragt das Framework, und das Framework nimmt den Beschleuniger. Deshalb war die Sache damals auch so unspektakulär, ich habe die Karte gesteckt und die Platten waren schneller.

Ganz so glatt war die Historie allerdings nicht. Im Basissystem gibt es qat(4) erst seit FreeBSD 13.0, und in 14.0 wurde dieser Treiber durch Intels Upstream-Variante ersetzt. Auf dem ersten Server, der noch älter war, kann es diesen Weg also nicht gegeben haben. Die aktuelle Manpage führt die Serie 8925 bis 8955 weiterhin als unterstützte Hardware, dieser Chip ist dort also nicht rausgefallen. Und noch etwas gegen zu viel Nostalgie: im FreeBSD-Forum gibt es einen Thread zur GELI-Performance, in dem QAT zunächst schlechter war als AES-NI. Erst nach dem Umstellen der QAT-Services berichtete der Autor rund 30 bis 34 Prozent Vorsprung bei AES-XTS mit SHA256, und bei anderen Lasten blieben Verluste. Die Karte war also auch damals kein bedingungsloser Gewinn, sondern eine, die zur Konfiguration passen musste.

Der zweite Dämpfer: Intels Userspace hat die Karte fallengelassen

Bleibt der andere große Anwendungsfall: TLS-Terminierung mit nginx oder HAProxy, OpenSSL-Offload. Dafür braucht man qatlib und dazu die QAT-Engine oder den Provider für OpenSSL. Auf meinem System ist davon nichts installierbar, kein Kandidat in den Repos, und OpenSSL 3.0.13 kennt nur den Default-Provider.

Schlimmer als „nicht gepackt“ ist aber der Grund: Intel hat dh895xcc aus qatlib entfernt. Die aktuellen Versionen unterstützen nur noch QAT Gen4, also die 4xxx- und 420xx-Serien. Die frühen Generationen sind nicht mehr dabei. Der Weg wäre der alte Out-of-Tree-Stack, und der baut gegen einen Kernel 7.0 nicht mehr.

Ich formuliere das bewusst nicht als „tot“. Behauptet ist: für diesen Chip ist der heutige Mainstream-Linux-Userspace praktisch abgehängt. Nicht behauptet ist, dass es global unmöglich wäre. Mit altem Kernel, altem Out-of-Tree-Treiber und passender Distribution ließe sich der Stack sicher noch aufbauen, und ältere DPDK-Versionen führten dh895xcc als unterstütztes Gerät. Es ist eine Frage von Aufwand und Kernel-Alter, nicht von Unmöglichkeit.

Die Ironie daran ist schön bitter. Intels eigener Userspace hat die Karte längst aufgegeben, während der Linux-Kernel sie weiter pflegt und beim Booten ohne ein einziges Paket zum Laufen bringt. Wer wissen will, warum In-Tree-Treiber so wertvoll sind: das hier ist der Beweis in einer Karte.

Ein dritter Befund noch, damit ihn niemand für einen Kartenfehler hält: ein einzelnes sendmsg über AF_ALG mit 256 KiB oder mehr blockiert dauerhaft. Aufgefallen ist mir das erst mit der Karte bei 1 MiB, reproduzieren lässt es sich aber mit dem CPU-Cipher genauso. Es ist also nicht die Hardware. Wo genau im AF_ALG-Pfad es hängt, habe ich nicht nachgewiesen, der Socket-Sendepuffer wäre die naheliegende Vermutung, aber eben eine ungeprüfte. Alle Messungen oben sind deshalb auf maximal 64 KiB begrenzt.

Kompression: der einzige Pfad, der von allein läuft, und er schadet

Erinnerst du dich an qat_deflate mit Priorität 4001 gegen deflate-generic mit 0? Jeder Kernel-Konsument, der nach deflate fragt, bekommt die Karte. Realistisch ist das zswap, die komprimierte Auslagerung im RAM.

Also nachgestellt: eine cgroup mit hartem Speicherlimit, 700 MiB gut komprimierbare anonyme Seiten, zswap auf deflate. Und tatsächlich, die Karte komprimiert die Auslagerung, 130.108 Firmware-Requests belegen das. Nur ist der direkte Vergleich bei gleicher Last ziemlich brutal:

zswap-CompressorwallusersysQAT-Requests
lzo (CPU)0,85 s0,11 s0,72 s0
deflate (QAT)9,47 s0,26 s5,13 s130.027

Elfmal langsamer. Und die System-CPU-Zeit steigt sogar von 0,72 auf 5,13 Sekunden, das Offload spart also nicht einmal CPU, es kostet zusätzlich welche. Der Grund ist derselbe wie überall in diesem Beitrag: zswap komprimiert eine 4-KiB-Seite pro Request, und das ist genau der Latenz-Worstcase.

Fair bleiben muss ich hier trotzdem: der Vergleich mischt zwei Effekte, denn Deflate ist algorithmisch auch schlicht teurer als LZO. Sauber wäre deflate auf CPU gegen deflate auf der Karte, und das habe ich nicht gemessen, weil sich deflate-generic bei Priorität 0 nicht ohne Weiteres erzwingen lässt. Die Aussage „QAT-zswap ist keine gute Idee“ trägt der Test, die Aufteilung zwischen Algorithmus und Latenz nicht.

Bleibt eine Frage, die ich hübsch finde: warum steht deflate überhaupt noch auf 4001? In der Datenkorruptions-Diskussion von 2022 schlug Intel vor, die Priorität der betroffenen Algorithmen auf 1 zu senken. Heute stehen Skcipher und AEAD bei 100, die Kompression aber weiter bei 4001. Die naheliegende Deutung: 4001 war ursprünglich die Politik „nimm immer den Beschleuniger“, sie wurde für Krypto nach dem Vorfall zurückgenommen und für Kompression nie. Damit wäre qat_deflate der letzte Überrest dieser alten Haltung, und ausgerechnet der Pfad, der heute noch stillschweigend gewinnt und dabei elfmal langsamer ist. Das ist allerdings meine Deutung, kein Beleg. Die Commit-Historie der Prioritätswerte für die Kompression habe ich nicht nachverfolgt.

Wer das nachbauen will: den Zustand danach wieder zurücksetzen, also enabled=N und compressor=lzo. Ein Dauerbetrieb mit dieser Einstellung wäre eine echte Verschlechterung der Maschine.

Die unvermeidliche Frage: kann man damit Bitcoin oder Monero minen?

Nein. Und zwar aus zwei völlig verschiedenen Gründen, und dieser Unterschied ist der eigentlich interessante Teil.

Monero geht prinzipiell nicht, nicht bloß langsam. Monero nutzt seit 2019 RandomX, und der Algorithmus wurde absichtlich so entworfen, dass Spezialhardware keinen Vorteil hat. Er generiert zur Laufzeit zufällige Programme aus Integer-, Fließkomma- und Branch-Instruktionen und führt sie in einer VM aus, teils JIT-compiliert. Er braucht 2 MiB Scratchpad pro Thread, permanent random-access beschrieben und gelesen. Und im Fast-Mode zusätzlich einen Datensatz von rund 2,08 GiB im RAM, aus dem die VM ständig liest. Das macht RandomX speichergebunden statt rechengebunden, und deshalb sind ASICs dort wirtschaftlich uninteressant.

Die QAT-Karte ist das exakte Gegenteil davon: eine Festfunktions-Pipeline. Sie kann AES, SHA, RSA, DH und Deflate, und nichts sonst. Sie hat keinen Befehlssatz, kein Scratchpad-Konzept und keinen Zugriff auf einen 2-GiB-Arbeitsdatensatz. AES kommt in RandomX zwar vor, aber als eingebetteter Schritt in der VM-Schleife, nicht als abtrennbare Massenoperation, die man an einen Coprozessor auslagern könnte. Die CPU dieser Maschine kann RandomX übrigens sehr wohl. Der Algorithmus ist ja genau für CPUs gemacht.

Bitcoin geht theoretisch, praktisch ist es absurd. Bitcoin ist doppeltes SHA-256 über einen 80 Byte großen Blockheader, und SHA-256 kann die Karte. Nur scheitert es an zwei harten Punkten.

Erstens ist es über diesen Stack nicht einmal ansprechbar. Der In-Kernel-Treiber registriert kein reines SHA-256, sondern nur authenc(hmac(sha256),cbc(aes)), also HMAC-authentifizierte Verschlüsselung. Nackte Hashes bekäme man nur über den Userspace-Stack, und der existiert für diesen Chip nicht mehr. Es gibt also gar keinen Weg, der Karte einen Blockheader zum Hashen zu geben.

Zweitens ist die Größenordnung hoffnungslos, und dafür genügt eine geschenkte Obergrenze. Selbst wenn die Karte ihre volle Datenblattleistung als reines Hashing liefern könnte, landet man bei realistischen 64 bis 128 Byte pro Hash-Operation in der Größenordnung von 10⁷ bis 10⁸ Hashes pro Sekunde, also höchstens einige zehn MH/s. Erreichen wird sie das nie, die gemessenen Latenzen zeigen ja, dass sie bei kleinen Nutzlasten zwei Größenordnungen unter ihrem Sweet Spot arbeitet.

HashrateCharakter der Zahl
QAT 8950höchstens 10⁸ H/sunerreichbare Obergrenze
ein aktueller ASIC-Mineretwa 2 mal 10¹⁴ H/sProduktangabe
Bitcoin-Gesamtnetz, 03.08.20269,3 mal 10²⁰ H/sgemessen, 932 EH/s

Selbst mit der geschenkten Obergrenze liegt ein einzelner ASIC noch um mindestens sechs Größenordnungen über der Karte, und das Gesamtnetz um dreizehn. Der Anteil am Netz wäre günstigstenfalls in der Größenordnung 10⁻¹³, bei zehn Minuten Blockzeit also eine erwartete Wartezeit jenseits jeder sinnvollen Zeitskala, bei 40 Watt Dauerlast. Ich verzichte hier absichtlich auf eine konkrete Jahreszahl. Die klingt zwar gut, wäre aber Scheinpräzision auf einer geschätzten Grundlage. Größenordnungen sind hier die ehrlichere Währung, und sie sind genauso eindrucksvoll.

Die eigentliche Pointe ist aber eine sprachliche. Krypto-Beschleuniger heißt Kryptographie, nicht Kryptowährung. Die Karte ist für das gebaut, was Verbindungen und Platten schützt: TLS-Handshakes, IPsec-Tunnel, AES-XTS auf Blockgeräten. Dass beide Bedeutungen dasselbe Wort benutzen, ist ein Sprachunfall, und ich bin ziemlich sicher, dass er der Kryptographie mehr geschadet hat als der Kryptowährung.

Betrieb: man fliegt thermisch blind

Eine praktische Warnung, falls jemand auf die Idee kommt, so ein Ding in einen Desktop zu stecken: die Karte hat keinen Temperatursensor. sensors zeigt nichts, der BMC kennt sie nicht, in der SDR steht kein Eintrag. Man sieht also nicht, wie warm sie wird.

Und die Randbedingungen sind ungünstig: der Kühlkörper ist rein passiv und für den Querstrom eines Rackgehäuses ausgelegt, spezifiziert sind 0 bis 55 Grad Umgebungstemperatur, es dürfen bis zu 40 Watt Verlustleistung sein, und mein Gehäuse ist auf Ruhe optimiert, mit bewusst langsam drehenden Lüftern. Die Netzwerkkarte im Nachbarslot liegt schon im Leerlauf bei 61 Grad, und die beiden teilen sich denselben schlechten Luftstrom. Wie warm die Karte unter der Last aus diesem Beitrag wirklich geworden ist, weiß ich nicht. Ohne Sensor bräuchte es ein IR-Thermometer oder ein Thermoelement. Steht auf der Liste.

Überflüssig gewordene Technik, und warum mir das trotzdem naheliegt

Diese Karte ist für mich so etwas wie eine Soundkarte. Es gab eine Zeit, da war eine dedizierte Soundkarte selbstverständlich, weil der Rest der Maschine das einfach nicht konnte. Ich hänge immer noch an den alten Creative-Karten, nicht nur an den ISA-Dingern, auch an den späteren. Die hatten eine Wertigkeit, ein Gewicht, eine Bestückung, die man ansehen konnte. Man hat ein Bauteil gekauft, das eine Aufgabe hatte, und das hat man auch gesehen.

Heute steckt in meiner Workstation nicht einmal mehr eine Onboard-Lösung im Einsatz, sondern ein Behringer 302USB, also ein kleines Mischpult mit USB-Audiointerface. Für meinen Fall reicht das absolut. Die Aufgabe ist nicht verschwunden, sie hat nur ihren Platz gewechselt, und die CPU rechnet das nebenbei mit, ohne dass es jemandem auffällt. Genau das ist mit Krypto passiert. AES-NI und VAES haben die Beschleunigerkarte nicht besiegt, sie haben sie aufgesogen.

Immer kleiner, komplexer und leistungsfähiger ist total geil. Ohne diese Entwicklung gäbe es die Hälfte von dem nicht, was wir heute technisch machen. Aber sie macht das Verstehen sehr viel schwieriger. Mein alter C64 und der VC20 davor, die Dinger hat man noch verstanden. Eine CPU mit rund einem Megahertz in einem 40-Pin-Gehäuse, groß genug und langsam genug, dass man mit dem Oszilloskop an einzelne Pins konnte und dabei etwas gesehen hat. Man konnte am Speicher nachmessen. Das war begreifbar im wörtlichen Sinn. An dem Xeon in dieser Maschine messe ich nichts nach. Der Die ist unter einem Heatspreader, die Signale sind differentiell und liegen im Gigahertzbereich, und selbst wenn ich rankäme, wäre mein Oszilloskop zu langsam. Dass bei dieser Karte das nackte Silizium offen liegt, ist ein Zufall der Bauform, und trotzdem freut es mich jedes Mal.

Dafür haben wir heute AI, um uns Dinge erklären zu lassen, und das ist ein echter Gewinn. Ich komme damit an Ecken, an die ich vor zehn Jahren nur mit sehr viel mehr Zeit gekommen wäre. Wir müssen nur alle aufpassen, dass wir dabei nicht aufhören zu verstehen. Der Unterschied zwischen „ich habe es erklärt bekommen“ und „ich habe es verstanden“ ist genau der Unterschied zwischen diesem Beitrag und einer Feature-Liste. Und ein Teil davon passiert ja bereits in der AI-Entwicklung selbst: wir verstehen nicht mehr im Detail, was in diesen Systemen passiert. Stand jetzt halte ich das für ein Problem. Vielleicht ist es aber auch bald einfach das Normale, und ich bin der Typ, der dem Oszilloskop nachtrauert.

Ehrliche Gesamteinschätzung

Als Produktivkomponente ist die Karte in dieser Maschine sinnlos. Eine einzige moderne CPU der Einstiegsklasse schlägt sie bei symmetrischer Krypto um Faktor 8,6, der Anwendungsfall, für den sie gekauft wurde, ist unter Linux versperrt, Intels Userspace hat sie aufgegeben, sie zieht laut Datenblatt bis zu 40 Watt für etwas, das die CPU besser kann, und gekühlt wird sie für einen Luftstrom, den mein Gehäuse nicht liefert.

Als Lehr- und Erzählobjekt ist sie ausgezeichnet. An diesem einen Stück Platine lassen sich Krypto-Offload als Architektur, Latenz gegen Durchsatz, warum Queue-Tiefe alles ist, PCIe-Generationen und Lane-Budgets, PCIe-Switches, SR-IOV, IOMMU-Gruppen und die Prioritätslogik der Linux Crypto API erklären. Ich kenne wenige Bauteile, die auf so kleiner Fläche so viele Anknüpfungspunkte haben. Und sie funktioniert einfach, nach 13 Jahren, ohne ein einziges installiertes Paket.

Genau diese Spannung war der Grund, sie überhaupt noch einmal einzustecken.

Was offen bleibt

  • Die Karte per VFIO an eine FreeBSD-VM durchreichen und dort GELI auf QAT laufen lassen, also die Original-Nutzung rekonstruieren. Sie ist allein in ihrer IOMMU-Gruppe, technisch steht dem nichts im Weg. Das ist der Versuch, auf den ich am meisten Lust habe.
  • SR-IOV aktivieren und sehen, was der Treiber mit 32 Virtual Functions macht. Völlig ungetestet.
  • PCIe-Zähler messen, um zu belegen oder zu widerlegen, dass wirklich der Bus limitiert und nicht der Zugangspfad. Der wichtigste offene Messpunkt.
  • Die Benchmarks methodisch nachziehen: Warmup, mehrere Läufe, Median und P95, CPU-Pinning.
  • Kernel-RSA über die Karte messen. qat-rsa gewinnt per Priorität, aber es gibt keinen AF_ALG-Zugang zu akcipher.
  • Temperatur und echte Leistungsaufnahme, beides nur extern messbar.
  • Das PLX-EEPROM auslesen, um die Lane-Aufteilung zu belegen statt zu vermuten.

Siehe auch

Hast du so eine Karte noch im Einsatz, vielleicht sogar noch produktiv? Dann würde mich das wirklich interessieren, und ihr dürft mich sehr gerne fragen.

Einen modernen OpenPGP-Schlüssel bauen: Ed25519, drei Unterschlüssel und die Härtung, die ihn geschwächt hat

Moin. Im letzten Beitrag ging es darum, wie ich meinen OpenPGP-Schlüssel an Domain, GitHub und Matrix gebunden habe, also um die Frage, woher eigentlich irgendwer wissen soll, dass dieser Schlüssel mir gehört. Ganz am Ende stand dort die Ankündigung eines zweiten Teils: wie das Ding überhaupt gebaut ist und wie sicher es wirklich ist. Der Teil ist jetzt hier.

OpenPGP-Schlüssel mit Ed25519-Hauptschlüssel, drei Unterschlüsseln und Warnung vor fehlenden Algorithmus-Präferenzen durch eine fehlerhafte 3DES-Härtung.

Der Beitrag hat zwei Hälften, und die kannst du unabhängig voneinander lesen. Teil A ist ein Rezept. Wenn du einen Schlüssel nach demselben Muster bauen willst, arbeitest du dich von oben nach unten durch, inklusive der kompletten GnuPG-Konfiguration, alles kopierbar und in der richtigen Reihenfolge. Teil B erklärt, was die einzelnen Bauteile bedeuten, und endet mit einer ehrlichen Einschätzung, wie sicher das alles am Ende ist.

Der eigentliche Grund, warum ich das aufschreibe, ist aber ein anderer. Zwischen „mein Schlüssel ist modern“ und „mein Schlüssel ist richtig konfiguriert“ liegt eine Lücke, und in die bin ich mit Anlauf hineingesprungen. Der Schlüssel ist Lehrbuch: Ed25519, ein Hauptschlüssel, der ausschließlich zertifiziert, drei getrennte Unterschlüssel, fünf Jahre Laufzeit, über sieben Kanäle veröffentlicht, dazu eine eID-Zertifizierung. Und trotzdem wurde er vom ersten Tag an schwächer angesprochen als der Schlüssel, den er ablöst.

Aufgefallen wäre das niemandem, und genau das ist der interessante Teil. Das einzige Symptom war eine Warnzeile auf dem Terminal von jemandem, der mir eine verschlüsselte Mail schreibt. Also auf einem Rechner, den ich nie zu Gesicht bekomme. Und die Ursache war ausgerechnet eine Härtungsmaßnahme, die ich in bester Absicht eingebaut hatte, um einen musealen Algorithmus loszuwerden. Härtung, die etwas leise verschlechtert, finde ich als Geschichte deutlich spannender als noch eine Schlüsselerzeugungsanleitung. Nachbauen kannst du das übrigens in zwei Minuten, das Labor dazu steht weiter unten.

Der Schlüssel, um den es geht

BestandteilWert
Hauptschlüsseled25519 0x893DE0CDDE986DEB, Verwendung [C], also nur zertifizieren
Fingerabdruck45FC D081 ADB5 4872 EA5B 06B9 893D E0CD DE98 6DEB
Signatur-Unterschlüsseled25519 0xD788641D8588A674
Verschlüsselungs-Unterschlüsselcv25519 0x429D03637892821A
Authentisierungs-Unterschlüsseled25519 0x22F2E3234664DDBE
UIDsSebastian van de Meer, dazu eine Foto-UID mit 10851 Byte
Gültigkeiterzeugt am 24.07.2026, läuft am 23.07.2031 ab
Vorgängered25519 0x5F279C362EEAB216, gültig bis 31.12.2026, zeichnet den neuen gegen
FremdzertifizierungGovernikus/eID 0x5E5CCCB4A4BF43D7, Level 3
UmgebungGnuPG 2.4.4, libgcrypt 1.10.3

Die Aufgabenteilung steht nicht nur in der Doku, die steht maschinenlesbar im Schlüssel selbst. Jede Bindungssignatur trägt ein Feld mit Schlüssel-Flags, und das sieht bei mir so aus:

primary  key flags: 01   certify
[S]      key flags: 02   sign
[E]      key flags: 0C   encrypt communications + encrypt storage
[A]      key flags: 20   authenticate

Teil A: den Schlüssel bauen

Teil A ist der Ablauf, den ich für richtig halte, und bis auf die markierten Stellen genau der, den ich gegangen bin. Dort, wo der Befehl unten besser ist als das, was ich damals getippt habe, steht ein Hinweis dazu.

Voraussetzungen

gpg --version
# gpg (GnuPG) 2.4.4
# libgcrypt 1.10.3

GnuPG 2.4.x ist die Annahme für den ganzen Beitrag. Zwei Dinge gleich vorweg, weil sie sonst später wehtun:

  • --quick-generate-key verweigert den Dienst, wenn die UID bereits auf einem anderen Schlüssel im Schlüsselbund existiert. Genau deshalb ist dieser Schlüssel über eine Parameterdatei mit --batch --gen-key entstanden. Das trifft dich zuverlässig genau dann, wenn du einen Schlüssel rotierst, also in dem Moment, in dem der alte noch da ist.
  • Nichts hier braucht eine Smartcard, und nichts hier braucht root.

Wenn du bei GPG ganz am Anfang stehst, sind die Grundlagen zum Signieren und Verschlüsseln hier im Blog der bessere Einstieg. Dieser Beitrag setzt voraus, dass du weißt, was ein öffentlicher Schlüssel und ein Unterschlüssel sind.

Erst aufräumen, dann anfangen

Und zwar wirklich vorher, nicht hinterher. Mein ~/.gnupg war in einem Zustand, den ich hier nur ungern zugebe.

chmod 700 ~/.gnupg
find ~/.gnupg -type f -exec chmod 600 {} +

# Erst alle GnuPG-Frontends schließen und die Daemons beenden, sonst löschst du
# unter Umständen echte, aktive Locks:
gpgconf --kill all

# Übriggebliebene Lock-Dateien abgestürzter Läufe. Bei mir waren es 93 Stück.
find ~/.gnupg -name '.#lk*' -delete

# Altlasten, die keine 2.4er Installation mehr braucht
mkdir -p ~/gnupg-attic-$(date +%F)
mv ~/.gnupg/{cert8.db,key3.db,secmod.db} ~/gnupg-attic-$(date +%F)/ 2>/dev/null

Ein ~/.gnupg mit 775 und Dateien mit 664 ist erschreckend verbreitet, und ich will das gar nicht größer machen als es ist: Auf einem Einzelplatzrechner ist das keine Katastrophe. Es wird aber genau in dem Moment eine, in dem das Home-Verzeichnis in ein Backup, in einen Container-Mount oder in einen synchronisierten Ordner wandert. Und das passiert schneller, als man denkt.

Die gpg.conf, komplett

Das ist die tatsächlich laufende Konfiguration, so wie sie bei mir liegt, mit ihren Originalkommentaren. Der Block mit dem Warnhinweis ist die wichtigste Stelle im gesamten Beitrag, warum, steht in Teil B.

# ---- UX / output ----
keyid-format 0xlong
with-fingerprint
with-subkey-fingerprint
utf8-strings

# ---- Key discovery (local first, then WKD/DANE; email-validated keyserver last) ----
auto-key-retrieve
auto-key-locate local,wkd,dane,keyserver
keyserver hkps://keys.openpgp.org

# ---- Strong defaults ----
# cipher-algo / digest-algo are intentionally NOT forced: forcing them would override
# the recipient's stated capabilities. personal-*-preferences below select strong
# algorithms interoperably instead.
cert-digest-algo SHA512

# ---- KDF hardening for passphrase-derived symmetric keys (gpg -c / key export) ----
s2k-mode 3
s2k-digest-algo SHA512
s2k-cipher-algo AES256
s2k-count 65011712

# ---- WARNING. Legacy ciphers: do NOT disable 3DES here ----
# An earlier version of this file carried:
#     disable-cipher-algo 3DES
#     disable-cipher-algo IDEA
#     disable-cipher-algo CAST5
#     disable-cipher-algo BLOWFISH
#     disable-cipher-algo TWOFISH
# The 3DES line alone silently strips ALL algorithm preferences from every key you
# generate while it is active, and it also blocks DECRYPTION of old archives.
# New encryption never selects a legacy cipher anyway, because
# personal-cipher-preferences lists AES only.

# ---- Privacy / minimal metadata ----
no-comments
no-emit-version
export-options export-minimal

# ---- Listing/verification quality-of-life ----
verify-options show-uid-validity
list-options show-uid-validity

# ---- Trust model ----
trust-model tofu+pgp

# ---- Your key ----
default-key 0x45FCD081ADB54872EA5B06B9893DE0CDDE986DEB
# hidden-encrypt-to 0x45FCD081ADB54872EA5B06B9893DE0CDDE986DEB   # optional, off

# ---- Local policy: what *you* prefer when sending ----
personal-cipher-preferences AES256 AES192 AES
personal-digest-preferences SHA512 SHA384 SHA256
weak-digest SHA1
force-ocb

# ---- Preferences baked into keys you create from here on ----
default-preference-list SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed

Zwei Zeilen darin sind Entscheidungen und keine Selbstverständlichkeiten, deshalb je ein Satz dazu.

  • force-ocb ist keine Präferenz, sondern eine Erzwingung. Es sorgt dafür, dass ausgehende Nachrichten die OCB-Variante aus der LibrePGP-Linie benutzen, und setzt sich dabei über das hinweg, was der Empfängerschlüssel ankündigt. Sehr alte oder anders implementierte Empfänger können solche Nachrichten nicht lesen. Das steht in einer gewissen Spannung zu dem Prinzip ein paar Zeilen weiter oben, wo ich cipher-algo bewusst nicht erzwinge. Für mich ist das in Ordnung, in einer Konfiguration zum Abschreiben würde ich die Zeile weglassen.
  • auto-key-retrieve holt unbekannte Schlüssel beim Prüfen automatisch nach. Sehr bequem, und es verrät dem Keyserver, welche signierten Nachrichten du wann liest. Für mich ein akzeptabler Tausch, bei einem anderen Bedrohungsmodell schaltest du das besser ab.

Dazu die beiden kleinen Geschwisterdateien. gpg-agent.conf:

enable-ssh-support
default-cache-ttl 180
max-cache-ttl 600
pinentry-program /usr/bin/pinentry-gnome3

Und dirmngr.conf:

honor-http-proxy
disable-ldap

Den Hauptschlüssel erzeugen

Ein Hauptschlüssel, der nur zertifiziert, über eine Parameterdatei. Die Zeile Preferences: war in meinem echten Lauf nicht drin, nimm sie mit. Sie kostet nichts und fängt genau den Fehler ab, um den es in Teil B geht. Das ist keine Vermutung, ich habe es nachgemessen: Mit dieser Zeile bleiben die Präferenzen selbst dann im Schlüssel, wenn die defekte Konfiguration aktiv ist. Die eigentliche Kontrolle bleibt trotzdem der Paket-Dump gleich darunter, denn eine Parameterdatei ersetzt nie die Prüfung des fertigen Artefakts.

cat > keyparams.txt <<'EOF'
Key-Type: eddsa
Key-Curve: Ed25519
Key-Usage: cert
Name-Real: Sebastian van de Meer
Name-Email: kernel-error@kernel-error.com
Expire-Date: 5y
Preferences: AES256 AES192 AES SHA512 SHA384 SHA256 ZLIB BZIP2 ZIP Uncompressed
%ask-passphrase
%commit
EOF

gpg --batch --gen-key keyparams.txt
shred -u keyparams.txt

Und jetzt sofort nachsehen, ob die Präferenzen auch wirklich im Schlüssel gelandet sind. Genau diese Prüfung fehlte bei mir im Juli, und deshalb steht sie hier direkt hinter der Erzeugung und nicht irgendwo weiter unten.

gpg --export kernel-error@kernel-error.com | gpg --list-packets | grep -E 'pref-|features'

Du willst dort pref-sym-algos, pref-hash-algos, pref-zip-algos und ein features mit gesetztem Bit 0x01 sehen. Wenn dort nur eine features-Zeile steht und sonst nichts, dann hör hier auf und lies erst den Abschnitt über den Defekt in Teil B. Dann stimmt etwas mit deiner Konfiguration nicht, und der Schlüssel trägt den Fehler ab sofort dauerhaft mit sich herum.

Zum shred oben noch ein Wort, weil es sonst falsche Sicherheit erzeugt: Es entfernt die sichtbare Arbeitsdatei, mehr nicht. Auf SSDs, auf ZFS, bei Snapshots und auf journalenden Dateisystemen ist damit überhaupt nicht garantiert, dass keine alten Blöcke mehr herumliegen. Solche Zwischendateien sollten deshalb von vornherein nur auf einem verschlüsselten Dateisystem entstehen.

Die drei Unterschlüssel

Interaktives --quick-add-key ist mir in einer nicht-interaktiven Shell mit einem /dev/tty-Fehler um die Ohren geflogen. Mit --batch läuft es durch.

FPR=45FCD081ADB54872EA5B06B9893DE0CDDE986DEB
gpg --batch --quick-add-key $FPR ed25519  sign 5y
gpg --batch --quick-add-key $FPR cv25519  encr 5y
gpg --batch --quick-add-key $FPR ed25519  auth 5y

Die Foto-UID

240×288 Pixel, Graustufen-JPEG, 10851 Byte. Halt das Bild klein, es reist in jedem vollständigen Export mit und ist der mit Abstand größte Posten in der Schlüsselgröße.

gpg --edit-key $FPR
> addphoto
> /pfad/zu/sebastian-photo-uid.jpg
> save

Eine Sache dazu, die man vorher wissen sollte: Keiner der drei Keyserver, die ich benutze, hat die Foto-UID nach dem Upload je wieder herausgerückt. Für jeden Keyserver da draußen lege ich die Hand nicht ins Feuer, für die drei, auf die es ankommt, schon. Das Bild überlebt damit nur auf den Kanälen, die ich selbst hoste. Damit ist die Foto-UID Dekoration und keine Funktion. Das ist ein völlig legitimer Grund, sie trotzdem mitzunehmen, man sollte sich nur nichts vormachen.

Backup und Widerrufszertifikat, bevor irgendetwas veröffentlicht wird

Zwei Dinge müssen existieren, bevor der Schlüssel irgendwo landet: ein Backup des geheimen Schlüssels und ein Widerrufszertifikat. Beides später nachzuholen ist der Klassiker, den man genau einmal bereut.

gpg --export-secret-keys --armor $FPR > secret-key-backup.asc
gpg --output revoke.asc --gen-revoke $FPR

Ein Widerrufszertifikat legt GnuPG bei der Erzeugung ohnehin schon selbst unter ~/.gnupg/openpgp-revocs.d/<FPR>.rev ab. Das Backup dagegen ist erst dann eines, wenn du es einmal zurückgespielt hast. Also rein damit in ein Wegwerf-GNUPGHOME und nachsehen, ob der geheime Hauptschlüssel dort auch wirklich auftaucht und nicht bloß die Datei lesbar war. Danach wandern beide auf Offline-Medien.

RESTORE=$(mktemp -d); chmod 700 "$RESTORE"
gpg --homedir "$RESTORE" --import secret-key-backup.asc
gpg --homedir "$RESTORE" --with-subkey-fingerprint --list-secret-keys $FPR
rm -rf "$RESTORE"

Wichtig, weil es ein sehr verbreitetes Missverständnis ist: Der s2k-*-Block aus der gpg.conf betrifft den passphrasenbasierten Schutz, also etwa gpg -c und den Export. Er sagt nichts darüber aus, wie der geheime Schlüssel in ~/.gnupg auf der Platte geschützt ist. Darum kümmert sich der gpg-agent mit eigenen Parametern, und bereits vorhandenes Schlüsselmaterial wird durch eine geänderte gpg.conf nicht rückwirkend neu verpackt. Wer die beiden verwechselt, glaubt an eine Härtung, die an dieser Stelle gar nicht wirkt. Was tatsächlich in deinem Backup steht, siehst du wie immer am Artefakt selbst, per gpg --list-packets secret-key-backup.asc.

Den Hauptschlüssel aus dem Alltag nehmen

Jetzt kommt der Schritt, der die ganze Aufteilung von oben überhaupt erst einlöst. Der Hauptschlüssel hat im Alltag nichts verloren. Gebraucht wird er nur, wenn ein Unterschlüssel verlängert, ersetzt oder widerrufen wird, wenn eine UID dazukommt oder wenn du einen fremden Schlüssel zertifizierst. Also fliegt sein geheimer Teil vom Arbeitsrechner herunter, und zwar genau jetzt, nachdem Backup und Widerrufszertifikat existieren und nicht vorher.

# nur die Unterschlüssel exportieren, ohne den geheimen Hauptschlüssel
gpg --export-secret-subkeys --armor $FPR > subkeys.asc

# das gesamte geheime Material aus dem Alltags-Keyring werfen
gpg --delete-secret-keys $FPR

# und danach ausschließlich die Unterschlüssel zurückholen
gpg --import subkeys.asc
shred -u subkeys.asc

Kontrolliert wird das an einem einzigen Zeichen:

gpg -K
# sec#  ed25519/0x893DE0CDDE986DEB
# ssb   ed25519/0xD788641D8588A674
# ssb   cv25519/0x429D03637892821A
# ssb   ed25519/0x22F2E3234664DDBE

Das Doppelkreuz hinter sec ist der ganze Punkt. Es bedeutet: GnuPG kennt den Hauptschlüssel, hat sein geheimes Gegenstück aber nicht mehr. Signieren, Entschlüsseln und Authentisieren laufen unverändert weiter, dafür sind die Unterschlüssel zuständig. Was nicht mehr geht, ist alles, was die Identität selbst betrifft, also neue Unterschlüssel anlegen, Laufzeiten verlängern, UIDs ergänzen und fremde Schlüssel zertifizieren.

Der geheime Hauptschlüssel liegt ab hier zusammen mit dem Widerrufszertifikat auf verschlüsseltem Wechselmedium. Wenn ich ihn brauche, kommt er ausdrücklich nicht zurück nach ~/.gnupg, sondern in ein temporäres GNUPGHOME, das hinterher wieder verschwindet.

Ein Detail dabei ist wichtig genug für einen eigenen Absatz, weil es sonst still danebengeht: Das Offline-Backup ist ein Schnappschuss. Es entstand oben, bevor der alte Schlüssel gegengezeichnet hat und bevor die Governikus-Zertifizierung da war. Diese Fremdsignaturen hängen am öffentlichen Teil, der im Alltags-Keyring liegt, nicht im Backup. Wer nur das Backup einspielt und dort arbeitet, exportiert hinterher einen Schlüssel, dem genau diese Signaturen fehlen. Also immer beides einspielen:

# aktuellen öffentlichen Stand samt Fremdsignaturen aus dem Alltags-Keyring mitnehmen
gpg --armor --export-options no-export-minimal --export $FPR > /tmp/pub-aktuell.asc

export GNUPGHOME=$(mktemp -d); chmod 700 "$GNUPGHOME"
trap 'rm -rf "$GNUPGHOME"' EXIT HUP INT TERM   # auch bei Abbruch aufräumen
gpg --import /media/offline/secret-key-backup.asc
gpg --import /tmp/pub-aktuell.asc

# hier die Arbeit am Schlüssel, etwa eine Laufzeit verlängern

gpg --armor --export-options no-export-minimal --export $FPR > /tmp/pub-neu.asc
rm -rf "$GNUPGHOME"; unset GNUPGHOME

Danach importierst du die aktualisierte öffentliche Hälfte in den Alltags-Keyring und rollst sie über alle Veröffentlichungskanäle aus. Das ist unbequem. Es soll unbequem sein, denn genau diese Unbequemlichkeit ist der Grund, warum der Hauptschlüssel selten angefasst wird und deshalb schwer zu verlieren ist.

Den alten Schlüssel den neuen gegenzeichnen lassen

Ein weicher Übergang statt einer harten Kante: Der alte Schlüssel bleibt bis zu seinem Ablauf gültig und zertifiziert den neuen. Wer dem alten Schlüssel schon vertraut, bekommt damit einen kryptografischen Pfad zum neuen, ohne mich irgendwo anrufen zu müssen.

gpg --default-key 0x5F279C362EEAB216 --sign-key $FPR

Die externe Zertifizierung über Governikus

Das ist ein eigenes Thema, deshalb hier nur zusammengefasst: Governikus ist nach meinem Stand vom August 2026 der einzige mir bekannte noch aktive Dienst in Deutschland, der OpenPGP-Schlüssel über die Online-Ausweisfunktion zertifiziert. Du schickst deinen öffentlichen Schlüssel hin, weist dich mit dem Personalausweis aus und bekommst eine Zertifizierung mit Level 3 zurück. Die Volksverschlüsselung fällt als Alternative aus, die macht ausschließlich S/MIME nach X.509, erzeugt die Schlüssel selbst und wird ohnehin eingestellt.

Strukturell wichtig für den Rest des Beitrags ist nur ein Punkt: Diese Zertifizierung ist eine Fremdsignatur. Und die Hälfte meiner Veröffentlichungskanäle wirft Fremdsignaturen weg. Warum, steht gleich.

Vier Exporte für vier Aufgaben

Das ist der Teil, den fast alle Anleitungen überspringen, und ausgerechnet hier fallen die echten Entscheidungen. Vier verschiedene Exporte desselben Schlüssels für vier verschiedene Jobs, mit den gemessenen Größen der Artefakte, die tatsächlich ausgeliefert werden:

ArtefaktBefehlGrößeInhalt
minimal, ASCIIgpg --armor --export $FPR16772 B1 UID plus Foto, 3 Unterschlüssel, 5 Signaturen
vollständig, ASCIIgpg --armor --export-options no-export-minimal --export $FPR17833 Bzusätzlich 3 Fremdsignaturen, also 8 Signaturen
WKD, binärgpg --no-armor --export-options no-export-minimal --export $FPR13108 Bwie vollständig, nur binär
DANE, binär minimalsiehe unten1298 B1 UID, kein Foto, 4 Signaturen

Alle vier Befehle habe ich gegen die live ausgelieferten Dateien geprüft, sie reproduzieren die Artefakte byteidentisch, sha256 stimmt jeweils überein. Du kannst sie also so übernehmen.

Und jetzt die fiese Falle. In meiner gpg.conf steht global export-options export-minimal. Export-Optionen summieren sich, sie ersetzen einander nicht. --export-options export-clean hebt export-minimal also nicht auf. Du bekommst weiterhin einen minimalen Export, still und ohne Warnung, mit weggeworfenen Fremdsignaturen. Nur die ausdrückliche Verneinung funktioniert:

gpg --export-options export-clean      --export $FPR | gpg --list-packets | grep -c '^:signature packet:'   # 5, immer noch minimal
gpg --export-options no-export-minimal --export $FPR | gpg --list-packets | grep -c '^:signature packet:'   # 8, korrekt

Das ist genau die Sorte Fehler, bei der du fest davon überzeugt bist, einen Schlüssel mit allen Signaturen veröffentlicht zu haben, während in Wahrheit die eID-Zertifizierung nie das Haus verlassen hat.

Die zweite Stolperfalle beim Export: export-minimal wirft zwar Fremdsignaturen weg, behält aber die Foto-UID. Für den DANE-Record muss auch das Bild raus, sonst wächst der Record von rund einem Kilobyte auf gute zwölf. Dieser Befehl hat den ausgelieferten Record mit 1298 Byte erzeugt:

gpg --no-armor --export-options export-minimal --export-filter keep-uid='mbox = kernel-error@kernel-error.com' --export $FPR > openpgpkey.bin

Zwei Anmerkungen noch zu den Formaten, weil sich in der 2.4er Reihe etwas geändert hat:

  • --print-dane-records gibt es nicht mehr. Der Nachfolger heißt --export-options export-dane.
  • Verwechsle die beiden Ausgaben aber nicht. export-dane liefert einen fertigen DNS-Präsentationsblock, also $ORIGIN, Kommentarzeilen und generische TYPE61-Rdata, bei mir 25642 Byte. Das ist kein binäres Schlüsselmaterial. Zum Lesen ist es praktisch, der ausgelieferte Record entstand aber aus dem binären Export oben plus base64 -w0 openpgpkey.bin. Beide Wege sind gültige Zonefile-Syntax, RFC 7929 definiert die base64-Präsentationsform und RFC 3597 die generische TYPE61-Form, BIND frisst beides. Der Einzeiler passte einfach besser in meine bestehende Zone.

Wo der Schlüssel liegt, und warum genau dort

Sieben Kanäle, und jeder existiert aus einem anderen Grund. Das „warum“ ist dabei die interessantere Hälfte:

KanalFormatWarum dieser Kanal
keys.openpgp.orgvoll hochgeladen, Server wirft Signaturen und Foto wegder einzige Keyserver, der die Mailadresse validiert, und der, den moderne Clients abfragen
keyserver.ubuntu.comvollklassischer SKS-Nachfolger, behält Fremdsignaturen
pgpkeys.euvollzweiter Klassiker, Redundanz
DANE / OPENPGPKEYminimal binär, 1298 BVertrauen hängt an DNSSEC statt an einem Signaturgraphen, muss klein bleiben, es ist ein DNS-Record
WKD advancedkein eigener Export, CNAME auf wkd.keys.openpgp.orgspiegelt keys.openpgp.org automatisch, lässt sich hier nicht selbst hosten
WKD direct (Apex)voll binär, 13108 Bselbst gehostet, deshalb die einzige Stelle mit Foto und Fremdsignaturen
security.txt und Direktdownloadvoll ASCII, 17833 Bauffindbar für Menschen und für Scanner

Zwei abgeleitete Namen tauchen dabei auf, und die werden regelmäßig falsch gebildet. Beide habe ich unabhängig nachgerechnet, sie stimmen mit dem überein, was ausgeliefert wird:

# WKD: z-base32 des SHA-1 vom lokalen Teil, ASCII-Großbuchstaben vorher klein gemacht
#   sha1("kernel-error") -> z-base32 -> 3gyjbxx9xfdggpkmx5qdd793xy431w5u
gpg --with-wkd-hash -k kernel-error@kernel-error.com

# DANE (RFC 7929): SHA-256 des kanonisierten UTF-8-Lokalteils, auf 28 Oktette gekürzt,
# hex-kodiert, dann ._openpgpkey.<domain>
#   70e1c7d87e825b3aba45e2a478025ea0d91d298038436abde5a4c2d0._openpgpkey.kernel-error.com

Zwei Präzisierungen, damit hier keine Regel steht, die breiter ist als die Spezifikationen: WKD bildet ASCII-Großbuchstaben auf Kleinbuchstaben ab, das ist kein allgemeines Unicode-Lowercasing. Und DANE übernimmt diese Abbildung nicht einfach, RFC 7929 hat seine eigene Kanonisierung des lokalen Teils. Bei mir fallen beide zusammen, weil kernel-error schon reines Kleinbuchstaben-ASCII ist. Genau deshalb ist mir der Unterschied nie begegnet. Wer einen Lokalteil mit Großbuchstaben oder Nicht-ASCII hat, darf die beiden auf keinen Fall gleichsetzen.

Und weil ich schon dabei bin, mich zu blamieren: Bei einem früheren Review habe ich DANE als „fehlt“ markiert, weil ich den z-base32-Namen aus WKD gegen das DNS geprüft habe. Also den falschen Namen. DANE lief die ganze Zeit. Wenn du deinen eigenen Aufbau prüfst, prüfe bitte den Namen, den die jeweilige Spezifikation vorschreibt, und nicht den, den du gerade im Kopf hast. Wie man einen OPENPGPKEY-Record überhaupt in die Zone bekommt und wie man die Zone dafür mit BIND signiert, steht hier im Blog.

Eine Konsequenz aus der Tabelle überrascht die Leute regelmäßig, deshalb schreibe ich sie deutlich hin:

Die Governikus-Zertifizierung ist nur auf den klassischen Keyservern und auf den selbst gehosteten Kopien sichtbar. keys.openpgp.org wirft sie weg, und gpg bevorzugt die WKD-advanced-Methode, die wiederum auf keys.openpgp.org zeigt. Der Kanal, den die meisten Clients benutzen, ist also ausgerechnet der mit den wenigsten Signaturen.

Prüfen wie ein Fremder

Der letzte Schritt in Teil A, und der wichtigste: Hol dir deinen eigenen Schlüssel so, wie es jemand tut, der dich nicht kennt. Einmal pro Suchpfad, jedes Mal in einem frischen Schlüsselbund.

# den Schlüssel holen wie ein Fremder, einmal pro Lookup-Pfad
for m in dane wkd keyserver; do
  GNUPGHOME=$(mktemp -d) gpg --auto-key-locate clear,$m --locate-external-keys kernel-error@kernel-error.com
done

# welche Präferenzen bewirbt der Schlüssel tatsächlich?
gpg --export $FPR | gpg --list-packets | grep -E 'pref-|features'

# welchen Cipher wählt ein Absender wirklich?
gpg --status-fd 1 -d message.gpg | grep DECRYPTION_INFO

Damit endet das Rezept. Ab hier geht es darum, was die Teile bedeuten, und um die Frage, wie sicher der Aufbau am Ende wirklich ist.

Teil B: was das alles bedeutet

Wenn du bis hierher gekommen bist, hast du ein funktionierendes Rezept. Was du noch nicht hast, ist ein Gefühl dafür, warum die Teile so und nicht anders geschnitten sind, und wo der Aufbau trotz allem nachgibt. Genau darum geht es jetzt, und zwar in dieser Reihenfolge: erst die Struktur, dann die Zahlen, dann der Fehler und am Ende die Rechnung ohne Schönfärberei.

Warum ein Hauptschlüssel, der nur zertifiziert

Der Hauptschlüssel hat genau eine Aufgabe: zu sagen, wer zu diesem Schlüssel gehört. Er signiert die UIDs und er signiert die Unterschlüssel. Er signiert nie eine Mail und entschlüsselt nie irgendetwas. Alles Operative ist delegiert:

[C] certify   Identitätsanker. Wird selten benutzt. Eine Kompromittierung ist
              nicht reparierbar, ein Verlust nur über das Offline-Backup.
[S] sign      Signieren im Alltag
[E] encrypt   Entschlüsseln im Alltag
[A] auth      Authentisierung, etwa als SSH-Schlüssel

Der Gewinn: Ein kompromittierter oder ausgedienter Unterschlüssel lässt sich widerrufen und ersetzen, ohne die Identität anzufassen, ohne die gesammelten Fremdsignaturen zu verlieren und ohne den Fingerabdruck zu ändern, der an sieben Stellen veröffentlicht ist. Dass [S] und [E] getrennt sind, hat noch einen zweiten Grund: Sie haben unterschiedliche Lebenszyklen. Einen alten Signaturschlüssel zu vernichten ist harmlos. Einen alten Verschlüsselungsschlüssel zu vernichten heißt, dass du an dein Archiv nicht mehr herankommst.

Das Ganze zahlt sich allerdings nur aus, wenn du auch die Konsequenz ziehst: Der volle Nutzen dieser Aufteilung stellt sich erst ein, wenn der Hauptschlüssel offline liegt. Genau deshalb liegt er hier offline, auf verschlüsseltem Wechselmedium und zusammen mit dem Widerrufszertifikat. Er kommt nur heraus, wenn ein Unterschlüssel verlängert, ersetzt oder widerrufen werden muss, und dann in ein temporäres GNUPGHOME und nicht zurück in den Alltags-Keyring. Auf der Arbeitsmaschine liegen ausschließlich die drei Unterschlüssel. Ein Hauptschlüssel, der nur zertifiziert, aber trotzdem neben dem Mailclient herumliegt, ist am Ende nur Kosmetik.

Der [A]-Unterschlüssel existiert übrigens, ist aber bewusst nicht im gpg-agent für SSH verdrahtet, sshcontrol ist leer. Ich habe ihn erzeugt, um mir die Option offenzuhalten. Mehr ist es im Moment nicht.

Ed25519 und X25519, was die Zahlen bedeuten

  • Ed25519 zum Signieren und X25519, in GnuPG als cv25519 bezeichnet, zum Verschlüsseln liegen beide bei rund 128 Bit klassischem Sicherheitsniveau. Das ist vergleichbar mit RSA-3072 und deutlich oberhalb von RSA-2048.
  • Sie sind dramatisch kleiner und schneller. Deshalb passt der komplette Schlüssel mit drei Unterschlüsseln, Foto und drei Fremdsignaturen immer noch in 13 KB.
  • Curve25519 ist fehlbedienungsresistent entworfen: keine Parameter, die man falsch wählen kann, keine Invalid-Curve-Fallen, deterministische Nonces bei Ed25519. Ein großer Teil der historischen Signatur-Unfälle in der Praxis kam aus genau den Ecken, die diese Kurve wegräumt.

Ein Punkt dazu, den du im Kopf behalten solltest, weil er gleich noch wichtig wird: 128 Bit klassische Sicherheit sind die Obergrenze dieses Schlüssels. Merk dir die Zahl. Sie ist der Grund, warum die AES-Geschichte weiter unten weniger dramatisch ist, als sie zunächst klingt.

Algorithmen-Präferenzen: was sie sind und wer sie liest

Dieses Konzept muss sitzen, sonst funktioniert die Geschichte danach nicht. Vorweg ein Satz, den viele nie gehört haben: Eine OpenPGP-Nachricht ist immer hybrid. Der asymmetrische Teil, also dein Ed25519- und X25519-Material, verpackt ausschließlich einen zufällig erzeugten symmetrischen Sitzungsschlüssel. Die eigentlichen Nutzdaten verschlüsselt ein symmetrisches Verfahren, in der Regel AES. Der Fehler, um den es gleich geht, saß in dieser symmetrischen Hälfte.

Ein OpenPGP-Schlüssel trägt nämlich nicht nur öffentliche Schlüssel spazieren. Die Selbstsignatur jeder UID enthält Subpakete, die ankündigen, was der Besitzer verarbeiten kann:

hashed subpkt 11 (pref-sym-algos:  9 8 7)      AES256, AES192, AES128
hashed subpkt 21 (pref-hash-algos: 10 9 8)     SHA512, SHA384, SHA256
hashed subpkt 22 (pref-zip-algos:  2 3 1 0)    ZLIB, BZIP2, ZIP, unkomprimiert
hashed subpkt 34 (pref-aead-algos: 2)          OCB, fehlt auf diesem Schlüssel
hashed subpkt 30 (features: 05)                Feature-Bits, siehe unten
hashed subpkt 23 (keyserver preferences: 80)   "no-modify"

Das features-Oktett ist die Stelle, an der ich mich in meinen eigenen Notizen vertan hatte, deshalb hier die Dekodierung. In der Linie, die GnuPG 2.4 implementiert, bedeuten die Bits des ersten Oktetts:

0x01  SEIPD-v1 mit Modification Detection Code (MDC)
0x02  AEAD
0x04  Unterstützung für das v5-Schlüssel- und Fingerabdruckformat

features 05 ist also 0x01 + 0x04, das heißt MDC und kein AEAD. features 07 wäre 0x01 + 0x02 + 0x04, und genau das bekommt ein Schlüssel, den GnuPG 2.4 mit Standardeinstellungen erzeugt. Der kaputte Zustand war features 04, also 0x04 ganz allein: weder MDC noch AEAD angekündigt.

Eine Versionsfußnote gehört dazu: Diese Bitbelegung und das Subpaket 34 stammen aus der LibrePGP-Linie, die GnuPG 2.4 umsetzt. RFC 9580 hat beides geändert, dort sind die AEAD-Präferenzen in Subpaket 39 umgezogen und das Feature-Modell wurde umgebaut. Diese Spaltung kommt weiter unten noch einmal zurück.

Wenn dir jemand etwas verschlüsselt, liest dessen GnuPG deine Präferenzliste und wählt daraus das stärkste Verfahren, das beide können. Das ist der ganze Mechanismus. Daraus folgen zwei Dinge, die den meisten Leuten gegen den Strich gehen:

  • Deine eigenen personal-cipher-preferences schützen dich nicht. Die bestimmen, was du verschickst. Was Leute dir schicken, bestimmt allein das, was dein veröffentlichter Schlüssel ankündigt.
  • Die Präferenzen stehen in einer Signatur. Sie zu ändern heißt, die Selbstsignatur neu auszustellen. Und damit ist jede veröffentlichte Kopie deines Schlüssels veraltet, bis du sie erneuerst. Bei mir sind das sieben Kanäle und eine Änderung an der DNS-Zone.

Und wenn ein Schlüssel gar nichts ankündigt? Dann greift der Rückfall. RFC 4880 schreibt dafür 3DES vor, den verpflichtend zu implementierenden Algorithmus. Moderne GnuPG-Versionen landen dort allerdings nicht mehr: Seit der 2.3er Reihe verschlüsselt gpg grundsätzlich nicht mehr mit 64-Bit-Blockchiffren, dafür müsstest du ausdrücklich --allow-old-cipher-algos setzen. Der Rückfall endet deshalb bei AES-128. Nicht kaputt, aber eben auch nicht das, was der Schlüssel bekommen sollte.

Der Defekt, gemessen

Ein x-beliebiger Absender, der meinem brandneuen Schlüssel etwas verschlüsselt, sah das hier:

gpg: WARNING: cipher algorithm AES not found in recipient preferences
gpg: AES.CFB encrypted data
[GNUPG:] DECRYPTION_INFO 2 7 0        # 7 = AES128

Derselbe Absender, wenn er dem Schlüssel von 2023 schreibt, den der neue gerade ablöst:

[GNUPG:] DECRYPTION_INFO 2 9 0        # 9 = AES256

Der neue Schlüssel hatte überhaupt keine pref-sym-algos, keine pref-hash-algos und keine pref-zip-algos, dazu features 04 statt 05. Er wurde also schwächer angesprochen als sein Vorgänger, lautlos, und der einzige Hinweis darauf erschien auf einem Terminal, das jemand anderem gehört.

Die Ursache, und eine Korrektur an mir selbst

In meinen Arbeitsnotizen stand als Ursache: „mit einer --batch --gen-key-Parameterdatei ohne Preferences:-Zeile gebaut“. Das ist falsch. Eine Parameterdatei ohne diese Zeile erzeugt völlig ordentliche Präferenzen aus den eingebauten Defaults von GnuPG. Nachgeprüft, es kommt das hier heraus:

pref-sym-algos: 9 8 7 2 · pref-aead-algos: 2 · pref-hash-algos: 10 9 8 11 2 · features: 07

Die echte Ursache saß in der gpg.conf. Ich habe die Juli-Konfiguration in einem Wegwerf-Schlüsselbund nachgestellt, der Defekt war sofort wieder da. Danach habe ich die Datei halbiert, bis eine einzige Zeile übrig blieb:

Konfiguration im TestErgebnis
Juli-gpg.conf, unverändertfeatures: 04, gar keine pref-*
ohne disable-cipher-algo 3DESpref-sym-algos: 9 8 7 2, features: 07
ohne alle disable-cipher-algo-Zeilenpref-sym-algos: 9 8 7 2, features: 07
ohne cipher-algo AES256weiterhin kaputt, features: 04
nur disable-cipher-algo 3DES vorhandenkaputt, features: 04
disable-cipher-algo 3DES plus explizite Preferences:-Zeilesauber, pref-sym-algos: 9 8 7, features: 05

disable-cipher-algo 3DES ist also notwendig und hinreichend, um den Defekt auszulösen. Eine Zeile, mehr nicht.

Der Mechanismus dahinter ist die eigentliche Pointe des ganzen Beitrags. 3DES ist in OpenPGP der verpflichtend zu implementierende Algorithmus, und in der Standardliste steht er auch sichtbar drin: pref-sym-algos: 9 8 7 2 endet auf der 2, und die 2 ist 3DES. Nimmst du GnuPG diesen einen Algorithmus lokal weg, verschwindet nicht nur er aus der Liste, sondern die Liste als Ganzes.

Beim Warum bleibe ich vorsichtig. Dass GnuPG intern keine gültige Liste mehr konstruieren kann und deshalb gar keine schreibt, ist die naheliegende Erklärung, beweisen lässt sie sich von der Kommandozeile aus nicht. Belegt ist der Auslöser, nicht der Code-Pfad dahinter. Der Effekt selbst ist dagegen eindeutig: Die Härtungszeile hat nicht einen schwachen Algorithmus aus der Liste entfernt, sie hat die komplette Liste entfernt. Und damit dafür gesorgt, dass Absender zurückfallen, und zwar auf etwas Schwächeres als das, was ich gerade weghärten wollte.

Das Labor zum Selbernachbauen

Zwei Minuten, kein echter Schlüssel wird angefasst, alles passiert in Wegwerf-Verzeichnissen unter /tmp. Wenn du mir nicht glaubst, ist das hier der schnellste Weg, es selbst zu sehen:

SP=$(mktemp -d)
mk() {  # $1 = Name, $2 = bad|good
  export GNUPGHOME="$SP/$1"; mkdir -p "$GNUPGHOME"; chmod 700 "$GNUPGHOME"
  [ "$2" = bad ] && echo "disable-cipher-algo 3DES" > "$GNUPGHOME/gpg.conf"
  cat > "$SP/p.txt" <<EOF
Key-Type: eddsa
Key-Curve: Ed25519
Key-Usage: sign
Subkey-Type: ecdh
Subkey-Curve: cv25519
Subkey-Usage: encrypt
Name-Real: Demo $1
Name-Email: $1@example.invalid
Expire-Date: 1y
%no-protection
%commit
EOF
  gpg --batch --gen-key "$SP/p.txt" 2>/dev/null
  gpg --export -a "$1@example.invalid" > "$SP/$1.asc"
}
mk nopref bad
mk withpref good

export GNUPGHOME="$SP/sender"; mkdir -p "$GNUPGHOME"; chmod 700 "$GNUPGHOME"
echo "auto-key-locate local" > "$GNUPGHOME/gpg.conf"
gpg -q --import "$SP"/*.asc
echo hi > "$SP/m.txt"
for r in nopref withpref; do
  gpg --trust-model always --yes -e -r "$r@example.invalid" -o "$SP/$r.gpg" "$SP/m.txt"
  printf '%-9s ' "$r:"
  GNUPGHOME="$SP/$r" gpg -q -d "$SP/$r.gpg" 2>&1 | grep -i 'encrypted data'
done

Bei mir kommt das hier heraus, und die eine Zeile Unterschied ist der ganze Defekt:

nopref:   gpg: WARNING: cipher algorithm AES not found in recipient preferences
          gpg: AES.CFB encrypted data
withpref: gpg: AES256.OCB encrypted data

Die Reparatur

Der Fix selbst ist unspektakulär, ein einziger Befehl im Editiermodus. Weil setpref die Selbstsignatur neu ausstellt, braucht er den Hauptschlüssel. Das Ganze läuft also einmal im temporären GNUPGHOME von weiter oben, mit dem Offline-Schlüssel und dem aktuellen öffentlichen Stand:

gpg --edit-key $FPR
> setpref AES256 AES192 AES SHA512 SHA384 SHA256 ZLIB BZIP2 ZIP Uncompressed
> y
> save

Danach steht da pref-sym 9 8 7, pref-hash 10 9 8, pref-zip 2 3 1 0 und features 05. Der Fingerabdruck bleibt unverändert, und die Governikus-Zertifizierung, die Gegensignatur des alten Schlüssels, die Foto-UID und sämtliche Ablaufdaten überleben. Das schreibe ich so ausdrücklich hin, weil viele Leute Angst vor setpref haben und glauben, es beschädige den Schlüssel. Tut es nicht. Was es tut: Es stellt die Selbstsignatur neu aus. Und damit müssen anschließend alle sieben Veröffentlichungskanäle aufgefrischt werden.

Zur Sicherheit gleich hinterher: default-preference-list in der gpg.conf festgenagelt, und die disable-cipher-algo-Zeilen sind endgültig raus.

Wie schlimm war es wirklich?

Hier will ich ehrlich sein statt dramatisch, sonst wird das hier auch nur wieder eine von diesen „ich habe einen Bug gefunden“-Geschichten.

AES-128 statt AES-256 ist real, aber überschaubar. AES-128 ist nicht gebrochen, es gibt keinen praktischen Angriff darauf. Und erinnere dich an die Zahl von weiter oben: Der asymmetrische Teil dieses Schlüssels liefert ohnehin rund 128 Bit klassische Sicherheit. Gemessen an der reinen Schlüsselsuche war AES-128 also nicht das schwächste Glied in der Kette, es hat lediglich mit dem Rest gleichgezogen. Über Implementierungsfehler, Seitenkanäle oder Protokollschwächen sagt dieser Vergleich nichts. Die faire Einordnung lautet: Das war ein Hygiene- und Signalisierungsfehler, keine ausnutzbare Schwachstelle. Bemerkenswert ist er, weil er lautlos war, automatisch passierte und von einer Härtungsmaßnahme verursacht wurde.

Das fehlende MDC-Bit sah schlimmer aus, als es war. features 04 heißt, dass das Bit 0x01 fehlte, mit dem ein Schlüssel Modification Detection ankündigt. Auf dem Papier lädt das einen Absender dazu ein, auf ein Paket ohne Integritätsschutz zurückzufallen, und das ist die Ecke, aus der EFAIL kam. Gemessen kam aber das hier heraus:

[GNUPG:] DECRYPTION_INFO 2 7 0
[GNUPG:] GOODMDC

Ein Absender mit GnuPG 2.4.x in Standardkonfiguration hat trotz des fehlenden Bits ein integritätsgeschütztes SEIPD-v1-Paket erzeugt und GOODMDC gemeldet. MDC ist dort schlicht immer an. Herauskommen aus dem Integritätsschutz muss man in dieser Version aktiv wollen, etwa über --rfc2440, das ausdrücklich den alten Modus ohne MDC erzeugt. Die tatsächliche Integritätslücke gegenüber einem 2.4.x-Absender mit Standardeinstellungen war damit null.

Diese Aussage gilt exakt so weit wie die Messung und keinen Meter weiter. Über andere Implementierungen oder ältere GnuPG-Versionen sagt sie nichts, und genau dort könnte ein fehlendes MDC-Signal im Prinzip sehr wohl noch eine Rolle spielen. „Kein aktueller Absender ist betroffen“ wäre schlicht gelogen, und irgendwer würde es nachprüfen.

Was wirklich Alarm verdient, ist keines von beiden für sich, sondern die Art des Versagens. Ein Schlüssel kann strukturell perfekt sein und trotzdem still auf den nackten Rückfallwert heruntergehandelt werden, ohne dass irgendwer etwas davon mitbekommt. Die einzige Diagnose erscheint auf einer Maschine, die dir nicht gehört. Weder gpg --list-keys noch --check-sigs noch irgendeine Keyserver-Seite zeigt dir das. Du musst dir Signatur-Subpakete ansehen, und das macht praktisch niemand.

Ein Punkt bleibt offen: AEAD

Der reparierte Schlüssel steht bei features 05 und hat keine pref-aead-algos. Gemessen bedeutet das:

AES256.CFB encrypted data      # reparierter Schlüssel, features 05
AES256.OCB encrypted data      # frisch erzeugter Schlüssel, features 07 plus pref-aead-algos: 2

setpref hat das MDC-Bit zurückgeholt, aber nie eine AEAD-Ankündigung ergänzt, weil der Schlüssel aus dem kaputten Zustand heraus repariert und nicht neu erzeugt wurde. Nachrüsten ginge, setpref … OCB liefert pref-aead-algos: 2 und features 07, auch das habe ich geprüft.

Ich lasse es trotzdem so. AES256-CFB mit MDC ist solide. Vor allem aber ist AEAD genau die Stelle, an der OpenPGP derzeit auseinanderläuft: Das AEAD von GnuPG 2.4 folgt der LibrePGP-Linie, RFC 9580 spezifiziert eine andere Konstruktion namens SEIPD v2. AEAD auf einem breit veröffentlichten Schlüssel anzukündigen bringt heute eine marginale Verbesserung und ein echtes Interoperabilitätsrisiko. Wer sich für die andere Baustelle im selben Themenfeld interessiert: Was in einem modernen Handshake steckt, habe ich am Beispiel X25519MLKEM768 auseinandergenommen. Dasselbe Argument gilt übrigens für force-ocb in meiner lokalen Konfiguration, das betrifft nur, was ich selbst verschicke, und ist eine bewusst etwas vorwärtsgewandte Entscheidung.

Ehrliche Gesamteinschätzung

  • Die Kryptografie ist in Ordnung. Ed25519, X25519, SHA-512 und AES-256 sind eine moderne, unaufgeregte Auswahl ohne bekannte praktische Schwächen, bei rund 128 Bit klassischer Sicherheit.
  • Die Struktur ist besser als der Durchschnitt. Hauptschlüssel nur zum Zertifizieren, getrennte Unterschlüssel, begrenzte Laufzeit von fünf Jahren, vorab erzeugtes Widerrufszertifikat und ein Backup, das nachweislich wiederherstellbar ist.
  • Das schwächste Glied ist kein Algorithmus. Es ist die Tatsache, dass die drei Alltags-Unterschlüssel auf einer dauerhaft laufenden Arbeitsmaschine liegen. Wer diese Maschine und die Passphrase bekommt, erzeugt bis zum Widerruf vollgültige Signaturen in meinem Namen und liest mein Archiv mit. Nach außen ist er also erst einmal ich. Was er nicht bekommt, ist der Identitätsanker: keine neue UID zertifizieren, keinen neuen Unterschlüssel binden, und der Fingerabdruck bleibt meiner. Ein kompromittierter Unterschlüssel fliegt raus und wird ersetzt, die Identität überlebt. Eine Smartcard würde den Rest weiter verkleinern, weil sich Schlüsselmaterial von der Karte nicht kopieren lässt. Sie löst es nicht: Steckt die Karte und ist sie entsperrt, wird eben auf der Karte signiert. Auf der Liste steht sie trotzdem.
  • Das Vertrauen in die Bindung ist geschichtet, nicht absolut. DNSSEC-signiertes DANE und selbst gehostetes WKD sind stark. Die Governikus-Zertifizierung ist die einzige echte Namens-Bindung. Das klassische Web of Trust trägt praktisch nichts mehr bei, die persönlichen Signaturen auf dieser Identität hängen an einem Schlüssel von 2011.
  • Post-Quantum: bewusst nicht adressiert. GnuPG 2.4.4 kann kein PQC, und die PQC-Arbeit in OpenPGP steckt noch im Entwurfsstadium mit praktisch keiner Interoperabilität. Curve25519 bietet gegen einen kryptografisch relevanten Quantencomputer keinerlei Widerstand, „heute sammeln, später entschlüsseln“ trifft also auf alles zu, was heute an diesen Schlüssel verschlüsselt wird. Ein hybrider Unterschlüssel lässt sich später ergänzen, ohne den Schlüssel neu zu bauen. Das ist eine der Auszahlungen des Unterschlüssel-Aufbaus von oben.

Was man daraus mitnimmt

Vier Punkte, und die gelten weit über OpenPGP hinaus:

  • Härtung kann schwächen. Einen verpflichtenden Basisalgorithmus zu entfernen hat die Verhandlung nicht verengt, sondern zerstört. Bevor du etwas abschaltest, das eine Spezifikation vorschreibt, sieh nach, was die Maschinerie drumherum macht, wenn es fehlt.
  • Prüfe das Artefakt, nicht den Befehl. gpg --batch --gen-key ist mit Rückgabewert 0 durchgelaufen und hat einen wunderschön aussehenden Schlüssel erzeugt. Sichtbar war der Defekt ausschließlich in --list-packets.
  • Konfiguration und Schlüssel sind nicht unabhängig. Der Schlüssel hat einen Defekt dauerhaft von der Konfiguration geerbt, die in den fünfzehn Sekunden aktiv war, in denen er entstand.
  • Manche Defekte sieht man nur von außen. Das Symptom erschien auf dem Terminal des Absenders. Teste deinen eigenen Schlüssel so, wie ein Fremder ihn benutzt. Genau dafür ist die Schleife mit mktemp -d und --locate-external-keys weiter oben da.

Und die Kurzfassung für alle, die nur bis hierher gescrollt haben: Ein moderner Schlüssel ist nicht automatisch ein korrekt konfigurierter Schlüssel. Der Unterschied steckt in Signatur-Subpaketen, die dir kein einziges Standardwerkzeug von sich aus zeigt.

Siehe auch

Wenn du deinen eigenen Schlüssel gerade nachgeprüft hast und dort etwas anderes steht als erwartet, oder wenn ich mich irgendwo irre, dann dürft ihr mich sehr gerne fragen.

Keyoxide: den OpenPGP-Schlüssel an Online-Identitäten binden, vier grüne Haken und ein rotes Kreuz

Moin. Seit dem 24. Juli 2026 habe ich einen neuen GPG-Schlüssel. Ed25519, gültig bis 2031, der Hauptschlüssel zertifiziert nur noch und hat für Signieren, Verschlüsseln und Authentisieren je einen eigenen Unterschlüssel. Technisch ein hübsches Ding. Der alte Schlüssel bleibt bis Ende des Jahres gültig und hat den neuen gegengezeichnet, damit der Wechsel keine harte Kante bekommt.

OpenPGP-Schlüssel mit Keyoxide-Identity-Claims für kernel-error.de, kernel-error.com, GitHub und Matrix, vier verifizierte Claims und ein entfernter Claim

Und dann stand da die Frage, die eigentlich immer die wichtigere ist und trotzdem meistens hintenrunterfällt: Woher soll irgendwer wissen, dass dieser Schlüssel mir gehört? Nicht „wo finde ich den Schlüssel“, das ist gelöst. Sondern: Warum sollte jemand glauben, dass die Person, die diesen Schlüssel kontrolliert, dieselbe ist wie die hinter github.com/Kernel-Error, hinter dem Mastodon-Account und hinter dieser Domain?

Die alten Antworten funktionieren nicht mehr

Die klassische Antwort auf diese Frage heißt Web of Trust. Ich unterschreibe deinen Schlüssel, du unterschreibst meinen, irgendwann spannt sich ein Netz aus Signaturen über die Welt und jeder kann über ein paar Ecken einen Pfad zu jedem finden. Elegante Idee. Nur: Wann warst du zuletzt auf einer Keysigning-Party? Eben. Ich auch nicht.

Dazu kommt ein sehr praktisches Problem, über das kaum jemand redet: keys.openpgp.org, der Keyserver, den heute praktisch alle benutzen, veröffentlicht Fremdsignaturen grundsätzlich nicht. Zuverlässig überleben dort nur Selbstsignaturen, einzige Ausnahme sind eigens attestierte Fremdzertifizierungen, und die benutzt in der Praxis kaum jemand. Das ist als Schutz gegen Signatur-Flooding absolut nachvollziehbar, bedeutet aber eben auch: Das Web of Trust reist nicht mehr mit. Du kannst deinen Schlüssel von hundert Leuten unterschreiben lassen, auf dem meistgenutzten Keyserver sieht ihn trotzdem niemand mit diesen Unterschriften.

Die zweite klassische Antwort waren Zertifizierungsstellen für Personen. CAcert habe ich hier vor Jahren mal vorgestellt, und die Volksverschlüsselung wurde 2025 eingestellt. Übrig geblieben ist bei mir eine Zertifizierung über Governikus, die die Beglaubigung im Auftrag des BSI macht, also eine eID-basierte Bestätigung mit Level 3, die tatsächlich meinen bürgerlichen Namen an den Schlüssel bindet. Das ist eine der letzten echten Klarnamen-Bindungen, die man noch bekommt. Und ausgerechnet die ist eine Fremdsignatur, fliegt auf keys.openpgp.org also raus.

Was schon vorher da war, und was es eben nicht beweist

Beim Thema Erreichbarkeit war ich schon vorher gut aufgestellt. Der Schlüssel liegt im Web Key Directory, sowohl in der advanced als auch in der direct method. Er steht als OPENPGPKEY-Record in einer DNSSEC-signierten Zone, wie ich das hier vor Jahren schon einmal beschrieben habe. Er liegt auf keys.openpgp.org, keyserver.ubuntu.com und pgpkeys.eu. Er ist in der clearsigned security.txt referenziert, die zu meinem Umgang mit Vulnerability Reports gehört. Und er liegt schlicht als Datei zum Direktdownload.

Sieben Kanäle also. Alle beweisen exakt eine Sache: dass man den Schlüssel findet. Die Governikus-Zertifizierung beweist zusätzlich einen Namen. Aber keiner dieser Kanäle beweist, dass derselbe Schlüssel auch das GitHub-Konto kontrolliert, oder den Matrix-Account, oder die Domain als Identität und nicht nur als Hostingort einer Datei. Genau diese Lücke füllen Identity Claims, und genau dafür gibt es Keyoxide.

Der wichtigste Satz zuerst: Keyoxide hat keine Accounts

Das ist das größte Missverständnis, und ich schreibe es deshalb so deutlich wie möglich hin: Es gibt bei Keyoxide keine Registrierung, keinen Login, kein Profil zum Ausfüllen, keine API-Keys und keine Daten auf deren Seite. Man legt dort nichts an. Man kann dort gar nichts anlegen.

keyoxide.org ist ein zustandsloser Betrachter. Wenn jemand ein Profil öffnet, passiert Folgendes:

  • Der öffentliche Schlüssel wird geholt, per WKD oder von einem Keyserver.
  • Aus der Selbstsignatur werden die Notationen mit dem Namen proof@ariadne.id ausgelesen.
  • Jeder Claim wird live gegen den genannten Endpunkt aufgelöst, je nach Claim-Typ direkt im Browser oder über einen Proxy. Ein TXT-Record lässt sich aus einer Webseite heraus nun mal nicht selbst abfragen.
  • Grüne Haken werden gerendert, und danach vergisst der Dienst alles wieder.

Die Konsequenz daraus ist der eigentliche Clou: Der Schlüssel ist das Profil. Wenn keyoxide.org morgen offline geht, ist nichts kaputt. Die Claims stehen weiter im Schlüssel, die Proofs liegen weiter an ihren Endpunkten, und jeder kann die komplette Prüfung mit dig und curl von Hand nachvollziehen. Weiter unten zeige ich genau das.

Das ist das genaue Gegenmodell zu Keybase, wo die Verknüpfungen auf einem Server einer Firma lagen. Als die Firma verkauft wurde, war der Ärger groß und die Frage berechtigt, was mit den Identitäten passiert. Bei diesem Modell stellt sich die Frage nicht, weil der Betreiber nichts hält, was verloren gehen könnte. Die zugrundeliegende Spezifikation heißt Ariadne Identity, Keyoxide ist nur eine Implementierung davon.

Zwei Hälften, die aufeinander zeigen

Jeder Proof besteht aus zwei Aussagen, die sich gegenseitig referenzieren:

HälfteLiegt inSagt
Claimder Selbstsignatur des Schlüssels„Ich kontrolliere kernel-error.de“
Proofgenau diesem Endpunkt„Der Schlüssel mit dem Fingerprint 45FC… gehört mir“

Auf den ersten Blick sieht das zirkulär aus. Der Schlüssel sagt, ihm gehöre die Domain, und die Domain sagt, ihr gehöre der Schlüssel. Beweist das nicht einfach gar nichts? Doch, und zwar genau deshalb, weil es zirkulär ist: Nur wer beides kontrolliert, kann beide Hälften zur Deckung bringen.

Spielen wir es durch. Jemand klaut meinen geheimen Schlüssel. Er kann jetzt beliebige Claims hineinschreiben, zum Beispiel „ich kontrolliere example.org“. Aber er kann bei example.org keinen TXT-Record setzen, der auf meinen Fingerprint zeigt. Der Claim bleibt rot. Umgekehrt: Jemand übernimmt eine meiner Domains und setzt dort einen TXT-Record mit meinem Fingerprint. Schön, aber ohne den geheimen Schlüssel kann er den passenden Claim nicht in die Selbstsignatur schreiben. Auch das ergibt keinen grünen Haken. Ein Angreifer mit einer der beiden Hälften bekommt nichts. Er braucht beide.

Warum eine Notation und keine Signatur

Ein Claim ist technisch ein Notation-Subpacket in der Selbstsignatur einer User-ID. Eine Notation ist ein frei definierbares Schlüssel-Wert-Paar, das im Signaturpaket mitsigniert wird. Der Name ist bei Ariadne immer proof@ariadne.id, der Wert beschreibt den Endpunkt.

Und hier schließt sich der Kreis zu dem Keyserver-Problem von oben. Weil die Claims in der Selbstsignatur stehen und nicht in einer Fremdsignatur, kann kein Keyserver sie wegputzen. Meine Governikus-Zertifizierung ist auf keys.openpgp.org verschwunden, weil sie eine Fremdsignatur ist. Die Keyoxide-Claims sind da, weil ein Keyserver eine Selbstsignatur nicht entfernen kann, ohne den Schlüssel dabei zu zerstören. Das ist für mich das stärkste praktische Argument, Notationen zu sammeln statt Unterschriften.

Vier Claims, vier sehr unterschiedliche Qualitäten

Live sind am Ende vier Claims, alle ausschließlich auf der Namens-UID:

proof@ariadne.id=dns:kernel-error.de?type=TXT
proof@ariadne.id=dns:kernel-error.com?type=TXT
proof@ariadne.id=https://gist.github.com/Kernel-Error/b51b81489a0600908cf09aca13765b9c
proof@ariadne.id=matrix:u/kernel-error:kernel-error.com?org.keyoxide.r=dBfQZxCoGVmSTujfiv:matrix.org&org.keyoxide.e=q3zhGJTbPWZD5LjUnsnAI9p43YzHXglniNv_SL7dldQ
Keyoxide-Profil von Sebastian van de Meer mit vier verifizierten Identity Claims: kernel-error.com und kernel-error.de per DNS, dazu GitHub und Matrix, alle mit grünem Haken.
Vier Claims, vier grüne Haken. Geprüft wird das erst beim Aufruf, gespeichert ist bei Keyoxide davon nichts. Der Fingerprint darunter ist der einzige Anker.

Das öffentliche Profil dazu liegt unter keyoxide.org/45FCD081ADB54872EA5B06B9893DE0CDDE986DEB. Bevor jetzt jemand vier grüne Haken sieht und denkt, das seien vier gleichwertige Beweise: sind sie nicht, und das gehört ausgesprochen.

  • Die beiden DNS-Claims sind mit Abstand die stärksten. Sie liegen in meinen eigenen, DNSSEC-signierten Zonen auf meinen eigenen Nameservern. Wer den Record fälschen will, muss meine Domain- oder DNS-Verwaltung übernehmen. Unterwegs fälschen reicht nicht, sofern der Prüfer DNSSEC auch wirklich validiert.
  • Der Gist-Claim hängt daran, dass GitHub sein URL-Schema beibehält und den Gist nicht wegräumt.
  • Der Matrix-Claim hängt an matrix.org und daran, dass ein bestimmter Raum weiter existiert und die Nachricht nicht gelöscht wird.

Ein Claim ist nie stärker als der Endpunkt, auf den er zeigt. Vier grüne Haken nebeneinander suggerieren eine Gleichwertigkeit, die es nicht gibt.

Domain-Proof: ein TXT-Record am Apex

Der Proof für eine Domain ist ein TXT-Record. Bei mir in beiden Zonen:

kernel-error.de.   IN TXT  "openpgp4fpr:45FCD081ADB54872EA5B06B9893DE0CDDE986DEB"
kernel-error.com.  IN TXT  "openpgp4fpr:45FCD081ADB54872EA5B06B9893DE0CDDE986DEB"

Am Apex, nicht unter www, und das ist eine bewusste Entscheidung. Der Claim behauptet etwas über die Identität der Domain, nicht über einen Webserver-Hostnamen. www ist nur ein Host darunter. Keyoxide rendert den Claim außerdem als den Domainnamen, und kernel-error.de liest sich als Identität, während www.kernel-error.de sich als Webseite liest. Kleiner Unterschied, aber genau der Punkt, um den es hier geht.

Beide TLDs bekommen den Record, weil sie bei mir verschiedene Rollen haben. Die .com ist die Mail-Identität mit WKD und DANE, die .de ist die Webpräsenz mit der security.txt. Beides bin ich, also gehört beides an den Schlüssel gebunden.

Beide Zonen sind DNSSEC-signiert und laufen mit inline-signing. Für so eine Bearbeitung heißt das: einfrieren, editieren, prüfen, wieder auftauen. Ein rndc reload ist hier der falsche Befehl.

rndc freeze kernel-error.de
# Zonendatei editieren, Serial hochzählen
named-checkzone kernel-error.de /path/to/kernel-error.de.zone
rndc thaw kernel-error.de

Angenehm unspektakulär war dabei, dass mehrere TXT-Records am selben Namen problemlos nebeneinander leben. Der neue Record ist einfach neben den SPF-Record und zwei Verifizierungs-Records gerutscht, ohne dass ich irgendetwas anfassen musste.

Einen Bluesky-Claim habe ich bewusst weggelassen. Meine did:plc:... hängt ohnehin schon über einen DNS-TXT-Record und /.well-known/atproto-did an der Domain. Wer den Domain-Claim prüft, hat Bluesky damit transitiv mit erledigt. Noch ein Claim mehr wäre nur mehr Fläche ohne mehr Aussage.

GitHub: ein öffentlicher Gist namens proof.md

Für GitHub braucht es einen öffentlichen Gist. Die Datei darin muss proof.md heißen, der Inhalt ist eine einzige Zeile:

openpgp4fpr:45FCD081ADB54872EA5B06B9893DE0CDDE986DEB

Das geht auch ohne Browser, mit der GitHub-CLI:

gh gist create --public --desc "OpenPGP identity proof (Keyoxide / Ariadne)" proof.md

Eine Abgrenzung, über die regelmäßig jemand stolpert, deshalb explizit: Einen GPG-Schlüssel im GitHub-Profil zu hinterlegen, damit signierte Commits als „Verified“ angezeigt werden, ist eine völlig andere Sache. Anderer Mechanismus, anderer Zweck, und mit dem Keyoxide-Proof hat das nichts zu tun. Der hinterlegte Schlüssel wird von GitHub automatisch für die Commit-Verifikation benutzt. Einen Schalter „diesen Schlüssel als Signing Key verwenden“ gibt es für GPG-Schlüssel übrigens gar nicht, den gibt es nur für SSH-Schlüssel.

Matrix: der Proof ist eine Nachricht

Bei Matrix hat der Proof wieder eine ganz andere Form. Er ist weder eine Datei noch ein DNS-Record, sondern eine Nachricht in einem öffentlichen Raum. Man postet in #doipver:matrix.org schlicht:

openpgp4fpr:45FCD081ADB54872EA5B06B9893DE0CDDE986DEB

Und zeigt dann in der Notation auf genau diese Nachricht. Beim Formatieren gibt es drei Kleinigkeiten, die man garantiert einmal falsch macht: Beim User wird das führende @ weggelassen, bei der Raum-ID das führende ! und bei der Event-ID das führende $. Aus @kernel-error:kernel-error.com wird also u/kernel-error:kernel-error.com, aus !dBfQZxCoGVmSTujfiv:matrix.org wird der nackte Rest, und genauso bei der Event-ID.

Wichtig ist außerdem, dass der Raum unverschlüsselt ist. Eine verschlüsselte Nachricht kann ein Prüfer nicht lesen, damit wäre der Proof wertlos. #doipver ist genau dafür da und entsprechend offen.

Der ganze Vorgang lässt sich komplett über die Client-Server-API erledigen, also POST /_matrix/client/v3/join/... und danach PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId}. Die Antwort auf das Senden enthält direkt die event_id, also genau das, was die Notation braucht. Das ist elegant, wenn man es scripten will.

Ehrlicherweise muss man dazu aber sagen: Ein Access-Token gibt volle Kontrolle über den Account. Das aus der Hand zu geben, nur um eine einzige öffentliche Zeile abzusetzen, ist ein schlechtes Geschäft. Der Weg über Element von Hand kostet dreißig Sekunden: Nachricht posten, „Teilen“ beziehungsweise Permalink aufrufen, und Raum-ID sowie Event-ID direkt aus der matrix.to-URL ablesen. Wer trotzdem ein Token benutzt, sollte das Gerät danach abmelden.

Die Claims in den Schlüssel schreiben

Das eigentliche Eintragen passiert in gpg --edit-key. Sieht harmlos aus, hat aber gleich in Zeile zwei die erste Falle:

gpg --edit-key 0x893DE0CDDE986DEB
uid 1
notation
proof@ariadne.id=dns:kernel-error.de?type=TXT
save

Entfernen geht über notation und dann den gleichen Ausdruck mit einem führenden Minus, none löscht alle. Und weil man sich auf Anzeigen ungern verlässt, hier der Blick auf das, was wirklich im Schlüssel steht:

gpg --export 0x893DE0CDDE986DEB | gpg --list-packets | grep notation

Falle 1: uid 1 ist nicht optional

Ohne vorher eine UID auszuwählen schreibt gpg die Notation in jede UID. Bei mir also auch in die Foto-UID. Keyoxide prüft dann denselben Claim zweimal und zeigt ihn doppelt an. Die offizielle Dokumentation erwähnt das im Nebensatz, überlesen ist es schnell, und aufräumen darf man es anschließend von Hand.

Falle 2: jede Änderung schreibt die Selbstsignatur neu

Das ist der eigentliche Preis, und davor warnt einen niemand. Ein Claim mehr bedeutet eine neue Selbstsignatur, und damit ist jede veröffentlichte Kopie des Schlüssels veraltet. Konkret hieß das bei mir jedes Mal: neu hochladen zu drei Keyservern, zwei selbst gehostete Dateien überschreiben und noch eine DNSSEC-Zonenbearbeitung für den DANE-Record hinterher.

Wer drei Claims einzeln nacheinander setzt, macht diese Runde dreimal. Also: Claims sammeln und in einem Rutsch eintragen. Nebenbei wächst der Schlüssel auch spürbar. Die DANE-rdata ist im Lauf des Tages von 909 auf 1130 Byte gewachsen, die WKD-Datei von 12703 auf 12940 Byte. Nichts Dramatisches, aber bei einem DNS-Record schaut man da schon hin.

Falle 3: drei Caches, drei vorgetäuschte Fehlschläge an einem Nachmittag

Das ist der Teil, bei dem vermutlich jeder Leser nickt. An einem einzigen Nachmittag haben mir drei verschiedene Caches je einen kaputten Rollout vorgespielt. In allen drei Fällen war das Deployment in Ordnung und die Prüfung hat gelogen.

Der eigene Resolver. Nach dem Setzen des TXT-Records schlug die lokale Prüfung immer weiter fehl. Google, Cloudflare und Quad9 hatten den neuen Record sofort. Der veraltete Eintrag kam ausgerechnet von meinem eigenen DoT/DoH-Resolver, auf den systemd-resolved zeigt, und der hielt die alte Antwort noch etwa zwei Stunden fest. Das sieht exakt aus wie ein kaputtes Deployment und kostet echte Debugging-Zeit.

ssh root@ns1 "rndc flushname kernel-error.de resolver"
resolvectl flush-caches

Der View-Name am Ende ist bei rndc flushname laut Handbuch optional. In meinem Setup mit einer eigenen resolver-View ist er aber Pflicht, sonst passiert schlicht nichts Sichtbares. Die allgemeine Lehre daraus: Wer eigene DNS-Änderungen prüft, fragt immer einen autoritativen Server und einen öffentlichen Resolver, niemals nur den Systemresolver.

Ein HTTP-Cache vor keyserver.ubuntu.com. Der lieferte einen veralteten Schlüssel mit nur der Hälfte der Claims aus. Ein angehängter Cache-Busting-Parameter brachte sofort die korrekte Version, und ein erneuter Upload wurde mit "ignored" quittiert. Der Server war also die ganze Zeit aktuell, nur der Cache davor nicht.

Und die Keyserver selbst. Keyserver mischen Selbstsignaturen, sie ersetzen sie nicht. Nach einem Update trägt die Keyserver-Kopie die alte und die neue Selbstsignatur. Ein naives grep -c notation zählt dann fünf Notationen, während die selbst gehostete Kopie drei zeigt. gpg nimmt beim Import die neueste, praktisch ist das also harmlos, aber jedes Verifikationsskript lügt einen dabei an. Man prüft besser, was ein frischer Import auflöst, nicht was im rohen Blob steht.

Die Moral aus allen drei Fällen ist dieselbe: gegen eine autoritative Quelle prüfen und gegen eine cache-busted Anfrage, niemals nur gegen die eine bequeme Quelle, die gerade zur Hand ist.

Falle 4: die Gist-Raw-URL springt den Host

Ein kurzer Schreckmoment mit einfacher Ursache. Die Adresse https://gist.github.com/<user>/<id>/raw/proof.md antwortet mit einem 301 auf gist.githubusercontent.com. Ein curl ohne -L liefert damit gar nichts zurück, was auf den ersten Blick nach einem kaputten Proof aussieht. Ist es nicht, es fehlt nur ein Buchstabe am Kommando.

Falle 5: –print-dane-records gibt es nicht mehr

In GnuPG 2.4 ist --print-dane-records weg. Der Aufruf bricht mit einem Fehler ab und verweist auf --export-options export-dane. Nur reicht export-minimal allein nicht: Die Foto-UID bleibt drin und bläst einen DANE-Record von rund einem Kilobyte auf etwa zwölf Kilobyte auf. Die kompakte Variante braucht zusätzlich einen Export-Filter:

gpg --export-filter "keep-uid=mbox = kernel-error@kernel-error.com" --export-options export-dane,export-minimal --export 0x893DE0CDDE986DEB

Der fünfte Claim: Fediverse, und warum er nicht live ist

Jetzt der ehrliche Teil. Ein fünfter Claim war gebaut, ausgerollt, nie verifiziert und noch am selben Tag wieder zurückgebaut. Ich schreibe das hier hin, weil vier aufgeräumte grüne Haken nichts erzählen und dieser eine Fehlschlag mehr über die Technik verrät als der Rest zusammen.

Der Plan war ein Claim auf https://www.kernel-error.de/@kernel-error.de, also den ActivityPub-Actor dieses Blogs, mit dem Proof in den Profil-Metadaten. Beim WordPress-ActivityPub-Plugin werden die „Extra Fields“ als PropertyValue-Attachments an den Actor gehängt. Zwei Entscheidungen dabei waren richtig und bleiben es auch nach dem Rückbau.

Erstens: Die Claim-URL leitet im Browser um, liefert aber unter Accept: application/activity+json sauber den Actor aus. „Mach das doch mal im Browser auf“ ist bei einem maschinenlesbaren Endpunkt schlicht kein gültiger Test. Wer so prüft, verwirft eine funktionierende Adresse.

Zweitens: Die Platzierung war wichtiger als gedacht. Keyoxide akzeptiert den Proof in der Bio, in einem Beitrag oder in den Profil-Metadaten. Bei einem WordPress-Blog-Actor ist die Bio aber der Blog-Slogan, und der steht auf jeder einzelnen Seite der öffentlichen Webseite. Ein Fingerprint im Header, weil man die erstbeste dokumentierte Möglichkeit genommen hat. Die Profil-Metadaten sind genauso gültig und für Leser unsichtbar. Lohnt sich also, erst alle Optionen zu lesen und dann zu wählen.

Ein Implementierungsdetail mit echter Fallhöhe: Das Feld wurde mit $wpdb->insert eingetragen, ausdrücklich nicht mit wp_insert_post(). Letzteres feuert die ActivityPub-Hooks und erzeugt einen Update-Eintrag in der Outbox, der dann als Geisterbeitrag durch das Fediverse geht. Kontrolliert habe ich das über den Zeilenstand der ap_outbox vor und nach dem Eingriff: 130 zu 130, sowohl beim Anlegen als auch beim Löschen. Danach Caches leeren, und zwar zuerst Redis, dann FastCGI, sonst füllt sich der FastCGI-Cache sofort wieder aus den alten Redis-Daten. Wer mehr über das Plugin-Innenleben lesen mag, findet in meinem Beitrag zum eigenen ActivityPub-Plugin für Grav die Mechanik dahinter.

Und dann zeigte Keyoxide trotzdem ein rotes Kreuz.

Ein rotes Kreuz und eine sehr überzeugende falsche Fährte

Der Claim stand auf dem Profil als rotes Kreuz mit der Beschriftung [---], was so viel heißt wie „kein Service Provider hat gematcht“. Die anderen vier waren grün. Das Access-Log sah so aus:

OPTIONS /@kernel-error.de                     301
GET     /.well-known/nodeinfo                 200
GET     /wp-json/activitypub/1.0/nodeinfo/2.1  200
GET     /@kernel-error.de/api/config           404   (doipjs/2.1.0)

Die naheliegende Lesart ist falsch, und sie ist verführerisch. /api/config ist ein Owncast-Endpunkt. Man denkt sofort: Keyoxide hält meinen Claim für einen Owncast-Server, da ist die Provider-Erkennung kaputt. Stimmt aber nicht. Die beiden NodeInfo-Abfragen darüber stammen aus dem ActivityPub-Postprozessor von doipjs, ActivityPub hatte also gematcht. Die Owncast-Abfrage ist der Fallback, der danach läuft, weil der ActivityPub-Test bereits gescheitert war. Symptom, nicht Ursache.

Die Ursache steht in der ersten Zeile. WordPress beantwortet ein OPTIONS auf die hübsche Actor-URL mit einem 301 auf die Startseite. Und ein umgeleiteter CORS-Preflight ist in allen heute relevanten Browsern ein harter Fehler. Die Fetch-Spezifikation ist an der Stelle inzwischen weniger strikt formuliert, in der Praxis bricht der Browser trotzdem ab, und der Prüfer kommt nie an den Actor heran. Das Ärgerliche daran: Die CORS-Header waren die ganze Zeit korrekt gesetzt, Access-Control-Allow-Origin: * und der ganze Rest. Sie kamen nur mit Status 301, und damit sind sie wertlos.

Eine andere Claim-URL hilft nicht heraus. /@handle und /@handle/ antworten beide mit 301 auf OPTIONS, und die eigentliche Actor-ID /?author=0 beantwortet OPTIONS mit 405 und ganz ohne CORS-Header. Es musste also in den nginx, mit einer Location, die den Preflight selbst beantwortet:

location ~ ^/@[^/]+/?$ {
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin  "*"                                  always;
        add_header Access-Control-Allow-Methods "GET, OPTIONS"                        always;
        add_header Access-Control-Allow-Headers "*"                                   always;
        add_header Access-Control-Max-Age       86400                                 always;
        return 204;
    }
    try_files $uri $uri/ /index.php?$args;
}

Platziert als erste Regex-Location, damit sie vor den späteren gewinnt, und der Nicht-OPTIONS-Zweig spiegelt exakt das, was auch location / macht, damit normaler Verkehr unverändert durchläuft. Der Preflight ging damit von 301 auf 204.

Und der erste Fix reichte nicht, das Log hat es leise gesagt

Nach dem Reload kam sauber der 204. Aber das anschließende GET /@kernel-error.de tauchte im Access-Log überhaupt nicht auf, und doipjs marschierte direkt wieder zur Owncast-Abfrage weiter. Genau diese Abwesenheit ist die Fehlermeldung: Ein Preflight, der erfolgreich ist und auf den dann gar keine Anfrage folgt, bedeutet, dass der Browser die Antwort auf den Preflight abgelehnt hat.

Die Liste der erlaubten Header war schlicht geraten. Accept, Authorization, Content-Type klingt vernünftig, deckte aber offensichtlich nicht ab, was doipjs tatsächlich schickt. Die Erweiterung auf Access-Control-Allow-Headers: * war die zweite Hälfte des Fixes. Der Wildcard wirkt hier, weil die Anfrage ohne Credentials und ohne HTTP-Auth läuft. Bei einer Anfrage mit Credentials wäre der Stern wirkungslos, und Authorization deckt er ohnehin nie ab, den müsste man immer namentlich nennen. Oben steht schon die korrigierte Fassung.

Noch eine Warnung für alle, die so etwas nachbauen: Access-Control-Max-Age: 86400 heißt, dass der Browser die alte Preflight-Antwort einen Tag lang behält. Ein normales Neuladen leert den CORS-Preflight-Cache nicht. Also im privaten Fenster nachtesten, sonst jagt man ein Gespenst.

Drei Lehren daraus, die weit über Keyoxide hinaus gelten:

  • Richtige Header auf einem falschen Statuscode sind trotzdem kaputt. Ein 301 mit perfekten CORS-Headern scheitert genauso hart wie gar keine Header.
  • Ein Log liest man von oben nach unten, nicht von unten nach oben. Die verdächtigste Zeile, der 404, war die Folge. Der langweilige 301 zwei Zeilen darüber war der Fehler.
  • Eine Anfrage, die im Log fehlt, ist auch ein Befund. Die zweite Runde habe ich ausschließlich aus etwas diagnostiziert, das nicht passiert ist.

Ein Nebeneffekt, der mir erst hinterher aufging: Das war nie ein Keyoxide-Problem. Jeder Cross-Origin-Konsument meines ActivityPub-Actors, der dafür im Browser läuft und einen Preflight braucht, ist an diesem Redirect gescheitert, seit es die Adresse gibt, und es ist niemandem aufgefallen. Serverseitige Abrufer waren nie betroffen, die kennen kein CORS. Der nginx-Fix ist deshalb geblieben, auch nachdem der Claim wieder weg war. Er repariert einen echten Defekt, völlig unabhängig von Keyoxide.

Und trotzdem blieb es rot

Nach dem CORS-Fix zeigte das Log endlich das gewünschte Bild: OPTIONS ... 204, gefolgt von GET /@kernel-error.de 200 6915 "doipjs/2.1.0". Der Actor wurde geholt. Und der Claim blieb rot. Der Reihe nach ausgeschlossen habe ich dann:

  • Provider-Matching. Das Regex in activitypub.js ist ein Catch-All, und die NodeInfo-Abfragen im Log beweisen ohnehin, dass der zugehörige Postprozessor gelaufen ist.
  • Feldplatzierung. attachment.value ist eines der drei Felder, die doipjs durchsucht.
  • Inhalt und Schreibweise. Fingerprint einmal in Großbuchstaben, einmal in Kleinbuchstaben probiert. Beides ohne Treffer. Die anderen vier Claims benutzen alle Großbuchstaben und verifizieren einwandfrei, an der Schreibweise liegt es also nicht.

Bleibt ein Verdacht, den ich ausdrücklich als unbewiesen kennzeichne: Das hier ist ein WordPress-ActivityPub-Blog-Actor. NodeInfo meldet software: wordpress, und der preferredUsername lautet kernel-error.de, ist also ein Benutzername mit Punkten darin, den Mastodon so gar nicht erzeugen könnte. Das ist ein deutlich weniger befahrener Pfad durch doipjs als ein normales Mastodon-Profil. Weiterzukommen hätte bedeutet, fremdes JavaScript zu debuggen, für den fünften von fünf Claims.

Also habe ich aufgehört, und die Entscheidung gehört zur Geschichte dazu. Rückbau komplett: Notation aus dem Schlüssel entfernt, alle sieben Kanäle neu ausgerollt, das Profilfeld gelöscht, Caches geleert. Der Claim ist damit vollständig verschwunden, es bleibt also kein dauerhaftes rotes Kreuz stehen. Das ist übrigens eine erfreuliche Eigenschaft: Ein Claim, der nicht funktioniert, lässt sich sauber zurückziehen. Nur die Historie der Notation liegt für immer auf den Keyservern. Wer die rohen Schlüsseldaten auseinandernimmt, findet die alten Selbstsignaturen samt Claim also weiterhin. Im aktuellen Profil taucht er nicht mehr auf.

Zwei Stolpersteine aus dem Rückbau, die man kennen sollte:

  • Das Entfernen einer Notation braucht eine Bestätigung. Die Reihenfolge ist uid 1, dann notation, dann der Ausdruck mit führendem Minus, dann ein y, dann erst save. Ohne das y schluckt gpg das nachfolgende save als Antwort auf seine Rückfrage, und das Entfernen passiert stillschweigend gar nicht.
  • Keyserver behalten jede Generation von Selbstsignaturen. Nach dem Rückbau lagen auf keys.openpgp.org zehn Selbstsignaturen auf dieser UID, mit der Reihe 4, 5, 4, 3, 2, 0 Claims. Harmlos, weil gpg wie openpgp.js die neueste nimmt. Aber --export-options export-clean reduziert das nicht auf eine Selbstsignatur. Wer Notationen über einen so bereinigten Export zählt, zählt zu hoch und hält einen erfolgreichen Rückbau für gescheitert. Besser die Zeitstempel pro Signatur vergleichen.

Falle 6: ein langsamer Matrix-Raum sieht aus wie kaputte Föderation

Zum Schluss noch eine schöne Fehldiagnose. Der erste Versuch, #doipver:matrix.org beizutreten, starb mit ResponseNeverReceived. Das liest sich wie ein glasklarer Föderationsausfall, und ich habe entsprechend angefangen zu suchen. Nur: federationtester.matrix.org meldete FederationOK: true mit gültigen Zertifikaten über IPv4 und IPv6, und blankes TCP wie auch HTTPS vom Server nach matrix.org liefen einwandfrei.

#doipver ist einfach ein großer, stark föderierter Raum. Der Alias löst auf hunderte beteiligte Server auf, und ein make_join gegen einen einzelnen ausgelasteten Server dauert entsprechend. Geholfen haben zwei Dinge: mehrere Server zum Beitreten mitgeben und einen großzügigen Timeout setzen, 180 Sekunden waren genug. Der Parameter heißt in der aktuellen Spezifikation via, ich habe hier noch den älteren Namen server_name benutzt.

POST /_matrix/client/v3/join/%21dBfQZxCoGVmSTujfiv%3Amatrix.org
     ?server_name=matrix.org&server_name=privacyguides.org&server_name=monero.social

Die übertragbare Lehre, gerade für dieses Publikum: Bevor man aus einer einzigen gescheiterten Anfrage eine Föderationsstörung diagnostiziert, sollte man prüfen, ob die Anfrage nur langsam war. Am selben Tag tauchten übrigens noch zwei 429 Too Many Requests bei Schlüsselabfragen im Log auf, ebenfalls reine Ablenkung.

Selbst nachprüfen, ganz ohne die Webseite

Und hier kommt der Punkt, an dem das ganze Modell überzeugt. Du musst keyoxide.org nicht vertrauen, du kannst die komplette Prüfung selbst machen. Vier Schritte, gegen meinen Schlüssel:

# 1. Schlüssel holen, so wie es ein Fremder tun würde
gpg --auto-key-locate clear,wkd --locate-external-keys kernel-error@kernel-error.com

# 2. Claims aus der Selbstsignatur lesen
gpg --export 0x893DE0CDDE986DEB | gpg --list-packets | grep 'proof@ariadne.id'

# 3. Claims auflösen
dig +short TXT kernel-error.de   | grep openpgp4fpr
dig +short TXT kernel-error.com  | grep openpgp4fpr
curl -sSL https://gist.github.com/Kernel-Error/b51b81489a0600908cf09aca13765b9c/raw/proof.md
# Matrix: Nachricht im Client öffnen oder das Event per API lesen
#   Raum  !dBfQZxCoGVmSTujfiv:matrix.org
#   Event $q3zhGJTbPWZD5LjUnsnAI9p43YzHXglniNv_SL7dldQ

# 4. Gegen den Fingerprint aus Schritt 1 vergleichen, das ist die gesamte Prüfung

Das ist Keyoxide. Mehr macht die Seite auch nicht, sie macht es nur hübscher und schneller. Wenn du diese vier Schritte einmal von Hand durchgehst, sitzt das Argument „du musst der Webseite nicht vertrauen“ deutlich besser als jede Erklärung.

Was das beweist, und was eben nicht

Damit hier niemand mit falschen Erwartungen rausgeht, die ehrlichen Grenzen:

  • Claims sind praktisch dauerhaft. Sie stehen in der Selbstsignatur, die auf Keyserver hochgeladen wird, und keys.openpgp.org kennt kein Zurücknehmen. Man sollte also nur Identitäten claimen, die man dauerhaft und öffentlich an den Schlüssel gebunden haben möchte. Genau deshalb habe ich Telegram und Threema bewusst weggelassen.
  • Xing und LinkedIn habe ich übersprungen, es gibt dort keinen brauchbaren Proof-Mechanismus. Eine Klarnamen-Bindung existiert bei mir ohnehin in stärkerer Form über die eID-Zertifizierung. Telefonnummern, SIP und Postanschrift sind nicht verifizierbar und ohnehin eine schlechte Idee.
  • Das beweist Kontrolle, nicht Identität. Ein grüner Haken sagt: Dieselbe Instanz kontrolliert diesen Schlüssel und dieses Konto. Er sagt nicht, wer diese Instanz ist. Dafür braucht es weiterhin so etwas wie eine eID-Zertifizierung oder ein persönliches Treffen. Das ist die ehrliche Grenze des ganzen Ansatzes, und sie zu überverkaufen wäre falsch.
  • Die Prüfung ist nur so stark wie der schwächste Endpunkt. Eine DNSSEC-signierte Zone auf eigenen Nameservern ist stark. Ein Gist auf einer Plattform, die morgen ihr URL-Schema ändern kann, ist es nicht.
  • Keyoxide selbst ist eine Bequemlichkeitsschicht. Verschwindet Keyoxide, funktionieren die Claims weiter. Verschwindet GitHub, ist genau dieser eine Claim tot.

Nachtrag: der neue Schlüssel hatte noch ein ganz anderes Problem

Beim Durchleuchten des Setups ist mir nebenbei ein echter Defekt am funkelnagelneuen Schlüssel aufgefallen: Er hatte überhaupt keine Algorithmus-Präferenzen. Die Folge war messbar und wirklich unangenehm. Ein Dritter, der an den alten Schlüssel verschlüsselt hat, bekam AES256. Beim neuen Schlüssel wurde daraus stillschweigend AES128. Der neue, modernere Schlüssel wurde also schwächer adressiert als der, den er ablöst. Das ist eine eigene Geschichte und die erzähle ich in einem eigenen Beitrag, denn der Unterschied zwischen „mein Schlüssel ist modern“ und „mein Schlüssel ist korrekt konfiguriert“ verdient mehr als eine Randnotiz.

Fazit

Das Web of Trust ist als Alltagsmechanismus tot, die Zertifizierungsstellen für Privatpersonen sind weitgehend weg, und trotzdem bleibt die Frage bestehen, wem ein Schlüssel gehört. Identity Claims sind darauf eine überraschend saubere Antwort: kein Account, kein Anbieter, der etwas hält, keine Abhängigkeit von einer Firma, die morgen verkauft wird. Nur zwei Aussagen, die aufeinander zeigen, und ein Schlüssel, der sein Profil selbst ist.

Der Aufwand ist überschaubar, wenn man die Claims sammelt und in einem Rutsch einträgt. Die Zeit geht nicht für Keyoxide drauf, sondern für die eigenen Caches und für einen alten Redirect, den nie jemand hinterfragt hat. So gesehen war der gescheiterte fünfte Claim der nützlichste von allen.

Siehe auch

Wie immer gilt: Wenn etwas unklar ist, wenn ich mich irgendwo irre oder wenn jemand doch noch weiß, warum ein WordPress-ActivityPub-Actor bei doipjs durchfällt, dann dürft ihr mich sehr gerne fragen.

X25519MLKEM768 zerlegt: was in einem Post-Quantum-Handshake wirklich steckt

X25519 und ML-KEM-768 führen ihre Schlüsselanteile in einem hybriden TLS-1.3-Handshake zu gemeinsamem Schlüsselmaterial zusammen.

Wer eine moderne TLS-Verbindung debuggt, stolpert früher oder später über eine Zeichenkette wie X25519MLKEM768. Sie steht im nginx-Log, sie taucht in der Ausgabe von openssl s_client auf, sie klebt in jeder Handshake-Analyse. Und sie sieht aus, als wären da zwei Dinge aus Versehen zusammengeschoben worden. Sind sie aber nicht.

Genau wie die klassischen Cipher Suites folgt auch dieser Name einem klaren Schema. Man muss nur wissen, wo man den Schnitt ansetzt. Vor Jahren habe ich hier schon einmal so eine Zeichenkette auseinandergenommen, damals TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384. Heute ist die Post-Quantum-Welt dran. Ich möchte X25519MLKEM768 einmal komplett durchleuchten: was jeder Teil bedeutet, warum das Ganze so gebaut ist und was es bewusst nicht abdeckt.

Kurze Auffrischung: die vier Teile einer klassischen Cipher Suite

Ein klassischer Suite-Name wie TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 beschreibt vier Bausteine. Der Schlüsselaustausch legt fest, wie sich beide Seiten auf ein gemeinsames Geheimnis einigen, hier ECDHE. Die Authentisierung sagt, womit das Serverzertifikat signiert ist, hier ECDSA. Die eigentliche Verschlüsselung der Nutzdaten macht AES-256-GCM. Und der Hash SHA384 steckt in der Schlüsselableitung und sichert das Handshake-Transkript ab. Die Integrität der übertragenen Nutzdaten übernimmt bei dieser Suite dagegen schon AES-256-GCM selbst, als AEAD-Verfahren braucht es dafür keinen separaten Hash mehr. Wer das im Detail nachlesen mag, findet es im verlinkten Altbeitrag.

Diese vier Slots sind der Rahmen. Post-Quantum betrifft in diesem Beitrag genau einen davon, den Schlüsselaustausch. Die Authentisierung ließe sich ebenfalls quantensicher machen, sie bleibt hier aber vorerst klassisch, dazu am Ende mehr. Der Schlüsselaustausch ist der springende Punkt, den man zuerst verstehen muss.

In TLS 1.3 wandert der Schlüsselaustausch aus dem Namen

Der erste Grund, warum X25519MLKEM768 nicht in der Cipher Suite steht, ist eine Änderung aus TLS 1.3. Dort ist der Schlüsselaustausch aus dem Suite-Namen herausgewandert. Eine TLS-1.3-Suite heißt nur noch TLS_AES_256_GCM_SHA384, also Verschlüsselung plus Hash. Vom Schlüsselaustausch steht da kein Wort mehr.

Stattdessen handeln Client und Server den Schlüsselaustausch separat aus, über die sogenannten Supported Groups. X25519MLKEM768 ist so eine Gruppe. Sie steht neben der Cipher Suite, nicht in ihr. Genau deshalb sitzt Post-Quantum an dieser Stelle: die Sache, die der Quantencomputer bedroht, ist der Schlüsselaustausch, und der wird in TLS 1.3 als eigene Gruppe verhandelt. Die Suite selbst bleibt unangetastet.

Diagramm: Zerlegung der Cipher Suite TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 in Schlüsselaustausch, Authentisierung, Verschlüsselung und Hash, darunter die Abtrennung der Named Group X25519MLKEM768 in TLS 1.3.
Der klassische Suite-String und wie TLS 1.3 den Schlüsselaustausch als eigene Named Group abtrennt.

Warum überhaupt Post-Quantum?

Ein ausreichend großer Quantencomputer bricht mit dem Shor-Algorithmus die Mathematik hinter dem klassischen Schlüsselaustausch. Diffie-Hellman, RSA, die elliptischen Kurven: alles, was auf dem diskreten Logarithmus oder der Faktorisierung großer Zahlen beruht, fällt. Nicht ein bisschen schwächer, sondern gebrochen.

Die symmetrische Seite trifft es weit weniger hart. Gegen AES und die SHA-2-Familie hilft einem Quantencomputer nur der Grover-Algorithmus, und der halbiert lediglich die effektive Schlüssellänge. AES-256 verhält sich gegen Grover ungefähr so wie AES-128 gegen einen klassischen Rechner, und das ist weiterhin weit außerhalb des Machbaren. Deshalb muss der symmetrische Teil der Cipher Suite gar nicht ersetzt werden. Man nimmt die größere Variante, und die Sache ist erledigt. Nur der asymmetrische Schlüsselaustausch braucht wirklich Ersatz.

Der Haken ist der Zeitfaktor. Ein Angreifer, der heute Datenverkehr mitschneidet und wegspeichert, braucht den Quantencomputer nicht heute. Er kann warten. Kommt die Maschine in zehn oder fünfzehn Jahren, entschlüsselt er den alten Mitschnitt rückwirkend. Man nennt das harvest now, decrypt later. Für alles, was auch in fünfzehn Jahren noch vertraulich sein soll, ist die Bedrohung damit schon heute real. Das ist der Grund, warum man nicht wartet, bis es den Quantencomputer gibt.

Was ist ein KEM, und wie unterscheidet es sich von Diffie-Hellman?

Bevor wir den Namen zerlegen, ein Begriff, den man dafür braucht. Diffie-Hellman, auch in seiner elliptischen Variante ECDHE, funktioniert symmetrisch: beide Seiten werfen einen öffentlichen Wert in die Leitung, jede rechnet mit ihrem eigenen geheimen Wert und dem öffentlichen der Gegenseite, und am Ende haben beide dasselbe gemeinsame Geheimnis heraus. Verschickt hat es niemand, es entsteht auf beiden Seiten gleichzeitig.

Ein KEM, ein Key Encapsulation Mechanism, geht anders vor. Es kennt drei Schritte. Eine Seite erzeugt ein Schlüsselpaar und schickt den öffentlichen Teil. Die andere Seite führt mit diesem öffentlichen Schlüssel die Encapsulation aus. Dabei entstehen in einem Schritt zwei zusammengehörige Dinge: ein frisches gemeinsames Geheimnis und ein Chiffretext dazu. Der Chiffretext geht zurück. Die erste Seite entkapselt ihn mit ihrem privaten Schlüssel und hält dasselbe Geheimnis in der Hand. Encapsulate und Decapsulate, daher der Name.

Diagramm: Vergleich von Diffie-Hellman und KEM. Links tauschen beide Seiten öffentliche Werte, rechts schickt der Client einen öffentlichen Schlüssel und der Server antwortet mit einem Chiffretext, Encapsulate und Decapsulate.
Diffie-Hellman gegen KEM: beim KEM erzeugt der Client das Schlüsselpaar, der Server kapselt das Geheimnis ein.

Im TLS-Handshake ist die Rollenverteilung dabei klar. Der Client erzeugt das ML-KEM-Schlüsselpaar und legt den öffentlichen Schlüssel in sein key_share, also gleich in den ClientHello. Der Server nimmt diesen Schlüssel, kapselt ein frisches Geheimnis ein und schickt den Chiffretext in seinem ServerHello zurück. Der Client entkapselt ihn und beide haben dasselbe Geheimnis. Genau deshalb ist es der 1184 Byte große öffentliche Schlüssel, der den ClientHello aufbläht, und nicht der Chiffretext. Klein bleibt der ServerHello damit trotzdem nicht: dort steckt der 1088 Byte lange ML-KEM-Chiffretext plus der 32 Byte lange X25519-Anteil. Beide Seiten schleppen also einen großen hybriden Share. Der Client-Share ist mit 1216 Byte nur rund 96 Byte größer als der 1120 Byte große Server-Share.

Warum nicht einfach Diffie-Hellman mit einem quantensicheren Verfahren? Weil die gitterbasierte Mathematik, auf der ML-KEM beruht, sich nicht sauber in das symmetrische DH-Schema pressen lässt. Die KEM-Form passt zu dem, was Gitter gut können: etwas einkapseln und wieder herausholen. Deshalb ist der neue Standard ein KEM und kein neues Diffie-Hellman. Wer die Denke von ECDHE im Kopf hat, muss hier einmal umschalten.

Der klassische Teil: X25519

Jetzt zum Namen selbst. X25519 ist der Teil, den es schon lange gibt. Es ist Diffie-Hellman über der elliptischen Kurve Curve25519, schnell, seit Jahren im breiten Einsatz und gründlich untersucht. In einer klassischen TLS-1.3-Verbindung macht X25519 den Schlüsselaustausch ganz allein. Sein öffentlicher Anteil ist mit 32 Byte winzig, die Rechnung ist billig, und in Sachen Vertrauen hat es sich über Jahre bewiesen.

Sein einziges Problem ist der Quantencomputer. Gegen Shor hält X25519 nicht. Es allein weiterzuverwenden hieße, sich genau der harvest-now-Bedrohung auszuliefern. Es einfach wegzuwerfen wäre aber auch schade, denn es ist bewährt. Diese Spannung löst der zweite Teil.

Der Post-Quantum-Teil: ML-KEM-768

MLKEM768 ist der neue Teil. ML-KEM steht für Module-Lattice-Based Key Encapsulation Mechanism, das quantensichere KEM, das die NIST im August 2024 als FIPS 203 standardisiert hat. Wem der Name CRYSTALS-Kyber etwas sagt: das ist der Vorgänger, ML-KEM ist die standardisierte Fassung davon.

Die Sicherheit von ML-KEM beruht nicht auf dem diskreten Logarithmus, sondern auf Gitterproblemen, konkret auf dem Module-LWE-Problem. Das ist eine ganz andere mathematische Baustelle, und nach heutigem Kenntnisstand hilft auch ein Quantencomputer dort nicht weiter. Die 768 im Namen ist keine Byte-Angabe, sondern der Parametersatz. Er ist der NIST-Sicherheitskategorie 3 zugeordnet, deren Referenzniveau sich grob am Aufwand eines Angriffs auf AES-192 orientiert. Eine exakte Zahl wie 192 Bit sollte man daraus aber nicht ableiten, die Kostenmodelle für Gitterangriffe und klassische Schlüsselsuche sind nicht dasselbe. Kategorie 3 ist der übliche Mittelweg zwischen dem kleineren ML-KEM-512 und dem größeren ML-KEM-1024.

Man muss die Gittermathematik nicht beherrschen, um die Grundidee zu greifen. Grob gesagt versteckt ML-KEM sein Geheimnis in einem System aus vielen Gleichungen, dem absichtlich ein kleines Rauschen beigemischt wurde. Ohne den privaten Schlüssel lässt sich das Rauschen nicht sauber herausrechnen, und den richtigen Wert trotzdem zu finden, gilt auch für einen Quantencomputer als hart. Shor greift hier ins Leere, weil es weder um Faktorisierung noch um diskrete Logarithmen geht. Das ist der ganze Trick: eine Härte, für die keine Quanten-Abkürzung bekannt ist.

Interessant sind die Größen. Der öffentliche Schlüssel von ML-KEM-768 ist 1184 Byte groß, der Chiffretext 1088 Byte. Zum Vergleich: der X25519-Anteil misst 32 Byte. Das gemeinsame Geheimnis, das am Ende herausfällt, ist bei beiden gleich klein, nämlich 32 Byte. Der Zugewinn an Sicherheit steckt also nicht im Ergebnis, sondern im Aufwand, es auszuhandeln. Diese Größe wird später noch wichtig.

Warum beide zusammen? Der Hybrid-Gedanke

Das Entscheidende an X25519MLKEM768 ist, dass beide Verfahren zugleich laufen. Es ist ein hybrider Schlüsselaustausch. X25519 liefert ein gemeinsames Geheimnis, ML-KEM-768 liefert ein zweites. Diese beiden Geheimnisse werden aneinandergehängt und wandern gemeinsam in den Schlüsselplan von TLS 1.3, also durch die HKDF-Ableitung, aus der am Ende die eigentlichen Sitzungsschlüssel fallen.

Diagramm: Hybrider Schlüsselaustausch. X25519 und ML-KEM-768 liefern je ein 32 Byte langes Geheimnis, beide werden konkateniert und über HKDF zum Sitzungsschlüssel abgeleitet.
Beide Schlüsselaustausche laufen parallel, ihre Geheimnisse landen gemeinsam im TLS-1.3-Schlüsselplan.

So, wie es für X25519MLKEM768 in TLS 1.3 standardisiert ist, bleibt das Ergebnis sicher, solange mindestens einer der beiden Schlüsselaustausche sicher ist. Erst wenn ein Angreifer beide bricht, fällt der Schlüssel. Diese Garantie hängt allerdings an der sauberen Einbindung in den TLS-1.3-Schlüsselplan, bloßes Aneinanderhängen zweier Geheimnisse ist nicht in jedem Protokoll automatisch sicher. Und das ist der ganze Sinn der Übung. ML-KEM ist quantensicher, aber noch jung. Sollte in der Gitterkryptografie doch eine Schwäche gefunden werden, hält immer noch das klassische X25519 die Stellung. Sollte umgekehrt der Quantencomputer kommen, fällt X25519, aber ML-KEM trägt weiter. Man müsste beide gleichzeitig knacken, und das ist mit heutigem Wissen für keine der beiden Seiten in Sicht. Man bekommt die neue Sicherheit, ohne die alte aufzugeben.

Kleine Kuriosität am Rande: im Namen steht X25519 vorne, auf dem Draht und bei der Kombination der Geheimnisse liegt der ML-KEM-Teil zuerst. Der Name folgt einer Konvention, die Byte-Anordnung einer anderen. Fürs Verständnis ist das egal, für eine eigene Implementierung nicht. Der allgemeine Rahmen für hybride Schlüsselaustausche in TLS 1.3 ist übrigens inzwischen als RFC 9954 veröffentlicht. Der konkrete Codepoint X25519MLKEM768 mitsamt seiner genauen Kodierung steckt dagegen noch in einem eigenen Entwurf, draft-ietf-tls-ecdhe-mlkem, den die TLS-Arbeitsgruppe im März 2025 angenommen hat. Zum Zeitpunkt dieses Beitrags ist dieser Teil noch kein veröffentlichtes RFC, er liegt aber schon beim RFC Editor. Im echten Datenverkehr ist er trotzdem längst unterwegs.

Der Preis: der Handshake wird größer

Die 1184 Byte haben eine Nebenwirkung. Ein klassischer ClientHello passt bequem in ein Paket. Packt man den ML-KEM-Schlüssel dazu, wird es eng, und manche fehlerhaften Middleboxes oder ältere Mailserver kommen mit dem größeren ClientHello nicht klar. Bei HTTPS bricht der TLS-Handshake dann in der Regel einfach ab, einen automatischen Rückfall auf Klartext gibt es dort nicht. Kritischer sind opportunistische Protokolle wie SMTP mit STARTTLS: dort kann, je nach lokaler TLS-Policy, nach einem gescheiterten TLS-Versuch tatsächlich unverschlüsselt weiter zugestellt werden. Das ist aber eine Eigenschaft des Anwendungsprotokolls und seiner Policy, nicht von TLS selbst.

Standardmäßig schickt OpenSSL 3.5 den hybriden Schlüssel gleich im ersten ClientHello mit, X25519MLKEM768 ist dort als vorhergesagter Key Share vorgesehen. Genau deshalb passt der ClientHello unter Umständen nicht mehr in ein einzelnes TCP-Segment. Wer das vermeiden will, kann Delayed Key-Share konfigurieren: der Client bietet die Gruppe dann zwar in den Supported Groups an, schickt den großen Share aber noch nicht mit. Bevorzugt der Server sie, fordert er ihn per HelloRetryRequest nach, was eine zusätzliche Rundreise kostet. Bei Postfix habe ich genau diesen Weg über das vorangestellte Fragezeichen in der Kurvenliste eingerichtet und auf dem Draht mit tcpdump nachvollzogen. Der Link steht unten. Für das Verständnis des Namens reicht: der Post-Quantum-Teil ist groß, und der Handshake muss sich darauf einstellen.

Was hier nicht post-quantum ist: die Authentisierung

Ein Punkt wird gern übersehen. X25519MLKEM768 schützt den Schlüsselaustausch, nicht die Authentisierung. Dass man wirklich mit dem richtigen Server spricht und nicht mit jemandem in der Mitte, hängt an zwei klassischen Signaturen. Die Zertifikatskette ist von der CA mit ECDSA oder RSA signiert, und der Server beweist zusätzlich im Handshake mit einer eigenen Signatur über das Transkript, dem CertificateVerify, dass er den passenden privaten Schlüssel besitzt. Beide sind heute noch klassisch, an dieser Stelle steckt also weiterhin nichts Quantensicheres.

Das ist kein Versehen, sondern eine Frage der Reihenfolge. Die quantensicheren Signaturverfahren gibt es durchaus, ML-DSA und SLH-DSA sind standardisiert. Nur bekommt man praktisch von keiner öffentlichen CA heute ein post-quantum signiertes Zertifikat. Die ganze Kette aus Wurzel, Zwischenzertifikat und Serverzertifikat müsste mitziehen, Browser und Betriebssysteme müssten die neuen Wurzeln kennen, und das dauert Jahre.

Warum ist das trotzdem vertretbar? Weil die Bedrohung bei der Server-Authentisierung im TLS-Handshake eine andere ist. Diese Signatur muss vor allem in dem Moment halten, in dem die Verbindung aufgebaut wird. Ein Quantencomputer in fünfzehn Jahren kann eine damals abgeschlossene Verbindung nicht rückwirkend fälschen, sie ist längst vorbei. Harvest now, decrypt later trifft also die Vertraulichkeit, nicht die Echtheit eines vergangenen Handshakes. Andere Signaturen, etwa unter Dokumenten oder Firmware, müssen dagegen oft noch Jahrzehnte halten, das ist ein eigener Fall. Deshalb ist der Schlüsselaustausch das dringende Problem und darf zuerst quantensicher werden. Die Signaturen kommen später, und dafür bleibt mehr Zeit.

Und in der Praxis?

Das alles klingt nach Zukunft, ist aber längst Gegenwart. Ich habe über fünfzehn Tage mitgeloggt, welche Gruppe die Clients auf diesem Blog tatsächlich aushandeln. Ergebnis: rund 57 Prozent aller Handshakes liefen bereits über X25519MLKEM768, bei aktuellen Browsern waren es um die 77 Prozent. Das ist kein Laborwert, sondern normaler Besucherverkehr. Der Schlüsselaustausch, den ich hier zerlegt habe, ist für den Großteil der Verbindungen zu dieser Seite schon der Normalfall.

Sehen kann man das selbst mit einem Blick von außen. Ein openssl s_client -connect host:443 zeigt in seiner Ausgabe die ausgehandelte Gruppe, und wenn dort X25519MLKEM768 steht, dann läuft genau der hybride Schlüsselaustausch, den wir hier auseinandergenommen haben. Auf der Serverseite kann man dieselbe Information über die Log-Variable $ssl_curve mitschreiben, was ich für die Auswertung unten genau so gemacht habe.

Wer das selbst einrichten will, muss vor allem eines wissen: es hängt an der Krypto-Bibliothek, also an OpenSSL 3.5 oder neuer, nicht am Webserver oder Mailserver selbst. Die konkreten Anleitungen für nginx sowie für Postfix und Dovecot habe ich getrennt aufgeschrieben, dazu die Auswertung, aus der die Zahlen oben stammen. Die Links stehen gleich hier drunter.

Kurz zusammengefasst

X25519MLKEM768 ist kein Tippfehler und kein Buzzword, sondern eine saubere Konstruktion. Ein klassischer Schlüsselaustausch und ein quantensicherer laufen parallel, ihre Geheimnisse werden zusammengeführt, und der Handshake ist nur zu knacken, wenn beide fallen. Der symmetrische Teil der Verbindung bleibt klassisch, weil er es sich leisten kann. Die Authentisierung bleibt vorerst auch klassisch, weil sie nicht so eilt. Und wenn man den Namen einmal an der richtigen Stelle auseinandernimmt, steht da nichts Geheimnisvolles mehr, sondern genau das, was drinsteckt.

Siehe auch

Etwas unklar geblieben, anderer Meinung oder eine Ergänzung? Gern einfach fragen.

AI, der Mensch als Flaschenhals und meine Sorgen für die nächsten 15 Jahre

Diesmal kein tiefer Tech-Dive. Ein paar Gedanken, die mich seit Wochen begleiten und die ich gerne aus dem Kopf herausschreibe. Es geht um AI, Large Language Models, die Geschwindigkeit in der das alles passiert, und um meine Sorge, ob wir als Menschheit damit überhaupt klarkommen. Kein AI-Bashing. AI kann zum einen nichts dafür und ist zum anderen sehr hilfreich. Aber die immer schnellere Verbreitung immer besser werdender AI-Systeme hat eben Konsequenzen, und genau die sortiere ich hier für mich.

Mensch zwischen KI-Automatisierung und gesellschaftlicher Unsicherheit über Arbeit, Bildung, Politik und Zukunft.

AI ist nicht böse, die Geschwindigkeit ist es

AI an sich ist nicht böse. Large Language Models sind nicht böse. Aus meiner Sicht ist das einfach der nächste Schritt in der Entwicklung, die wir als Menschheit hinlegen. Beim Rad war es nicht anders, beim Feuer, bei der Dampfmaschine, beim automatischen Webstuhl, bei der Elektrizität in den Häusern. Alles Techniken, die irgendwann neu zur Menschheit gekommen sind und die das Leben und Arbeiten verändert haben.

Der Unterschied zu vielen dieser vorangegangenen Technologien ist die Geschwindigkeit. Pferde und Kutschen gegen das Automobil: inzwischen sind wir in der Autozeit angekommen, alles um uns herum ist darauf ausgelegt, dass das funktioniert. Aber dazwischen lag fast ein ganzes Jahrhundert. Eine Generation konnte ihren Job noch zu Ende machen, ihr Geschäft noch zu Ende führen, manchmal sogar noch an die Kinder übergeben. Der Stellmacher als Beispiel. Bei AI haben wir diese Zeit nicht.

Der Mensch ist inzwischen der Flaschenhals

Bei vielen anderen Techniken war der Mensch bei der Verbreitung zwar auch ein Flaschenhals, aber es gab tausend andere Dinge, die zusätzlich gebremst haben. Bei AI ist inzwischen der Mensch der eigentliche Flaschenhals. Solche Systeme könnten sich fast schon selbst weiterentwickeln, oder Menschen könnten zumindest in Zusammenarbeit mit AI viel schneller neue Systeme auf den Markt bringen. Nur sind die Menschen noch nicht da.

Seit ungefähr 4 bis 5 Jahren ist das ganze AI-Thema immer stärker geworden. Seit zwei Jahren ist es auch in der breiten Öffentlichkeit angekommen und sickert dort immer tiefer ein. Aber selbst die Menschen um uns herum, selbst ich, sind noch nicht so darauf eingestellt, wie es bei dieser Geschwindigkeit eigentlich nötig wäre.

Und das gilt nicht nur für die Allgemeinheit. Selbst Experten, die den AI-Hype-Train voll mitreiten und das zu ihrer Profession gemacht haben, schaffen es nicht, sich jedes neue Modell wirklich anzugucken und auszuprobieren. Einfach weil es so schnell und so viel ist. Die Menschen können es gar nicht mehr greifen.

Sprachen, Code-Massen und der schleichende Kontrollverlust

In der Systemadministration, im DevOps-Bereich oder in der Entwicklung sieht man die Geschwindigkeit an vielen Stellen. Es gibt immer eine neue Sprache, die irgendwelche Vorteile gegenüber einer alten hat oder besser für einen Nischenbereich passt. Leute steigen ein, werden gut darin, entwickeln die Sprache weiter, dann kommt die nächste Iteration. Ob Go, TypeScript, Rust, irgendeine Sprache ist immer gerade die gehypte.

Aber da muss man als Mensch ja erstmal reinkommen. Und das alles ist für Menschen gemacht. Eine AI könnte im Zweifel direkt in etwas deutlich Simpleres schreiben, oder sich eine komplett neue Sprache ausdenken, wenn man sie lassen würde. Eine Sprache, die für die AI selbst optimiert ist, um Dinge umzusetzen. Ich will nicht sagen, dass ich das alles wüsste oder dass das wahr ist, das sind nur meine Gedanken. Aber ich glaube, dass Menschen Sorge haben, an dieser Stelle Kontrolle abzugeben. Wenn eine AI ihre eigene Programmiersprache nutzt und in dieser Sprache Software entwickelt, dann versteht das am Ende keiner mehr. Und da frage ich mich auch: muss das denn überhaupt noch jemand verstehen?

Wenn ich mir anschaue, was in meinem Berufsumfeld aktuell passiert, würde ich fast nein sagen. Riesige Softwareprojekte und komplexe Themen werden vollständig durch AI generiert. Fast keiner fängt mehr wirklich damit an, echten Code von Hand zu schreiben. Was bei einem Pull-Request herausfällt, sind Massen an Code, die kein Mensch mehr wirklich liest. Wenn eine AI einen Tag lang Software entwickelt und das Ganze in einen Commit packt: welcher Mensch setzt sich dann hin und liest diese 100 Seiten Code einmal gegen, um zu prüfen ob er gut oder schlecht ist? Wer soll das bewerten?

Das kann sowieso schon keiner mehr. An der Stelle lässt man verschiedene Tools, und auch wieder eine AI, bewerten ob das gut oder schlecht ist. Dann mergt man nach Empfehlung. Sonst baut die AI die Software in einem Tag und ein Team von Entwicklern muss diesen Code eine Woche prüfen. Das ist Quatsch.

Wenn man das weiterspinnt, kommt man irgendwann an den Punkt, wo in der Softwareentwicklung und in der Systemadministration ganz viel echte Kontrolle über IT-Systeme verloren geht. Die wird am Ende an die AI abgegeben. Klar kann man die AI das schön dokumentieren lassen, sich Anleitungen schreiben lassen, theoretisch kann sich am Ende auch wieder ein Mensch einarbeiten. Aber wenn wir ehrlich sind: die Leute wollen Geld verdienen. Das passiert an dieser Stelle nicht.

Mythos, Open Source und der neue Patch-Druck

Vor allem in Richtung IT-Security ist in den letzten Wochen und Monaten viel durch die Presse gegangen. Anthropic, der Hersteller von Claude Code, soll mit einem Modell namens Mythos arbeiten, das in der Berichterstattung als sehr leistungsfähig beim Code-Audit beschrieben wurde. Wo genau das im Vergleich zu den jeweils aktuellen Modellen anderer Anbieter steht, kann ich nicht seriös einschätzen. Spannender ist sowieso, was so ein System angeblich kann: so schnell und zuverlässig Sicherheitslücken im Code finden, dass es katastrophal wäre, das einfach in die freie Welt rauszulassen. Stattdessen bekommen scheinbar nur sehr ausgewählte Leute und Unternehmen Zugriff darauf, meist US-Unternehmen, um ihre eigenen Dinge und Dienste zu prüfen.

Diese neuen Modelle sind nicht primär für klassisches Pentesting gegen eine Blackbox gedacht. Sie schauen in den Code und finden dort die Lücken, die man ausnutzen kann. Klar, fürs Pentesting kann man AI auch benutzen. Aber die große Angst beim Mythos-Thema ist genau dieser Code-Audit-Modus.

Aus dem ersten Blickwinkel sind damit Open-Source-Projekte besonders gefährdet, weil deren Quellcode offen im Internet steht. Früher wurde gesagt: genau das macht Open Source sicher, weil viele Leute reinschauen und Sicherheitslücken finden, die dann gefixt werden. Im Vergleich zu Closed-Source-Code, etwa bei Microsoft Windows, wo nicht jeder einfach in den Code schauen kann.

Bei Open Source sind in letzter Zeit viele Fixes gekommen. Mozilla hat Fixes gemacht, FreeBSD hat Fixes gemacht, viele Sicherheitslücken wurden geschlossen. Die Software wurde sicherer, fertig. Man kann jetzt sagen: ich halte das nächste, bessere AI-Modell wieder dagegen, und es wird wahrscheinlich wieder etwas finden. Hundertprozentige Sicherheit ist eh schwierig. Aber zumindest ist die Software gerade einen Schritt sicherer geworden.

Auf der anderen Seite setzt das die Systembetreiber unter Druck. Die müssen sich überlegen, wie sie diese schnellen, aufeinanderfolgenden Sicherheitsupdates in ihre Systeme bekommen. Klingt erstmal einfach. Ich sitze am Notebook, das Notebook sagt es gibt Updates, ich sage ja, installiere die Updates, starte das Notebook neu, alles funktioniert, ich bin aktuell. Wer aber eine normale Linux-Distribution mit vielen Zusatzpaketen auf dem Arbeitsplatz hat, sieht, dass mehrfach am Tag Updates kommen können.

Bei einfachen Security-Fixes sollten die Funktionen einer Library, einer Anwendung oder des Betriebssystems eigentlich nicht extrem auf den Kopf gestellt werden. Sie sollten nicht dafür sorgen, dass Abhängigkeiten brechen und plötzlich etwas nicht mehr funktioniert. Das Problem hat man eher, wenn man ganze Versionen wechselt, also auf das nächste Major Release geht.

Trotzdem gibt es Bereiche, in denen man nicht einfach mal einen Patch einspielen kann, weil Patches vorher geprüft und getestet werden müssen. Etwas Geheimes, etwas Staatliches, der Bankensektor, kritische Bereiche, die nicht ausfallen dürfen und bei denen alles zertifiziert sein muss. Das aktuelle Regelwerk steht dem entgegen, dass man im Zweifel drei oder fünf Mal am Tag etwas patchen müsste. Das lässt sich schwer miteinander vereinbaren.

Plötzlich kann jeder einen Cloud-Service starten

Der Patch-Druck ist nur eine Seite. Auf der anderen verändert AI gerade, wer überhaupt Software auf den Markt bringen kann. Plötzlich ist jeder mit einer Kreditkarte und einem Computer in der Lage, eine eigene Software, einen eigenen Service, eine eigene Dienstleistung anzubieten, in einer Cloud seiner Wahl zu hosten, global verteilt nah bei den jeweiligen Kunden. Das kann jetzt wirklich jeder.

Und das, was dabei als Code und Anwendung herausfällt, ist nicht mehr wie in den Anfängen der AI-Modelle. Es wird immer besser und stabiler. Im Grunde kann ein Ein-Mann-CEO-Unternehmen einen kompletten Software-Service aufmachen: automatisiertes Ticketsystem per AI, KI-Hotline, AI-Werbung, AI-Webseite, AI-Marketing, das AI-Produkt läuft vor sich hin.

Der Aufwand, ein solches Produkt überhaupt zu entwickeln und auf den Markt zu bringen, ist extrem gering geworden. In den nächsten zwei oder drei Jahren werden wir mit Sicherheit feststellen, dass der Markt überall auf der Erde mit solchen Programmen und Diensten regelrecht überschwemmt wird. Die werden sich im Preis immer weiter unterbieten. Die Baseline wird irgendwo bei den Kosten des Cloud-Providers liegen, plus dem monatlichen AI-Modell der einen Person dahinter.

Schwieriger sind nur sehr spezielle Nischen in einem bestimmten Markt, etwa eine deutsche Buchhaltungsanwendung. Oder Zertifizierungen bei sicherheitskritischen Themen, ISO 27001 oder BSI C5. Das wird für solche Solo-CEO-Firmen noch einige Jahre schwieriger zu erreichen sein. Aber auch das schützt den Markt nicht für Jahrzehnte. Das ist ein Deckel für die nächsten fünf bis zehn Jahre, und auch der wird kräftig anfangen zu bröckeln.

Verlieren wir die Übung im logischen Denken?

AI sorgt vielleicht auch dafür, dass wir Übung und Routine darin verlieren, logisch Probleme zu lösen. Man hört das nicht immer direkt, aber ich glaube, da ist etwas dran. In manchen Ländern liegt schon etwas mehr Augenmerk darauf, dass Schüler und Lehrkräfte keine AI benutzen, wenn es um Schulstoff oder Aufgaben geht. Einfach damit die Leute in dieser Fähigkeit drinbleiben.

Ob das besser oder schlechter ist als der Ansatz hier, wo AI zum Teil schon mitbenutzt wird, weiß ich nicht. Man muss sich mit der Technik auseinandersetzen, man muss verstehen wie sie funktioniert, um hineinzukommen. Aber wahrscheinlich ist auch das in fünf Jahren nicht mehr nötig. Die Modelle sind dann so weit, dass man keinerlei Vorahnung mehr braucht. Man geht zum Handy, oder was wir bis dahin als Gerät haben, sagt: hier ist mein Problem. Und das Ding baut die Lösung.

Brauchen wir noch Code-Repositories?

Im Moment haben wir noch Code-Repositories, Pipelines zum Deployen, Linter, Sicherheitsscanner, SonarQube oder Ähnliches. Wir machen Commits, schreiben Kommentare in den Quellcode, legen das alles dort ab. Wir müssen zu alten Releases zurückrollen können. Wir machen Releases, wir haben Software-Lifetime.

Aber wer sagt, dass das so bleibt? Warum kann man nicht einfach jedes Mal, wenn man ein Stück Software braucht, der AI sagen: bau mir das. Die AI baut es. Und wenn ich es nicht mehr brauche, wird es weggeworfen. Wofür hebe ich den Code überhaupt auf?

In wenigen Jahren, wenn die Entwicklung so weitergeht, baut mir die AI die Anwendung, die ich gerade brauche, in Echtzeit nebenher. Wenn ich kurz warten muss: so what? Sobald ich den Service nicht mehr brauche oder ein neues Feature will, wird das ganze Ding einfach neu gebaut. Was soll es? Wo ist das Problem?

Junioren, Ausbildung und der Druck auf die Sozialsysteme

Jetzt zu sagen: liebe Leute, lernt alle naturwissenschaftliche Fächer, Mathematik, Physik. Ja, das ist gut. Aber auch da wird AI eine Rolle spielen, und ich glaube, wir sind näher an einer kritischen Stelle dran als wir denken. In einigen Jahren weiß vielleicht niemand mehr, wie etwas gebaut wurde, wenn die Leute, die es noch wirklich verstanden haben, aus dem Berufsleben verschwunden sind.

Guckt in die Softwareentwicklung. Da werden im Grunde keine Junioren mehr eingestellt. Fachinformatiker Anwendungsentwicklung: wer macht diesen Ausbildungsberuf noch? Wer bildet diese Leute noch aus? Im Moment braucht man seniorige Menschen, die diese AI bedienen können. Die Aufgaben, die ein Junior oder ein Azubi gemacht hat, sind jetzt schon von der AI übernommen. Und das wird weitergehen.

Roboter werden trainiert, um Arbeiten zu übernehmen. Auch schön. Wenn wir nicht mehr selber arbeiten müssen, ist das doch toll. Der einzige Punkt, der mir Bauchschmerzen macht: aus meiner Sicht ist diese Zeit, die uns jahrhundertelang begleitet hat, Arbeitszeit gegen Geld, irgendwie vorbei. Wir brauchen also eine andere Lösung, wie wir Einkommen sicherstellen, um weiter ordentlich zu leben. Denn Menschen sind eine Spezies, die selten den Hals vollkriegt. Sie wollen immer mehr, immer besser. Und das funktioniert halt nicht.

Diese Lösung muss außerdem nicht nur für Europa, Deutschland oder die USA funktionieren, sondern für die ganze Welt. Wir alle haben in Anführungszeichen das gleiche Problem.

Politik und Gesellschaft kommen nicht hinterher

Wenn ich mir die Welt so angucke, sehe ich im Moment kaum Zusammenarbeit, kein gemeinsames Ziehen an einem Strang. Nicht auf internationalem Level. Da treten sich Leute aus irgendwelchen Gründen gegenseitig vor die Schienbeine. Ich will die einzelnen Konflikte nicht werten. Aber ich glaube nicht, dass wir so sinnvoll nach vorne kommen.

Auch auf kleinerem Level: wie viele tolle Geschichten aus dem Schwarzbuch der Steuerzahler oder bei extra 3 hat jeder schon bewundert. In der deutschen Politik brauchen schon Kleinigkeiten Ewigkeiten. Da ist auf einer Brücke einfach mal 18 Jahre Baustelle und es ist immer noch nicht fertig. Wie sollen wir mit solchen Strukturen ein Problem dieser Größenordnung schaffen?

Ich will damit nicht sagen, dass die alle wegmüssen. Aber ich glaube, wir haben das Problem an dieser Stelle noch nicht einmal verstanden. Wer diesen Beitrag liest, sieht das wahrscheinlich ähnlich wie ich, das ist meine Blase. Aber wenn ich später im Lidl stehe und von links nach rechts gucke, und das ist keine Wertung, leben viele Menschen einfach in anderen Themenfeldern. Nicht in einer anderen Realität, sie haben andere Punkte, die sie bewegen. Wie sehr uns AI gerade überrollt, kommt da kaum an. Es ist alles noch zu neu.

Auch unser Bundeskanzler und die aktuelle Regierung, darüber kann man sich streiten. Manche Sachen machen sie gut, manche schlecht. Niemand ist perfekt. Aber viele dieser Menschen sind in einem Alter, und nein, ich sage nicht, dass man es nur deshalb nicht verstehen kann, weil man älter ist. Aber ich würde behaupten: das Thema AI und die Frage, was das gerade für die Welt bedeutet, wirklich zu greifen, wird mit zunehmendem Alter schwieriger. Einfach schwieriger.

Wie damals bei der Elektrizität in den Häusern

Wenn man jetzt überlegt was man tun sollte, sagen viele: ich reite den AI-Hype-Train, ich gehe voll rein und mache nur noch AI. Das ist auch richtig. Leute, die Agentic Engineering oder Prompt Engineering richtig für sich adaptiert haben, sind im Moment extrem gefragt. Die haben gerade Hochzeit.

Aber wenn ich auf den Stand der Modelle schaue, sind wir trotzdem noch ganz am Anfang. Es ist eher so, als wären gerade die ersten Autos gekommen. Oder noch passender: als die Elektrizität in normalen Häusern eingeführt wurde. Das war gefährlich. Sicherungen? Mit Stoff umwickelte Drähtchen. Keine richtige Erdung. Keiner wusste, wie das wirklich funktioniert. Da ist viel schiefgegangen. Man musste extra vorsichtig sein und Dinge dreimal kontrollieren, damit nichts brennt.

Dann kamen mehr Regeln. Mehr Sicherheit. Es hat sich alles weiterentwickelt. Heute passieren auch noch Unfälle mit Elektrizität, aber im Grunde ist die Technik in unserer Gesellschaft so weit angekommen, dass man kein Super-Fachexperte mehr sein muss, um mit dem Waffeleisen sicher Waffeln zu machen. Steckdose, los geht’s. Und wenn der Defekt im Gerät ist, greifen Schutzmechanismen mit hoher Wahrscheinlichkeit.

Auf AI gemünzt: wir stehen gerade am Anfang dieses Prozesses. Die Experten, die im Moment full commitment reingegangen sind, profitieren gerade. Klar wird da auch mal etwas schiefgehen. Aber das sind die Leute, die ihre Hochzeit haben. Bis zu dem Moment, wo die Technik so weit ist, dass es einfach jeder kann. Wirklich jeder.

Und weil AI sich so schnell weiterentwickelt, wird das nicht lange dauern. Selbst jetzt zu sagen: ich mache Deep Dive in AI und bin in einem oder zwei Jahren der absolute Profi: schon auf dem Weg dahin wird man feststellen, dass man gar nicht mehr so tief einsteigen muss, weil es fast jeder kann.

Das sieht man an tausend Kleinigkeiten. Welche Skills und Abhängigkeiten ich mir vor einem Jahr noch in meinen Claude Code eingebaut habe und wie sehr sich das alles allein weiterentwickelt hat. Wie gut ich die größeren neueren Modelle jetzt schon auf Dinge loslassen kann. Alles nicht perfekt. Nichts davon kann ich zu 100 Prozent unbeaufsichtigt laufen lassen. Aber die Veränderung in diesem einen Jahr ist brutal.

Das sieht auch jeder, der sich AI-Videos anschaut. Will Smith isst Nudeln, damals 2023 oder 2024, und was generiert AI heute für Filmchen? Wenn man durch Social Media oder YouTube scrollt: wie viele AI-Geschichten sind da inzwischen drin, und wie viele davon erkennt man noch als AI? Die meisten ja, manchmal muss ich zweimal hingucken. Bei längeren Videos ist es einfacher. Aber so ein YouTube Short, runtergerechnet auf schlechte Kameraauflösung, vielleicht im Stil eines Bodycam-Shots, da wird es schon schwierig.

Nicht AI ist das Problem, wir sind es

Wie gesagt: AI ist nicht das Problem. Das Problem ist, wie wir Menschen damit umgehen. Wie wir es nicht schaffen, zusammen in eine Richtung zu gehen. Wie wir es nicht schaffen, als Gemeinschaft eine Lösung zu finden. Das ist viel eher das Problem als zu sagen, die AI wird uns alle töten. Das können wir selber am besten.

Was ich daraus mache, und warum ich keine Lösung habe

Was machen wir jetzt daraus? Ich versuche, im Thema zu bleiben. Ich versuche, AI dort einzusetzen, wo sie mich unterstützt und mir hilft. Ich versuche, ein Ohr an der Entwicklung zu halten, auch wenn ich sie nicht wirklich komplett durchdringen kann. Es ist einfach zu viel und zu schnell. Selbst Vollzeit würde mich überfordern.

Ich stelle mich darauf ein, die Systeme, die ich baue und betreibe, mit mehr als einer Sicherheitshürde auszustatten. Ich plane sie so, dass sie kein Problem damit haben, regelmäßig und wirklich regelmäßig Patches zu bekommen. Ich denke sie außerdem so, dass sie von AI-Systemen selbst gebaut, weiterentwickelt, betrieben und überwacht werden können. Das wird mit eingeplant.

Was das große, allgemeine Problem angeht: ich habe keine Lösung. Wirklich keine. Hinzugehen und Entscheidungsträgern das zu erklären, ich glaube nicht, dass ich diese Leute erreichen werde. Vielleicht ist das mein Problem. Technisch bin ich gut, das würde ich mir jetzt einfach mal unterstellen. Aber ich bin vielleicht nicht in der Lage, das vernünftig an den Mann zu bringen.

Ich habe schon mehrfach erlebt, wie ich versucht habe, IT-Security-Probleme möglichst einfühlsam und auf einfachem Level zu erklären, und trotzdem auf taube Ohren gestoßen bin. Für viele Leute, die nicht in der Technik drin sind, ist das einfach zu abstrakt und zu schlecht greifbar. Das ist wahrscheinlich ein Manko bei mir. Ich kriege es nicht so weit heruntergebrochen, dass es für Menschen ohne Tech-Background wirklich anfassbar wird.

Was hört man dann? Vielleicht auch nur, weil sie nichts anderes sagen können: das wird schon. Es wird etwas im Markt geben. Es schafft ja auch neue Jobs. Laberlaber. Da sind wir uns vermutlich einig: das wird nicht der Fall sein. Klar, ein paar neue Spezialjobs werden entstehen. Aber die Masse an Menschen, deren Arbeitskraft plötzlich nicht mehr gebraucht wird, weil sie von AI übernommen wurde, kommt nicht einfach in diese neuen Spezialbereiche hinein. Das wird nicht reichen für alle, die plötzlich aus dem Regal fallen.

Und weil unsere ganzen Sozialsysteme darauf aufgebaut sind, dass viele Leute einzahlen und Steuern zahlen, sehe ich da ein Problem. Ich habe keine Lösung dafür. Mir fällt nichts ein, was funktionieren könnte.

Vielleicht bin ich ein bisschen schwarzmalend. Aber ich glaube nicht, dass wir das gut hinkriegen. Ich mache mir Sorgen, was uns in den nächsten 15 bis 20 Jahren erwartet. Das wird extrem spannend. Aber die wenigsten Dinge daran geben mir ein gutes Gefühl.

Trotzdem können wir uns jetzt nicht alle ein Loch in den Garten buddeln und uns da hineinsetzen. Wir müssen weitermachen und das Beste aus dem ganzen Thema herausholen. Früher oder später wird es auch bei den Entscheidern ankommen. Sie werden es verstehen, oder sie werden die Augen nicht mehr davor verschließen können. Aber selbst dann glaube ich nicht, dass sie eine echte Lösung finden werden.

Siehe auch

Wie seht ihr das? Schreibt es gerne in die Kommentare oder per fragen.

Post-Quantum TLS auf Nginx: 15 Tage $ssl_curve ausgewertet, wer macht mit?

Post-Quantum TLS auf Nginx – Auswertung der Key-Exchange-Gruppen (X25519MLKEM768 vs. klassisch)

Vor einigen Wochen habe ich Nginx hier auf X25519MLKEM768 umgestellt und den Weg dorthin in einem eigenen Beitrag dokumentiert: Post-Quantum TLS für Nginx auf FreeBSD 15. Am Ende des Beitrags stand ein kleines Versprechen. Ich erweitere das Logging um die ausgehandelte Key-Exchange-Gruppe, lasse das ein paar Wochen laufen und werte dann aus, wer was tatsächlich spricht. Das ist jetzt eingelöst.

Der Messaufbau, in zwei Zeilen

Nginx kennt die Variable $ssl_curve. Die ist seit Ewigkeiten verfügbar und liefert pro Verbindung zurück, welche Kurve bzw. Gruppe beim TLS-Handshake benutzt wurde. Also X25519MLKEM768, X25519, secp384r1, prime256v1 und so weiter. Im Log-Format einfach nach $ssl_cipher eingehängt, einen Reload in den Nginx geschickt, fertig.

log_format goa_ext
  '$remote_addr - $remote_user [$time_local] '
  '"$request" $status $body_bytes_sent '
  '"$http_referer" "$http_user_agent" '
  '$host $server_protocol $scheme '
  '$request_time $upstream_response_time $upstream_status $upstream_addr '
  '$ssl_protocol $ssl_cipher $ssl_curve $upstream_cache_status';

Nach rund 15 Tagen liegen grob 180.000 HTTPS-Handshakes im Log, verteilt über alles, was so an HTTPS-Clients vorbeikommt. Das ist genug Masse, um ein paar belastbare Muster zu sehen, ohne dass einzelne Ausreißer das Gesamtbild kippen. Exakte Besucherzahlen werde ich in diesem Beitrag bewusst nicht auflisten, das ist auch nicht das Thema. Mich interessiert die relative Verteilung. Wer macht PQ, wer macht es nicht?

Die eine Zahl vorweg

Über alle HTTPS-Verbindungen (Browser, Bots, Crawler, Monitore, alles) sieht die Verteilung der Kurven so aus:

X25519MLKEM768   57,0 %   <- Post-Quantum-Hybrid
X25519           40,0 %   <- klassisches TLS 1.3
secp384r1         2,2 %   <- meist TLS 1.2
prime256v1        0,8 %   <- meist TLS 1.2

Klingt erstmal ordentlich. 57 % PQ über das komplette Gemisch, 98 % TLS 1.3, lediglich 2 % TLS 1.2. Aber in dieser Zahl stecken ein paar Dinge versteckt drin, die man erst sieht, sobald man die User-Agents grob auseinandersortiert. Ich habe das in ein paar Kübel geworfen: Browser, AI-Crawler, klassische Suchmaschinen, SEO-Spider, Fediverse-Software, RSS-Reader, Monitore und CLI-Tools. Nicht perfekt, aber brauchbar.

Browser, die Post-Quantum-Avantgarde

Bei echten Browsern (Chrome, Firefox, Safari, Edge) sieht es deutlich freundlicher aus. Zusammengenommen sprechen rund 77 % der Browser-Verbindungen bereits MLKEM768. Aufgeschlüsselt:

Firefox    87 % PQ   <- klarer Champion
Safari     75 % PQ
Edge       73 % PQ
Chrome     72 % PQ
Opera       2 % PQ   <- haengt seltsam weit hinten

Firefox liegt erkennbar vorn. Nicht weltbewegend weit, aber deutlich. Mozilla hat MLKEM768 früh aktiviert und nutzt standardmäßig die Hybrid-Gruppe, wenn der Server sie anbietet. Chrome hängt etwas hinter den anderen und ich vermute, das liegt an den ganzen älteren Chrome-Builds, die in Embedded-Devices, WebViews und seltsamen Apps stecken und auch als Chrome im User-Agent stehen. Opera dagegen nimmt fast immer nur klassisches X25519. Keine Ahnung warum, schaue ich mir vielleicht mal gesondert an.

Das Schöne daran ist: Ich habe für diese 77 % exakt nichts getan außer die Nginx-Konfiguration anzupassen. Kein Opt-in, kein Banner, keine Weiche. X25519MLKEM768 steht ganz oben in ssl_ecdh_curve und wird genommen, wenn der Client es kann. Der Rest ist reines Client-Upgrade-Verhalten. Das ist eigentlich die schönste Erkenntnis aus der ganzen Auswertung. Wenn die Serverseite rechtzeitig aktualisiert wird, zieht die Clientseite fast geräuschlos nach.

AI-Crawler, flächendeckend bei null

Jetzt kommt der Teil, den ich so nicht erwartet hätte. Die großen AI-Crawler holen sich hier regelmäßig Inhalte ab (siehe auch von SEO zu AEO: llms.txt und llms-full.txt), der TLS-Stack dahinter ist bei praktisch allen auf dem Stand von vor zwei Jahren:

OpenAI GPTBot             0 % PQ    -> X25519
Anthropic ClaudeBot       0 % PQ    -> X25519
Meta AI (ExternalAgent)   0 % PQ    -> X25519
PerplexityBot             0 % PQ    -> X25519
ChatGPT-User              0 % PQ    -> X25519
OpenAI SearchBot          0 % PQ    -> X25519
GoogleOther               0 % PQ    -> X25519
Amazonbot                 0 % PQ    -> TLS 1.2 + secp384r1

Amazon ist nochmal ein eigenes Kapitel. Amazonbot kommt hier ausschließlich mit TLS 1.2 und secp384r1. Das ist ein TLS-Stack, den ich persönlich bei einem Unternehmen, das Cloud-Sicherheit verkauft, nicht mehr erwartet hätte. Aber Messungen lügen nicht.

Zwei echte Ausnahmen gibt es:

ByteDance Bytespider     91 % PQ   <- ueberraschend
DuckAssistBot           100 % PQ   <- auch ueberraschend

Auf diese zwei hätte ich nicht gesetzt. TikToks Crawler macht zu über 90 % PQ, DuckDuckGos AI-Helper zu 100 %. Wer hätte das gedacht.

Klassische Suchmaschinen, auch nicht besser

Bei den traditionellen Suchmaschinen ist das Bild fast identisch zum AI-Lager:

Googlebot       0 % PQ
Applebot        0 % PQ
PetalBot        0 % PQ
Baiduspider     0 % PQ
SeznamBot       0 % PQ
Qwant           0 % PQ
YandexBot       1 % PQ
Bingbot         0 % PQ und zu 99,6 % auf TLS 1.2 + secp384r1
DuckDuckBot    84 % PQ   <- der einzige helle Fleck

Besonders bitter: Bingbot. Der läuft hier praktisch ausschließlich auf TLS 1.2 mit secp384r1. Microsofts Produktions-Webcrawler, 2026, mit einem TLS-Stack, den Webauditoren seit Jahren rot anstreichen. Googlebot ist immerhin auf TLS 1.3, aber halt ohne PQ. Der einzige, der fürs Thema etwas tut, ist DuckDuckBot. Respekt dafür.

SEO-Spider, am weitesten hinten

Der traurigste Haufen. AhrefsBot, SemrushBot, MJ12bot, DotBot, Barkrowler, DataForSeoBot, SeekportBot, Vebidoobot, alle zwischen 0 % und 3 % PQ. Vebidoobot kommt komplett auf TLS 1.2 rein. Das sind kommerzielle Produkte, die von Seitenbetreibern dafür bezahlt werden, Webseiten zu analysieren und Empfehlungen auszusprechen. Analysieren tun sie mit einem TLS-Stack, den sie ihren eigenen Kunden vermutlich als kritischen Finding in den Bericht schreiben würden. Kurios.

Fediverse, alles drin je nach Codebase

Seit dem Anschluss ans Fediverse ist das hier die zweitgrößte Traffic-Quelle nach den Browsern, deshalb habe ich mir das extra angeschaut. Mastodon stellt davon den Löwenanteil, weil es schlicht die meisten Instanzen gibt:

Mastodon                  62 % PQ
snac / GoToSocial /
  Friendica / Hubzilla    73 % PQ   <- Go/C-basiert
Misskey / Sharkey / ...   34 % PQ
Akkoma                     0 % PQ
Pleroma                    0 % PQ

Bei Mastodon gibt es eine große Varianz zwischen den Instanzen, weil die Ruby- und OpenSSL-Version des jeweiligen Server-Hosts entscheidet, ob PQ geht. Aktuelle Distribution mit OpenSSL 3.5 oder neuer: dabei. Noch auf OpenSSL 3.0 festhängend: nicht dabei. Der Schnitt liegt bei 62 %, was für ein so diverses Ökosystem schon erstaunlich ordentlich ist.

snac und GoToSocial liegen deutlich höher, weil sie in Go beziehungsweise C geschrieben sind und moderne TLS-Stacks mitbringen. Akkoma und Pleroma (beide Elixir/Erlang) zeigen dagegen gar keine PQ-Adoption. Das hängt an der OpenSSL-Version, die die BEAM-VM dort nutzt. Misskey und die ganzen Forks dazwischen liegen bei rund einem Drittel.

RSS-Reader, unerwartet modern

Hätte ich vorher schätzen sollen, hätte ich RSS-Reader eher am Ende dieser Liste verortet. Alte Technologie, alte Software, wahrscheinlich alter TLS-Stack. Stimmt aber nicht:

Miniflux              100 % PQ   <- Go-basiert
FreshRSS               82 % PQ
NextCloud-News         81 % PQ   <- zahlenmaessig vorn
Tiny Tiny RSS          37 % PQ
Inoreader               0 % PQ
Feedly                  0 % PQ   <- haengt auch hinten

Miniflux macht 100 % PQ, weil es in Go geschrieben ist und ab Go 1.24 MLKEM768 standardmäßig im TLS-Stack sitzt. FreshRSS und NextCloud-News laufen meist auf aktuellen PHP/curl-Umgebungen und ziehen MLKEM darüber mit. Feedly als kommerzieller Anbieter: 0 %. Also genau das Gegenteil dessen, was ich erwartet hätte. Self-Hosted ist hier eindeutig moderner unterwegs als die SaaS-Variante.

CLI-Werkzeuge und Kuriositäten

go-http-client     93 % PQ   <- Go 1.24+
curl               30 % PQ   <- je nach OpenSSL-Build
python-requests    16 % PQ
Node (axios)       61 % PQ
okhttp              0 % PQ
wget                0 % PQ
Twitterbot         97 % PQ   <- unerwartet weit vorn

Das Muster ist eigentlich immer dasselbe. Der TLS-Stack der Laufzeitumgebung entscheidet. Go ≥ 1.24 macht es automatisch, moderne Node-Versionen bringen einen aktuellen OpenSSL mit, Python und curl hängen an der Distribution. okhttp auf Android und wget: Fehlanzeige.

Kleiner Spaß am Rande. Twitterbot ist zu 97 % auf MLKEM. Also ausgerechnet der Link-Preview-Crawler von X/Twitter ist moderner unterwegs als alle anderen Social-Preview-Bots zusammen. WhatsApp: 4 %, Discord: 0 %, LinkedIn ist kaum vertreten. Warum Twitter? Keine Ahnung. Vermutlich ein moderner Go-Client unter der Haube.

TLS 1.2, wer hängt noch ganz unten?

2 % des Traffics kommen komplett mit TLS 1.2, also ohne jede Chance auf PQ. Die Top-Kandidaten sind:

vebidoobot (SEO-Spider)
Amazonbot
DotBot, MJ12bot (SEO)
theoldreader.com (RSS-SaaS)
http.rb/Mastodon auf aelteren Instanzen
ein paar vereinzelte Alt-Browser und Skype-Link-Previews

Also fast ausschließlich kommerzielle Crawler, deren TLS-Library vor der ganzen 1.3-Welle kompiliert wurde, und ein paar ältere Mastodon-Instanzen. Reale Leser sind so gut wie nicht betroffen. Wer heute einen Feed-Reader mit TLS 1.2 nutzt, hat vermutlich andere Probleme zuerst zu lösen.

Gibt es einen Trend in den 15 Tagen?

Nicht wirklich. Der PQ-Anteil pro Tag schwankt zwischen 46 % und 65 %, je nach Traffic-Mix (mehr Browser an Wochentagen, mehr Crawler nachts und am Wochenende). Einen klaren Aufwärts- oder Abwärtstrend gibt es nicht. Wir sind im Plateau. Die Browser haben den Sprung gemacht, der Rest der Welt noch nicht. Der nächste Sprung kommt, wenn die großen Crawler-Betreiber ihre Go-, Python- oder Node-Stacks aktualisieren oder OpenSSL 3.5+ in den gängigen Distributionen ankommt. Bei Debian Trixie, RHEL 10 und Ubuntu 26.04 sollte das passieren, dann reden wir in einem Jahr nochmal.

Was ich daraus mitnehme

  • Echte Besucher sind bei MLKEM768 schon sehr weit. Browser-Entwicklung funktioniert erstaunlich gut.
  • AI- und SEO-Crawler sind deutlich hinter dem, was man erwarten würde. Cutting Edge in der Marketing-Abteilung, Uralt-Stack im Maschinenraum.
  • Fediverse-Software ist so divers wie ihre Codebasen. Bei Mastodon entscheidet die Instanz, nicht die Software.
  • RSS-Reader sind unerwartet modern unterwegs. Self-Hosted schlägt SaaS auch hier.
  • Am Ende hängt fast alles an der OpenSSL-Version unter der Anwendung. Wieder mal.
  • Bingbot auf TLS 1.2 ist 2026 trotzdem noch bemerkenswert.

Was mich am meisten gefreut hat: Ich habe keinerlei Reibungsverluste gesehen. Kein Client ist wegen PQ gestolpert, keine Verbindung ist fehlgeschlagen, die vorher funktioniert hätte. Der Handshake wählt einfach die beste gemeinsame Gruppe und gut ist. Deshalb nochmal der Appell an alle, die den Einstellungs-Beitrag noch vor sich haben: Das ist wirklich ein Zweizeiler. Macht es einfach.

Nächster Check in ein paar Monaten

Ich lasse das Logging weiterlaufen und schaue in ein paar Monaten nochmal rein. Was mich besonders interessiert:

  • Wann machen OpenAI, Anthropic und Meta ihren Crawler modern? Bleibt das auf Jahre bei 0 % PQ, oder kommt da plötzlich ein Sprung?
  • Schafft es OpenSSL 3.5 in die nächsten Long-Term-Release-Linuxe, und wie schnell ziehen Mastodon-Instanzen nach?
  • Springt Googlebot irgendwann auf PQ um? Bisher nein. Wenn das kommt, dürfte das unmittelbar sichtbar sein.
  • Kommt TLS 1.3 für Amazonbot? Zumindest das wäre ein Anfang.

Siehe auch

Update Juli 2026: Was hinter der Gruppe X25519MLKEM768 steckt, die hier in rund 57 Prozent der Handshakes auftaucht, zerlege ich in einem eigenen Beitrag: X25519MLKEM768 zerlegt.

Wie immer: Bei Fragen, fragen.

DNS missbrauchen: Dateisysteme, DOOM und Tunnel durch Port 53

DNS-Missbrauch: Datenübertragung, Tunnel und C2-Kommunikation über DNS (Port 53)

Es gibt Dinge, bei denen man sich fragt, ob die Menschheit vielleicht einfach zu viel Freizeit hat. DNS ist so ein Protokoll, das eigentlich nur eine Aufgabe hat: Namen in IP-Adressen auflösen. Fertig. Simpel. Seit 1983 im Dienst. Aber nein, das reicht manchen Leuten natürlich nicht. Irgendwer schaut sich DNS an und denkt: „Da geht noch was.“ Und dann passieren Dinge. Michael hat mich zuletzt noch einmal daran erinnert (dankööö).

Ich habe mir mal ein paar Projekte angeschaut, die DNS auf eine Art und Weise nutzen, die man nur als kreative Vergewaltigung bezeichnen kann. Jedes einzelne davon ist gleichzeitig brillant und komplett wahnsinnig.

dnsfs: DNS-Resolver als Festplatte

Diagramm: dnsfs speichert Datei-Chunks als TXT-Records in fremden DNS-Resolver-Caches

Ben Cox, bekannt als benjojo, hatte offensichtlich eines Tages die Idee: Was, wenn man die Caches von DNS-Resolvern weltweit als verteiltes Dateisystem benutzt? Nicht die eigenen Resolver. Die von anderen Leuten. Einfach so.

dnsfs zerlegt Dateien in Chunks, kodiert sie als Base64 und schiebt sie als TXT-Records mit einem TTL von 2.147.483.646 Sekunden raus. Das sind knapp 68 Jahre. An fremde, offene DNS-Resolver. Die cachen das brav, und wenn man die Datei wieder haben will, fragt man dieselben Resolver einfach nochmal. Jeder Chunk wird auf drei verschiedenen Resolvern abgelegt, falls einer mal seinen Cache leert.

Verschlüsselung? Nein. Integritätsprüfung? Auch nein. Die Fehlerbehandlung beim Upload besteht darin, 2,5 Sekunden zu warten und bei Misserfolg ein :3 auszugeben. Als Null-Wert verwendet der Code den String „kittens“. Ich liebe es.

Ben hat das Ganze natürlich auch verbloggt und den Titel „true cloud storage“ gegeben. Technisch korrekt. Die Daten liegen ja wirklich verteilt in der Cloud. Nur halt in der Cloud anderer Leute.

DOOM over DNS: Es läuft immer DOOM

Screenshot: DOOM laeuft, geladen aus DNS TXT-Records ueber Cloudflare

Man kennt das Meme: „But can it run DOOM?“ Die Antwort ist immer ja. Taschenrechner, Schwangerschaftstests, Geldautomaten, alles läuft DOOM. Aber doom-over-dns treibt das auf die Spitze.

Die komplette DOOM-WAD (~4 MB) und die Game-Engine werden in fast 2.000 DNS TXT-Records zerlegt und über die Cloudflare API hochgeladen. Ein PowerShell-Skript fragt diese Records zur Laufzeit ab, setzt alles im RAM zusammen und startet das Spiel. Keine Datei wird jemals auf die Festplatte geschrieben. DOOM materialisiert sich quasi aus dem DNS.

Der besondere Clou: Cloudflare liefert die Records über sein globales CDN aus. Man bekommt also kostenloses, weltweites Content-Delivery für DOOM. Im Free-Tier. Man braucht allerdings mehrere Domains, weil Cloudflare die Anzahl der TXT-Records pro Zone begrenzt. Kein Sound, nur Windows, aber hey, es ist DOOM. Aus DNS. Was will man mehr.

iodine: VPN durch die Hintertür

Diagramm: iodine tunnelt IPv4-Traffic durch DNS-Queries an einem restriktiven Netzwerk vorbei

Während die ersten beiden Projekte eher in die Kategorie „weil man es kann“ fallen, ist iodine bitterer Ernst. Seit 2006 aktiv, 7.700 Stars auf GitHub, in C geschrieben und absolut produktionstauglich.

iodine tunnelt kompletten IPv4-Traffic durch DNS-Queries. Daten werden in Subdomain-Labels kodiert (Base32, Base64 oder Base128, je nachdem was der Resolver durchlässt), die Antworten kommen als NULL-, TXT-, SRV-, MX- oder A-Records zurück. Auf beiden Seiten wird ein TUN-Device erstellt und man hat einen vollständigen IP-Tunnel. Durch DNS. Port 53.

Der klassische Use-Case: Du sitzt im Hotel oder am Flughafen, das WLAN kostet 15 Euro pro Stunde, aber DNS-Queries gehen durch. iodine raus, SSH drüber, fertig. Kein besonders schneller Tunnel, einstellige Mbit/s wenn man Glück hat, aber es funktioniert. Seit fast 20 Jahren. Weil DNS-Traffic einfach fast nie geblockt wird.

dnscat2: Command & Control für Pentester

Diagramm: dnscat2 C2-Framework kommuniziert verschluesselt ueber die DNS-Hierarchie

Wo iodine ein Tunnel ist, ist dnscat2 ein komplettes C2-Framework. Ron Bowes hat das Ding für Penetration-Tests gebaut, und es kann deutlich mehr als nur Daten durchschleusen.

Ein C-Client auf dem Zielrechner kommuniziert über DNS-Queries (TXT, CNAME, MX) mit einem Ruby-Server auf dem eigenen autoritativen Nameserver. Der Traffic traversiert die normale DNS-Hierarchie, sieht also für jeden Beobachter aus wie ganz normales DNS. Das Framework bietet interaktive Shells, Dateitransfer, Port-Forwarding und Multi-Session-Management mit einer Metasploit-artigen Konsole.

Die Verschlüsselung nutzt ECDH, Salsa20 und SHA3. Allerdings ist das Crypto selbst designed und wurde nie professionell auditiert. In einem Pentest ist das okay. Für alles andere, naja.

DNSExfiltrator: Daten rausschmuggeln

Screenshot: DNSExfiltrator exfiltriert verschluesselte Dateien ueber DNS-Subdomain-Queries

DNSExfiltrator macht genau das, was der Name sagt: Dateien über DNS-Queries aus einem Netzwerk schmuggeln. Die Datei wird komprimiert, mit RC4 oder AES verschlüsselt und in Base64-kodierte Subdomain-Labels zerlegt. Jedes Label ist eine DNS-Query an den eigenen autoritativen Nameserver, der die Chunks reassembliert.

Das ist kein Spaßprojekt mehr. DNSExfiltrator wird in echten Red-Team-Assessments eingesetzt. Es funktioniert fast überall, weil kaum eine Firewall DNS-Traffic komplett blockiert. Der Client läuft als PowerShell-Skript oder kompiliertes C#, also genau das, was man auf einer Windows-Kiste in einem Unternehmensnetzwerk vorfindet.

Warum DNS?

Die ehrliche Antwort: Weil DNS überall durchkommt. Port 53 ist der eine Port, den wirklich jede Firewall aufmacht. DNS-Traffic wird selten inspiziert, selten rate-limited, selten als verdächtig eingestuft. Das Protokoll ist so fundamental für das Funktionieren des Internets, dass man es schlecht abdrehen kann. Und genau das macht es zum perfekten Kanal für alles, wofür es nie gedacht war.

TXT-Records nehmen quasi beliebigen Text auf. Subdomain-Labels können kodierte Daten enthalten. TTLs bestimmen, wie lange Resolver Daten cachen. Das sind alles Features, die für völlig legitime Zwecke existieren, aber in Kombination ein erstaunlich flexibles Daten-Transportmedium ergeben.

Von „Dateisystem in fremden Resolver-Caches“ über „DOOM aus TXT-Records“ bis hin zu „vollständiges C2-Framework für Pentester“: DNS hält das alles aus. Das Protokoll ist 43 Jahre alt und wurde seitdem in einer Art missbraucht, die sich Paul Mockapetris 1983 sicherlich nicht vorgestellt hat. Aber es funktioniert. Und das ist irgendwie das Schönste daran.

Sollte man eines dieser Projekte produktiv einsetzen? Auf gar keinen Fall. Sind sie trotzdem großartig? Absolut. Manchmal ist die richtige Reaktion auf „Aber warum?“ einfach: „Weil es geht.“

Ich bin vor kurzem auf Podcast „Security as a Podcast“ aufmerksam gemacht worden. Hier werden verschiedene Themen um DNS und Security behandelt. Ebenfalls wird dort erklärt, warum DNS hin und wieder so „missbraucht“ wird, speziell das DNSExfiltrations Thema hat mir gefallen.

Siehe auch:

Fragen oder eigene DNS-Verbrechen zu gestehen? Dann kannst du mich gerne fragen.

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.

Update Juli 2026: Was in X25519MLKEM768 eigentlich steckt, KEM, Hybrid und die Byte-Reihenfolge, habe ich Bestandteil für Bestandteil hier zerlegt: X25519MLKEM768 zerlegt.

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?

SSH-Bruteforce, DigitalOcean und AbuseIPDB – warum Blocken das Problem nicht löst

Aus gegebenem Anlass möchte ich ein paar Gedanken zu DigitalOcean aufschreiben. Nicht, weil ich glaube, dass DigitalOcean ein grundsätzliches Problem hat oder etwas falsch macht. Sondern weil DigitalOcean in meinen Logs seit Jahren besonders auffällt. Am Ende steht DigitalOcean hier eher sinnbildlich für ein größeres Thema.

Wer Systeme im Internet betreibt, kennt das Spiel. Server werden dauerhaft von außen angefasst. SSH-Ports werden gescannt, Login-Versuche laufen durch, Webseiten bekommen Requests auf bekannte Pfade, WordPress-Logins, XML-RPC, das volle Programm. Das ist kein gezielter Angriff, sondern automatisiertes Dauerrauschen. Bots, Skripte, Scanner, manchmal Security-Tools, manchmal schlicht schlecht konfigurierte Kisten.

Darstellung von automatisierten SSH-Bruteforce-Angriffen und Server-Härtung in Cloud-Umgebungen

Findet so ein Bot irgendwo ein offenes Loch, einen Standard-Login, ein vergessenes Passwort oder eine ungepatchte Schwachstelle, dann geht es weiter. Meistens wird erst einmal weitere Software nachgeladen. Der Host wird Teil eines Botnetzes, scannt selbst weiter, verteilt Spam, nimmt an DDoS-Aktionen teil oder schürft Kryptowährungen. Nichts davon ist neu, nichts davon ist überraschend.

Was mir allerdings seit mindestens vier Jahren auffällt: Ein sehr großer Teil dieser Brute-Force-Versuche, insbesondere auf SSH, kommt bei mir aus Netzen von DigitalOcean. Nicht ein bisschen mehr, sondern konstant irgendwo im Bereich von achtzig bis neunzig Prozent. Über Jahre. Über verschiedene Systeme hinweg.

Der erste Reflex liegt nahe. Wenn so viel aus einem Netz kommt, warum blockt man dann nicht einfach alle Netze dieses Anbieters? Dann ist Ruhe. Und wenn das alle machen würden, müsste der Anbieter ja reagieren. Der Gedanke ist verständlich. Ich hatte ihn selbst. Er ist aber aus meiner Sicht der falsche.

Ein pauschales Blocken ist im Grunde nichts anderes als eine Decke über das eigentliche Problem zu werfen. Das Problem ist damit nicht weg, es ist nur woanders. Die Bots wechseln dann eben zum nächsten Cloud-Provider. Außerdem produziert man sich damit ganz eigene Probleme. DigitalOcean-Netze komplett zu sperren heißt auch, legitimen Traffic auszusperren. APIs, Dienste, Kunden, Monitoring, externe Abhängigkeiten. Je nach Setup schneidet man sich damit schneller ins eigene Fleisch, als einem lieb ist.

Relativ schnell landet man dann bei Reputation-Diensten wie AbuseIPDB. Dort melden Betreiber IPs, von denen Scans, Brute-Force-Versuche oder andere Auffälligkeiten ausgehen. Auch ich melde dort seit Jahren IPs, automatisiert und manuell. Formal funktioniert das gut. IPs bekommen Scores, werden gelistet, tauchen in Datenbanken auf.

Das Problem ist nur: Diese Systeme arbeiten IP-basiert. Und genau das passt schlecht zur Realität moderner Netze. In Cloud-Umgebungen sind IPs kurzlebig. Heute gehört sie einem kompromittierten Host, morgen einem völlig legitimen Kunden. Ein hoher Abuse-Score sagt wenig über den aktuellen Nutzer dieser IP aus. Reputation ist träge, Infrastruktur ist schnell.

Jan  6 22:58:08 honeypot03 sshd-session[61904]: Invalid user sonar from 64.23.228.101 port 38610
Jan  6 22:58:08 honeypot03 sshd-session[61904]: Connection closed by invalid user sonar 64.23.228.101 port 38610 [preauth]
Jan  6 23:02:13 honeypot03 sshd-session[62101]: Invalid user sonar from 64.23.228.101 port 38174
Jan  6 23:02:13 honeypot03 sshd-session[62101]: Connection closed by invalid user sonar 64.23.228.101 port 38174 [preauth]
Jan  6 23:06:12 honeypot03 sshd-session[62175]: Invalid user sonar from 64.23.228.101 port 35952
Jan  6 23:06:12 honeypot03 sshd-session[62175]: Connection closed by invalid user sonar 64.23.228.101 port 35952 [preauth]
Jan  6 23:10:10 honeypot03 sshd-session[62248]: Invalid user steam from 64.23.228.101 port 38236
Jan  6 23:10:10 honeypot03 sshd-session[62248]: Connection closed by invalid user steam 64.23.228.101 port 38236 [preauth]
Jan  6 23:14:17 honeypot03 sshd-session[62335]: Invalid user steam from 64.23.228.101 port 35952
Jan  6 23:14:18 honeypot03 sshd-session[62335]: Connection closed by invalid user steam 64.23.228.101 port 35952 [preauth]
Jan  6 23:18:22 honeypot03 sshd-session[62455]: Invalid user steam from 64.23.228.101 port 50096
Jan  6 23:18:22 honeypot03 sshd-session[62455]: Connection closed by invalid user steam 64.23.228.101 port 50096 [preauth]
Jan  6 23:22:24 honeypot03 sshd-session[62599]: Invalid user sugi from 64.23.228.101 port 53212
Jan  6 23:22:25 honeypot03 sshd-session[62599]: Connection closed by invalid user sugi 64.23.228.101 port 53212 [preauth]
Jan  6 23:26:26 honeypot03 sshd-session[62671]: Invalid user svnuser from 64.23.228.101 port 44820
Jan  6 23:26:26 honeypot03 sshd-session[62671]: Connection closed by invalid user svnuser 64.23.228.101 port 44820 [preauth]
Jan  6 23:30:26 honeypot03 sshd-session[62763]: Invalid user svnuser from 64.23.228.101 port 52156
Jan  6 23:30:27 honeypot03 sshd-session[62763]: Connection closed by invalid user svnuser 64.23.228.101 port 52156 [preauth]
Jan  6 23:34:30 honeypot03 sshd-session[62867]: Invalid user taryn from 64.23.228.101 port 54128
Jan  6 23:34:31 honeypot03 sshd-session[62867]: Connection closed by invalid user taryn 64.23.228.101 port 54128 [preauth]
Jan  6 23:38:41 honeypot03 sshd-session[62939]: Invalid user taryn from 64.23.228.101 port 39894
Jan  6 23:38:42 honeypot03 sshd-session[62939]: Connection closed by invalid user taryn 64.23.228.101 port 39894 [preauth]
Jan  6 23:42:44 honeypot03 sshd-session[63013]: Invalid user taryn from 64.23.228.101 port 57728
Jan  6 23:42:45 honeypot03 sshd-session[63013]: Connection closed by invalid user taryn 64.23.228.101 port 57728 [preauth]
Jan  6 23:46:45 honeypot03 sshd-session[63160]: Invalid user taryn from 64.23.228.101 port 38438
Jan  6 23:46:45 honeypot03 sshd-session[63160]: Connection closed by invalid user taryn 64.23.228.101 port 38438 [preauth]
Jan  6 23:50:49 honeypot03 sshd-session[63252]: Invalid user taryn from 64.23.228.101 port 54070
Jan  6 23:50:49 honeypot03 sshd-session[63252]: Connection closed by invalid user taryn 64.23.228.101 port 54070 [preauth]
Jan  6 23:54:55 honeypot03 sshd-session[63354]: Invalid user terrance from 64.23.228.101 port 57960
Jan  6 23:54:55 honeypot03 sshd-session[63354]: Connection closed by invalid user terrance 64.23.228.101 port 57960 [preauth]
Jan  6 23:59:05 honeypot03 sshd-session[63472]: Invalid user terrance from 64.23.228.101 port 47558
Jan  6 23:59:05 honeypot03 sshd-session[63472]: Connection closed by invalid user terrance 64.23.228.101 port 47558 [preauth]
Jan  7 00:03:11 honeypot03 sshd-session[64731]: Invalid user terrance from 64.23.228.101 port 42938
Jan  7 00:03:11 honeypot03 sshd-session[64731]: Connection closed by invalid user terrance 64.23.228.101 port 42938 [preauth]

Damit erklärt sich auch, warum Provider solche externen Feeds nicht einfach hart umsetzen. Würde man IPs automatisiert abschalten, nur weil sie in einer Datenbank schlecht bewertet sind, träfe man regelmäßig Unbeteiligte. False Positives wären vorprogrammiert. Rechtlich, operativ und wirtschaftlich ist das für Provider kaum tragbar.

Warum also fällt DigitalOcean so stark auf? Das kann ich nicht belegen, nur einordnen. DigitalOcean ist günstig, schnell, einfach. In wenigen Minuten hat man dort eine VM mit öffentlicher IP. Das ist für legitime Nutzer attraktiv, aber eben auch für Leute mit schlechten Absichten. Wenn Infrastruktur billig und niedrigschwellig ist, taucht sie zwangsläufig häufiger in Logs auf. Dazu kommt, dass viele Systeme dort von Menschen betrieben werden, die vielleicht noch nicht so tief im Thema Security stecken. Offene Dienste, schwache Konfigurationen, fehlendes Hardening – all das macht solche Hosts wiederum zu guten Kandidaten für Kompromittierung und Weiterverwendung.

Wichtig dabei: DigitalOcean selbst macht aus meiner Sicht nichts grundlegend falsch. Der Abuse-Prozess funktioniert. Meldungen lassen sich automatisiert einreichen, werden angenommen, werden beantwortet, werden bearbeitet. Ich habe das über Jahre hinweg genutzt, sowohl manuell als auch automatisiert. Das ist sauber umgesetzt.

Was sich dadurch aber nicht ändert, ist die Menge der Versuche. Die wird nicht weniger. Sie bleibt konstant. Einzelne Hosts verschwinden, neue tauchen auf. Abuse-Meldungen – egal ob direkt beim Provider oder über Plattformen wie AbuseIPDB – wirken immer nur lokal und zeitverzögert. Gegen ein strukturelles Phänomen kommen sie nicht an.

Aus Sicht eines Providers ist das auch logisch. Ein paar tausend fehlgeschlagene SSH-Logins sind kein Incident. Kein DDoS, kein Ausfall, kein messbarer Schaden. Das fällt unter Hintergrundrauschen. Niemand bezahlt dafür, dieses Rauschen global zu eliminieren. Und ehrlich gesagt: Das kann auch niemand realistisch leisten.

Die eigentliche Konsequenz daraus ist unbequem, aber klar. Man darf nicht erwarten, dass Provider oder Reputation-Datenbanken einem dieses Problem abnehmen. Scan- und Brute-Force-Traffic gehört heute zum Betrieb eines öffentlich erreichbaren Systems dazu. Die einzige Stelle, an der man sinnvoll ansetzen kann, ist das eigene Setup.

Saubere Konfiguration. Keine Passwort-Logins per SSH. Kein Root-Login. Rate-Limits. Monitoring, das zwischen Rauschen und echten Zustandsänderungen unterscheidet. Fail2Ban als Dämpfer, nicht als Illusion von Sicherheit. Und vor allem: Gelassenheit gegenüber Logs, die voll sind, aber nichts bedeuten.

DigitalOcean ist hier nicht der Feind. AbuseIPDB ist kein Allheilmittel. Beides sind sichtbare Teile eines größeren Bildes. Das eigentliche Thema ist, wie man Systeme so betreibt, dass dieses Hintergrundrauschen irrelevant wird.

Siehe auch: SSH-Server absichern mit MFA

« Ältere Beiträge

© 2026 -=Kernel-Error=-RSS

Theme von Anders NorénHoch ↑