ClickHouse provides encryption at rest through an encrypted virtual disk type that wraps an underlying physical disk. Data is encrypted as it lands on disk and decrypted on read using a configurable AES cipher. The feature integrates with the standard storage policy mechanism, so MergeTree tables opt in by selecting an encrypted policy at creation time. This guide covers the configuration layout, key management options, table setup, monitoring, and observed performance impact.
Configuration layout
Encryption configuration lives in a config file under /etc/clickhouse-server/config.d/, for example encrypted_storage.xml. It has two pieces: a <disks> block that defines a physical disk and an encrypted overlay, and a <policies> block that references the overlay.
<storage_configuration>
<disks>
<disk1>
<type>local</type>
<path>/data/clickhouse_encrypted/</path>
</disk1>
<encrypted_disk>
<type>encrypted</type>
<disk>disk1</disk>
<path>encrypted/</path>
<algorithm>AES_128_CTR</algorithm>
<key_hex id="0">00112233445566778899aabbccddeeff</key_hex>
<current_key_id>0</current_key_id>
</encrypted_disk>
</disks>
<policies>
<encrypted>
<volumes>
<encrypted_volume>
<disk>encrypted_disk</disk>
</encrypted_volume>
</volumes>
</encrypted>
</policies>
</storage_configuration>
The path inside encrypted_disk is relative to disk1. Data written through the encrypted disk lands at /data/clickhouse_encrypted/encrypted/.
Preparing the storage directory
Create the target directory and give the ClickHouse user ownership before restarting:
mkdir -p /data/clickhouse_encrypted
chown clickhouse:clickhouse /data/clickhouse_encrypted
systemctl restart clickhouse-server
Key management
Inline hexadecimal keys
The simplest option is embedding the key directly in the config with <key_hex id="N">. The id attribute supports rotation: add a new key with a new id, set <current_key_id> to the new value, and existing data still decrypts using the older key while new writes use the current one.
<key_hex id="0">00112233445566778899aabbccddeeff</key_hex>
<key_hex id="1">ffeeddccbbaa99887766554433221100</key_hex>
<current_key_id>1</current_key_id>
Environment variables
Storing keys in the config file ships them with backups. Reading from the environment keeps the key out of config:
<key_hex from_env="DiskKey"/>
Set the variable in /etc/default/clickhouse-server:
DiskKey=00112233445566778899aabbccddeeff
Restart clickhouse-server for the variable to be picked up.
Creating an encrypted table
Bind a MergeTree table to the encrypted policy with storage_policy:
CREATE TABLE bench_encrypted
(
c_int Int64,
c_str varchar(255),
c_float Float64
)
ENGINE = MergeTree
ORDER BY c_int
SETTINGS storage_policy = 'encrypted';
All parts written for this table are encrypted on disk. The catalog, system tables, and logs are not affected, so plan separately for protecting those if your threat model requires it.
Verifying encryption is active
Query system.disks to see which disks report as encrypted:
SELECT name, path, type, is_encrypted
FROM system.disks;
Inspect storage policies and their volumes:
SELECT *
FROM system.storage_policies;
For a single table, check the policy column in system.tables:
SELECT name, storage_policy
FROM system.tables
WHERE database = 'default' AND name = 'bench_encrypted';
Performance impact
Benchmarks with 100M rows show small overhead for writes and a single-digit percentage hit on reads:
| Operation | Observation |
|---|---|
| Bulk insert | Comparable to unencrypted |
| Single-threaded SELECT | Roughly 6 to 9 percent slower |
| Mixed read workload | Around 10 to 15 percent slower due to decryption |
Decryption happens per granule on read paths, so workloads dominated by primary-key lookups and partition pruning see less overhead than full scans.
Common Pitfalls
- Storing the key in the same place as backups. Encrypting data with a key sitting next to the backup defeats the purpose. Use
from_envor an external secret store. - Forgetting
current_key_idafter adding a new key. Reads will still work, but new writes continue using the previous key. - Mismatched key length and algorithm.
AES_128_CTRexpects a 16-byte (32 hex char) key;AES_256_CTRexpects 32 bytes (64 hex char). The server refuses to start on mismatch. - Permissions on the underlying path. Without
chown clickhouse:clickhouse, the server cannot write through to the wrapped disk. - Expecting in-memory data to be encrypted. Encryption applies only to data at rest. Logs, caches, and the query path operate on plaintext.
- Mixing encrypted and unencrypted volumes inside one policy without understanding tier behavior. Parts move between volumes by storage policy rules and may end up unencrypted on the wrong tier.
Frequently Asked Questions
Q: Which algorithms are supported?
A: AES in CTR mode, with 128, 192, or 256 bit keys (AES_128_CTR, AES_192_CTR, AES_256_CTR). Key length must match the algorithm.
Q: Can I encrypt an existing table?
A: Not in place. Create a new table with the encrypted policy and copy data with INSERT INTO new SELECT * FROM old, then swap names with RENAME TABLE.
Q: Does encryption protect logs and system tables?
A: Only if those paths sit on an encrypted disk too. Configure system_log storage policies and the server log directory accordingly.
Q: How do I rotate keys without re-encrypting existing data?
A: Add a new <key_hex> with a higher id, then update <current_key_id>. New parts use the new key, existing parts continue using their original key id stored in part metadata.
Q: Does encryption affect replication? A: No. Replication transfers parts as bytes; the receiving replica decrypts and re-encrypts under its own configured key. Both replicas need their own encryption configured.