Encryption — per-dataset, native, and replication-aware.
ZFS native encryption, introduced in OpenZFS 0.8.0 (2019), operates at the dataset level, not full-disk. Each dataset can have its own key. Keys can be a passphrase, a raw 32-byte key, or a hex key. They can be prompted interactively, loaded from a file, or fetched from a URL. And crucially: ZFS can replicate encrypted datasets without ever decrypting them. The receiving machine stores the ciphertext but cannot read it. This changes everything about offsite backup, multi-tenant isolation, and compliance.
Encryption is set at dataset creation and cannot be added later. You cannot convert an
unencrypted dataset to an encrypted one in place. The only path is zfs send to a new encrypted
dataset. Plan accordingly — enable encryption from day one if there is any chance you will need it.
zfs send -w (raw send) feature alone justifies using ZFS encryption
over any alternative. If you handle sensitive data and replicate offsite, this is the feature that should
decide your encryption strategy.
ZFS native encryption vs LUKS — comparison
LUKS (dm-crypt) encrypts an entire block device. ZFS encrypts individual datasets within a pool. This is a fundamental architectural difference that affects every operational decision.
| Capability | ZFS Native Encryption | LUKS / dm-crypt |
|---|---|---|
| Granularity | Per-dataset. Each dataset can have a different key. | Per-volume. One key for the entire block device. |
| Snapshot support | Native. Snapshots are encrypted with the same key, zero overhead. | Snapshots require LVM or filesystem cooperation. Clumsy. |
| Replication | zfs send -w replicates without decrypting. Receiver cannot read data. |
Must decrypt, transfer plaintext, re-encrypt at destination. Or dd the raw volume. |
| Key management | Passphrase, raw key file, or hex key. Stored as ZFS properties. Key rotation built in. | Passphrase or key file. LUKS header stores key slots. Key rotation requires re-encryption. |
| Algorithm | AES-256-GCM (default) or AES-256-CCM. Authenticated encryption. | AES-XTS-256 (default). Not authenticated — no integrity guarantee beyond checksums. |
| Integrity | ZFS checksums + GCM authentication tag. Double integrity verification. | No built-in integrity. dm-integrity can be layered but adds complexity and overhead. |
| Compression | Compresses before encrypting. Full compression benefit preserved. | Encrypted data is incompressible. Must layer compression below LUKS (LVM) or not at all. |
| Multi-user / multi-tenant | Different datasets, different keys. Users cannot access each other's data even with root on the pool. | One key per volume. Multi-tenant isolation requires separate LUKS volumes. |
| Performance | AES-NI accelerated. 5–15% overhead on modern CPUs. Negligible on NVMe. | AES-NI accelerated. 3–10% overhead. Slightly less overhead due to simpler XTS mode. |
| Header risk | No separate header to lose. Encryption metadata lives in the dataset's ZFS properties. | LUKS header corruption = permanent data loss. Must backup headers separately. |
| Boot integration | Works with ZFSBootMenu, systemd, or initramfs key prompting. | Well-integrated into GRUB, systemd-cryptsetup, dracut. |
Encryption algorithms
ZFS supports two AES-256 modes. Both use 256-bit keys and provide authenticated encryption, meaning the ciphertext includes a tag that detects tampering. This is on top of ZFS's own checksums — you get double integrity verification.
# Specify algorithm explicitly (aes-256-gcm is the default)
zfs create -o encryption=aes-256-gcm \
-o keyformat=passphrase \
-o keylocation=prompt \
tank/secrets
# Check what algorithm a dataset uses
zfs get encryption tank/secrets
# NAME PROPERTY VALUE SOURCE
# tank/secrets encryption aes-256-gcm -
Encryption properties
Four ZFS properties control encryption behavior. They are set at dataset creation and
(with the exception of keylocation) cannot be changed after the fact without
zfs change-key.
aes-256-gcm (default), aes-256-ccm, or off. Set at creation. Cannot be changed. Cannot be added to an existing unencrypted dataset.passphrase (human-readable, PBKDF2-stretched), raw (32-byte binary file), or hex (64 hex characters). Can be changed via zfs change-key -o keyformat=....prompt (interactive), file:///path/to/keyfile, or https://keyserver/key. Can be changed at any time with zfs set keylocation=... — this is the only encryption property you can change freely.keyformat=passphrase.# View all encryption properties for a dataset
zfs get encryption,keyformat,keylocation,pbkdf2iters,keystatus tank/secrets
# NAME PROPERTY VALUE SOURCE
# tank/secrets encryption aes-256-gcm -
# tank/secrets keyformat passphrase -
# tank/secrets keylocation prompt local
# tank/secrets pbkdf2iters 350000 default
# tank/secrets keystatus available -
Key management — passphrase, raw key, hex key
Passphrase (interactive, human-memorable)
The simplest option. ZFS prompts for a passphrase at dataset creation and key load time. The passphrase is stretched using PBKDF2-SHA512 with the configured iteration count. Minimum length is 8 characters.
# Create with passphrase — ZFS prompts you
zfs create -o encryption=aes-256-gcm \
-o keyformat=passphrase \
-o keylocation=prompt \
tank/home
# Create with higher PBKDF2 iterations for paranoid security
zfs create -o encryption=aes-256-gcm \
-o keyformat=passphrase \
-o keylocation=prompt \
-o pbkdf2iters=1000000 \
tank/vault
Raw key (32-byte binary file)
A 32-byte (256-bit) binary key file. No PBKDF2 stretching — the key is used directly. This is the highest-security option for automated systems where key files are managed by a secrets manager (Vault, AWS KMS, etc.). The key file must be exactly 32 bytes.
# Generate a random 256-bit raw key
dd if=/dev/urandom of=/root/keys/tank-secrets.key bs=32 count=1
chmod 000 /root/keys/tank-secrets.key
# Create dataset with raw key file
zfs create -o encryption=aes-256-gcm \
-o keyformat=raw \
-o keylocation=file:///root/keys/tank-secrets.key \
tank/secrets
# The key loads automatically at boot if keylocation points to a readable file
Hex key (64 hex characters)
Same 256-bit key as raw, but encoded in hexadecimal. Useful when you need a text-safe representation that can be stored in environment variables, config files, or REST API responses without binary encoding issues.
# Generate a hex key
dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p -c 64 > /root/keys/tank-data.hex
chmod 000 /root/keys/tank-data.hex
# Create dataset with hex key
zfs create -o encryption=aes-256-gcm \
-o keyformat=hex \
-o keylocation=file:///root/keys/tank-data.hex \
tank/data
Creating encrypted pools and datasets
Encrypted pool (root dataset encrypted)
When you set encryption on the pool's root dataset, all child datasets inherit encryption by default. This is the simplest approach: one key for everything.
# Create a pool with encryption on the root dataset
zpool create -o ashift=12 \
-O encryption=aes-256-gcm \
-O keyformat=passphrase \
-O keylocation=prompt \
-O compression=lz4 \
-O atime=off \
tank mirror /dev/sda /dev/sdb
# All child datasets inherit encryption automatically
zfs create tank/home # encrypted, same key as tank
zfs create tank/data # encrypted, same key as tank
zfs create tank/backups # encrypted, same key as tank
Selective encryption (some datasets encrypted, some not)
More commonly, you leave the pool root unencrypted and encrypt specific datasets. This gives maximum flexibility — boot environments stay unencrypted for ZFSBootMenu, while sensitive data gets per-dataset encryption with independent keys.
# Create pool without encryption on root
zpool create -o ashift=12 \
-O compression=lz4 -O atime=off \
rpool mirror /dev/sda /dev/sdb
# Boot environment — unencrypted (ZFSBootMenu needs to read this)
zfs create rpool/ROOT
zfs create rpool/ROOT/default
# Home directories — encrypted with passphrase
zfs create -o encryption=aes-256-gcm \
-o keyformat=passphrase \
-o keylocation=prompt \
rpool/home
# Application secrets — encrypted with a different key file
zfs create -o encryption=aes-256-gcm \
-o keyformat=raw \
-o keylocation=file:///root/keys/app-secrets.key \
rpool/secrets
# Database — encrypted with yet another key
zfs create -o encryption=aes-256-gcm \
-o keyformat=raw \
-o keylocation=file:///root/keys/postgres.key \
rpool/postgres
Per-dataset encryption with different keys
Child datasets inherit the parent's encryption by default, sharing the same key. To give a
child its own key, specify keyformat and keylocation at creation.
The child will use the parent's encryption algorithm but a completely independent key.
# Parent encrypted with passphrase
zfs create -o encryption=aes-256-gcm \
-o keyformat=passphrase \
-o keylocation=prompt \
tank/clients
# Children with their own independent keys
zfs create -o keyformat=passphrase -o keylocation=prompt tank/clients/acme
zfs create -o keyformat=passphrase -o keylocation=prompt tank/clients/globo
zfs create -o keyformat=raw -o keylocation=file:///keys/initech.key tank/clients/initech
# Each child has its own key — loading the parent's key does NOT unlock children
zfs load-key tank/clients # unlocks only tank/clients
zfs load-key tank/clients/acme # unlocks only acme — separate passphrase
Encryption inheritance
Encryption inheritance follows specific rules that differ from most ZFS properties:
Algorithm inherits, key does not (by default)
When you create a child of an encrypted dataset without specifying new key properties, the child
shares the parent's encryption key. The algorithm inherits. Loading the parent's key
unlocks the child automatically. This is the common case for home directories under an encrypted
/home parent.
Specifying keyformat creates an encryption root
If you specify keyformat when creating a child, it becomes its own encryption
root with an independent key. The parent's key cannot unlock it. Use zfs get encryptionroot
to see which dataset owns the key for any given dataset.
Cannot encrypt under an unencrypted parent retroactively
You can create an encrypted child under an unencrypted parent — but you must specify
all encryption properties explicitly. The child will not inherit encryption=off if you
provide encryption=aes-256-gcm at creation.
Cannot disable encryption on a child of an encrypted parent
If the parent is encrypted, all children are encrypted. You cannot create an unencrypted dataset under an encrypted parent. The inheritance is mandatory downward.
# Check which dataset owns the encryption key
zfs get encryptionroot tank/clients/acme
# NAME PROPERTY VALUE SOURCE
# tank/clients/acme encryptionroot tank/clients/acme -
# ^ This dataset is its own encryption root — it has its own key
zfs get encryptionroot tank/clients/acme/docs
# NAME PROPERTY VALUE SOURCE
# tank/clients/acme/docs encryptionroot tank/clients/acme -
# ^ This dataset inherits acme's key — loading acme's key unlocks it too
Loading and unloading keys
Encrypted datasets start in a locked state after pool import. You must load the key before the dataset can be mounted and read. Unloading the key unmounts the dataset and makes the data inaccessible again.
# Check key status
zfs get keystatus tank/secrets
# NAME PROPERTY VALUE SOURCE
# tank/secrets keystatus unavailable -
# Load key — prompts for passphrase if keylocation=prompt
zfs load-key tank/secrets
# Enter passphrase for 'tank/secrets':
# Load key from file (if keylocation=file://...)
zfs load-key tank/data
# Key loaded automatically from file — no prompt
# Load ALL keys for all encrypted datasets in a pool
zfs load-key -a
# Mount after loading key
zfs mount tank/secrets
# Or load + mount in one step
zfs load-key tank/secrets && zfs mount tank/secrets
# Unload key — unmounts the dataset first
zfs unload-key tank/secrets
# Unload all keys (unmounts all encrypted datasets)
zfs unload-key -a
zfs load-key -a command is what you want in most boot
scripts. It loads every key for every encrypted dataset in every imported pool, using whatever
keylocation each dataset has configured. Datasets with keylocation=prompt
will prompt interactively; datasets with keylocation=file://... load silently. Mix both
in the same pool and -a handles them all.
Key rotation
zfs change-key lets you change the passphrase, switch from passphrase to raw key,
change the key file location, or adjust PBKDF2 iterations. The underlying data encryption key
(the actual AES key used to encrypt blocks) does not change — ZFS wraps
the data key with a key-encryption-key (KEK) derived from your passphrase/file. Changing the
key changes the KEK wrapping, not the data encryption itself. This is fast (instant) because
no data is re-encrypted.
# Change passphrase (prompts for new passphrase)
zfs change-key tank/secrets
# Switch from passphrase to raw key file
zfs change-key -o keyformat=raw \
-o keylocation=file:///root/keys/new.key \
tank/secrets
# Switch from raw key to passphrase
zfs change-key -o keyformat=passphrase \
-o keylocation=prompt \
tank/secrets
# Increase PBKDF2 iterations on existing passphrase-encrypted dataset
zfs change-key -o pbkdf2iters=1000000 tank/secrets
# Change key for a child to make it its own encryption root
zfs change-key -o keyformat=passphrase -o keylocation=prompt tank/clients/acme
# Revert a child to inherit parent's key
zfs change-key -i tank/clients/acme
zfs send | zfs recv to a
new dataset with a new encryption root.
What metadata is NOT encrypted — the privacy boundary
ZFS encryption encrypts file contents, file names, extended attributes, symlink targets, and ACLs. But significant metadata remains visible in plaintext. Understanding this boundary is critical for compliance and threat modeling.
Dataset names
tank/clients/acme is visible to anyone with pool access. Dataset names are structural
metadata that ZFS needs to manage the pool. If dataset names are sensitive (client names, project codes),
use opaque identifiers like tank/enc/d7f3a.
Dataset sizes and space usage
zfs list shows USED, AVAIL, REFER for encrypted datasets even without the key loaded.
An adversary can see how much data is stored and how it changes over time. Size metadata is inherent to
pool management and cannot be encrypted.
ZFS properties
All dataset properties (compression, recordsize, quota,
mountpoint, creation timestamp) are visible. Properties are stored in pool
metadata, outside the encryption boundary.
Snapshot names and counts
Snapshot names like tank/secrets@2024-01-15-incident-response are visible. Snapshot
creation times, counts, and sizes are all plaintext. Use non-descriptive snapshot names if this matters.
Pool topology and disk layout
zpool status shows the full disk layout, vdev types, and device paths. The physical
structure of the pool is always visible.
Dedup tables (pre-2.2)
Before OpenZFS 2.2, deduplication tables stored block hashes in plaintext, which could reveal whether two encrypted datasets contained identical blocks. OpenZFS 2.2+ introduced encrypted dedup that hashes after encryption, closing this leak.
Encrypted send/receive — raw send (zfs send -w)
This is the feature that changes everything. zfs send -w (raw send) transmits an encrypted
dataset in its encrypted form. The data is never decrypted during transfer. The receiving
machine stores the ciphertext but cannot read it — it does not have the key.
Only you do.
Raw send preserves the encryption, compression, and dedup properties of the source dataset. The receiver stores an exact byte-for-byte copy of the encrypted blocks. This means:
# Full encrypted send to a remote machine
zfs send -w rpool/secrets@monday | ssh backup-server "zfs recv tank/offsite/secrets"
# Incremental encrypted send — only changed blocks
zfs send -w -i @monday @tuesday rpool/secrets | ssh backup-server "zfs recv tank/offsite/secrets"
# The backup server has the data but CANNOT decrypt it
# It does not have the key. It cannot mount the dataset.
# zfs load-key on the backup server will fail — it has no key to load.
# To restore: send the encrypted data back to a machine that has the key
ssh backup-server "zfs send -w tank/offsite/secrets@tuesday" | zfs recv rpool/restored
zfs load-key rpool/restored # enter your passphrase — data is back
zfs mount rpool/restored
# Raw send also works with compressed sends
zfs send -w --compressed rpool/secrets@snap | ssh backup "zfs recv tank/offsite/secrets"
Encryption + compression order
ZFS compresses data before encrypting it. This is the correct order and one of ZFS encryption's advantages over LUKS. Encrypted data is random and incompressible. If encryption happened first (as with LUKS on top of a filesystem), compression would be useless.
The processing pipeline for a write:
Application data
↓ compression (lz4, zstd, gzip)
↓ encryption (aes-256-gcm)
↓ checksum (sha256, fletcher4)
↓ write to disk
# This means you get FULL compression benefit even with encryption enabled.
# A 100GB dataset that compresses 2:1 uses 50GB on disk, encrypted.
# Verify: check compressratio on an encrypted dataset
zfs get compressratio,encryption tank/data
# NAME PROPERTY VALUE SOURCE
# tank/data compressratio 2.31x -
# tank/data encryption aes-256-gcm -
compression=lz4 (or zstd) on
every encrypted dataset. There is no reason not to.
Encryption + deduplication
Before OpenZFS 2.2, encryption and deduplication were fundamentally incompatible in a meaningful way. The dedup table hashed plaintext blocks before encryption, which meant two encrypted datasets with identical plaintext would show matching hashes in the dedup table — leaking information about whether datasets contained the same data.
OpenZFS 2.2+ introduced encrypted dedup. The dedup hash is computed on the encrypted blocks, so identical plaintext encrypted with different keys produces different hashes. No information leaks. Dedup still works within a single encryption root (same key = same ciphertext for identical plaintext), which is the useful case anyway.
# OpenZFS 2.2+: encrypted dedup is safe
zfs set dedup=on tank/vms # assuming tank/vms is encrypted
# Check OpenZFS version
zfs version
# zfs-2.2.7-1
# Pre-2.2: do NOT enable dedup on encrypted datasets
Unlocking encrypted datasets at boot
The biggest operational challenge with ZFS encryption is key loading at boot time.
An encrypted root dataset means the system cannot mount / until the key is available.
There are several strategies, from simple to fully automated.
Strategy 1: Unencrypted root, encrypted data
The simplest and most common approach. Leave the boot environment and root filesystem unencrypted.
Encrypt only data datasets (/home, /srv, /var/lib/postgres).
The system boots normally, and a systemd service loads keys after boot.
# systemd service to load all ZFS keys at boot
# /etc/systemd/system/zfs-load-keys.service
[Unit]
Description=Load ZFS encryption keys
After=zfs-import.target
Before=zfs-mount.service
[Service]
Type=oneshot
ExecStart=/sbin/zfs load-key -a
RemainAfterExit=yes
[Install]
WantedBy=zfs-mount.service
# Enable the service
systemctl enable zfs-load-keys.service
If all your encrypted datasets use keylocation=file://..., this works silently
with no human interaction. If any use keylocation=prompt, the service will hang
at boot waiting for console input — which is usually not what you want on a server.
Strategy 2: Key file on a separate partition or USB drive
Store the key file on a small partition or USB drive that is only plugged in during boot. After boot, remove the USB drive. This gives you physical key management without interactive prompts.
# Mount USB key drive at boot, load keys, unmount
# /etc/systemd/system/zfs-load-keys-usb.service
[Unit]
Description=Load ZFS keys from USB
After=zfs-import.target
Before=zfs-mount.service
ConditionPathExists=/dev/disk/by-label/ZFSKEYS
[Service]
Type=oneshot
ExecStartPre=/bin/mount /dev/disk/by-label/ZFSKEYS /mnt/keys
ExecStart=/sbin/zfs load-key -a
ExecStartPost=/bin/umount /mnt/keys
RemainAfterExit=yes
[Install]
WantedBy=zfs-mount.service
Strategy 3: Remote unlock via SSH (dropbear in initramfs)
For headless servers in a data center, embed a minimal SSH server (dropbear) in the initramfs. The server boots to initramfs, starts dropbear on a static IP, and waits for you to SSH in and enter the passphrase. Once unlocked, the boot continues normally.
# Install dropbear for initramfs (Debian/Ubuntu)
apt install dropbear-initramfs
# Configure authorized keys for initramfs SSH
echo "ssh-ed25519 AAAA... admin@workstation" >> /etc/dropbear/initramfs/authorized_keys
# Configure static IP for initramfs networking
# In /etc/initramfs-tools/initramfs.conf:
# IP=192.168.1.100::192.168.1.1:255.255.255.0::eth0:off
# Rebuild initramfs
update-initramfs -u
# On reboot, from your workstation:
ssh -p 22 root@192.168.1.100 # connects to dropbear in initramfs
# Then enter the passphrase to unlock the pool
zfs load-key -a
exit
# Boot continues normally
# For RHEL/CentOS/Rocky — use dracut-crypt-ssh or dracut-sshd
dnf install dracut-sshd
dracut -f
Strategy 4: Network keyserver (HTTP/HTTPS)
Set keylocation to an HTTPS URL. The dataset fetches its key from a network keyserver
at boot. This works well for automated environments where a central secrets manager (Vault, AWS KMS
proxy, etc.) serves keys over HTTPS.
# Set keylocation to a network URL
zfs set keylocation=https://vault.internal:8200/v1/zfs/keys/tank-secrets tank/secrets
# At boot, zfs load-key fetches the key via HTTPS
# The keyserver must be reachable before zfs-mount.service runs
# For Vault, a simple wrapper script:
#!/bin/bash
# /usr/local/bin/zfs-vault-unlock.sh
VAULT_TOKEN=$(cat /etc/vault-token)
for ds in $(zfs list -H -o name -r tank); do
keystatus=$(zfs get -H -o value keystatus "$ds")
if [ "$keystatus" = "unavailable" ]; then
curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
"https://vault:8200/v1/secret/data/zfs/$ds" | \
jq -r '.data.data.key' | \
zfs load-key "$ds"
fi
done
keylocation=file:// and a
systemd service. For laptops, use keylocation=prompt so you enter the passphrase at login.
For headless data center machines, use dropbear in initramfs or a network keyserver. The passphrase prompt
approach is the simplest but the worst for automation. Pick the strategy that matches your operational
reality — if nobody is sitting at the console, a passphrase prompt means the server hangs at boot
until someone SSHes in.
Performance impact of encryption
On any CPU with AES-NI (every Intel since 2010, every AMD since Bulldozer), ZFS encryption overhead is modest. The impact depends on the workload and storage speed.
| Workload | Storage type | Overhead | Notes |
|---|---|---|---|
| Sequential read/write | HDD | 1–3% | Disk is the bottleneck. CPU encrypts faster than HDD can feed data. |
| Sequential read/write | SATA SSD | 3–5% | Still mostly disk-bound. AES-NI keeps up easily. |
| Sequential read/write | NVMe | 5–15% | NVMe can saturate a CPU core. Multi-threaded workloads spread the load. |
| Random 4K IOPS | NVMe | 8–20% | Per-block encryption overhead adds up at high IOPS. Still fast in absolute terms. |
| Database (PostgreSQL) | NVMe mirror | 5–10% | Real-world database workloads are mixed. Encryption overhead is lost in the noise of query processing. |
| VM storage | NVMe mirror | 8–15% | VMs generate high random IOPS. Encryption adds per-IO overhead. Still acceptable for most workloads. |
# Verify AES-NI is available
grep -o aes /proc/cpuinfo | head -1
# aes
# If this returns nothing, your CPU lacks AES-NI.
# Encryption will use software fallback — expect 50-80% overhead.
# Every CPU made after ~2010 has AES-NI. If yours doesn't, it's time for new hardware.
# Benchmark: compare encrypted vs unencrypted dataset
zfs create tank/bench-plain
zfs create -o encryption=aes-256-gcm -o keyformat=passphrase -o keylocation=prompt tank/bench-enc
fio --name=test --filename=/tank/bench-plain/testfile --rw=write --bs=1M --size=4G --direct=1
fio --name=test --filename=/tank/bench-enc/testfile --rw=write --bs=1M --size=4G --direct=1
FIPS considerations
AES-256-GCM is a FIPS 140-2/140-3 approved algorithm. The algorithm itself meets federal cryptographic standards. However, the OpenZFS implementation of AES-256-GCM has not undergone CMVP (Cryptographic Module Validation Program) testing by an accredited laboratory. This distinction matters for compliance.
Algorithm: FIPS-approved
AES-256-GCM is listed in FIPS 197 (AES) and SP 800-38D (GCM). The cryptographic algorithm meets all federal requirements. No weakness in the math.
Implementation: not CMVP-validated
The OpenZFS kernel module's AES implementation has not been submitted to a CMVP lab for validation. It uses the kernel's crypto API (which may or may not be FIPS-validated depending on the distro), but the ZFS module itself is not a validated cryptographic module. For environments that require CMVP validation (FedRAMP, DoD IL4+, FISMA High), this is a gap.
Practical guidance
For internal compliance, SOC 2, HIPAA, PCI-DSS: ZFS encryption with AES-256-GCM satisfies the "encryption at rest" requirement. Auditors care about the algorithm and key management, not CMVP validation. For federal contracts requiring FIPS 140-2 Level 1+: layer LUKS (which has CMVP-validated implementations on RHEL/Ubuntu) underneath ZFS, or use ZFS encryption with a risk acceptance memo.
Backup implications
Encryption changes how you think about backups. There are two fundamentally different approaches, and picking the wrong one can leave you with unrecoverable backups.
-w)# Option A: Raw send — backup stays encrypted, receiver can't read it
zfs send -w rpool/secrets@daily | ssh backup "zfs recv tank/offsite/secrets"
# Restore requires the original key
# Option B: Regular send — decrypts at source, re-encrypts at destination
zfs send rpool/secrets@daily | ssh backup "zfs recv -o encryption=aes-256-gcm \
-o keyformat=raw -o keylocation=file:///backup/keys/secrets.key tank/offsite/secrets"
# Backup has its own independent key — survives loss of the original key
# Option C: Regular send to unencrypted backup (trusted destination)
zfs send rpool/secrets@daily | ssh backup "zfs recv tank/offsite/secrets"
# Backup is plaintext — fast restore, no key management
Converting unencrypted datasets to encrypted
You cannot encrypt an existing unencrypted dataset in place. There is no
zfs set encryption=on. The only path is to create a new encrypted dataset and
send/receive the data into it.
# Step 1: Snapshot the unencrypted dataset
zfs snapshot rpool/data@migrate
# Step 2: Create a new encrypted dataset
zfs create -o encryption=aes-256-gcm \
-o keyformat=passphrase \
-o keylocation=prompt \
rpool/data-encrypted
# Step 3: Send/receive into the encrypted dataset
# Note: send is a regular (non-raw) send because the source is unencrypted
zfs send rpool/data@migrate | zfs recv -F rpool/data-encrypted
# Step 4: Verify the data
diff -r /rpool/data /rpool/data-encrypted
# Step 5: Swap the datasets
zfs rename rpool/data rpool/data-old
zfs rename rpool/data-encrypted rpool/data
# Step 6: Update mountpoints if needed
zfs set mountpoint=/data rpool/data
# Step 7: After verification, destroy the old unencrypted dataset
zfs destroy -r rpool/data-old
Common pitfalls
Forgetting the passphrase
There is no recovery mechanism. No backdoor. No master key. If you forget the passphrase and have no backup of the key, the data is permanently gone. This is by design — if there were a recovery mechanism, it would not be encryption. Use a password manager.
Storing the key on the same pool
If keylocation=file:///tank/keys/secret.key and tank is the encrypted pool,
you have a chicken-and-egg problem: the key is inside the encrypted dataset it needs to unlock. Store keys
on a separate filesystem, a USB drive, or a remote keyserver.
Raw-sending backups without key escrow
Raw send preserves encryption. If the source machine dies and the key is lost, the backups are unreadable. Always store a copy of the key (or passphrase) separate from the encrypted data and separate from the machines being backed up.
Thinking encryption hides metadata
Dataset names, sizes, properties, snapshot names, and creation timestamps are all visible without the key. If metadata is sensitive, use opaque dataset names and non-descriptive snapshot naming.
Expecting in-place encryption
You cannot add encryption to an existing dataset. The only path is send/receive to a new encrypted dataset. This requires temporary double storage. Enable encryption from day one.
Using keylocation=prompt on headless servers
A passphrase prompt on a server with no console means the server hangs at boot until someone SSHes into the initramfs or connects a crash cart. Use key files or network keyservers for servers.
Dedup on encrypted datasets (pre-OpenZFS 2.2)
Before 2.2, dedup tables leaked information about identical blocks across encrypted datasets. Only enable dedup on encrypted datasets if running OpenZFS 2.2 or later.
Confusing key rotation with re-encryption
zfs change-key changes the wrapping key, not the data encryption key. If an attacker
previously obtained both the old wrapping key and a copy of the encrypted blocks, changing the key
does not protect those blocks. True re-encryption requires send/receive.
Real-world scenarios
Compliance: HIPAA / PCI-DSS / SOC 2
Regulations require "encryption at rest" for sensitive data. ZFS encryption with AES-256-GCM
satisfies this requirement. Create encrypted datasets for PHI, cardholder data, or customer PII.
Use separate keys per data classification. Document the encryption properties (zfs get encryption,keyformat)
for your auditor. Raw-send backups to demonstrate encrypted replication.
Laptop encryption
Encrypt /home with a passphrase. Leave the boot environment unencrypted. The user
enters the passphrase at login (or via ZFSBootMenu if the root dataset is encrypted). If the laptop
is stolen, the data is inaccessible without the passphrase. Suspend-to-RAM keeps keys in memory —
for paranoid security, use suspend-to-disk (hibernate) which drops keys from RAM.
# Laptop encryption setup
zfs create -o encryption=aes-256-gcm \
-o keyformat=passphrase \
-o keylocation=prompt \
-o pbkdf2iters=1000000 \
rpool/home/todd
# Higher PBKDF2 iterations: 1M iterations makes brute-force 3x slower
# than the default 350K, at the cost of ~0.5 seconds longer unlock time.
# Worth it for a device that might be physically stolen.
Multi-tenant isolation
A hosting company creates per-client encrypted datasets, each with a separate raw key.
tank/clients/acme is encrypted with Acme's key. tank/clients/globo with theirs.
Even root on the host machine cannot read a client's data without their key. Snapshots are retained
for years. Backups replicated offsite with raw send — the backup operator cannot read any client's data.
# Per-tenant setup
for client in acme globo initech; do
dd if=/dev/urandom of=/keys/${client}.key bs=32 count=1 2>/dev/null
chmod 000 /keys/${client}.key
zfs create -o encryption=aes-256-gcm \
-o keyformat=raw \
-o keylocation=file:///keys/${client}.key \
tank/clients/${client}
done
# Client offboarding: unload key, destroy dataset
zfs unload-key tank/clients/initech
zfs destroy -r tank/clients/initech
shred -u /keys/initech.key
Key escrow pattern
For enterprise environments, store a recovery copy of each encryption key in a separate, secure location. This is critical when using raw send for backups — without key escrow, losing the primary machine means losing access to all backups.
# Key escrow workflow
# 1. Generate key
dd if=/dev/urandom of=/tmp/dataset.key bs=32 count=1 2>/dev/null
# 2. Store primary copy on the machine
cp /tmp/dataset.key /root/keys/dataset.key
chmod 000 /root/keys/dataset.key
# 3. Escrow copy to Vault (or any secrets manager)
cat /tmp/dataset.key | base64 | vault kv put secret/zfs-keys/dataset key=-
# 4. Escrow copy to physical safe (printed hex)
xxd -p -c 64 /tmp/dataset.key # print this, store in safe
# 5. Destroy the temp copy
shred -u /tmp/dataset.key
# 6. Create the encrypted dataset
zfs create -o encryption=aes-256-gcm \
-o keyformat=raw \
-o keylocation=file:///root/keys/dataset.key \
tank/sensitive-data
kldload encryption — one checkbox
In the kldload installer, encryption is a single option. The web UI offers a dropdown: No encryption (default) or Encrypted — AES-256-GCM, passphrase at boot. When you select encrypted, you enter a passphrase, and the installer creates the ZFS pool with encryption enabled on the root dataset. All child datasets inherit encryption automatically.
Under the hood, the installer sets three properties on the root dataset:
# What kldload does when you select "Encrypted":
zpool create ... \
-O encryption=aes-256-gcm \
-O keyformat=passphrase \
-O keylocation=prompt \
rpool mirror /dev/sda /dev/sdb
# The passphrase you entered in the web UI is used during pool creation.
# At boot, ZFSBootMenu or the initramfs will prompt for the passphrase.
# To change encryption settings post-install:
zfs change-key -o keylocation=file:///root/keys/rpool.key rpool # switch to key file
zfs change-key rpool # change passphrase
Quick reference — common commands
| Task | Command |
|---|---|
| Create encrypted dataset | zfs create -o encryption=aes-256-gcm -o keyformat=passphrase -o keylocation=prompt tank/secure |
| Check key status | zfs get keystatus tank/secure |
| Load key (unlock) | zfs load-key tank/secure |
| Load all keys | zfs load-key -a |
| Unload key (lock) | zfs unload-key tank/secure |
| Change passphrase | zfs change-key tank/secure |
| Switch to key file | zfs change-key -o keyformat=raw -o keylocation=file:///path/key tank/secure |
| Check encryption root | zfs get encryptionroot tank/secure |
| View all encryption props | zfs get encryption,keyformat,keylocation,keystatus,pbkdf2iters tank/secure |
| Raw send (encrypted) | zfs send -w tank/secure@snap | ssh remote "zfs recv pool/backup" |
| Incremental raw send | zfs send -w -i @snap1 @snap2 tank/secure | ssh remote "zfs recv pool/backup" |
| Generate raw key | dd if=/dev/urandom of=/path/key bs=32 count=1 |