
Un plugin de filtro Kroxylicious que proporciona cifrado transparente poscuántico (ML-KEM + AES-256-GCM) a nivel de registro para Apache Kafka, sin requerir ningún cambio en el código del cliente.
Un complemento de filtro de Kroxylicious que proporciona cifrado transparente a nivel de registro Post-Cuántico (PQC) para Apache Kafka mediante encapsulación de claves ML-KEM (FIPS 203) con cifrado simétrico AES-256-GCM.
Los productores y consumidores de Kafka requieren cero cambios de código. El proxy de Kroxylicious intercepta el tráfico y cifra en Produce / descifra en Fetch automáticamente.``` Producer ──plaintext──> Kroxylicious ──encrypted──> Kafka Broker Consumer <──plaintext── Kroxylicious <──encrypted── Kafka Broker
## ¿Por qué PQC para Kafka?
Los algoritmos clásicos de establecimiento de claves (RSA, ECDH) son vulnerables
a futuros ordenadores cuánticos. Si las claves de cifrado por registro se
establecen mediante un KEM clásico, un adversario cuántico podría recuperar esas
claves a partir de las encapsulaciones almacenadas junto con el texto cifrado en
el broker.
Este plugin utiliza ML-KEM (FIPS 203) para la encapsulación de claves resistente a
la computación cuántica, garantizando que los datos en reposo en el broker de
Kafka no puedan ser descifrados ni siquiera por un adversario con un ordenador
cuántico criptográficamente relevante.
**Nota:** Este filtro protege **los datos en reposo en el broker**, no el
canal TLS en tránsito. Consulta [THREAT_MODEL.md](https://github.com/oscerd/kroxylicious-pqc-filter/blob/HEAD/THREAT_MODEL.md) para un
análisis completo de qué está y qué no está protegido.
| Standard | Algoritmo | Propósito en este plugin |
|----------|-----------|------------------------|
| FIPS 203 | ML-KEM (Kyber) | Encapsulación de claves - establece de forma segura una clave AES por mensaje |
| N/A | AES-256-GCM | Cifrado autenticado simétrico de la carga útil del registro |
| N/A | X25519 ECDH | Acuerdo de claves clásico para defensa en profundidad en modo híbrido |
## Características
- **Cifrado/descifrado transparente** - no requiere cambios en el lado del cliente
- **ML-KEM-512, ML-KEM-768 (predeterminado), ML-KEM-1024** conjuntos de parámetros
- **Modo híbrido** (predeterminado) - combina ML-KEM + X25519 ECDH de modo que ambos deben ser rotos
- **Cifrado por registro** - cada registro obtiene una nueva encapsulación KEM + IV aleatorio
- **Filtrado por tema** - los patrones regex seleccionan qué temas cifrar
- **Detección de manipulación** - el cifrado autenticado AES-GCM rechaza el texto cifrado modificado
- **Seguridad semántica** - textos planos idénticos producen textos cifrados diferentes (IND-CCA2)
- **Autogeneración de claves** - genera y guarda claves ML-KEM en el primer arranque si no existen
- **Cabecera `x-pqc-encrypted`** - marca los registros cifrados para que los componentes posteriores lo sepan
- **Proveedores de claves conectables** - la SPI `KeyProvider` admite sistema de archivos (predeterminado) y backends de HashiCorp Vault
## Requisitos previos
| Requisito | Versión |
|-------------|---------|
| JDK | 17+ (se recomienda 21+) |
| Maven | 3.8+ |
| Kroxylicious | 0.19.0 |
| Apache Kafka | 3.9.x |
## Inicio rápido
### 1. Compilar el plugin```bash
git clone <this-repo>
cd kroxylicious-pqc-filter
mvn clean package -DskipTests
El JAR sombreado en target/kroxylicious-pqc-filter-1.0.0-SNAPSHOT.jar incluye Bouncy Castle, por lo que se puede incorporar a Kroxylicious sin dependencias adicionales.
Para incluir soporte del proveedor de claves de HashiCorp Vault, compila con el perfil vault:```bash
mvn clean package -Pvault -DskipTests
Esto agrupa `spring-vault-core` y el `VaultKeyProvider` en el JAR.
### 2. Generar claves 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 need to translate content, but the input is empty.``` 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
Alternativamente, omita las rutas de clave en la configuración y el filtro generará
claves automáticamente en el primer arranque.
### 3. Configurar Kroxylicious
Añade el filtro a tu configuración 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
Coloque el JAR en un directorio accesible para Kroxylicious y agréguelo al
classpath mediante la variable de entorno KROXYLICIOUS_CLASSPATH:```bash
export KROXYLICIOUS_CLASSPATH="/opt/kroxylicious/plugins/*"
Cuando uses Docker, establécelo en el entorno de tu contenedor:```yaml
environment:
KROXYLICIOUS_CLASSPATH: /opt/kroxylicious/plugins/*
Luego inicie el proxy. Los productores y consumidores se conectan al puerto del proxy en lugar de hacerlo directamente al broker.
Filesystem (keyProviderType: filesystem, predeterminado):
Carga claves ML-KEM desde archivos DER en disco. Si los archivos no existen, genera
un nuevo par de claves y los guarda. Requiere publicKeyPath y privateKeyPath.
HashiCorp Vault (keyProviderType: vault, requiere compilación con -Pvault):
Obtiene claves ML-KEM de un motor de secretos Vault KV v2. Las claves se almacenan como
DER codificadas en base64 en los campos publicKey y privateKey. Las versiones de secretos
de Vault se asignan a IDs de clave para admitir la rotación de claves.
Propiedades de keyProviderConfig de Vault:
Ejemplo de configuración de Vault:```yaml filterDefinitions:
### Conjuntos de parámetros ML-KEM
| Algoritmo | Nivel de seguridad | Clave pública | Clave privada | Sobrecarga de texto cifrado | Caso de uso |
|-----------|---------------|------------|-------------|--------------------:|----------|
| ML-KEM-512 | 128-bit | 822 B | 1,730 B | ~854 B | Ligero, IoT |
| ML-KEM-768 | 192-bit | 1,206 B | 2,498 B | ~1,174 B | **Recomendado por defecto** |
| ML-KEM-1024 | 256-bit | 1,590 B | 3,266 B | ~1,654 B | Datos clasificados / de larga duración |
### Modos de cifrado
**Solo PQC** (`hybridMode: false`):
Utiliza exclusivamente ML-KEM. La clave AES-256 se deriva del secreto compartido ML-KEM
mediante `SHA-256(0x01 || "kroxylicious-pqc-v1" || secret)`.
**Híbrido** (`hybridMode: true`, por defecto):
Combina ML-KEM + X25519. La clave AES-256 se deriva de ambos secretos mediante
`SHA-256(0x02 || "kroxylicious-pqc-hybrid-v1" || pqcSecret || x25519Secret)`.
Esto garantiza la seguridad incluso si uno de los algoritmos resulta comprometido.
## Formato de sobre cifrado
Cada valor de registro cifrado se sustituye por un sobre binario:```
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 | | |
+--------+-----------+--------------------+---------+--------+-----------------+
El byte de versión permite al descifrador determinar el modo sin configuración.
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
### Flujo de datos
#### Qué se almacena en Vault
Vault KV v2 almacena el par de claves ML-KEM en `secret/<secretPath>` (p. ej., `secret/kroxylicious/pqc`):
| Campo | Contenido | Formato |
|-------|-----------|---------|
| `publicKey` | Clave pública ML-KEM (utilizada para encapsulación) | X.509 DER codificado en Base64 |
| `privateKey` | Clave privada ML-KEM (utilizada para desencapsulación) | PKCS#8 DER codificado en Base64 |
Cada versión del secreto de Vault actúa como un identificador de clave, lo que permite la rotación de claves. Las versiones nuevas
cifran registros nuevos; las versiones antiguas aún pueden descifrar los registros cifrados con ellas.
#### Flujo de inicio```
┌─────────────────────────────────────────────────────────────────────┐
│ 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
#### Flujo de fetch (descifrado)```
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
### Clases clave
**`PqcRecordEncryptionFilterFactory`** implementa `FilterFactory<PqcEncryptionConfig, SharedPqcContext>`.
Anotada con `@Plugin(configType = PqcEncryptionConfig.class)`.
Registrada mediante `META-INF/services/io.kroxylicious.proxy.filter.FilterFactory`.
Se llama una vez al inicio (`initialize`) y una vez por conexión de cliente (`createFilter`).
**`PqcRecordEncryptionFilter`** implementa `ProduceRequestFilter` y `FetchResponseFilter`.
Intercepta `onProduceRequest` para cifrar y `onFetchResponse` para descifrar.
Una instancia por conexión; no se necesita sincronización (modelo de hilos de Kroxylicious).
**`PqcCryptoEngine`** realiza todas las operaciones criptográficas.
Sin estado, excepto por el material de claves y `SecureRandom`.
`encrypt()` devuelve un sobre autodescriptivo; `decrypt()` lo analiza.
Registra los proveedores de Bouncy Castle (`BC`, `BCPQC`) en un inicializador estático.
**`PqcKeyManager`** resuelve un `KeyProvider` mediante `ServiceLoader`, haciendo coincidir por
`keyProviderType`. Delega todas las operaciones de clave al proveedor resuelto.
**`KeyProvider`** es la interfaz SPI para backends de almacenamiento de claves conectables.
Las implementaciones se descubren mediante `META-INF/services`. Proveedores integrados:
`FileSystemKeyProvider` (predeterminado) y `VaultKeyProvider` (con `-Pvault`).
**`PqcEncryptionConfig`** es un POJO anotado con Jackson.
Se deserializa desde el bloque `config:` del YAML del proxy Kroxylicious.
Admite `keyProviderType` y `keyProviderConfig` para la selección del backend.
## Compilación y pruebas```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
Total: 55 pruebas (38 del núcleo + 17 de Vault)
JUnit 5.11.4, Mockito 5.15.2, AssertJ 3.27.3, Jackson Databind 2.18.3.
Medido en la demo independiente (JVM caliente, 1000 iteraciones, mensajes de 1 KB):
| Algoritmo | Cifrado | Descifrado |
|---|---|---|
| ML-KEM-512 | ~7,700 msg/s (0.13 ms/msg) | ~7,800 msg/s (0.13 ms/msg) |
| ML-KEM-768 |
La sobrecarga por mensaje es inferior al milisegundo. Para cargas de trabajo típicas de Kafka (mensajes en el rango de KB-MB), el coste de cifrado es insignificante en comparación con la E/S de red y de disco.
Para un modelo de amenazas completo que cubra contra qué se defiende este filtro y contra qué no, consulte THREAT_MODEL.md.
chmod 600).approle o kubernetes en lugar de tokens estáticos
en producción. Nunca incluya tokens de Vault en el control de versiones.null (tombstones de Kafka) se pasan
sin cifrar.Compression.NONE
porque los datos cifrados no se comprimen bien.Apache License 2.0. Consulte LICENSE para más detalles.
| Propiedad | Tipo | Requerido | Predeterminado | Descripción |
|---|
kemAlgorithm | enum | No | ML_KEM_768 | Conjunto de parámetros ML-KEM. Uno de ML_KEM_512, ML_KEM_768, ML_KEM_1024. |
hybridMode | boolean | No | true | Combina ML-KEM con X25519 ECDH para defensa en profundidad. |
publicKeyPath | string | Solo sistema de archivos | - | Ruta en el sistema de archivos a la clave pública ML-KEM (codificada en X.509 DER). |
privateKeyPath | string | Solo sistema de archivos | - | Ruta en el sistema de archivos a la clave privada ML-KEM (codificada en PKCS#8 DER). |
topicPatterns | list<string> | No | [".*"] | Patrones regex de Java. Solo los registros en temas que coinciden se cifran/descifran. |
keyProviderType | string | No | filesystem | Backend de almacenamiento de claves. Uno de filesystem, vault. |
keyProviderConfig | map<string, string> | Solo Vault | {} | Configuración específica del backend (consulte la sección de Vault a continuación). |
| Propiedad | Requerido | Predeterminado | Descripción |
|---|
vaultAddress | Sí | Env. VAULT_ADDR | URL del servidor Vault (p. ej., http://vault:8200) |
vaultToken | Para autenticación token | Env. VAULT_TOKEN | Token de autenticación de Vault |
secretPath | Sí | -- | Ruta dentro del motor de secretos (p. ej., kroxylicious/pqc) |
secretEngine | No | secret | Nombre de montaje del motor de secretos KV v2 |
authMethod | No | token | Método de autenticación: token, approle o kubernetes |
roleId | Para approle | -- | ID de rol de AppRole |
secretId | Para approle | -- | ID de secreto de AppRole |
kubeRole | Para kubernetes | -- | Nombre del rol de autenticación de Kubernetes |
kubeTokenPath | No | /var/run/secrets/.../token | Ruta del archivo de token de la cuenta de servicio |
| Clase de prueba | Pruebas | Qué se verifica |
|---|
PqcCryptoEngineTest | 12 | Cifrado/descifrado de ida y vuelta para las 3 variantes de ML-KEM, manejo de null, cargas útiles vacías y de 1 MB, seguridad semántica, detección de manipulación, rechazo de versión no válida, generación de claves, bytes de versión del sobre |
PqcEncryptionConfigTest | 9 | Valores predeterminados, valores explícitos, rechazo de null, deserialización JSON, inmutabilidad, propiedades de enumeración, deserialización de keyProviderConfig |
PqcKeyManagerTest | 6 | Resolución de KeyProvider, creación del motor, delegación a los proveedores, respaldo para el tipo de sistema de archivos |
FileSystemKeyProviderTest | 11 | Generación de claves, carga de claves existentes, ID de clave predeterminado, rechazo de ID de clave desconocido, validación de ruta null, descubrimiento mediante ServiceLoader, ida y vuelta criptográfico |
VaultKeyProviderTest | 17 | Validación de configuración (ruta/dirección/token/approle/kube faltantes), obtención de claves desde Vault, recuperación de claves versionadas, claves en caché, versiones no válidas, campos faltantes en el secreto, ida y vuelta criptográfico, comportamiento de cierre |
| Dependencia | Versión | Ámbito | Propósito |
|---|
io.kroxylicious:kroxylicious-api | 0.19.0 | provided | Interfaces de la API de filtro |
org.apache.kafka:kafka-clients | 3.9.0 | provided | Tipos de mensajes del protocolo Kafka |
com.fasterxml.jackson.core:jackson-annotations | 2.18.3 | provided | Vinculación de configuración |
org.bouncycastle:bcprov-jdk18on | 1.83 | compile | ML-KEM, AES-GCM, X25519 (incluidos en el shaded JAR) |
org.bouncycastle:bcutil-jdk18on | 1.83 | compile | Utilidades de Bouncy Castle (incluidas en el shaded JAR) |
org.slf4j:slf4j-api | 2.0.17 | provided | Registro |
org.springframework.vault:spring-vault-core | 3.1.2 | compile (perfil vault) | Cliente Vault KV v2 (incluido al compilar 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) |