
Un plugin filtro Kroxylicious che fornisce crittografia trasparente post-quantum (ML-KEM + AES-256-GCM) a livello di record per Apache Kafka, senza richiedere alcuna modifica al codice client.
Un plugin filtro per Kroxylicious che fornisce una trasparente crittografia a livello di record con Crittografia Post-Quantum (PQC) per Apache Kafka utilizzando ML-KEM (FIPS 203) key encapsulation con crittografia simmetrica AES-256-GCM.
I produttori e i consumatori Kafka richiedono zero modifiche al codice. Il proxy Kroxylicious intercetta il traffico e crittografa in Produce / decrittografa in Fetch automaticamente.``` Producer ──plaintext──> Kroxylicious ──encrypted──> Kafka Broker Consumer <──plaintext── Kroxylicious <──encrypted── Kafka Broker
## Perché PQC per Kafka?
Gli algoritmi classici di stabilimento delle chiavi (RSA, ECDH) sono vulnerabili ai
futuri computer quantistici. Se le chiavi di cifratura per singolo record vengono stabilite utilizzando un
KEM classico, un avversario quantistico potrebbe recuperare quelle chiavi dalle
incapsulazioni memorizzate insieme al ciphertext sul broker.
Questo plugin utilizza ML-KEM (FIPS 203) per l'incapsulamento delle chiavi resistente ai computer quantistici,
garantendo che i dati a riposo sul broker Kafka non possano essere decifrati nemmeno da
un avversario dotato di un computer quantistico crittograficamente rilevante.
**Nota:** Questo filtro protegge **i dati a riposo sul broker**, non il canale TLS
in transito. Vedere [THREAT_MODEL.md](https://github.com/oscerd/kroxylicious-pqc-filter/blob/HEAD/THREAT_MODEL.md) per un'analisi
completa di ciò che viene difeso e ciò che non lo è.
| Standard | Algoritmo | Scopo in questo plugin |
|----------|-----------|------------------------|
| FIPS 203 | ML-KEM (Kyber) | Incapsulamento delle chiavi - stabilisce in modo sicuro una chiave AES per messaggio |
| N/D | AES-256-GCM | Cifratura autenticata simmetrica del payload del record |
| N/D | X25519 ECDH | Accordo di chiave classico per difesa in profondità in modalità ibrida |
## Caratteristiche
- **Cifratura/decifratura trasparente** - nessuna modifica lato client richiesta
- **Set di parametri ML-KEM-512, ML-KEM-768 (predefinito), ML-KEM-1024**
- **Modalità ibrida** (predefinita) - combina ML-KEM + X25519 ECDH così che entrambi debbano essere compromessi
- **Cifratura per singolo record** - ogni record ottiene una nuova incapsulazione KEM + IV casuale
- **Filtro per argomento** - pattern regex selezionano quali argomenti cifrare
- **Rilevamento manomissioni** - la cifratura autenticata AES-GCM rifiuta ciphertext modificati
- **Sicurezza semantica** - plaintext identici producono ciphertext diversi (IND-CCA2)
- **Generazione automatica delle chiavi** - genera e salva le chiavi ML-KEM al primo avvio se assenti
- **Header `x-pqc-encrypted`** - contrassegna i record cifrati per la consapevolezza a valle
- **Provider di chiavi componibili** - la SPI `KeyProvider` supporta backend filesystem (predefinito) e HashiCorp Vault
## Prerequisiti
| Requisito | Versione |
|-------------|---------|
| JDK | 17+ (21+ consigliato) |
| Maven | 3.8+ |
| Kroxylicious | 0.19.0 |
| Apache Kafka | 3.9.x |
## Avvio rapido
### 1. Compilare il plugin```bash
git clone <this-repo>
cd kroxylicious-pqc-filter
mvn clean package -DskipTests
Il JAR shaded in target/kroxylicious-pqc-filter-1.0.0-SNAPSHOT.jar include
Bouncy Castle così può essere inserito in Kroxylicious senza dipendenze extra.
Per includere il supporto per il key provider HashiCorp Vault, compila con il profilo vault:```bash
mvn clean package -Pvault -DskipTests
Questo include `spring-vault-core` e `VaultKeyProvider` nel JAR.
### 2. Genera le chiavi ML-KEM```bash
java -cp target/kroxylicious-pqc-filter-1.0.0-SNAPSHOT.jar \
io.kroxylicious.filter.pqc.PqcKeyGeneratorCli \
ML_KEM_768 \
/etc/kroxylicious/pqc/
I'm unable to produce a translation because no source text was included in this request. The INPUT: section is empty, so there is no content to translate.```
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
In alternativa, ometti i percorsi delle chiavi nella configurazione e il filtro genererà le chiavi automaticamente al primo avvio.
### 3. Configura Kroxylicious
Aggiungi il filtro alla configurazione YAML del proxy Kroxylicious:```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
Posiziona il JAR in una directory accessibile a Kroxylicious e aggiungilo al
classpath tramite la variabile d'ambiente KROXYLICIOUS_CLASSPATH:```bash
export KROXYLICIOUS_CLASSPATH="/opt/kroxylicious/plugins/*"
Quando usi Docker, impostalo nell'ambiente del container:```yaml
environment:
KROXYLICIOUS_CLASSPATH: /opt/kroxylicious/plugins/*
Then start the proxy. Producers and consumers connect to the proxy port instead of the broker directly.
Filesystem (keyProviderType: filesystem, default):
Loads ML-KEM keys from DER files on disk. If the files do not exist, generates
a fresh key pair and saves them. Requires publicKeyPath and privateKeyPath.
HashiCorp Vault (keyProviderType: vault, requires -Pvault build):
Fetches ML-KEM keys from a Vault KV v2 secrets engine. Keys are stored as
base64-encoded DER in publicKey and privateKey fields. Vault secret versions
map to key IDs for key rotation support.
Vault keyProviderConfig properties:
Example Vault configuration:```yaml filterDefinitions:
### Set di parametri ML-KEM
| Algorithm | Security Level | Public Key | Private Key | Ciphertext Overhead | Use Case |
|-----------|---------------|------------|-------------|--------------------:|----------|
| ML-KEM-512 | 128-bit | 822 B | 1,730 B | ~854 B | Leggero, IoT |
| ML-KEM-768 | 192-bit | 1,206 B | 2,498 B | ~1,174 B | **Default consigliato** |
| ML-KEM-1024 | 256-bit | 1,590 B | 3,266 B | ~1,654 B | Dati classificati / di lunga durata |
### Modalità di cifratura
**Solo PQC** (`hybridMode: false`):
Utilizza esclusivamente ML-KEM. La chiave AES-256 viene derivata dal segreto condiviso ML-KEM
tramite `SHA-256(0x01 || "kroxylicious-pqc-v1" || secret)`.
**Ibrido** (`hybridMode: true`, predefinito):
Combina ML-KEM + X25519. La chiave AES-256 viene derivata da entrambi i segreti tramite
`SHA-256(0x02 || "kroxylicious-pqc-hybrid-v1" || pqcSecret || x25519Secret)`.
Ciò garantisce la sicurezza anche se un algoritmo viene compromesso.
## Formato della busta cifrata
Ogni valore di record cifrato viene sostituito con una busta binaria:```
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 | | |
+--------+-----------+--------------------+---------+--------+-----------------+
Il byte di versione consente al decriptatore di determinare la modalità senza configurazione.
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
### Data Flow
#### What is stored in Vault
Vault KV v2 holds the ML-KEM key pair at `secret/<secretPath>` (e.g., `secret/kroxylicious/pqc`):
| Field | Content | Format |
|-------|---------|--------|
| `publicKey` | ML-KEM public key (used for encapsulation) | Base64-encoded X.509 DER |
| `privateKey` | ML-KEM private key (used for decapsulation) | Base64-encoded PKCS#8 DER |
Each Vault secret version acts as a key ID, enabling key rotation. New versions
encrypt new records; old versions can still decrypt records encrypted with them.
#### Startup flow```
┌─────────────────────────────────────────────────────────────────────┐
│ 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
#### Flusso di recupero (decrittazione)```
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
### Classi chiave
**`PqcRecordEncryptionFilterFactory`** implementa `FilterFactory<PqcEncryptionConfig, SharedPqcContext>`.
È annotata con `@Plugin(configType = PqcEncryptionConfig.class)`.
Registrata tramite `META-INF/services/io.kroxylicious.proxy.filter.FilterFactory`.
Viene chiamata una volta all'avvio (`initialize`) e una volta per connessione client (`createFilter`).
**`PqcRecordEncryptionFilter`** implementa `ProduceRequestFilter` e `FetchResponseFilter`.
Intercetta `onProduceRequest` per cifrare e `onFetchResponse` per decifrare.
Una istanza per connessione; non è necessaria alcuna sincronizzazione (modello di thread di Kroxylicious).
**`PqcCryptoEngine`** esegue tutte le operazioni crittografiche.
Senza stato, ad eccezione del materiale delle chiavi e di `SecureRandom`.
`encrypt()` restituisce un involucro auto-descrittivo; `decrypt()` lo analizza.
Registra i provider Bouncy Castle (`BC`, `BCPQC`) in un inizializzatore statico.
**`PqcKeyManager`** risolve un `KeyProvider` tramite `ServiceLoader`, effettuando la corrispondenza in base a
`keyProviderType`. Delega tutte le operazioni sulle chiavi al provider risolto.
**`KeyProvider`** è l'interfaccia SPI per backend di archiviazione delle chiavi componibili.
Le implementazioni vengono scoperte tramite `META-INF/services`. Provider integrati:
`FileSystemKeyProvider` (predefinito) e `VaultKeyProvider` (con `-Pvault`).
**`PqcEncryptionConfig`** è un POJO annotato con Jackson.
Deserializzato dal blocco `config:` nello YAML del proxy Kroxylicious.
Supporta `keyProviderType` e `keyProviderConfig` per la selezione del backend.
## Compilazione e Test```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
Totale: 55 test (38 core + 17 vault)
JUnit 5.11.4, Mockito 5.15.2, AssertJ 3.27.3, Jackson Databind 2.18.3.
Misurato sulla demo standalone (JVM a regime, 1,000 iterazioni, messaggi da 1 KB):
| Algoritmo | Cifratura | Decifratura |
|---|---|---|
| ML-KEM-512 | ~7,700 msg/s (0.13 ms/msg) | ~7,800 msg/s (0.13 ms/msg) |
| ML-KEM-768 |
L'overhead per messaggio è inferiore al millisecondo. Per i tipici carichi di lavoro Kafka (messaggi nell'intervallo KB-MB), il costo della cifratura è trascurabile rispetto all'I/O di rete e di disco.
Per un modello di minaccia completo che copra ciò da cui questo filtro difende e ciò da cui non difende, vedere THREAT_MODEL.md.
chmod 600).approle o kubernetes ai token statici
in produzione. Non committare mai i token di Vault nel sistema di controllo versione.null (tombstone di Kafka) vengono inoltrati
senza cifratura.Compression.NONE
perché i dati cifrati non si comprimono bene.Apache License 2.0. Vedere LICENSE per i dettagli.
| Property | Type | Required | Default | Description |
|---|
kemAlgorithm | enum | No | ML_KEM_768 | ML-KEM parameter set. One of ML_KEM_512, ML_KEM_768, ML_KEM_1024. |
hybridMode | boolean | No | true | Combine ML-KEM with X25519 ECDH for defense-in-depth. |
publicKeyPath | string | Filesystem only | - | Filesystem path to the ML-KEM public key (X.509 DER encoded). |
privateKeyPath | string | Filesystem only | - | Filesystem path to the ML-KEM private key (PKCS#8 DER encoded). |
topicPatterns | list<string> | No | [".*"] | Java regex patterns. Only records in matching topics are encrypted/decrypted. |
keyProviderType | string | No | filesystem | Key storage backend. One of filesystem, vault. |
keyProviderConfig | map<string, string> | Vault only | {} | Backend-specific configuration (see Vault section below). |
| Property | Required | Default | Description |
|---|
vaultAddress | Yes | VAULT_ADDR env | Vault server URL (e.g., http://vault:8200) |
vaultToken | For token auth | VAULT_TOKEN env | Vault authentication token |
secretPath | Yes | -- | Path within the secrets engine (e.g., kroxylicious/pqc) |
secretEngine | No | secret | KV v2 secrets engine mount name |
authMethod | No | token | Auth method: token, approle, or kubernetes |
roleId | For approle | -- | AppRole role ID |
secretId | For approle | -- | AppRole secret ID |
kubeRole | For kubernetes | -- | Kubernetes auth role name |
kubeTokenPath | No | /var/run/secrets/.../token | Service account token file path |
| Classe di test | Test | Cosa viene verificato |
|---|
PqcCryptoEngineTest | 12 | Roundtrip di cifratura/decifratura per tutte e tre le varianti ML-KEM, gestione dei valori null, payload vuoti e da 1MB, sicurezza semantica, rilevamento di manomissioni, rifiuto di versioni non valide, generazione delle chiavi, byte di versione dell'envelope |
PqcEncryptionConfigTest | 9 | Valori predefiniti, valori espliciti, rifiuto dei null, deserializzazione JSON, immutabilità, proprietà enum, deserializzazione di keyProviderConfig |
PqcKeyManagerTest | 6 | Risoluzione del KeyProvider, creazione del motore, delega ai provider, fallback per il tipo filesystem |
FileSystemKeyProviderTest | 11 | Generazione delle chiavi, caricamento delle chiavi esistenti, ID chiave predefinito, rifiuto di ID chiave sconosciuto, validazione del percorso null, discovery tramite ServiceLoader, roundtrip crittografico |
VaultKeyProviderTest | 17 | Validazione della configurazione (path/address/token/approle/kube mancanti), recupero della chiave da Vault, recupero di chiavi versionate, chiavi in cache, versioni non valide, campi mancanti nel secret, roundtrip crittografico, comportamento alla chiusura |
| Dipendenza | Versione | Ambito | Scopo |
|---|
io.kroxylicious:kroxylicious-api | 0.19.0 | provided | Interfacce API del filtro |
org.apache.kafka:kafka-clients | 3.9.0 | provided | Tipi di messaggio del protocollo Kafka |
com.fasterxml.jackson.core:jackson-annotations | 2.18.3 | provided | Binding della configurazione |
org.bouncycastle:bcprov-jdk18on | 1.83 | compile | ML-KEM, AES-GCM, X25519 (inclusi nel JAR shaded) |
org.bouncycastle:bcutil-jdk18on | 1.83 | compile | Utility di Bouncy Castle (incluse nel JAR shaded) |
org.slf4j:slf4j-api | 2.0.17 | provided | Logging |
org.springframework.vault:spring-vault-core | 3.1.2 | compile (profilo vault) | Client Vault KV v2 (incluso quando compilato con -Pvault) |
| ~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) |