
Un plugin de filtrage Kroxylicious qui fournit un chiffrement transparent au niveau des enregistrements, post-quantique (ML-KEM + AES-256-GCM), pour Apache Kafka, ne nécessitant aucune modification du code client.
Un plugin de filtre Kroxylicious qui fournit un chiffrement transparent au niveau des enregistrements en cryptographie post-quantique (PQC) pour Apache Kafka, utilisant ML-KEM (FIPS 203) pour la encapsulation de clé avec un chiffrement symétrique AES-256-GCM.
Les producteurs et consommateurs Kafka ne nécessitent aucune modification de code. Le proxy Kroxylicious intercepte le trafic et chiffre lors du Produce / déchiffre lors du Fetch automatiquement.``` Producer ──plaintext──> Kroxylicious ──encrypted──> Kafka Broker Consumer <──plaintext── Kroxylicious <──encrypted── Kafka Broker
## Pourquoi la PQC pour Kafka ?
Les algorithmes classiques d'établissement de clés (RSA, ECDH) sont vulnérables aux futurs ordinateurs quantiques. Si les clés de chiffrement par enregistrement sont établies à l'aide d'un KEM classique, un adversaire quantique pourrait récupérer ces clés à partir des encapsulations stockées aux côtés du texte chiffré sur le broker.
Ce plugin utilise ML-KEM (FIPS 203) pour l'encapsulation de clés résistante aux ordinateurs quantiques, garantissant que les données au repos sur le broker Kafka ne peuvent pas être déchiffrées même par un adversaire disposant d'un ordinateur quantique cryptographiquement pertinent.
**Remarque :** Ce filtre protège **les données au repos sur le broker**, et non le canal TLS en transit. Voir [THREAT_MODEL.md](https://github.com/oscerd/kroxylicious-pqc-filter/blob/HEAD/THREAT_MODEL.md) pour une analyse complète de ce qui est protégé et de ce qui ne l'est pas.
| Norme | Algorithme | Rôle dans ce plugin |
|----------|-----------|------------------------|
| FIPS 203 | ML-KEM (Kyber) | Encapsulation de clés - établit de manière sécurisée une clé AES par message |
| N/A | AES-256-GCM | Chiffrement authentifié symétrique de la charge utile de l'enregistrement |
| N/A | X25519 ECDH | Accord de clés classique pour la défense en profondeur du mode hybride |
## Fonctionnalités
- **Chiffrement/déchiffrement transparent** - aucune modification côté client requise
- **ML-KEM-512, ML-KEM-768 (défaut), ML-KEM-1024** ensembles de paramètres
- **Mode hybride** (défaut) - combine ML-KEM + X25519 ECDH de sorte que les deux doivent être cassés
- **Chiffrement par enregistrement** - chaque enregistrement reçoit une nouvelle encapsulation KEM + un IV aléatoire
- **Filtrage des topics** - les motifs regex sélectionnent les topics à chiffrer
- **Détection de falsification** - le chiffrement authentifié AES-GCM rejette tout texte chiffré modifié
- **Sécurité sémantique** - des textes clairs identiques produisent des textes chiffrés différents (IND-CCA2)
- **Génération automatique de clés** - génère et enregistre les clés ML-KEM au premier démarrage si elles sont absentes
- **En-tête `x-pqc-encrypted`** - marque les enregistrements chiffrés pour les composants en aval
- **Fournisseurs de clés enfichables** - le SPI `KeyProvider` prend en charge les backends filesystem (défaut) et HashiCorp Vault
## Prérequis
| Prérequis | Version |
|-------------|---------|
| JDK | 17+ (21+ recommandé) |
| Maven | 3.8+ |
| Kroxylicious | 0.19.0 |
| Apache Kafka | 3.9.x |
## Démarrage rapide
### 1. Construire le plugin```bash
git clone <this-repo>
cd kroxylicious-pqc-filter
mvn clean package -DskipTests
Le JAR fusionné situé à target/kroxylicious-pqc-filter-1.0.0-SNAPSHOT.jar embarque Bouncy Castle, de sorte qu'il peut être déposé dans Kroxylicious sans dépendances supplémentaires.
Pour inclure la prise en charge du fournisseur de clés HashiCorp Vault, construisez avec le profil vault :```bash
mvn clean package -Pvault -DskipTests
Ceci regroupe `spring-vault-core` et le `VaultKeyProvider` dans le JAR.
### 2. Générer des clés 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/
The input content is empty — no source text was provided for chunk 9/33. Please re-send the chunk content so I can translate it into French.``` 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
Alternativement, omettez les chemins des clés dans la configuration et le filtre générera
automatiquement les clés au premier démarrage.
### 3. Configurer Kroxylicious
Ajoutez le filtre à votre configuration YAML du 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
Placez le JAR dans un répertoire accessible à Kroxylicious et ajoutez-le au
classpath via la variable d'environnement KROXYLICIOUS_CLASSPATH :```bash
export KROXYLICIOUS_CLASSPATH="/opt/kroxylicious/plugins/*"
Lorsque vous utilisez Docker, définissez-le dans votre environnement de conteneur:```yaml
environment:
KROXYLICIOUS_CLASSPATH: /opt/kroxylicious/plugins/*
Puis démarrez le proxy. Les producteurs et les consommateurs se connectent au port du proxy au lieu de se connecter directement au courtier.
Filesystem (keyProviderType: filesystem, par défaut) :
Charge les clés ML-KEM depuis des fichiers DER sur le disque. Si les fichiers n'existent pas, génère
une nouvelle paire de clés et les enregistre. Requiert publicKeyPath et privateKeyPath.
HashiCorp Vault (keyProviderType: vault, requiert la compilation -Pvault) :
Récupère les clés ML-KEM depuis un moteur de secrets Vault KV v2. Les clés sont stockées sous forme
de DER encodé en base64 dans les champs publicKey et privateKey. Les versions des secrets Vault
correspondent aux identifiants de clé pour la prise en charge de la rotation des clés.
Propriétés de keyProviderConfig pour Vault :
Exemple de configuration Vault :```yaml filterDefinitions:
### Ensembles de paramètres ML-KEM
| Algorithme | Niveau de sécurité | Clé publique | Clé privée | Surcharge du texte chiffré | Cas d'utilisation |
|-----------|---------------|------------|-------------|--------------------:|----------|
| ML-KEM-512 | 128 bits | 822 o | 1 730 o | ~854 o | Léger, IoT |
| ML-KEM-768 | 192 bits | 1 206 o | 2 498 o | ~1 174 o | **Valeur par défaut recommandée** |
| ML-KEM-1024 | 256 bits | 1 590 o | 3 266 o | ~1 654 o | Données classifiées / à long terme |
### Modes de chiffrement
**PQC uniquement** (`hybridMode: false`) :
Utilise exclusivement ML-KEM. La clé AES-256 est dérivée du secret partagé ML-KEM
via `SHA-256(0x01 || "kroxylicious-pqc-v1" || secret)`.
**Hybride** (`hybridMode: true`, par défaut) :
Combine ML-KEM + X25519. La clé AES-256 est dérivée des deux secrets via
`SHA-256(0x02 || "kroxylicious-pqc-hybrid-v1" || pqcSecret || x25519Secret)`.
Cela garantit la sécurité même si l'un des algorithmes est compromis.
## Format d'enveloppe chiffrée
Chaque valeur d'enregistrement chiffrée est remplacée par une enveloppe binaire :```
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 | | |
+--------+-----------+--------------------+---------+--------+-----------------+
L'octet de version permet au déchiffreur de déterminer le mode sans configuration.
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
### Flux de données
#### Qu'est-ce qui est stocké dans Vault
Vault KV v2 stocke la paire de clés ML-KEM dans `secret/<secretPath>` (par exemple, `secret/kroxylicious/pqc`) :
| Champ | Contenu | Format |
|-------|---------|--------|
| `publicKey` | Clé publique ML-KEM (utilisée pour l'encapsulation) | X.509 DER encodée en Base64 |
| `privateKey` | Clé privée ML-KEM (utilisée pour la décapsulation) | PKCS#8 DER encodée en Base64 |
Chaque version du secret Vault agit comme un identifiant de clé, permettant la rotation des clés. Les nouvelles versions
chiffrent de nouveaux enregistrements ; les anciennes versions peuvent toujours déchiffrer les enregistrements chiffrés avec elles.
#### Flux de démarrage```
┌─────────────────────────────────────────────────────────────────────┐
│ 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
#### Flux de récupération (déchiffrement)```
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
### Classes clés
**`PqcRecordEncryptionFilterFactory`** implémente `FilterFactory<PqcEncryptionConfig, SharedPqcContext>`.
Annotée avec `@Plugin(configType = PqcEncryptionConfig.class)`.
Enregistrée via `META-INF/services/io.kroxylicious.proxy.filter.FilterFactory`.
Appelée une fois au démarrage (`initialize`) et une fois par connexion client (`createFilter`).
**`PqcRecordEncryptionFilter`** implémente `ProduceRequestFilter` et `FetchResponseFilter`.
Intercepte `onProduceRequest` pour chiffrer et `onFetchResponse` pour déchiffrer.
Une instance par connexion ; aucune synchronisation nécessaire (modèle de threads de Kroxylicious).
**`PqcCryptoEngine`** effectue toutes les opérations cryptographiques.
Sans état, à l'exception du matériel de clé et de `SecureRandom`.
`encrypt()` renvoie une enveloppe auto-descriptive ; `decrypt()` l'analyse.
Enregistre les fournisseurs Bouncy Castle (`BC`, `BCPQC`) dans un initialiseur statique.
**`PqcKeyManager`** résout un `KeyProvider` via `ServiceLoader`, en fonction de
`keyProviderType`. Délègue toutes les opérations de clé au fournisseur résolu.
**`KeyProvider`** est l'interface SPI pour les backends de stockage de clés enfichables.
Les implémentations sont découvertes via `META-INF/services`. Fournisseurs intégrés :
`FileSystemKeyProvider` (par défaut) et `VaultKeyProvider` (avec `-Pvault`).
**`PqcEncryptionConfig`** est un POJO annoté par Jackson.
Désérialisé à partir du bloc `config:` dans le YAML du proxy Kroxylicious.
Prend en charge `keyProviderType` et `keyProviderConfig` pour la sélection du backend.
## Compilation et tests```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 tests (38 de base + 17 Vault)
JUnit 5.11.4, Mockito 5.15.2, AssertJ 3.27.3, Jackson Databind 2.18.3.
Mesuré sur la démo autonome (JVM à chaud, 1 000 itérations, messages de 1 Ko) :
| Algorithme | Chiffrement | Déchiffrement |
|---|---|---|
| ML-KEM-512 | ~7,700 msg/s (0.13 ms/msg) | ~7,800 msg/s (0.13 ms/msg) |
| ML-KEM-768 |
Le surcoût par message est sous la milliseconde. Pour les charges de travail Kafka typiques (messages de l'ordre du Ko au Mo), le coût du chiffrement est négligeable par rapport aux E/S réseau et disque.
Pour un modèle de menace complet couvrant ce contre quoi ce filtre défend et ce contre quoi il ne défend pas, voir THREAT_MODEL.md.
chmod 600).approle ou kubernetes aux jetons statiques
en production. Ne committez jamais de jetons Vault dans le contrôle de version.null (tombstones Kafka) sont transmis
sans chiffrement.Compression.NONE
car les données chiffrées ne se compressent pas bien.Apache License 2.0. Voir LICENSE pour plus de détails.
| Property | Type | Required | Default | Description |
|---|
kemAlgorithm | enum | Non | ML_KEM_768 | Ensemble de paramètres ML-KEM. Une valeur parmi ML_KEM_512, ML_KEM_768, ML_KEM_1024. |
hybridMode | boolean | Non | true | Combine ML-KEM avec X25519 ECDH pour une défense en profondeur. |
publicKeyPath | string | Système de fichiers uniquement | - | Chemin du système de fichiers vers la clé publique ML-KEM (encodée X.509 DER). |
privateKeyPath | string | Système de fichiers uniquement | - | Chemin du système de fichiers vers la clé privée ML-KEM (encodée PKCS#8 DER). |
topicPatterns | list<string> | Non | [".*"] | Expressions régulières Java. Seuls les enregistrements des sujets correspondants sont chiffrés/déchiffrés. |
keyProviderType | string | Non | filesystem | Backend de stockage des clés. Une valeur parmi filesystem, vault. |
keyProviderConfig | map<string, string> | Vault uniquement | {} | Configuration spécifique au backend (voir la section Vault ci-dessous). |
| Property | Required | Default | Description |
|---|
vaultAddress | Oui | variable d'environnement VAULT_ADDR | URL du serveur Vault (par ex. http://vault:8200) |
vaultToken | Pour l'authentification token | variable d'environnement VAULT_TOKEN | Jeton d'authentification Vault |
secretPath | Oui | -- | Chemin dans le moteur de secrets (par ex. kroxylicious/pqc) |
secretEngine | Non | secret | Nom du point de montage du moteur de secrets KV v2 |
authMethod | Non | token | Méthode d'authentification : token, approle ou kubernetes |
roleId | Pour approle | -- | Identifiant de rôle AppRole |
secretId | Pour approle | -- | Identifiant secret AppRole |
kubeRole | Pour kubernetes | -- | Nom du rôle d'authentification Kubernetes |
kubeTokenPath | Non | /var/run/secrets/.../token | Chemin du fichier de jeton du compte de service |
| Classe de test | Tests | Ce qui est vérifié |
|---|
PqcCryptoEngineTest | 12 | Aller-retour chiffrement/déchiffrement pour les 3 variantes ML-KEM, gestion des valeurs nulles, charges utiles vides et de 1 Mo, sécurité sémantique, détection de falsification, rejet de version invalide, génération de clés, octets de version de l'enveloppe |
PqcEncryptionConfigTest | 9 | Valeurs par défaut, valeurs explicites, rejet des valeurs nulles, désérialisation JSON, immutabilité, propriétés d'énumération, désérialisation de keyProviderConfig |
PqcKeyManagerTest | 6 | Résolution du KeyProvider, création du moteur, délégation aux fournisseurs, repli pour le type filesystem |
FileSystemKeyProviderTest | 11 | Génération de clés, chargement des clés existantes, ID de clé par défaut, rejet d'ID de clé inconnu, validation de chemin nul, découverte ServiceLoader, aller-retour cryptographique |
VaultKeyProviderTest | 17 | Validation de la configuration (chemin/adresse/jeton/approle/kube manquants), récupération de clé depuis Vault, récupération de clé versionnée, clés en cache, versions invalides, champs manquants dans le secret, aller-retour cryptographique, comportement de fermeture |
| Dépendance | Version | Portée | But |
|---|
io.kroxylicious:kroxylicious-api | 0.19.0 | provided | Interfaces de l'API Filter |
org.apache.kafka:kafka-clients | 3.9.0 | provided | Types de messages du protocole Kafka |
com.fasterxml.jackson.core:jackson-annotations | 2.18.3 | provided | Liaison de configuration |
org.bouncycastle:bcprov-jdk18on | 1.83 | compile | ML-KEM, AES-GCM, X25519 (inclus dans le JAR shaded) |
org.bouncycastle:bcutil-jdk18on | 1.83 | compile | Utilitaires Bouncy Castle (inclus dans le JAR shaded) |
org.slf4j:slf4j-api | 2.0.17 | provided | Journalisation |
org.springframework.vault:spring-vault-core | 3.1.2 | compile (profil vault) | Client Vault KV v2 (inclus lors de la compilation avec -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) |