Envelope Encryption
By default, Kryptonite for Kafka applies encryption using key material sourced directly from Tink keysets. An alternative mode is to opt in for envelope encryption. This mode randomly generates a short-lived Data Encryption Key (DEK) which is used to encrypt field data, and uses a long-lived Key Encryption Key (KEK) to wrap the DEK. The wrapped DEK then travels with the ciphertext which guarantees there is no direct relationship between the ciphertext and the long-lived KEK material.
Key benefits:
- KEK never touches field data: the KEK is only used to wrap/unwrap DEKs; all field encryption uses the short-lived DEK
- Independent DEK and KEK lifecycle: DEKs rotate automatically on a high-frequency operational schedule; the KEK rotates on a slower administrative schedule; these two concerns are fully decoupled
- Reduced blast radius: a compromised DEK only affects records encrypted during that DEK's lifetime which is configurable
Two Variants
Kryptonite for Kafka supports two envelope encryption variants which differ in where the KEK lives and how the wrapped DEK is stored alongside the ciphertext.
| Keyset-based | KMS-based | |
|---|---|---|
| Algorithm | TINK/AES_GCM_ENVELOPE_KEYSET |
TINK/AES_GCM_ENVELOPE_KMS |
| KEK | a Tink keyset (sourced from config or cloud KMS) | a cloud KMS key which never leaves the KMS |
| Wrapped DEK | bundled as is with the ciphertext | reference to wrapped DEK bundled with ciphertext |
| Extra Infrastructure | None | Persistent EdekStore implementation (defaults to KCache/Kafka) |
Keyset-based Envelope Encryption
Uses a regular Tink keyset as the KEK. The wrapped DEK is bundled directly inside the ciphertext alongside the encrypted field data, so no external store is needed.
Wire format: [4-byte wrappedDekLen | wrappedDek | dekCiphertext]
Configuration:
cipher_algorithm=TINK/AES_GCM_ENVELOPE_KEYSET
cipher_data_key_identifier=my-kek-keyset
cipher_data_keys=[{"identifier":"my-kek-keyset","material":{...}}]
The cipher_data_key_identifier and cipher_data_keys refer to the keyset acting as the KEK. All the usual key_source options apply which means you can source the KEK keyset from configuration (plain or encrypted) or from a cloud secret manager.
DEK session caching
By default a single DEK session is reused for up to 100,000 encryptions or 720 minutes, whichever comes first. Tune dek_max_encryptions and dek_ttl_minutes to balance KMS call frequency against DEK lifetime. See Configuration for details.
KMS-based Envelope Encryption
Uses a cloud KMS key as the KEK. The KEK never leaves the cloud KMS and all wrap/unwrap operations happen remotely. Because the wrapped DEK would make every ciphertext considerably larger if bundled inline, only a compact 16-byte fingerprint is embedded in the ciphertext; the actual wrapped DEK is stored externally using implementations of EdekStore for which the default is KCache/Kafka.
Wire format: [16-byte fingerprint | dekCiphertext]
Configuration:
cipher_algorithm=TINK/AES_GCM_ENVELOPE_KMS
envelope_kek_configs=[{"identifier":"my-kek","type":"GCP","uri":"gcp-kms://...","config":{"credentials":"...","projectId":"..."}}]
envelope_kek_identifier=my-kek
edek_store_config={"kafkacache.bootstrap.servers":"localhost:9092","kafkacache.topic":"_k4k_edeks"}
An EdekStore implementation is required
KMS-based envelope encryption will refuse to start if edek_store_config is not provided. The EdekStore is load-bearing as without it the wrapped DEK could not be derived at decrypt time.
EdekStore Implementations
The EdekStore defaults to KCache which persists into a compacted Kafka topic to permanently store the wrapped DEKs. Each record maps a 16-byte fingerprint (a SHA-256 prefix of the wrapped DEK) to the actual wrapped DEK bytes.
Encrypt path:
- When a new DEK session is created, Kryptonite publishes the wrapped DEK to the
EdekStorebefore any encryption happens, and only then makes the DEK session available for use - Each ciphertext carries only the 16-byte fingerprint as a compact pointer to the wrapped DEK in the
EdekStore
Decrypt path:
- Extract the 16-byte fingerprint from the ciphertext
- Check the L1 in-process cache (fingerprint → Aead): if hit, decrypt directly; no
EdekStoreor KMS involved - On L1 miss: look up the fingerprint in the
EdekStoreto retrieve the wrapped DEK - Call the KMS to unwrap the DEK, build the Aead, and populate the L1 cache for future lookups
- Decrypt the field data with the Aead
Topic requirements:
- Must be a compacted topic —
cleanup.policy=compact kafkacache.topic.require.compact=trueapplies by default; Kryptonite for Kafka will refuse to start against a non-compacted topic- Replication factor and partition count follow standard Kafka best practices for your environment; one partition is enough in low-write-volume setups like this one (one record per DEK session)
Cross-Instance Consumer Lag
Each instance maintains a local in-memory mirror of the EdekStore topic via a background KCache consumer thread. After a new wrapped DEK is published, other instances will only see it once their consumer thread has caught up to that offset. If a decrypt request for a freshly-encrypted record reaches an instance whose consumer hasn't yet processed the new entry, the fingerprint lookup will fail. In practice this lag should be sub-second under normal conditions, and retry logic in the consumer application is the recommended mitigation.
EdekStore Configuration
The edek_store_config value is a JSON object and is supposed to contain the expected configuration based on the chosen EdekStore implementation. For KCache/Kafka it might look like this:
Required keys for KCache/Kafka EdekStore
| Key | Description |
|---|---|
kafkacache.bootstrap.servers |
Kafka bootstrap address for the EdekStore topic |
kafkacache.topic |
Name of the compacted topic to use |
Optional overrides (defaults shown):
| Key | Default | Description |
|---|---|---|
kafkacache.backing.cache |
memory |
In-memory map; the compacted topic is the durable store |
kafkacache.topic.require.compact |
true |
Fail at startup if the topic is not compacted |
Note
For security settings (SSL, SASL, authentication) use the corresponding kafkacache.* prefixed keys defined by KCache — for example kafkacache.security.protocol, kafkacache.ssl.truststore.location, kafkacache.sasl.mechanism. Raw Kafka ssl.* / sasl.* keys are not forwarded directly.
DEK Session Lifecycle
Both variants share the same DEK session management. Instead of generating a fresh DEK for every field encryption (which would mean a KMS/keyset operation per record), a DEK session is reused across a configurable window.
A session expires when either threshold is reached first:
dek_max_encryptions: upper limit for field encryptions with this DEK (default:100,000)dek_ttl_minutes: longest allowed age of the DEK session (default:720= 12 hours)
When a session expires, a new DEK is generated and wrapped transparently without the need for intervention. For KMS-based envelope encryption the new wrapped DEK is published to the EdekStore before the session becomes active.
Tuning Guidance
Lower dek_max_encryptions or dek_ttl_minutes to increase DEK freshness at the cost of more frequent KMS calls. The defaults are reasonable and suit most workloads. For KMS-based envelope encryption each session creation involves one KMS network call, so aggressive rotation (e.g., dek_max_encryptions=1) will reduce throughput, might saturate the KMS quickly, and can noticeably increase cloud KMS costs.
KEK configuration for KMS-based envelope encryption
The envelope_kek_configs parameter takes a JSON array of KEK entries. Each entry identifies which cloud KMS key to use as the KEK:
[
{
"identifier": "my-azure-kek",
"type": "AZURE",
"uri": "azure-kv://<vault-name>.vault.azure.net/keys/<key-name>",
"config": {
"clientId": "...",
"tenantId": "...",
"clientSecret": "...",
"keyVaultUrl": "https://<vault-name>.vault.azure.net"
}
}
]
AAD not supported for Azure
Azure Key Vault's RSA-OAEP-256 key wrap/unwrap has no associated-data parameter. The wrapAad binding used by GCP and AWS is silently ignored for Azure. Security relies on the EdekStore fingerprint lookup chain.
You can define more than one KEK entry. Each field can reference a different KEK identifier, enabling per-field or per-topic key isolation.
See Cloud KMS for provider-specific IAM permissions and setup details.