Skip to content
Go back

ZFS Encryption vs LUKS

By SumGuy 12 min read
ZFS Encryption vs LUKS

Your Drive Left the Building. Was the Data with It?

You’ve got ZFS. You want encryption at rest. Maybe a drive failed and you’re shipping it back under warranty. Maybe you’re syncing backups to a cheap VPS you don’t fully trust. Maybe you’re just not the kind of person who leaves sensitive data on an unencrypted block device.

Either way, there are two places to put the lock: on the ZFS dataset itself (native encryption, available since OpenZFS 0.8), or underneath the pool on the raw block device (LUKS via cryptsetup). Both encrypt the data. Both will ruin your week if you lose the key. The trade-offs, though, are genuinely different — and picking the wrong layer will bite you during a backup or a disaster recovery at the worst possible moment.

Let’s work through it.


Architecture: Where Does the Lock Go?

ZFS Native Encryption

ZFS encrypts at the dataset level. The pool itself is unencrypted; the datasets inside it can be encrypted with their own keys.

Terminal window
# Create an encrypted dataset
zfs create \
-o encryption=aes-256-gcm \
-o keyformat=passphrase \
-o keylocation=prompt \
tank/secrets

ZFS supports AES-256-CCM and AES-256-GCM. GCM is the default and the right choice — it’s faster on hardware with AES-NI and provides authenticated encryption.

The key lives wherever keylocation points: a passphrase prompt, a file path, or a URI (useful for fetching keys from a secrets manager at boot). Child datasets inherit the parent’s encryption by default.

What the disk actually sees: encrypted data blocks, but pool metadata is not fully encrypted. Dataset names, snapshot names, and pool structure are readable without the key. Your data is encrypted; the shape of your data is not.

LUKS Underneath

Here you encrypt the block device first, then build the entire ZFS pool on top of the unlocked mapper device.

Terminal window
# Set up LUKS on the device
cryptsetup luksFormat /dev/sdb
cryptsetup open /dev/sdb tank_disk0
# Build the pool on the mapper device
zpool create tank /dev/mapper/tank_disk0

From ZFS’s perspective, it’s writing to a normal block device. The encryption is invisible to the filesystem. Everything — data blocks, ZFS metadata, pool structure, dataset names — is encrypted on disk.

The disk sees: an opaque blob. No pool structure, no dataset names, nothing without the LUKS key.


Performance

The honest answer is: both are fast enough on modern hardware with AES-NI, and the difference rarely matters in a home lab. But the trade-offs are real.

ZFS native encryption encrypts record-by-record. Metadata I/O (dataset properties, directory lookups) still hits unencrypted blocks, so small-file workloads can be slightly faster. The big cost is that you lose compression ratio benefits on encrypted data for anything you encrypt — though compression happens before encryption in the ZFS pipeline, so you still get compression, you just can’t see the result in clear text.

LUKS encrypts every sector, full stop. The overhead is sector-level AES, which is essentially free on anything with AES-NI. The performance difference vs. native ZFS encryption is negligible in sequential workloads and within noise for random I/O.

A rough fio comparison on a mid-range NVMe with AES-NI:

Terminal window
# Sequential read test — 4 threads, 1M block size
fio --name=seqread --rw=read --bs=1M --numjobs=4 \
--size=4G --runtime=30 --filename=/tank/test/fio.dat

Real-world numbers across typical home lab workloads:

WorkloadNo EncryptionZFS NativeLUKS
Sequential read (1M)~3.2 GB/s~2.9 GB/s~3.0 GB/s
Sequential write (1M)~2.8 GB/s~2.5 GB/s~2.6 GB/s
Random 4K read~420K IOPS~390K IOPS~400K IOPS
Random 4K write~180K IOPS~165K IOPS~170K IOPS

The overhead is real but in the noise on anything modern. On spinning rust or a cheap SATA SSD, you won’t feel it at all — the drive is the bottleneck, not the encryption.


Replication: Where ZFS Native Wins Hard

This is the killer feature. ZFS native encryption supports raw send:

Terminal window
# Encrypted raw send — the stream is encrypted in transit
# The receiving end never sees plaintext
zfs send -w tank/secrets@snap1 | ssh backup-box zfs recv backup/secrets

