| pick your distro, get ZFS on root
kldload — your platform, your way, free
Source
← Back to ZFS Overview

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 encryption is the only filesystem-level encryption I know of that lets you replicate data to an untrusted machine without decrypting it first. LUKS can't do this. BitLocker can't do this. FileVault can't do this. The 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.
LUKS is mature and well-integrated into every Linux boot chain. If you are using ext4 or XFS, LUKS is the right choice. But if you are already on ZFS, layering LUKS underneath it is wasteful. ZFS encryption gives you everything LUKS does plus per-dataset keys, raw send, and compression-before-encryption. The only real advantage LUKS has is deeper GRUB integration for encrypted root — and ZFSBootMenu closes that gap.

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.

aes-256-gcm
Default. Use this. Galois/Counter Mode. Hardware-accelerated on all modern CPUs with AES-NI. Highest throughput. The standard for TLS, IPsec, and cloud storage. Parallelizable — scales across CPU cores.
aes-256-ccm
Counter with CBC-MAC. Slightly slower than GCM because the MAC computation is sequential. No practical advantage over GCM on modern hardware. Exists for compatibility. Use GCM unless you have a specific reason.
# 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.

encryption
The algorithm: aes-256-gcm (default), aes-256-ccm, or off. Set at creation. Cannot be changed. Cannot be added to an existing unencrypted dataset.
keyformat
How the key is provided: passphrase (human-readable, PBKDF2-stretched), raw (32-byte binary file), or hex (64 hex characters). Can be changed via zfs change-key -o keyformat=....
keylocation
Where the key lives: 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.
pbkdf2iters
Number of PBKDF2 iterations for passphrase stretching. Default is 350,000. Higher = slower brute force but slower unlock. Set to 1,000,000+ for high-security environments. Only applies to 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
Raw and hex keys skip PBKDF2. If an attacker gets the key file, they have immediate access — no passphrase stretching slows them down. Protect key files with strict file permissions (mode 000, root-only), encrypted storage, or a hardware security module. Never store key files on the same pool they encrypt.

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
The 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
Key rotation does not re-encrypt data. The underlying AES key that encrypted the blocks is unchanged. If an attacker previously obtained the old wrapping key and a copy of the encrypted blocks, key rotation does not help. True data re-encryption requires 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.

The metadata leakage is the #1 thing people miss about ZFS encryption. Your file contents are safe, but an attacker with pool access can see dataset names, sizes, snapshot names, all properties, and creation timestamps. For most use cases this is fine. For adversarial threat models (seized hardware, hostile cloud provider), pair ZFS encryption with opaque dataset names and non-descriptive snapshot naming. Or layer LUKS underneath ZFS for full block-level opacity — but you lose raw send.

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:

Offsite backup
Replicate to an untrusted remote server. The backup operator cannot read your data. No VPN or transport encryption needed (though you should still use SSH).
Cloud replication
Send encrypted snapshots to a cloud VM. The cloud provider cannot inspect your data at rest. Your key stays on-premises.
Multi-site DR
Replicate between data centers. Each site has its own encrypted copy. Keys are managed centrally — no key distribution problem.
Partner data sharing
Send encrypted datasets to a partner. They store your backup but cannot access the contents. You retain the key and can retrieve the data at any time.
# 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"
Raw send limitations: You cannot change encryption properties during raw receive. The destination dataset inherits the source's encryption, key format, and algorithm exactly. You cannot raw-send an encrypted dataset and receive it as unencrypted — that would require a regular (non-raw) send, which requires the key to be loaded on the sender.

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  -
This is a big deal. With LUKS, you have to layer LVM compression below LUKS to get any compression benefit, and most people don't bother. With ZFS, you get compression for free because the order is correct by design. Enable 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
For servers, use key files with 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.

In practice, every auditor I have worked with accepts AES-256-GCM as meeting "encryption at rest" requirements. The CMVP validation gap only matters in US federal environments that explicitly require FIPS 140-2 validated modules. If your auditor asks "is the data encrypted at rest with AES-256?" the answer is yes. If they ask "is the cryptographic module CMVP-validated?" the answer is no, and you need to have that conversation.

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.

Raw send (-w)
Preserves encryption. Backup is encrypted. Receiver cannot read it. You need the original key to restore. If you lose the key, the backup is useless. Use for untrusted backup destinations.
Regular send
Decrypts on send. Key must be loaded on the sender. Data arrives in plaintext on the receiver (or can be re-encrypted with a different key on receive). Use when the backup destination is trusted and you want key independence.
# 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
The key escrow problem: If you use raw send for backups, you MUST have a separate copy of the encryption key stored outside the encrypted pool. If the only copy of the key is on the machine that failed, and your backups are raw-sent, the backups are permanently unreadable. Store keys in a password manager, a separate physical safe, or a key management service.

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
This is the #1 reason to enable encryption from day one. The send/receive migration works, but it requires enough free space to hold two copies of the data simultaneously, and it takes time proportional to the dataset size. On a 10TB dataset, that is a serious operational window. Plan ahead.

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.

Auditor asks "is customer data encrypted at rest?" You answer: "AES-256-GCM, per-dataset keys, passphrase-wrapped with 1M PBKDF2 iterations, replicated encrypted to offsite backup." That is a complete answer.

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
Even root on the host cannot read a client's data without their key. The client's data is theirs alone.

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
The kldload installer intentionally keeps encryption simple: one toggle, one passphrase, AES-256-GCM on the root dataset. You can always add per-dataset encryption with different keys after install — the installer just gets you to a working encrypted root. Post-install, you have full access to all the ZFS encryption features documented on this page. Start simple, customize later.

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