
Плагин-фильтр Kroxylicious, обеспечивающий прозрачное постквантовое (ML-KEM + AES-256-GCM) шифрование на уровне записей для Apache Kafka, не требующее изменений клиентского кода.
A Kroxylicious filter plugin that provides transparent Post-Quantum Cryptography (PQC) record-level encryption for Apache Kafka using ML-KEM (FIPS 203) key encapsulation with AES-256-GCM symmetric encryption.
Kafka producers and consumers require zero code changes. The Kroxylicious proxy intercepts traffic and encrypts on Produce / decrypts on Fetch automatically.``` Producer ──plaintext──> Kroxylicious ──encrypted──> Kafka Broker Consumer <──plaintext── Kroxylicious <──encrypted── Kafka Broker
## Зачем нужен PQC для Kafka?
Классические алгоритмы установки ключей (RSA, ECDH) уязвимы перед будущими
квантовыми компьютерами. Если ключи шифрования для каждой записи устанавливаются
с помощью классического KEM, квантовый противник сможет восстановить эти ключи
из капсуляций, хранящихся вместе с шифротекстом на брокере.
Этот плагин использует ML-KEM (FIPS 203) для квантово-устойчивой инкапсуляции ключей,
гарантируя, что данные, хранящиеся на брокере Kafka, не могут быть расшифрованы даже
противником, обладающим криптографически значимым квантовым компьютером.
**Примечание:** Этот фильтр защищает **данные, хранящиеся на брокере**, а не
TLS-канал при передаче. Полный анализ того, от чего обеспечивается и не обеспечивается
защита, см. в [THREAT_MODEL.md](https://github.com/oscerd/kroxylicious-pqc-filter/blob/HEAD/THREAT_MODEL.md).
| Стандарт | Алгоритм | Назначение в этом плагине |
|----------|-----------|------------------------|
| FIPS 203 | ML-KEM (Kyber) | Инкапсуляция ключа — надёжно устанавливает AES-ключ для каждого сообщения |
| N/A | AES-256-GCM | Симметричное аутентифицированное шифрование полезной нагрузки записи |
| N/A | X25519 ECDH | Классическое согласование ключей для гибридного режима (эшелонированная защита) |
## Возможности
- **Прозрачное шифрование/дешифрование** — не требует изменений на стороне клиента
- **Наборы параметров ML-KEM-512, ML-KEM-768 (по умолчанию), ML-KEM-1024**
- **Гибридный режим** (по умолчанию) — объединяет ML-KEM + X25519 ECDH, так что взломать нужно оба
- **Шифрование каждой записи** — каждая запись получает свежую капсуляцию KEM + случайный IV
- **Фильтрация топиков** — регулярные выражения выбирают, какие топики шифровать
- **Обнаружение подделки** — аутентифицированное шифрование AES-GCM отклоняет изменённый шифротекст
- **Семантическая безопасность** — одинаковые открытые тексты дают разные шифротексты (IND-CCA2)
- **Автоматическая генерация ключей** — при первом запуске создаёт и сохраняет ключи ML-KEM, если они отсутствуют
- **Заголовок `x-pqc-encrypted`** — помечает зашифрованные записи для информирования нижестоящих компонентов
- **Подключаемые поставщики ключей** — SPI `KeyProvider` поддерживает файловую систему (по умолчанию) и бэкенды HashiCorp Vault
## Предварительные требования
| Требование | Версия |
|-------------|---------|
| JDK | 17+ (рекомендуется 21+) |
| Maven | 3.8+ |
| Kroxylicious | 0.19.0 |
| Apache Kafka | 3.9.x |
## Быстрый старт
### 1. Сборка плагина```bash
git clone <this-repo>
cd kroxylicious-pqc-filter
mvn clean package -DskipTests
Shaded JAR-файл по пути target/kroxylicious-pqc-filter-1.0.0-SNAPSHOT.jar включает Bouncy Castle, поэтому его можно просто поместить в Kroxylicious без дополнительных зависимостей.
Чтобы включить поддержку поставщика ключей HashiCorp Vault, выполните сборку с профилем vault:```bash
mvn clean package -Pvault -DskipTests
Это объединяет `spring-vault-core` и `VaultKeyProvider` в JAR.
### 2. Сгенерируйте ключи 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/
Вывод:``` 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
В качестве альтернативы можно опустить пути к ключам в конфигурации — фильтр сгенерирует ключи автоматически при первом запуске.
### 3. Настройка Kroxylicious
Добавьте фильтр в YAML-конфигурацию вашего прокси 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
Поместите JAR-файл в каталог, доступный для Kroxylicious, и добавьте его в
classpath через переменную окружения KROXYLICIOUS_CLASSPATH:```bash
export KROXYLICIOUS_CLASSPATH="/opt/kroxylicious/plugins/*"
При использовании Docker задайте его в среде вашего контейнера:```yaml
environment:
KROXYLICIOUS_CLASSPATH: /opt/kroxylicious/plugins/*
Затем запустите прокси. Производители и потребители подключаются к порту прокси, а не напрямую к брокеру.
Filesystem (keyProviderType: filesystem, по умолчанию):
Загружает ключи ML-KEM из DER-файлов на диске. Если файлы не существуют, генерирует новую пару ключей и сохраняет их. Требует publicKeyPath и privateKeyPath.
HashiCorp Vault (keyProviderType: vault, требует сборки с -Pvault):
Получает ключи ML-KEM из движка секретов Vault KV v2. Ключи хранятся в виде DER в кодировке base64 в полях publicKey и privateKey. Версии секретов Vault сопоставляются с идентификаторами ключей для поддержки ротации ключей.
Свойства Vault keyProviderConfig:
Пример конфигурации Vault:```yaml filterDefinitions:
### Параметры наборов ML-KEM
| Алгоритм | Уровень безопасности | Открытый ключ | Закрытый ключ | Накладные расходы на шифртекст | Вариант использования |
|-----------|---------------|------------|-------------|--------------------:|----------|
| ML-KEM-512 | 128-бит | 822 Б | 1 730 Б | ~854 Б | Лёгкий, IoT |
| ML-KEM-768 | 192-бит | 1 206 Б | 2 498 Б | ~1 174 Б | **Рекомендуемый по умолчанию** |
| ML-KEM-1024 | 256-бит | 1 590 Б | 3 266 Б | ~1 654 Б | Секретные / долгоживущие данные |
### Режимы шифрования
**Только PQC** (`hybridMode: false`):
Используется исключительно ML-KEM. Ключ AES-256 выводится из общего секрета ML-KEM
через `SHA-256(0x01 || "kroxylicious-pqc-v1" || secret)`.
**Гибридный** (`hybridMode: true`, по умолчанию):
Объединяет ML-KEM + X25519. Ключ AES-256 выводится из обоих секретов через
`SHA-256(0x02 || "kroxylicious-pqc-hybrid-v1" || pqcSecret || x25519Secret)`.
Это обеспечивает безопасность, даже если один алгоритм будет взломан.
## Формат зашифрованного конверта
Каждое зашифрованное значение записи заменяется двоичным конвертом:```
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 | | |
+--------+-----------+--------------------+---------+--------+-----------------+
Байт версии позволяет дешифратору определить режим без конфигурации.
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
### Поток данных
#### Что хранится в Vault
Vault KV v2 хранит пару ключей ML-KEM по пути `secret/<secretPath>` (например, `secret/kroxylicious/pqc`):
| Поле | Содержимое | Формат |
|-------|---------|--------|
| `publicKey` | Открытый ключ ML-KEM (используется для инкапсуляции) | Base64-кодированный X.509 DER |
| `privateKey` | Приватный ключ ML-KEM (используется для декапсуляции) | Base64-кодированный PKCS#8 DER |
Каждая версия секрета Vault действует как идентификатор ключа, обеспечивая ротацию ключей. Новые версии
шифруют новые записи; старые версии по-прежнему могут расшифровывать записи, зашифрованные с их помощью.
#### Порядок запуска```
┌─────────────────────────────────────────────────────────────────────┐
│ 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
#### Процесс получения (расшифровка)```
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
### Ключевые классы
**`PqcRecordEncryptionFilterFactory`** реализует `FilterFactory<PqcEncryptionConfig, SharedPqcContext>`.
Аннотирован `@Plugin(configType = PqcEncryptionConfig.class)`.
Зарегистрирован через `META-INF/services/io.kroxylicious.proxy.filter.FilterFactory`.
Вызывается один раз при запуске (`initialize`) и один раз на каждое клиентское соединение (`createFilter`).
**`PqcRecordEncryptionFilter`** реализует `ProduceRequestFilter` и `FetchResponseFilter`.
Перехватывает `onProduceRequest` для шифрования и `onFetchResponse` для дешифрования.
Один экземпляр на соединение; синхронизация не требуется (потоковая модель Kroxylicious).
**`PqcCryptoEngine`** выполняет все криптографические операции.
Не сохраняет состояние, кроме ключевого материала и `SecureRandom`.
`encrypt()` возвращает самодокументируемый конверт; `decrypt()` разбирает его.
Регистрирует провайдеры Bouncy Castle (`BC`, `BCPQC`) в статическом инициализаторе.
**`PqcKeyManager`** разрешает `KeyProvider` через `ServiceLoader`, сопоставляя по
`keyProviderType`. Делегирует все операции с ключами разрешённому провайдеру.
**`KeyProvider`** — это SPI-интерфейс для подключаемых бэкендов хранения ключей.
Реализации обнаруживаются через `META-INF/services`. Встроенные провайдеры:
`FileSystemKeyProvider` (по умолчанию) и `VaultKeyProvider` (с `-Pvault`).
**`PqcEncryptionConfig`** — это POJO с Jackson-аннотациями.
Десериализуется из блока `config:` в YAML-конфигурации прокси Kroxylicious.
Поддерживает `keyProviderType` и `keyProviderConfig` для выбора бэкенда.
## Сборка и тестирование```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
Всего: 55 тестов (38 основных + 17 Vault)
JUnit 5.11.4, Mockito 5.15.2, AssertJ 3.27.3, Jackson Databind 2.18.3.
Измерено на автономном демо (прогретый JVM, 1,000 итераций, сообщения по 1 КБ):
| Алгоритм | Шифрование | Дешифрование |
|---|---|---|
| ML-KEM-512 | ~7,700 msg/s (0.13 ms/msg) | ~7,800 msg/s (0.13 ms/msg) |
| ML-KEM-768 |
Накладные расходы на одно сообщение составляют менее миллисекунды. Для типичных рабочих нагрузок Kafka (сообщения размером от КБ до МБ) стоимость шифрования пренебрежимо мала по сравнению с сетевым и дисковым вводом-выводом.
Полную модель угроз, описывающую, от чего защищает этот фильтр и от чего не защищает, см. в THREAT_MODEL.md.
chmod 600).approle или kubernetes статическим токенам.
Никогда не коммитьте токены Vault в систему контроля версий.null (tombstones в Kafka) передаются
без шифрования.Compression.NONE,
потому что зашифрованные данные плохо сжимаются.Apache License 2.0. Подробности см. в LICENSE.
| Свойство | Тип | Обязательность | По умолчанию | Описание |
|---|
kemAlgorithm | enum | Нет | ML_KEM_768 | Параметр ML-KEM. Один из ML_KEM_512, ML_KEM_768, ML_KEM_1024. |
hybridMode | boolean | Нет | true | Объединяет ML-KEM с X25519 ECDH для эшелонированной защиты. |
publicKeyPath | string | Только для файловой системы | - | Путь в файловой системе к открытому ключу ML-KEM (в кодировке X.509 DER). |
privateKeyPath | string | Только для файловой системы | - | Путь в файловой системе к закрытому ключу ML-KEM (в кодировке PKCS#8 DER). |
topicPatterns | list<string> | Нет | [".*"] | Шаблоны Java regex. Шифруются/расшифровываются только записи в темах, соответствующих шаблонам. |
keyProviderType | string | Нет | filesystem | Бэкенд хранения ключей. Один из filesystem, vault. |
keyProviderConfig | map<string, string> | Только для Vault | {} | Конфигурация, специфичная для бэкенда (см. раздел Vault ниже). |
| Свойство | Обязательность | По умолчанию | Описание |
|---|
vaultAddress | Да | переменная окружения VAULT_ADDR | URL сервера Vault (например, http://vault:8200) |
vaultToken | Для аутентификации token | переменная окружения VAULT_TOKEN | Токен аутентификации Vault |
secretPath | Да | -- | Путь внутри движка секретов (например, kroxylicious/pqc) |
secretEngine | Нет | secret | Имя точки монтирования движка секретов KV v2 |
authMethod | Нет | token | Метод аутентификации: token, approle или kubernetes |
roleId | Для approle | -- | Идентификатор роли AppRole |
secretId | Для approle | -- | Секретный идентификатор AppRole |
kubeRole | Для kubernetes | -- | Имя роли аутентификации Kubernetes |
kubeTokenPath | Нет | /var/run/secrets/.../token | Путь к файлу токена сервисного аккаунта |
| Тестовый класс | Тесты | Что проверяется |
|---|
PqcCryptoEngineTest | 12 | Циклы шифрования/дешифрования для всех 3 вариантов ML-KEM, обработка null, пустые и полезные нагрузки по 1 МБ, семантическая безопасность, обнаружение модификаций, отклонение недопустимых версий, генерация ключей, байты версии конверта |
PqcEncryptionConfigTest | 9 | Значения по умолчанию, явные значения, отклонение null, десериализация JSON, неизменяемость, свойства enum, десериализация keyProviderConfig |
PqcKeyManagerTest | 6 | Разрешение KeyProvider, создание engine, делегирование провайдерам, резервный вариант для типа filesystem |
FileSystemKeyProviderTest | 11 | Генерация ключей, загрузка существующих ключей, идентификатор ключа по умолчанию, отклонение неизвестного идентификатора ключа, проверка null-пути, обнаружение ServiceLoader, цикл шифрования/дешифрования |
VaultKeyProviderTest | 17 | Проверка конфигурации (отсутствие пути/адреса/токена/approle/kube), получение ключа из Vault, получение версионированных ключей, кэшированные ключи, недопустимые версии, отсутствующие поля в секрете, цикл шифрования/дешифрования, поведение при закрытии |
| Зависимость | Версия | Область | Назначение |
|---|
io.kroxylicious:kroxylicious-api | 0.19.0 | provided | Интерфейсы Filter API |
org.apache.kafka:kafka-clients | 3.9.0 | provided | Типы сообщений протокола Kafka |
com.fasterxml.jackson.core:jackson-annotations | 2.18.3 | provided | Привязка конфигурации |
org.bouncycastle:bcprov-jdk18on | 1.83 | compile | ML-KEM, AES-GCM, X25519 (включены в shaded JAR) |
org.bouncycastle:bcutil-jdk18on | 1.83 | compile | Утилиты Bouncy Castle (включены в shaded JAR) |
org.slf4j:slf4j-api | 2.0.17 | provided | Логирование |
org.springframework.vault:spring-vault-core | 3.1.2 | compile (профиль vault) | Клиент Vault KV v2 (включается при сборке с -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) |