dm-crypt & LUKS
Full-disk encryption the Linux way
Two Levels: dm-crypt and LUKS
Linux full-disk encryption is built from two independent layers that are almost always used together, but it's critical to understand them as separate pieces. Conflating them is the source of most confusion when you read cryptsetup man pages or debug an unlock failure.
The crypto engine. A device-mapper target (see Storage 04 — Device Mapper) that sits between a backing block device and a virtual one, transparently encrypting every sector that passes through.
- Lives entirely in the kernel (
dm-crypt.ko) - Stores no metadata on disk
- Needs to be told: cipher, mode, key size, hash, IV mode, and the key itself — every time
- Produces
/dev/mapper/<name>— a regular block device
A standard disk-resident metadata format that sits on top of dm-crypt. LUKS (Linux Unified Key Setup) defines how to store cipher parameters and multiple encrypted copies of a master key in a header at the start of the device.
- Pure userspace spec, implemented by
cryptsetup - Header is on the disk — self-describing
- Multiple key slots → multiple passphrases / keyfiles unlock the same volume
- When you "open" a LUKS device,
cryptsetupreads the header and configures dm-crypt for you
Mental Model
dm-crypt is the encryption engine. LUKS is the manifest that tells the engine how to run and holds the encrypted keys. When you unlock a LUKS volume:
cryptsetupreads the LUKS header from the disk- It uses your passphrase (through a KDF) to decrypt the master key
- It calls the kernel to create a dm-crypt mapping with that master key and the header's cipher parameters
- The master key lives only in kernel memory; the header on disk never holds it in plaintext
Plain dm-crypt: No Header, No Safety Net
You can use dm-crypt directly without LUKS. Because nothing is stored on the disk, you must remember every crypto parameter. Forget any of them — cipher, mode, key size, hash, IV — and the data is gone. There's no header to look at, no tool that can introspect the device.
# Plain mode — no metadata. You must never forget these params.
$ cryptsetup open --type plain \
--cipher aes-xts-plain64 \
--key-size 512 \
--hash sha256 \
/dev/sdX plainvol
Enter passphrase: ****
Warning
When to use plain mode: almost never. It exists for plausible deniability (the disk looks like random data — no header, no "this is encrypted" signal) and for special cases like encrypting swap with a random key at boot. For anything you care about keeping, use LUKS.
Why LUKS Exists
Problems LUKS Solves
- Self-describing volumes — you can plug a LUKS disk into any Linux system and it knows exactly how to attempt to decrypt it.
- Multiple unlock credentials — up to 8 (LUKS1) or 32 (LUKS2) key slots, each independently unlocking the same master key. Add a colleague's keyfile without reencrypting anything.
- Passphrase change without reencryption — the master key stays the same; only the slot that holds an encrypted copy of it changes.
- Cryptographic erase — destroying the 16 MiB header wipes the only decryptable copy of the master key. Faster than overwriting a 20 TB disk.
- Standard tooling —
cryptsetupis everywhere; systemd integrates via/etc/crypttabandsystemd-cryptsetup-generator.
Cipher Modes
The --cipher option takes a triple: <algorithm>-<mode>-<iv>. The kernel's crypto API assembles the corresponding transformation.
| Spec | Status | Notes |
|---|---|---|
aes-xts-plain64 |
Standard modern choice | XTS is a tweakable block mode specifically designed for disk encryption. It preserves sector length (ciphertext size == plaintext size). plain64 means the IV / tweak is the 64-bit sector number. AES hardware accelerated on any modern CPU (AES-NI). |
aes-cbc-essiv:sha256 |
Legacy | Old LUKS1 default. ESSIV (Encrypted Salt-Sector IV) derives per-sector IVs from SHA-256(master_key) to defeat watermarking attacks that plain CBC was vulnerable to. Still secure if used correctly, but XTS is preferred for new setups. |
serpent-xts-plain64 |
Alternative | Serpent cipher instead of AES. Conservative margin against future AES attacks, but no hardware acceleration — much slower. Rarely used outside of paranoid / AES-distrusting setups. |
twofish-xts-plain64 |
Alternative | Another AES-alternative cipher. Similar profile to Serpent. Rarely used. |
Key Sizes
For XTS mode, the "key" is split into two halves: one for encryption, one as the tweak key. That's why AES-256 in XTS takes a --key-size 512: two 256-bit halves = 512 bits total.
| Option | Actual AES strength | Notes |
|---|---|---|
--key-size 256 with XTS | AES-128 | Two 128-bit halves. |
--key-size 512 with XTS | AES-256 | Two 256-bit halves. Default and recommended. |
--key-size 256 with CBC | AES-256 | CBC uses the whole key for encryption. |
Sector-Level Encryption
dm-crypt encrypts each disk sector (typically 512 B or 4 KiB) independently. The IV / tweak for sector N is derived from N itself (with plain64, it literally is N). This has important consequences:
- Random access — reading sector 1 000 000 decrypts only that sector, not everything before it.
- No compression / integrity by default — ciphertext is the same size as plaintext. For integrity, layer dm-integrity (see below).
- Identical plaintext in two sectors yields different ciphertext — because the tweak differs.
- Identical plaintext written to the same sector twice yields the same ciphertext — an attacker with snapshots can see which sectors changed. This is inherent to disk encryption; the only mitigation is authenticated encryption with a nonce (dm-integrity + dm-crypt).
LUKS1 vs LUKS2
LUKS2 is the default on all modern distros (cryptsetup ≥ 2.1, ~2018). You'll only see LUKS1 on older systems or when explicitly requested for compatibility.
| Aspect | LUKS1 | LUKS2 |
|---|---|---|
| Header location | Fixed 1 MiB at start of device, binary layout | Flexible JSON metadata, 16 MiB by default, with a secondary backup copy in the header area |
| KDF (passphrase → slot key) | PBKDF2 (SHA-256/SHA-512) | Argon2id by default — memory-hard, resistant to GPU/ASIC brute force |
| Key slots | 8 fixed slots | Up to 32 slots (practical limit; header size scales) |
| Tokens | Not supported | Yes — pluggable unlock methods (TPM2, FIDO2, systemd, clevis pins) stored as JSON token objects referencing a key slot |
| Online reencryption | Offline only (cryptsetup-reencrypt) |
Yes — change cipher / master key while the device is mounted and in use. Crash-safe and resumable. |
| Header integrity | No checksum on metadata | Metadata is checksummed; updates are atomic (primary + backup) |
| AEAD (dm-integrity + dm-crypt) | Not supported at format time | --integrity option at format |
| Anti-forensic splitter | AFsplitter on key material (explicit) | Equivalent protection via JSON key-material segments |
Tip
Converting LUKS1 → LUKS2: cryptsetup convert --type luks2 /dev/sdX. Backup the header first. Some features (e.g. tokens) require LUKS2, so this is usually worth doing on older installs.
Master Key and Key Slots
This is the most important concept in LUKS. Once it clicks, most of the tooling makes sense.
Life of a Master Key
- When you run
luksFormat,cryptsetupgenerates a random master key from/dev/urandom. This is the actual AES-XTS key that encrypts every sector. - The master key is never stored on disk in plaintext. It lives in kernel memory while the device is open, and disappears when it's closed.
- Each key slot stores an encrypted copy of the master key. The slot's encryption key is derived from a passphrase (or keyfile) via the KDF (PBKDF2 for LUKS1, Argon2id for LUKS2).
- To open: passphrase → KDF → slot key → decrypt slot → master key → dm-crypt.
Unlock Flow
Passphrase
User input or keyfile bytes
KDF
Argon2id / PBKDF2
memory + time costSlot key
Decrypts slot payload
Master key
Loaded into kernel
dm-crypt active
/dev/mapper/name
Consequences
- Changing a passphrase re-encrypts only that slot. The master key doesn't change, so the disk itself isn't rewritten.
luksChangeKeyis instant even on a 20 TB disk. - Adding a user is zero-cost.
luksAddKeyfills an empty slot with the same master key, encrypted under a new passphrase. - Removing a passphrase is cheap.
luksRemoveKeyorluksKillSlotoverwrites only the slot, not the payload. - The header is the single point of failure. No header, no decryptable master key, no data. This is a feature for cryptographic erase — and a disaster if the header is lost to disk corruption.
Warning
Backing up the LUKS header is critical. A failed write to the first 16 MiB of a 20 TB disk can leave the payload intact but the master key unreadable forever. Always run luksHeaderBackup after format and keep the backup offsite.
Anti-Forensic Splitter (AFsplitter)
A subtle but clever LUKS feature. Without AF, each key slot would contain ~64 bytes of ciphertext — the encrypted master key. If an attacker could recover any sector of the header from, say, a scrubbed platter or an SSD's reallocation pool, they'd have a cracking target.
AFsplitter "stretches" the encrypted master key across many disk blocks (thousands) by mixing it with random data using a diffusion hash. The full block must be present to reconstruct the key; losing even one byte of the stretched material makes reconstruction infeasible.
- LUKS1: AFsplitter is the explicit mechanism (part of the on-disk spec).
- LUKS2: JSON metadata references "keyslots segments" which serve the equivalent role.
cryptsetup Command Walkthrough
Format a new device
# Simple — distro defaults (LUKS2, argon2id, aes-xts-plain64 on modern cryptsetup)
$ cryptsetup luksFormat /dev/sdX
WARNING!
========
This will overwrite data on /dev/sdX irrevocably.
Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for /dev/sdX: ****
Verify passphrase: ****
# Explicit — pin every parameter for reproducibility
$ cryptsetup luksFormat \
--type luks2 \
--cipher aes-xts-plain64 \
--key-size 512 \
--hash sha512 \
--pbkdf argon2id \
--pbkdf-memory 1048576 \
--pbkdf-parallel 4 \
/dev/sdX
Open and close
# Open — creates /dev/mapper/cryptroot
$ cryptsetup open /dev/sdX cryptroot
Enter passphrase for /dev/sdX: ****
# Both forms work (luksOpen is the old alias)
$ cryptsetup luksOpen /dev/sdX cryptroot
# Verify
$ ls -l /dev/mapper/cryptroot
lrwxrwxrwx 1 root root 7 Apr 20 09:12 /dev/mapper/cryptroot -> ../dm-3
# Close
$ cryptsetup close cryptroot
Manage key slots
# Add a passphrase to the next free slot (prompts for existing + new)
$ cryptsetup luksAddKey /dev/sdX
# Add a keyfile to the next free slot
$ cryptsetup luksAddKey /dev/sdX /root/keys/data.key
# Remove a passphrase (by supplying it — asks which one)
$ cryptsetup luksRemoveKey /dev/sdX
# Force-remove a slot by number (destructive, no prompt for slot's key)
$ cryptsetup luksKillSlot /dev/sdX 2
# Change a passphrase in place (old passphrase → new passphrase, same slot)
$ cryptsetup luksChangeKey /dev/sdX
# Destroy ALL key slots (device becomes unopenable forever)
$ cryptsetup luksErase /dev/sdX
Inspect the header
$ cryptsetup luksDump /dev/sdX
LUKS header information
Version: 2
Epoch: 6
Metadata area: 16384 [bytes]
Keyslots area: 16744448 [bytes]
UUID: 3e2d6bc8-8d1c-4a51-9f2b-a9b07e3c21a0
Label: (no label)
Subsystem: (no subsystem)
Flags: (no flags)
Data segments:
0: crypt
offset: 16777216 [bytes]
length: (whole device)
cipher: aes-xts-plain64
sector: 512 [bytes]
Keyslots:
0: luks2
Key: 512 bits
Priority: normal
Cipher: aes-xts-plain64
Cipher key: 512 bits
PBKDF: argon2id
Time cost: 7
Memory: 1048576
Threads: 4
Salt: 62 4c 1e a8 7d ... 3b
AF stripes: 4000
AF hash: sha256
Area offset: 32768 [bytes]
Area length: 258048 [bytes]
Digest ID: 0
1: luks2
... (TPM2-sealed, smaller memory cost)
4: luks2
... (FIDO2-sealed)
Tokens:
0: systemd-tpm2
tpm2-hash-pcrs: 7
tpm2-pcrlock: false
tpm2-pubkey: (null)
Keyslot: 1
1: systemd-fido2
fido2-credential: Bq2...
Keyslot: 4
Digests:
0: pbkdf2
Hash: sha256
Iterations: 95320
Salt: a1 2f ...
Digest: fa 8c ...
Header backup and restore
# Backup the 16 MiB header to a file — do this after every slot change
$ cryptsetup luksHeaderBackup \
--header-backup-file /root/cryptroot-header.bin \
/dev/sdX
# Store the backup OFF the encrypted disk (USB, another host, offsite)
$ chmod 0400 /root/cryptroot-header.bin
# Restore (dangerous — overwrites current header)
$ cryptsetup luksHeaderRestore \
--header-backup-file /root/cryptroot-header.bin \
/dev/sdX
Warning
Header backups are sensitive. They contain the encrypted master key. An attacker with your header backup + a weak passphrase can brute-force offline, with unlimited parallelism. Treat header backup files with the same care as the passphrases themselves.
Layering: LUKS with LVM
Two common topologies. Both are valid; the choice is about granularity of encryption vs. operational simplicity.
RAID (mdraid or hardware)
└─ PV
└─ VG
├─ LV: root → LUKS → ext4
├─ LV: home → LUKS → ext4 (different key)
├─ LV: data → LUKS → xfs (keyfile-only)
└─ LV: swap → LUKS → swap (random key at boot)- Per-LV cipher choice and per-LV passphrase
- Can leave some LVs unencrypted (e.g.
/boot) - Each LV has its own
/dev/mapper/luks-<uuid>entry - More
crypttabentries, more unlock prompts
RAID (mdraid or hardware)
└─ partition
└─ LUKS (cryptroot)
└─ PV
└─ VG
├─ LV: root → ext4
├─ LV: home → ext4
├─ LV: data → xfs
└─ LV: swap → swap- Single passphrase unlocks the whole VG
- Ubuntu/Debian installer default
- Simpler for root / laptops
- All LVs share the same master key — no per-LV key rotation
- Growing the VG means growing the LUKS device (resize dance)
| Criterion | LUKS on LVM | LVM on LUKS |
|---|---|---|
| Number of passphrases at boot | One per LV (or keyfiles) | One |
| Per-LV encryption choice | Yes | No (all or nothing) |
| Key rotation granularity | Per-LV | Whole VG |
| Growing the pool | Add new PV, encrypt new LVs | Resize LUKS then PV then VG |
| Snapshots | LVM snapshots live above LUKS, unencrypted in memory but encrypted on disk when written | Same, but inside the single cryptdevice |
| Typical use | Servers with selective encryption | Laptops, full-disk encryption installers |
Cryptographic Erase
Because the master key is only recoverable from the header, destroying the header destroys the only path to the plaintext. This is orders of magnitude faster than overwriting the payload.
# Official: erase all LUKS key slots
$ cryptsetup erase /dev/sdX
# Brute force: overwrite the first 16 MiB with random data
$ dd if=/dev/urandom of=/dev/sdX bs=1M count=16 status=progress
# Verification — header is gone
$ cryptsetup luksDump /dev/sdX
Device /dev/sdX is not a valid LUKS device.
Tip
Why this matters for SRE: decommissioning a 20 TB disk by wiping 16 MiB takes seconds instead of hours. Combined with SED (self-encrypting drives) or TCG Opal, you can "instantly retire" disks at scale. Just be sure the header backup is also destroyed.
Online Reencryption (LUKS2)
LUKS2 can re-encrypt an in-use device while applications keep reading and writing. Useful for rotating the master key after a suspected compromise, or migrating from CBC to XTS.
# Rotate the master key (same cipher). Active and resumable.
$ cryptsetup reencrypt /dev/sdX
# Migrate cipher
$ cryptsetup reencrypt --cipher aes-xts-plain64 /dev/sdX
# Resume an interrupted reencryption (after crash or SIGINT)
$ cryptsetup reencrypt --resume-only /dev/sdX
Internally, LUKS2 keeps a reencryption journal in the header that records progress. If the system crashes mid-reencryption, the operation resumes from the last committed segment. No data loss, no "broken" state.
Authenticated Encryption: dm-integrity + dm-crypt
Plain dm-crypt gives you confidentiality but not integrity. An attacker with write access to the raw disk can flip ciphertext bits; after decryption, the corresponding plaintext bits are scrambled — but the filesystem above may not notice until files misbehave. Silent bit rot from failing disks has the same effect.
LUKS2 can layer dm-integrity underneath dm-crypt to store a per-sector authentication tag (HMAC or Poly1305), turning the stack into AEAD on a block device.
# Format with integrity (LUKS2 only)
$ cryptsetup luksFormat \
--type luks2 \
--cipher aes-gcm-random \
--integrity aead \
--key-size 256 \
/dev/sdX
| Aspect | Cost / Benefit |
|---|---|
| Detects bit rot | Yes — mismatched tag → I/O error bubbles up, filesystem sees a read failure |
| Detects tampering | Yes — attacker cannot forge sector + tag without the key |
| Write performance | ~20% penalty (extra tag write per sector) |
| Disk space overhead | ~1% for per-sector tags (16 bytes per 4 KiB sector) |
| Maturity | Production on modern kernels (≥ 4.12), widely used for sensitive data-at-rest |
Discard / TRIM on LUKS
SSDs rely on the filesystem sending TRIM commands to know which sectors are free, so the FTL can garbage-collect them. By default, dm-crypt does NOT pass TRIM through — every sector looks "in use" to the SSD, even if the filesystem has freed it. Over time, this hurts write amplification and wear leveling.
Enable pass-through with --allow-discards:
# Per-open (not persisted)
$ cryptsetup open --allow-discards /dev/sdX cryptroot
# In /etc/crypttab (persistent)
cryptroot UUID=... none luks,discard
Note
Trade-off: TRIM pass-through leaks information — an attacker with raw disk access can see which sectors are in use and which are free. Depending on filesystem usage patterns, this can reveal file size distributions. For SSDs in server fleets where the performance and longevity win is significant, most operators accept the leak. For highly adversarial threat models (seized laptop), you may want to disable discards.
Next: Automatic Unlock
This page covered the core of dm-crypt and LUKS. The companion page — LUKS Keys, crypttab & Auto-Unlock — covers the key-slot ecosystem (keyfiles, TPM2 tokens, FIDO2, Tang/Clevis NBDE), /etc/crypttab syntax, initramfs unlock, and headless remote unlock via dropbear.
TPM-based unlock in particular is fleshed out in Storage 07 — TPM & Secure Boot (forward reference — covers PCRs, sealing, and Measured Boot).