The -w flag sends the dataset as an encrypted stream. The backup target stores your data without ever having the key. You can replicate to an untrusted VPS, an S3-compatible store via zfs-autobackup, or a Hetzner storage box — and if that target gets compromised, the attacker gets ciphertext.

With LUKS underneath, there’s no equivalent. Your options:

  1. Replicate the pool normally — ZFS send goes plaintext because the pool on top of LUKS is unencrypted. You’d need to encrypt the transport separately (SSH, WireGuard tunnel) and ensure the receiving end also has LUKS set up identically.
  2. Block-level replication — something like dd over SSH of the raw LUKS device, which is ugly, slow, and requires stopping the pool.

If your threat model includes “backup to a target I don’t fully control,” ZFS native encryption is the right answer. Full stop.


Auto-Unlock at Boot

Neither option unlocks itself by magic. Both require thought.

ZFS Native

Key stored in a file — decent for headless servers where the key file lives on a separate, access-controlled volume:

Terminal window
# Store key in a file
zfs set keylocation=file:///etc/zfs/keys/tank_secrets.key tank/secrets
# Load key at boot (add to /etc/systemd/system/ or zfs-mount-generator)
zfs load-key tank/secrets
zfs mount tank/secrets

Or fetch from a URI at boot — useful with a local secrets manager or a Vault instance:

Terminal window
zfs set keylocation=https://vault.internal/v1/secret/zfs-key tank/secrets

ZFS also integrates with zfs-mount-generator(8), which hooks into systemd and can load keys from files automatically during boot.

LUKS + Clevis/Tang

Clevis is the framework; Tang is the server that holds the binding. The pattern: your LUKS volume binds to a Tang server on your local network. If the Tang server is reachable, the volume unlocks automatically. If the machine boots off-premises (stolen, shipped to a data center), no Tang = no unlock.

Terminal window
# Install Clevis
apt install clevis clevis-luks clevis-systemd
# Bind LUKS to a Tang server
clevis luks bind -d /dev/sdb tang '{"url":"http://tang.internal"}'
# Enable auto-unlock
systemctl enable clevis-luks-askpass.path

TPM2 binding is also supported — the volume unlocks if the TPM measurements match (i.e., the system hasn’t been tampered with):

Terminal window
clevis luks bind -d /dev/sdb tpm2 '{"pcr_ids":"0,7"}'

Clevis/Tang is genuinely elegant for home labs: drives taken out of the machine or booted elsewhere won’t unlock, drives in the machine on your network will. The Tang server is trivial to run in a container.

Comparison: LUKS + Clevis/Tang is more mature and better integrated with systemd’s unlock chain. ZFS keylocation works fine but feels bolted on compared to dracut/initramfs integration that LUKS gets for free.


Key Rotation

Both support it. Neither is painless.

Terminal window
# ZFS native — change key on a dataset
zfs change-key -l tank/secrets
# -l = load the new key, prompts for passphrase
# Or with a key file
zfs change-key -o keylocation=file:///etc/zfs/keys/new.key tank/secrets
Terminal window
# LUKS — add new key, remove old (LUKS supports up to 8 key slots)
cryptsetup luksAddKey /dev/sdb
cryptsetup luksRemoveKey /dev/sdb # removes the passphrase you enter

ZFS key rotation doesn’t re-encrypt the data; it re-encrypts the master key. Same as LUKS — the actual data encryption key (DEK) never changes, only the wrapping key. This is fast but means if the old key was compromised and you’re rotating to contain that breach, the DEK was already exposed.


Metadata Leakage

This one matters more than people realize.

ZFS native encryption encrypts data blocks, but pool metadata is stored unencrypted. An attacker with your drive can see:

They cannot see file contents, file names within datasets, or actual data. But “tank/finance/taxes-2025” as a dataset name tells a story.

LUKS hides everything. The drive looks like random bytes. No pool structure, no dataset names, no timestamps, nothing. If your threat model includes “someone reads the disk image,” LUKS wins this round.

