
Ein Kroxylicious-Filter-Plugin, das transparente Post-Quanten-Verschlüsselung (ML-KEM + AES-256-GCM) auf Datensatzebene für Apache Kafka bereitstellt – ohne dass Änderungen am Client-Code erforderlich sind.
Ein Kroxylicious-Filter-Plugin, das transparente Post-Quanten-Kryptografie (PQC)-Verschlüsselung auf Datensatzebene für Apache Kafka mithilfe von ML-KEM (FIPS 203)-Schlüsselkapselung mit AES-256-GCM-symmetrischer Verschlüsselung bietet.
Kafka-Producer und -Consumer benötigen keinerlei Codeänderungen. Der Kroxylicious-Proxy fängt den Datenverkehr ab und verschlüsselt bei Produce / entschlüsselt bei Fetch automatisch.``` Producer ──plaintext──> Kroxylicious ──encrypted──> Kafka Broker Consumer <──plaintext── Kroxylicious <──encrypted── Kafka Broker
## Warum PQC für Kafka?
Klassische Schlüsselvereinbarungsalgorithmen (RSA, ECDH) sind anfällig für zukünftige
Quantencomputer. Wenn Verschlüsselungsschlüssel pro Datensatz mithilfe eines
klassischen KEM etabliert werden, könnte ein Quantengegner diese Schlüssel aus den
Verkapselungen wiederherstellen, die neben dem Chiffrat auf dem Broker gespeichert sind.
Dieses Plugin verwendet ML-KEM (FIPS 203) zur quantenresistenten Schlüsselverkapselung,
sodass Daten im Ruhezustand auf dem Kafka-Broker selbst von einem
Gegner mit einem kryptografisch relevanten Quantencomputer nicht entschlüsselt werden können.
**Hinweis:** Dieser Filter schützt **Daten im Ruhezustand auf dem Broker**, nicht den TLS-
Kanal während der Übertragung. Siehe [THREAT_MODEL.md](https://github.com/oscerd/kroxylicious-pqc-filter/blob/HEAD/THREAT_MODEL.md) für eine vollständige
Analyse dessen, was verteidigt wird und was nicht.
| Standard | Algorithmus | Zweck in diesem Plugin |
|----------|-----------|------------------------|
| FIPS 203 | ML-KEM (Kyber) | Schlüsselverkapselung - etabliert sicher einen AES-Schlüssel pro Nachricht |
| N/A | AES-256-GCM | Symmetrische authentifizierte Verschlüsselung der Datensatz-Nutzlast |
| N/A | X25519 ECDH | Klassische Schlüsselvereinbarung für den Hybrid-Modus mit Verteidigung in der Tiefe |
## Funktionen
- **Transparente Ver-/Entschlüsselung** - keine clientseitigen Änderungen erforderlich
- **Parametersätze ML-KEM-512, ML-KEM-768 (Standard), ML-KEM-1024**
- **Hybrid-Modus** (Standard) - kombiniert ML-KEM + X25519 ECDH, sodass beide gebrochen werden müssen
- **Verschlüsselung pro Datensatz** - jeder Datensatz erhält eine frische KEM-Verkapselung + zufälligen IV
- **Topic-Filterung** - Regex-Muster wählen aus, welche Topics verschlüsselt werden
- **Manipulationserkennung** - die authentifizierte AES-GCM-Verschlüsselung weist verändertes Chiffrat zurück
- **Semantische Sicherheit** - identische Klartexte erzeugen unterschiedliche Chiffrate (IND-CCA2)
- **Automatische Schlüsselerzeugung** - erzeugt und speichert ML-KEM-Schlüssel beim ersten Start, falls nicht vorhanden
- **`x-pqc-encrypted`-Header** - kennzeichnet verschlüsselte Datensätze für nachgelagerte Komponenten
- **Plugbare Schlüsselanbieter** - das `KeyProvider`-SPI unterstützt Dateisystem- (Standard) und HashiCorp-Vault-Backends
## Voraussetzungen
| Anforderung | Version |
|-------------|---------|
| JDK | 17+ (21+ empfohlen) |
| Maven | 3.8+ |
| Kroxylicious | 0.19.0 |
| Apache Kafka | 3.9.x |
## Schnellstart
### 1. Plugin bauen```bash
git clone <this-repo>
cd kroxylicious-pqc-filter
mvn clean package -DskipTests
Das Shaded-JAR unter target/kroxylicious-pqc-filter-1.0.0-SNAPSHOT.jar bündelt
Bouncy Castle, sodass es ohne zusätzliche Abhängigkeiten in Kroxylicious eingesetzt werden kann.
Um die Unterstützung für den HashiCorp-Vault-Schlüsselanbieter einzubinden, bauen Sie mit dem vault-Profil:```bash
mvn clean package -Pvault -DskipTests
Dies bündelt `spring-vault-core` und den `VaultKeyProvider` in das JAR.
### 2. ML-KEM-Schlüssel generieren```bash
java -cp target/kroxylicious-pqc-filter-1.0.0-SNAPSHOT.jar \
io.kroxylicious.filter.pqc.PqcKeyGeneratorCli \
ML_KEM_768 \
/etc/kroxylicious/pqc/
I didn't receive any content to translate. The chunk appears to be empty. Please provide the text you'd like translated, and I'll be happy to help.``` Generating ML-KEM-768 key pair... Public key: /etc/kroxylicious/pqc/pqc-public.der Size: 1206 bytes Format: X.509 Private key: /etc/kroxylicious/pqc/pqc-private.der Size: 2498 bytes Format: PKCS#8
Alternativ können Sie die Schlüsselpfade in der Konfiguration weglassen, und der Filter generiert die Schlüssel beim ersten Start automatisch.
### 3. Kroxylicious konfigurieren
Fügen Sie den Filter zu Ihrer Kroxylicious-Proxy-YAML-Konfiguration hinzu:```yaml
filterDefinitions:
- name: pqc-encryption
type: PqcRecordEncryptionFilterFactory
config:
kemAlgorithm: ML_KEM_768
hybridMode: true
publicKeyPath: /etc/kroxylicious/pqc/pqc-public.der
privateKeyPath: /etc/kroxylicious/pqc/pqc-private.der
topicPatterns:
- "sensitive-.*"
- "pii-.*"
defaultFilters:
- pqc-encryption
Legen Sie die JAR-Datei in einem Verzeichnis ab, das für Kroxylicious zugänglich ist, und fügen Sie sie
über die Umgebungsvariable KROXYLICIOUS_CLASSPATH zum Klassenpfad hinzu:```bash
export KROXYLICIOUS_CLASSPATH="/opt/kroxylicious/plugins/*"
Wenn Sie Docker verwenden, setzen Sie es in Ihrer Container-Umgebung:```yaml
environment:
KROXYLICIOUS_CLASSPATH: /opt/kroxylicious/plugins/*
Starten Sie dann den Proxy. Producer und Consumer verbinden sich mit dem Proxy-Port statt direkt mit dem Broker.
Filesystem (keyProviderType: filesystem, Standard):
Lädt ML-KEM-Schlüssel aus DER-Dateien von der Festplatte. Falls die Dateien nicht existieren, erzeugt es
ein neues Schlüsselpaar und speichert diese. Erfordert publicKeyPath und privateKeyPath.
HashiCorp Vault (keyProviderType: vault, erfordert -Pvault-Build):
Ruft ML-KEM-Schlüssel aus einer Vault-KV-v2-Secrets-Engine ab. Schlüssel werden als
base64-codiertes DER in den Feldern publicKey und privateKey gespeichert. Vault-Secret-Versionen
werden für die Schlüsselrotation auf Schlüssel-IDs abgebildet.
Vault-keyProviderConfig-Eigenschaften:
Beispiel einer Vault-Konfiguration:```yaml filterDefinitions:
### ML-KEM-Parametersätze
| Algorithmus | Sicherheitsstufe | Öffentlicher Schlüssel | Privater Schlüssel | Chiffrat-Overhead | Anwendungsfall |
|-----------|---------------|------------|-------------|--------------------:|----------|
| ML-KEM-512 | 128-Bit | 822 B | 1,730 B | ~854 B | Leichtgewichtig, IoT |
| ML-KEM-768 | 192-Bit | 1,206 B | 2,498 B | ~1,174 B | **Empfohlene Standardoption** |
| ML-KEM-1024 | 256-Bit | 1,590 B | 3,266 B | ~1,654 B | Klassifizierte / langlebige Daten |
### Verschlüsselungsmodi
**Nur-PQC** (`hybridMode: false`):
Verwendet ausschließlich ML-KEM. Der AES-256-Schlüssel wird aus dem gemeinsamen ML-KEM-Geheimnis abgeleitet über `SHA-256(0x01 || "kroxylicious-pqc-v1" || secret)`.
**Hybrid** (`hybridMode: true`, Standard):
Kombiniert ML-KEM + X25519. Der AES-256-Schlüssel wird aus beiden Geheimnissen abgeleitet über `SHA-256(0x02 || "kroxylicious-pqc-hybrid-v1" || pqcSecret || x25519Secret)`.
Dadurch ist die Sicherheit auch dann gewährleistet, wenn ein Algorithmus gebrochen wird.
## Format des verschlüsselten Umschlags
Jeder verschlüsselte Record-Wert wird durch einen binären Umschlag ersetzt:```
PQC-only (version 0x01):
+--------+-----------+--------------------+--------+---------------------------+
| 1 byte | 2 bytes | N bytes | 12 B | remaining |
| 0x01 | encap len | ML-KEM encapsulat. | AES IV | AES-GCM ciphertext + tag |
+--------+-----------+--------------------+--------+---------------------------+
Hybrid (version 0x02):
+--------+-----------+--------------------+---------+--------+-----------------+
| 1 byte | 2 bytes | N bytes | 32 B | 12 B | remaining |
| 0x02 | encap len | ML-KEM encapsulat. | X25519 | AES IV | AES-GCM ct+tag |
| | | | eph pub | | |
+--------+-----------+--------------------+---------+--------+-----------------+
Das Versionsbyte ermöglicht es dem Entschlüsseler, den Modus ohne Konfiguration zu bestimmen.
kroxylicious-pqc-filter/ src/main/java/io/kroxylicious/filter/pqc/ PqcRecordEncryptionFilterFactory.java # FilterFactory entry point (@Plugin) PqcRecordEncryptionFilter.java # ProduceRequestFilter + FetchResponseFilter PqcKeyGeneratorCli.java # CLI key generation utility config/ PqcEncryptionConfig.java # Jackson-deserialized configuration POJO crypto/ KeyProvider.java # SPI interface for pluggable key backends FileSystemKeyProvider.java # Default: loads/generates keys from disk PqcCryptoEngine.java # ML-KEM encapsulation + AES-256-GCM PqcKeyManager.java # Resolves KeyProvider via ServiceLoader src/main/java-vault/ # (vault profile only) .../crypto/ VaultKeyProvider.java # HashiCorp Vault KV v2 key provider src/main/resources/ META-INF/services/ io.kroxylicious.proxy.filter.FilterFactory # ServiceLoader: filter registration io.kroxylicious.filter.pqc.crypto.KeyProvider # ServiceLoader: key provider src/main/resources-vault/ # (vault profile only) META-INF/services/ io.kroxylicious.filter.pqc.crypto.KeyProvider # Registers both FS + Vault providers src/test/java/... # Unit tests (49 tests) src/test/java-vault/... # Vault provider tests (17 tests) examples/ standalone/ # Runs without Kafka (crypto engine demo) docker/ # Docker Compose: Kafka + Vault + Kroxylicious
### Datenfluss
#### Was im Vault gespeichert wird
Vault KV v2 speichert das ML-KEM-Schlüsselpaar unter `secret/<secretPath>` (z. B. `secret/kroxylicious/pqc`):
| Feld | Inhalt | Format |
|-------|---------|--------|
| `publicKey` | ML-KEM öffentlicher Schlüssel (für die Verkapselung) | Base64-kodiertes X.509-DER |
| `privateKey` | ML-KEM privater Schlüssel (für die Entkapselung) | Base64-kodiertes PKCS#8-DER |
Jede Vault-Secret-Version fungiert als Schlüssel-ID und ermöglicht so die Schlüsselrotation. Neue Versionen verschlüsseln neue Datensätze; alte Versionen können weiterhin Datensätze entschlüsseln, die mit ihnen verschlüsselt wurden.
#### Ablauf beim Start```
┌─────────────────────────────────────────────────────────────────────┐
│ STARTUP │
│ │
│ 1. Vault starts (dev mode, port 8200) │
│ │ │
│ 2. vault-init container: │
│ │ • Reads pre-generated ML-KEM DER key files from disk │
│ │ • Base64-encodes them │
│ │ • POST /v1/secret/data/kroxylicious/pqc │
│ │ • Stores publicKey + privateKey in Vault KV v2 │
│ │ • Exits │
│ │ │
│ 3. Kafka starts (KRaft mode, port 9092) │
│ │ │
│ 4. Kroxylicious starts (port 9192): │
│ │ │
│ │ FilterFactory.initialize() │
│ │ → PqcKeyManager resolves VaultKeyProvider │
│ │ via ServiceLoader (matches keyProviderType: "vault") │
│ │ → VaultKeyProvider.configure() │
│ │ connects to Vault (token/approle/kubernetes auth) │
│ │ → Fetches ML-KEM key pair from Vault KV v2 │
│ │ (GET /v1/secret/data/kroxylicious/pqc) │
│ │ → Decodes: base64 → DER bytes → Java Key objects │
│ │ → PqcCryptoEngine initialized with key pair │
│ ▼ │
│ Proxy ready — Vault is NOT contacted again per-message │
└─────────────────────────────────────────────────────────────────────┘
Kroxylicious (:9192)
┌───────────────────────────────────────────┐
Producer ──plaintext──> │ PqcRecordEncryptionFilter │ │ .onProduceRequest() │ │ │ │ For each record in matching topic: │ │ 1. ML-KEM encapsulate (public key) │ │ → fresh shared secret │ │ → encapsulation blob │ │ 2. SHA-256(shared secret) → AES-256 key │ │ 3. AES-GCM encrypt record value │ │ 4. Build binary envelope: │ │ [ver|encap_len|encap|IV|ciphertext] │ │ 5. Add x-pqc-encrypted header │ └────────────────┬──────────────────────────┘ │ ▼ Kafka Broker (:9092) stores encrypted blob
#### Fetch-Ablauf (Entschlüsselung)```
Kafka Broker (:9092)
returns encrypted blob
│
▼
┌────────────────┴──────────────────────────┐
│ PqcRecordEncryptionFilter │
│ .onFetchResponse() │
│ │
│ For each record with x-pqc-encrypted: │
│ 1. Parse envelope → extract encap + │
│ ciphertext │
│ 2. ML-KEM decapsulate (private key) │
│ → recover shared secret │
│ 3. SHA-256(shared secret) → AES-256 key │
│ 4. AES-GCM decrypt → plaintext │
│ 5. Remove x-pqc-encrypted header │
Consumer <──plaintext── │ │
└───────────────────────────────────────────┘
Kroxylicious (:9192)
Startup: FilterFactory.initialize() -> PqcKeyManager loads/generates ML-KEM key pair -> PqcCryptoEngine created with key pair -> Topic patterns compiled -> SharedPqcContext returned
Per connection: FilterFactory.createFilter() -> New PqcRecordEncryptionFilter instance -> Shares the same PqcCryptoEngine (thread-safe)
Produce request: onProduceRequest() -> For each topic matching topicPatterns: -> For each partition: -> For each record: -> ML-KEM encapsulate (fresh shared secret + encapsulation) -> Derive AES-256 key from shared secret -> AES-GCM encrypt the record value -> Replace value with encrypted envelope -> Add x-pqc-encrypted header -> Forward to broker
Fetch response: onFetchResponse() -> For each topic matching topicPatterns: -> For each partition: -> For each record with x-pqc-encrypted header: -> Read version + encapsulation from envelope -> ML-KEM decapsulate (recover shared secret) -> Derive AES-256 key -> AES-GCM decrypt -> Replace value with plaintext -> Remove x-pqc-encrypted header -> Forward to client
### Wichtige Klassen
**`PqcRecordEncryptionFilterFactory`** implementiert `FilterFactory<PqcEncryptionConfig, SharedPqcContext>`.
Annotiert mit `@Plugin(configType = PqcEncryptionConfig.class)`.
Registriert über `META-INF/services/io.kroxylicious.proxy.filter.FilterFactory`.
Wird einmal beim Start (`initialize`) und einmal pro Client-Verbindung (`createFilter`) aufgerufen.
**`PqcRecordEncryptionFilter`** implementiert `ProduceRequestFilter` und `FetchResponseFilter`.
Fängt `onProduceRequest` ab, um zu verschlüsseln, und `onFetchResponse`, um zu entschlüsseln.
Eine Instanz pro Verbindung; keine Synchronisierung erforderlich (Kroxylicious-Threadmodell).
**`PqcCryptoEngine`** führt alle kryptografischen Operationen aus.
Zustandslos, abgesehen von Schlüsselmaterial und `SecureRandom`.
`encrypt()` gibt einen selbstbeschreibenden Umschlag zurück; `decrypt()` parst ihn.
Registriert Bouncy-Castle-Provider (`BC`, `BCPQC`) in einem statischen Initialisierer.
**`PqcKeyManager`** löst einen `KeyProvider` über `ServiceLoader` auf, wobei nach
`keyProviderType` abgeglichen wird. Delegiert alle Schlüsseloperationen an den aufgelösten Provider.
**`KeyProvider`** ist die SPI-Schnittstelle für austauschbare Schlüsselspeicher-Backends.
Implementierungen werden über `META-INF/services` ermittelt. Integrierte Provider:
`FileSystemKeyProvider` (Standard) und `VaultKeyProvider` (mit `-Pvault`).
**`PqcEncryptionConfig`** ist ein Jackson-annotiertes POJO.
Wird aus dem `config:`-Block in der Kroxylicious-Proxy-YAML deserialisiert.
Unterstützt `keyProviderType` und `keyProviderConfig` für die Backend-Auswahl.
## Erstellen und Testen```bash
# Compile
mvn compile
# Run unit tests (38 core tests)
mvn test
# Package (creates shaded JAR with Bouncy Castle bundled)
mvn package
# Build with Vault support (55 tests: 38 core + 17 vault)
mvn package -Pvault
# Install to local Maven repository
mvn install
Insgesamt: 55 Tests (38 Kern + 17 Vault)
JUnit 5.11.4, Mockito 5.15.2, AssertJ 3.27.3, Jackson Databind 2.18.3.
Gemessen anhand der eigenständigen Demo (vorgewärmte JVM, 1.000 Iterationen, 1-KB-Nachrichten):
| Algorithmus | Verschlüsselung | Entschlüsselung |
|---|---|---|
| ML-KEM-512 | ~7.700 msg/s (0,13 ms/msg) | ~7.800 msg/s (0,13 ms/msg) |
Der Overhead pro Nachricht liegt unter einer Millisekunde. Bei typischen Kafka-Workloads (Nachrichten im KB-MB-Bereich) ist der Verschlüsselungsaufwand im Vergleich zu Netzwerk- und Festplatten-I/O vernachlässigbar.
Ein vollständiges Bedrohungsmodell, das beschreibt, wogegen dieser Filter schützt und wogegen nicht, finden Sie in THREAT_MODEL.md.
chmod 600).approle- oder kubernetes-Authentifizierung gegenüber statischen Tokens in der Produktion. Committen Sie Vault-Tokens niemals in die Versionskontrolle.null-Werten (Kafka-Tombstones) werden unverschlüsselt durchgereicht.Compression.NONE, da verschlüsselte Daten sich nicht gut komprimieren lassen.Apache License 2.0. Siehe LICENSE für Details.
| Eigenschaft | Typ | Erforderlich | Standard | Beschreibung |
|---|
kemAlgorithm | enum | Nein | ML_KEM_768 | ML-KEM-Parametersatz. Einer von ML_KEM_512, ML_KEM_768, ML_KEM_1024. |
hybridMode | boolean | Nein | true | Kombiniert ML-KEM mit X25519 ECDH für Defense-in-Depth. |
publicKeyPath | string | Nur Dateisystem | - | Dateisystempfad zum ML-KEM-öffentlichen Schlüssel (X.509-DER-codiert). |
privateKeyPath | string | Nur Dateisystem | - | Dateisystempfad zum ML-KEM-privaten Schlüssel (PKCS#8-DER-codiert). |
topicPatterns | list<string> | Nein | [".*"] | Java-Regex-Muster. Nur Records in passenden Topics werden verschlüsselt/entschlüsselt. |
keyProviderType | string | Nein | filesystem | Schlüsselspeicher-Backend. Eines von filesystem, vault. |
keyProviderConfig | map<string, string> | Nur Vault | {} | Backend-spezifische Konfiguration (siehe Vault-Abschnitt unten). |
| Eigenschaft | Erforderlich | Standard | Beschreibung |
|---|
vaultAddress | Ja | VAULT_ADDR-Umgebungsvariable | Vault-Server-URL (z. B. http://vault:8200) |
vaultToken | Für token-Auth | VAULT_TOKEN-Umgebungsvariable | Vault-Authentifizierungstoken |
secretPath | Ja | -- | Pfad innerhalb der Secrets-Engine (z. B. kroxylicious/pqc) |
secretEngine | Nein | secret | Mount-Name der KV-v2-Secrets-Engine |
authMethod | Nein | token | Authentifizierungsmethode: token, approle oder kubernetes |
roleId | Für approle | -- | AppRole-Rollen-ID |
secretId | Für approle | -- | AppRole-Secret-ID |
kubeRole | Für kubernetes | -- | Name der Kubernetes-Authentifizierungsrolle |
kubeTokenPath | Nein | /var/run/secrets/.../token | Dateipfad zum Serviceaccount-Token |
| Test-Klasse | Tests | Was überprüft wird |
|---|
PqcCryptoEngineTest | 12 | Verschlüsselungs-/Entschlüsselungs-Roundtrip für alle 3 ML-KEM-Varianten, Null-Behandlung, leere und 1-MB-Payloads, semantische Sicherheit, Manipulationserkennung, Ablehnung ungültiger Versionen, Schlüsselerzeugung, Envelope-Versionsbytes |
PqcEncryptionConfigTest | 9 | Standardwerte, explizite Werte, Null-Ablehnung, JSON-Deserialisierung, Unveränderlichkeit, Enum-Eigenschaften, keyProviderConfig-Deserialisierung |
PqcKeyManagerTest | 6 | KeyProvider-Auflösung, Engine-Erstellung, Delegation an Provider, Fallback für Dateisystemtyp |
FileSystemKeyProviderTest | 11 | Schlüsselerzeugung, Laden vorhandener Schlüssel, Standard-Schlüssel-ID, Ablehnung unbekannter Schlüssel-ID, Null-Pfad-Validierung, ServiceLoader-Erkennung, Krypto-Roundtrip |
VaultKeyProviderTest | 17 | Konfigurationsvalidierung (fehlender Pfad/Adresse/Token/AppRole/kube), Schlüsselabruf von Vault, Abruf versionierter Schlüssel, zwischengespeicherte Schlüssel, ungültige Versionen, fehlende Felder im Secret, Krypto-Roundtrip, Schließverhalten |
| Abhängigkeit | Version | Geltungsbereich | Zweck |
|---|
io.kroxylicious:kroxylicious-api | 0.19.0 | provided | Filter-API-Schnittstellen |
org.apache.kafka:kafka-clients | 3.9.0 | provided | Kafka-Protokoll-Nachrichtentypen |
com.fasterxml.jackson.core:jackson-annotations | 2.18.3 | provided | Konfigurationsbindung |
org.bouncycastle:bcprov-jdk18on | 1.83 | compile | ML-KEM, AES-GCM, X25519 (im shaded JAR gebündelt) |
org.bouncycastle:bcutil-jdk18on | 1.83 | compile | Bouncy-Castle-Hilfsbibliotheken (im shaded JAR gebündelt) |
org.slf4j:slf4j-api | 2.0.17 | provided | Protokollierung |
org.springframework.vault:spring-vault-core | 3.1.2 | compile (Vault-Profil) | Vault-KV-v2-Client (gebündelt bei Build mit -Pvault) |
| ML-KEM-768 |
| ~9.600 msg/s (0,10 ms/msg) |
| ~10.200 msg/s (0,10 ms/msg) |
| ML-KEM-1024 | ~8.500 msg/s (0,12 ms/msg) | ~6.900 msg/s (0,14 ms/msg) |