
클라이언트 코드 변경이 전혀 필요 없이 Apache Kafka에 투명한 포스트퀀텀(ML-KEM + AES-256-GCM) 레코드 수준 암호화를 제공하는 Kroxylicious 필터 플러그인입니다.
Apache Kafka를 위한 투명한 양자 내성 암호화(PQC) 레코드 수준 암호화를 제공하는 Kroxylicious 필터 플러그인으로, ML-KEM(FIPS 203) 키 캡슐화와 AES-256-GCM 대칭 암호화를 사용합니다.
Kafka 프로듀서와 컨슈머는 코드 변경이 전혀 필요 없습니다. Kroxylicious 프록시가 트래픽을 가로채서 Produce 시 암호화하고 Fetch 시 자동으로 복호화합니다.``` Producer ──plaintext──> Kroxylicious ──encrypted──> Kafka Broker Consumer <──plaintext── Kroxylicious <──encrypted── Kafka Broker
## Kafka용 PQC가 필요한 이유?
기존 키 설정 알고리즘(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` 헤더** - 다운스트림 인식을 위해 암호화된 레코드 표시
- **플러그형 키 제공자** - `KeyProvider` SPI는 파일시스템(기본값) 및 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
target/kroxylicious-pqc-filter-1.0.0-SNAPSHOT.jar의 shaded 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/
I don't see any source text in the input. Please provide the chunk content you'd like me 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
또는 구성에서 키 경로를 생략하면 필터가 첫 시작 시 키를 자동으로
생성합니다.
### 3. Kroxylicious 구성
Kroxylicious 프록시 YAML 구성에 필터를 추가하세요:```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가 접근할 수 있는 디렉터리에 배치하고 KROXYLICIOUS_CLASSPATH 환경 변수를 통해
클래스패스에 추가하세요:```bash
export KROXYLICIOUS_CLASSPATH="/opt/kroxylicious/plugins/*"
Docker 사용 시, 컨테이너 환경에서 설정하십시오:```yaml
environment:
KROXYLICIOUS_CLASSPATH: /opt/kroxylicious/plugins/*
그런 다음 프록시를 시작합니다. 생산자와 소비자는 브로커에 직접 연결하는 대신 프록시 포트에 연결합니다.
파일 시스템(keyProviderType: filesystem, 기본값):
디스크의 DER 파일에서 ML-KEM 키를 로드합니다. 파일이 없으면 새 키 쌍을 생성하여 저장합니다. publicKeyPath와 privateKeyPath가 필요합니다.
HashiCorp Vault(keyProviderType: vault, -Pvault 빌드 필요):
Vault KV v2 시크릿 엔진에서 ML-KEM 키를 가져옵니다. 키는 publicKey 및 privateKey 필드에 base64로 인코딩된 DER로 저장됩니다. Vault 시크릿 버전은 키 회전 지원을 위해 키 ID에 매핑됩니다.
Vault keyProviderConfig 속성:
Vault 구성 예시:```yaml filterDefinitions:
### ML-KEM 파라미터 세트
| 알고리즘 | 보안 수준 | 공개 키 | 개인 키 | 암호문 오버헤드 | 사용 사례 |
|-----------|---------------|------------|-------------|--------------------:|----------|
| ML-KEM-512 | 128-bit | 822 B | 1,730 B | ~854 B | 경량, IoT |
| ML-KEM-768 | 192-bit | 1,206 B | 2,498 B | ~1,174 B | **권장 기본값** |
| ML-KEM-1024 | 256-bit | 1,590 B | 3,266 B | ~1,654 B | 기밀 / 장기 보존 데이터 |
### 암호화 모드
**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는 `secret/<secretPath>` (예: `secret/kroxylicious/pqc`)에 ML-KEM 키 쌍을 보관합니다:
| 필드 | 내용 | 형식 |
|-------|---------|--------|
| `publicKey` | ML-KEM 공개 키 (캡슐화에 사용) | Base64로 인코딩된 X.509 DER |
| `privateKey` | ML-KEM 개인 키 (역캡슐화에 사용) | Base64로 인코딩된 PKCS#8 DER |
각 Vault secret 버전은 키 ID 역할을 하여 키 순환(rotation)을 지원합니다. 새 버전은 새 레코드를 암호화하고, 이전 버전은 해당 버전으로 암호화된 레코드를 여전히 복호화할 수 있습니다.
#### 시작 흐름```
┌─────────────────────────────────────────────────────────────────────┐
│ 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()`는 자체 설명형 봉투(self-describing envelope)를 반환하고, `decrypt()`는 이를 구문 분석합니다.
정적 초기화자에서 Bouncy Castle 프로바이더(`BC`, `BCPQC`)를 등록합니다.
**`PqcKeyManager`**는 `ServiceLoader`를 통해 `KeyProvider`를 확인하고,
`keyProviderType`으로 일치시킵니다. 모든 키 작업을 확인된 프로바이더에 위임합니다.
**`KeyProvider`**는 플러그형 키 저장 백엔드를 위한 SPI 인터페이스입니다.
구현체는 `META-INF/services`를 통해 발견됩니다. 기본 제공 프로바이더:
`FileSystemKeyProvider`(기본값) 및 `VaultKeyProvider`(`-Pvault` 포함).
**`PqcEncryptionConfig`**는 Jackson 애너테이션이 적용된 POJO입니다.
Kroxylicious 프록시 YAML의 `config:` 블록에서 역직렬화됩니다.
백엔드 선택을 위해 `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개)
JUnit 5.11.4, Mockito 5.15.2, AssertJ 3.27.3, Jackson Databind 2.18.3.
독립 실행형 데모에서 측정(JVM 워밍업, 1,000회 반복, 1KB 메시지):
| 알고리즘 | 암호화 | 복호화 |
|---|---|---|
| ML-KEM-512 | ~7,700 msg/s (0.13 ms/msg) | ~7,800 msg/s (0.13 ms/msg) |
| ML-KEM-768 | ~9,600 msg/s (0.10 ms/msg) |
메시지당 오버헤드는 서브밀리초 수준입니다. 일반적인 Kafka 워크로드 (KB~MB 범위의 메시지)의 경우 암호화 비용은 네트워크 및 디스크 I/O에 비해 무시할 만한 수준입니다.
이 필터가 방어하는 대상과 방어하지 않는 대상을 포함한 전체 위협 모델은 THREAT_MODEL.md를 참조하세요.
chmod 600)을 사용하세요.approle 또는
kubernetes 인증을 선호하세요. Vault 토큰을 버전 관리에 커밋하지 마세요.null 값을 가진 레코드(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 정규식 패턴입니다. 일치하는 토픽의 레코드만 암호화/복호화됩니다. |
keyProviderType | string | 아니요 | filesystem | 키 저장 백엔드. filesystem, vault 중 하나입니다. |
keyProviderConfig | map<string, string> | Vault 전용 | {} | 백엔드별 구성입니다(아래 Vault 섹션 참조). |
| 속성 | 필수 | 기본값 | 설명 |
|---|
vaultAddress | 예 | VAULT_ADDR 환경 변수 | Vault 서버 URL(예: http://vault:8200) |
vaultToken | token 인증용 | VAULT_TOKEN 환경 변수 | Vault 인증 토큰 |
secretPath | 예 | -- | 시크릿 엔진 내 경로(예: kroxylicious/pqc) |
secretEngine | 아니요 | secret | KV v2 시크릿 엔진 마운트 이름 |
authMethod | 아니요 | token | 인증 방법: token, approle 또는 kubernetes |
roleId | approle용 | -- | AppRole 역할 ID |
secretId | approle용 | -- | AppRole 시크릿 ID |
kubeRole | kubernetes용 | -- | Kubernetes 인증 역할 이름 |
kubeTokenPath | 아니요 | /var/run/secrets/.../token | 서비스 계정 토큰 파일 경로 |
| 테스트 클래스 | 테스트 수 | 검증 내용 |
|---|
PqcCryptoEngineTest | 12 | 3가지 ML-KEM 변형 모두에 대한 암호화/복호화 라운드트립, null 처리, 빈 페이로드 및 1MB 페이로드, 의미론적 보안, 변조 감지, 잘못된 버전 거부, 키 생성, 엔벨로프 버전 바이트 |
PqcEncryptionConfigTest | 9 | 기본값, 명시적 값, null 거부, JSON 역직렬화, 불변성, enum 속성, keyProviderConfig 역직렬화 |
PqcKeyManagerTest | 6 | KeyProvider 해석, 엔진 생성, 프로바이더 위임, 파일시스템 타입 폴백 |
FileSystemKeyProviderTest | 11 | 키 생성, 기존 키 로드, 기본 키 ID, 알 수 없는 키 ID 거부, null 경로 검증, ServiceLoader 디스커버리, 암호화 라운드트립 |
VaultKeyProviderTest | 17 | 구성 검증(누락된 path/address/token/approle/kube), Vault에서 키 가져오기, 버전별 키 검색, 캐시된 키, 잘못된 버전, 시크릿의 누락된 필드, 암호화 라운드트립, close 동작 |
| 의존성 | 버전 | 범위 | 용도 |
|---|
io.kroxylicious:kroxylicious-api | 0.19.0 | provided | 필터 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 profile) | Vault KV v2 클라이언트 (-Pvault로 빌드 시 번들됨) |
| ~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) |