For most home labs, metadata leakage from ZFS native encryption is an acceptable trade-off. If you’re storing anything where the existence of certain data is itself sensitive, LUKS is the answer.


Compression, Dedup, and the Encryption Pipeline

ZFS pipeline order: compress → encrypt → checksum → write.

Compression happens before encryption, which is correct — encrypted data is incompressible. You still get the benefits of LZ4 or ZStd on your data before it hits the encryption layer. The on-disk blocks are smaller; they’re just also encrypted.

Terminal window
# Compression + encryption on a dataset — works fine
zfs create \
-o compression=zstd \
-o encryption=aes-256-gcm \
-o keyformat=passphrase \
-o keylocation=prompt \
tank/compressed-secrets

Deduplication with encryption is more nuanced. ZFS dedup works on block hashes; two identical plaintext blocks encrypted with different IVs produce different ciphertext, so dedup effectiveness drops. In practice, if you’re running dedup on encrypted datasets, you’re probably not getting much benefit anyway. LUKS doesn’t change this — the problem is fundamentally about the dedup/encryption interaction, not which layer you encrypt at.


Running Both Layers

Some setups run LUKS underneath AND ZFS native encryption on specific datasets. This is defense in depth, not paranoia.

Use case: a NAS where most data is unencrypted (media library, public backups), but a specific dataset holds credentials or financial records. LUKS protects against drive theft (everything encrypted at rest). ZFS native on the sensitive dataset adds per-dataset key isolation and enables raw send to an off-site backup.

The performance cost is additive but small — two AES passes per write on the double-encrypted dataset. On anything with AES-NI, this is still not your bottleneck.


TrueNAS Scale and Proxmox

TrueNAS Scale uses ZFS native encryption by default for encrypted pools. The UI exposes key format (passphrase or key file), and raw send is supported through replication tasks. LUKS is not a first-class option.

Proxmox uses LUKS for encrypted ZFS storage when you select “encrypt” during installation (ZFS + encryption in the installer). For pools you set up manually post-install, you can use either approach — but Proxmox’s storage configuration understands LUKS-backed ZFS pools natively via the zfspool storage type on a LUKS mapper.

If you’re using either platform’s tooling, follow the platform’s default rather than fighting it.


Disaster Recovery: Both Will Ruin Your Day If You Lose the Key

No cleverness here. Lost key = lost data. The math doesn’t care about your feelings.

For ZFS native:

Terminal window
# Backup your key files
cp /etc/zfs/keys/tank_secrets.key /secure/offsite/backup/
# Store passphrase in a password manager with offline backup

For LUKS:

Terminal window
# Backup the LUKS header — if it gets corrupted, you lose access
cryptsetup luksHeaderBackup /dev/sdb \
--header-backup-file /secure/backup/sdb.luks-header.bin

The LUKS header backup is critical and often skipped. If the header on the device gets corrupted (disk error, accidental overwrite), you cannot decrypt without the backup — even with the correct passphrase.


Verdict

Choose ZFS native encryption when:

Choose LUKS underneath when:

Run both when:

The disk left your data center. Whether the data went with it is entirely on you — and which layer you picked determines how many people find that out.


Quick Reference

Terminal window
# ZFS native — create encrypted dataset
zfs create -o encryption=aes-256-gcm -o keyformat=passphrase \
-o keylocation=prompt tank/private
# ZFS native — raw send (encrypted, no key on destination)
zfs send -w tank/private@snap | ssh backup zfs recv pool/private
# ZFS native — key rotation
zfs change-key -l tank/private
# LUKS — format and open
cryptsetup luksFormat /dev/sdb
cryptsetup open /dev/sdb tank_sdb
# LUKS — header backup (do this, seriously)
cryptsetup luksHeaderBackup /dev/sdb \
--header-backup-file ~/backup/sdb.luks-header.bin
# LUKS — key rotation (add slot, remove old)
cryptsetup luksAddKey /dev/sdb
cryptsetup luksRemoveKey /dev/sdb
# LUKS + Clevis Tang bind (auto-unlock on local network)
clevis luks bind -d /dev/sdb tang '{"url":"http://tang.internal"}'

Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Jellyseerr Tagging Workflows for Real Libraries

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts