Encryption at Rest: Protecting Kafka Passive Data from Physical Theft #
When messages sent by producers arrive at Kafka brokers, those messages aren’t only stored in memory, but persistently written into log segment files on local disk storage systems. These log segments contain raw data payloads along with their metadata. Without encryption at rest, attackers who successfully steal physical storage media (like server SSDs/HDDs) or manage to copy log segment files from outside the system (e.g., through insecure disk snapshot backups) can read all our data without needing to connect to the Kafka cluster.
Encryption at rest protects data from physical theft threats and storage media leaks in datacenters and cloud service providers.
In this article, we’ll deeply review the two main methods of encryption at rest in Apache Kafka: OS/Disk-level Encryption (Infrastructure) and Application-level Encryption (Envelope Encryption). We’ll also build a KMS transaction diagram, learn concrete disk encryption implementation steps with LUKS, and write Java Serializer/Deserializer code examples for safe, efficient Envelope Encryption using caching.
Encryption Method Comparison: Disk-Level vs Application-Level #
In modern architectures, we have two main choices for securing passive data:
| Comparison Dimension | OS/Disk-Level Encryption (Infrastructure) | Application-Level Encryption (Envelope Encryption) |
|---|---|---|
| Encryption Location | Block Storage Layer (Operating System / Hypervisor) | Client Side (Producer & Consumer SDK) |
| Application Transparency | 100% Transparent. Applications don’t need modification. | Requires Serializer/Deserializer code modifications. |
| Zero-Trust Broker | No. Root users on broker servers or JVM memory can still see plaintext data. | Yes. Brokers only store encrypted bytes. Broker administrators can’t see raw data. |
| Key Management | Automatically managed by the OS (LUKS) or Cloud Providers (AWS KMS / GCP Cloud KMS). | Managed by client applications interacting with KMS (Key Management Service). |
| Performance Overhead | Very small (Hardware-level CPU acceleration). | Medium-High (Per-record encryption/decryption processes in applications). |
| Ecosystem Impact | Compatible with all Kafka Connectors, KsqlDB, and Kafka Streams. | Breaks Kafka Connect (Connectors) functionality and message visualization in UIs if decryption keys aren’t shared. |
Envelope Encryption Transaction Flow with KMS #
To adopt the Zero-Trust security principle, Envelope Encryption is the highly recommended industry standard. This method uses two layers of encryption keys:
- Data Encryption Key (DEK): A one-time-use symmetric key (usually AES-256) used to encrypt the message payload itself.
- Key Encryption Key (KEK): A master key securely stored in a Key Management Service (KMS) like AWS KMS or HashiCorp Vault. The KEK is used to encrypt (wrap) and decrypt (unwrap) DEKs.
Let’s study the Envelope Encryption transaction flow diagram from producer to consumer:
sequenceDiagram
autonumber
actor Producer as Kafka Producer (Client)
participant KMS as KMS (AWS / Vault)
participant Broker as Kafka Broker
actor Consumer as Kafka Consumer (Client)
Note over Producer, KMS: Publication & Encryption Process
Producer->>KMS: Fetch a New Key (GenerateDataKey request to the KEK)
KMS-->>Producer: Return: Plaintext DEK & Encrypted DEK
Producer->>Producer: Encrypt the Message Payload using the Plaintext DEK
Producer->>Producer: Wipe the Plaintext DEK from memory (or store in a secure cache)
Producer->>Broker: Send the Encrypted Message + Encrypted DEK (in headers or payload)
Note over Broker: Passive Storage
Broker->>Broker: Write encrypted bytes to disk (the Broker doesn't know the message contents)
Note over Consumer, KMS: Consumption & Decryption Process
Consumer->>Broker: Fetch the Encrypted Message
Broker-->>Consumer: Return the Encrypted Message + Encrypted DEK
Consumer->>KMS: Request Decryption of the Encrypted DEK (Decrypt request)
KMS-->>Consumer: Return the Plaintext DEK (if IAM/ACL allows)
Consumer->>Consumer: Decrypt the Message Payload using the Plaintext DEK
Consumer->>Consumer: Present plaintext data to application logicImplementation Guide 1: OS/Disk-Level Encryption (LUKS) #
If we manage Kafka clusters on bare-metal infrastructure or standalone virtual machines (VMs), we can use LUKS (Linux Unified Key Setup) to transparently encrypt the storage volume of Kafka data directories.
Here are concrete steps for formatting and mounting LUKS-encrypted disks in Linux:
1. Format a New Disk with LUKS #
Warning: This command deletes all data on the target partition. Make sure /dev/sdb is the correct empty disk.
# Initializing a LUKS encrypted partition on the /dev/sdb disk
# We'll be asked to enter a master passphrase to lock the disk
sudo cryptsetup luksFormat /dev/sdb
2. Open the Encrypted Disk #
Opening a LUKS encrypted disk creates a new device mapping under /dev/mapper/:
# Opening the encrypted disk and naming it "kafka_data_crypt"
sudo cryptsetup open /dev/sdb kafka_data_crypt
3. Create a File System #
Create an ext4 or XFS file system on top of the opened encrypted disk:
# Creating an ext4 file system
sudo mkfs.ext4 /dev/mapper/kafka_data_crypt
4. Mount the Disk to the Kafka Data Directory #
# Creating the mount point directory
sudo mkdir -p /var/lib/kafka/data
# Mounting the encrypted volume to the Kafka data directory
sudo mount /dev/mapper/kafka_data_crypt /var/lib/kafka/data
# Changing directory ownership so the kafka user can access it
sudo chown -R kafka:kafka /var/lib/kafka/data
5. Boot Automation (Optional with Key Files) #
For production, we don’t want to manually enter passphrases every time the server reboots. We can use key files secured on other servers or injected through Vault integrations at boot:
# Creating a random key file
sudo dd if=/dev/urandom of=/etc/security/kafka-disk.key bs=1024 count=4
sudo chmod 400 /etc/security/kafka-disk.key
# Adding the key file to the disk's LUKS key slots
sudo cryptsetup luksAddKey /dev/sdb /etc/security/kafka-disk.key
Add the following line to /etc/crypttab so the OS automatically unlocks the key at boot:
kafka_data_crypt /dev/sdb /etc/security/kafka-disk.key luks
And add to /etc/fstab for automatic mounting:
/dev/mapper/kafka_data_crypt /var/lib/kafka/data ext4 defaults,noatime 0 2
Implementation Guide 2: Envelope Encryption with the Java SDK #
For the highest security level, we can modify producers and consumers using Custom Serializers/Deserializers in the Kafka Java SDK. Below are encryption class implementation examples integrating Envelope Encryption with DEK caching to avoid excessive KMS API calls (which can damage throughput and increase latency).
1. Custom Serializer Code: EnvelopeEncryptionSerializer
#
package com.mycompany.kafka.security;
import org.apache.kafka.common.serialization.Serializer;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class EnvelopeEncryptionSerializer implements Serializer<byte[]> {
private String kmsKeyId;
private MockKmsClient kmsClient;
private SecureRandom secureRandom;
// Secure local DEK cache to optimize performance
private CachedDek currentCachedDek;
private static final long DEK_ROTATION_INTERVAL_MS = 300000; // Rotate the DEK every 5 minutes
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
this.kmsKeyId = (String) configs.get("encryption.kms.key.id");
// In production, replace with the real AWS KMS client / HashiCorp Vault client
this.kmsClient = new MockKmsClient();
this.secureRandom = new SecureRandom();
}
private synchronized CachedDek getOrGenerateDek() {
long now = System.currentTimeMillis();
if (currentCachedDek == null || (now - currentCachedDek.createdAtMs) > DEK_ROTATION_INTERVAL_MS) {
// Request a new DEK from the KMS
KmsDekEnvelope envelope = kmsClient.generateDataKey(kmsKeyId);
currentCachedDek = new CachedDek(envelope.plaintextKey, envelope.encryptedKey, now);
}
return currentCachedDek;
}
@Override
public byte[] serialize(String topic, byte[] data) {
if (data == null) {
return null;
}
try {
// 1. Get the DEK (from the cache or create a new one via the KMS)
CachedDek dek = getOrGenerateDek();
// 2. Create a random IV (Initialization Vector) for AES-GCM (12 bytes)
byte[] iv = new byte[12];
secureRandom.nextBytes(iv);
// 3. Do AES-GCM encryption on the data payload
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(dek.plaintextKey, "AES");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); // 128-bit tag length
cipher.init(Cipher.ENCRYPT_MODE, keySpec, parameterSpec);
byte[] encryptedPayload = cipher.doFinal(data);
// 4. Assemble the output message with the format:
// [Encrypted DEK Length (4 bytes)] + [Encrypted DEK] + [IV (12 bytes)] + [Encrypted Payload]
byte[] encryptedDekBytes = dek.encryptedKey;
int totalLength = 4 + encryptedDekBytes.length + 12 + encryptedPayload.length;
ByteBuffer buffer = ByteBuffer.allocate(totalLength);
buffer.putInt(encryptedDekBytes.length);
buffer.put(encryptedDekBytes);
buffer.put(iv);
buffer.put(encryptedPayload);
return buffer.array();
} catch (Exception e) {
throw new RuntimeException("Failed to encrypt the Kafka payload: ", e);
}
}
@Override
public void close() {
if (currentCachedDek != null) {
currentCachedDek.wipe();
}
}
// Helper class for holding the DEK currently in use
private static class CachedDek {
final byte[] plaintextKey;
final byte[] encryptedKey;
final long createdAtMs;
CachedDek(byte[] plaintextKey, byte[] encryptedKey, long createdAtMs) {
this.plaintextKey = plaintextKey;
this.encryptedKey = encryptedKey;
this.createdAtMs = createdAtMs;
}
void wipe() {
java.util.Arrays.fill(plaintextKey, (byte) 0); // Wipe the plain key from memory
}
}
}
2. Custom Deserializer Code: EnvelopeDecryptionDeserializer
#
package com.mycompany.kafka.security;
import org.apache.kafka.common.serialization.Deserializer;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class EnvelopeDecryptionDeserializer implements Deserializer<byte[]> {
private MockKmsClient kmsClient;
// Cache of Encrypted DEK -> Plaintext DEK to cut decryption latency
private ConcurrentHashMap<ByteBuffer, byte[]> decryptedKeysCache;
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
this.kmsClient = new MockKmsClient();
this.decryptedKeysCache = new ConcurrentHashMap<>();
}
private byte[] getOrDecryptDek(byte[] encryptedDek) {
ByteBuffer wrapper = ByteBuffer.wrap(encryptedDek);
if (decryptedKeysCache.containsKey(wrapper)) {
return decryptedKeysCache.get(wrapper);
}
// Decrypt using the KMS if not in the cache
byte[] plaintextKey = kmsClient.decryptDataKey(encryptedDek);
decryptedKeysCache.put(wrapper, plaintextKey);
return plaintextKey;
}
@Override
public byte[] deserialize(String topic, byte[] data) {
if (data == null) {
return null;
}
try {
ByteBuffer buffer = ByteBuffer.wrap(data);
// 1. Extract the Encrypted DEK
int encryptedDekLength = buffer.getInt();
byte[] encryptedDek = new byte[encryptedDekLength];
buffer.get(encryptedDek);
// 2. Extract the IV
byte[] iv = new byte[12];
buffer.get(iv);
// 3. Extract the Encrypted Payload
int payloadLength = buffer.remaining();
byte[] encryptedPayload = new byte[payloadLength];
buffer.get(encryptedPayload);
// 4. Get the Plaintext DEK
byte[] plaintextKey = getOrDecryptDek(encryptedDek);
// 5. Decrypt the data using AES-GCM
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(plaintextKey, "AES");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, parameterSpec);
return cipher.doFinal(encryptedPayload);
} catch (Exception e) {
throw new RuntimeException("Failed to decrypt the Kafka payload: ", e);
}
}
@Override
public void close() {
decryptedKeysCache.values().forEach(key -> java.util.Arrays.fill(key, (byte) 0));
decryptedKeysCache.clear();
}
}
3. Mock KMS Client (For Demonstration) #
package com.mycompany.kafka.security;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class MockKmsClient {
private final byte[] localMasterKek;
private final SecureRandom secureRandom;
public MockKmsClient() {
this.secureRandom = new SecureRandom();
this.localMasterKek = new byte[32]; // Simulate a 256-bit KEK
secureRandom.nextBytes(localMasterKek);
}
public KmsDekEnvelope generateDataKey(String kmsKeyId) {
// 1. Generate a Plaintext DEK (256-bit AES)
byte[] plaintextKey = new byte[32];
secureRandom.nextBytes(plaintextKey);
// 2. Encrypt (Wrap) the DEK using localMasterKek (KEK)
byte[] encryptedKey = wrapKey(plaintextKey);
return new KmsDekEnvelope(plaintextKey, encryptedKey);
}
public byte[] decryptDataKey(byte[] encryptedKey) {
// Decrypt (Unwrap) the DEK using localMasterKek (KEK)
return unwrapKey(encryptedKey);
}
private byte[] wrapKey(byte[] plaintextKey) {
try {
Cipher cipher = Cipher.getInstance("AES");
SecretKeySpec keySpec = new SecretKeySpec(localMasterKek, "AES");
cipher.init(Cipher.WRAP_MODE, keySpec);
return cipher.wrap(new SecretKeySpec(plaintextKey, "AES"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private byte[] unwrapKey(byte[] encryptedKey) {
try {
Cipher cipher = Cipher.getInstance("AES");
SecretKeySpec keySpec = new SecretKeySpec(localMasterKek, "AES");
cipher.init(Cipher.UNWRAP_MODE, keySpec);
return cipher.unwrap(encryptedKey, "AES", Cipher.SECRET_KEY).getEncoded();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
class KmsDekEnvelope {
final byte[] plaintextKey;
final byte[] encryptedKey;
KmsDekEnvelope(byte[] plaintextKey, byte[] encryptedKey) {
this.plaintextKey = plaintextKey;
this.encryptedKey = encryptedKey;
}
}
Performance Analysis & Encryption at Rest Overhead Impacts #
Choosing the right encryption method heavily depends on our cluster’s CPU budget and latency requirements:
- Disk-Level Encryption Efficiency: Disk-level encryption (LUKS or AWS EBS KMS) runs at the operating system/driver layer and leverages AES-NI cryptographic units built into CPU cores. Latency impact is usually below 1-2%, and it almost doesn’t significantly increase CPU utilization. This method is the best option if our Kafka cluster is CPU-bound.
- Envelope Encryption Costs: Application-level encryption (Envelope Encryption) forces the JVM to encrypt every individual record line. This can increase client CPU consumption by 10-25% and add several milliseconds of latency per delivery batch. However, by adopting DEK caching (like implemented in the code above) and using the AES-GCM algorithm, we can limit network call frequencies to KMS servers, keeping throughput high.
Encryption at Rest Security Audit Checklist #
Do the following audit steps before marking the Kafka cluster safe from passive data leaks:
| No | Security Audit Criteria | Verification Method | Status |
|---|---|---|---|
| 1 | Verify Disk Volume Encryption | Run the lsblk command on broker servers. Kafka log data directories (log.dirs) must be mounted under crypt-type devices (LUKS). | [ ] |
| 2 | Log Segment Leak Testing | Copy one .log file randomly from broker disks, try running strings /path/to/segment.log. Plaintext data must not be readable if using Envelope Encryption. | [ ] |
| 3 | EBS KMS Configuration Audit (Cloud) | If on AWS, make sure EBS volumes used by brokers have Encrypted=true status using customer KMS keys (CMK), not default built-in keys (aws/ebs). | [ ] |
| 4 | Client Key Caching Testing | Make sure producer applications don’t trigger GenerateDataKey network calls to the KMS for every single record delivery. Audit KMS logs to validate performance. | [ ] |
| 5 | Master Key Rotation (KEK) | Make sure automatic annual key rotation policies are enabled on KMS services (AWS KMS / HashiCorp Vault). | [ ] |
Summary #
- Disk-Level Is Easiest — OS-level encryption (LUKS) or KMS-encrypted AWS EBS is the easiest and fastest route to secure passive data with minimal performance impact without changing application code.
- Envelope Encryption Is Safest — If we don’t trust cloud infrastructure or broker OS system admins, use Envelope Encryption (client-side encryption) so data stays encrypted at all times, whether on networks, broker memory, or disks.
- Use DEK Caching — Don’t call KMS APIs every time you send messages. Implement local DEK caching on the producer side with periodic rotation mechanisms (e.g., 5-10 minutes) to maintain performance.
← Previous: Encryption in Transit Next: Multi-Tenant Security →