3 of 3

Recovering File Systems and LVMLesson 3 of 3

LUKS headers, key slots and a file system that is 'full'

Back up and restore a LUKS header, manage key slots, and find whether full means blocks, inodes or deleted-but-open files.

  • cryptsetup
  • cryptsetup luksDump
  • cryptsetup luksHeaderBackup
  • cryptsetup luksHeaderRestore
  • cryptsetup luksAddKey
  • cryptsetup luksRemoveKey
  • /etc/crypttab
  • df
  • df -i
  • du
  • lsof
  • ls -l /proc/PID/fd

50 min3 exercises

After this lesson you can

  • Explain what the LUKS header holds, why losing it loses the data, and how to back it up and restore it.
  • Add and remove key slots safely and write a correct /etc/crypttab line.
  • Tell apart a file system that is out of blocks, out of inodes, or holding space in deleted files that are still open.

The idea

Two storage problems look nothing alike but share a lesson: the failure is in a small piece of metadata, not in the data. A LUKS volume is unreadable if its header is damaged, however intact the encrypted blocks are. A file system can report “no space” while most of its bytes are free, because space is not one resource.

LUKS in one page

LUKS (Linux Unified Key Setup) stores encryption metadata in a header at the start of the device. The header holds the cipher settings, a random master key encrypted separately by each of up to 32 (LUKS2) key slots, one per passphrase or key file, and a UUID. The data area follows the header. To open the volume, cryptsetup derives a key from your passphrase, uses it to decrypt a slot, gets the master key, and maps the data area as /dev/mapper/NAME.

Consequences you must remember:

  • The header is a single point of failure. If it is overwritten, nothing, not even the right passphrase, can recover the master key. A header backup is the only way back.
  • Passphrases are independent slots. Adding a passphrase adds a slot; removing one deletes only that slot. This is how you rotate passwords without re-encrypting.
  • Removing the last slot makes the data permanently unreadable. cryptsetup refuses without --force-style confirmation in many cases; do not rely on that.
CommandPurpose
cryptsetup luksDump DEVShow header: version, UUID, cipher, key slots
cryptsetup isLuks DEVExit status 0 if a LUKS header exists (script-friendly)
cryptsetup luksHeaderBackup DEV --header-backup-file FSave the header
cryptsetup luksHeaderRestore DEV --header-backup-file FWrite it back
cryptsetup luksAddKey DEV [keyfile]Add a passphrase or key file
cryptsetup luksRemoveKey DEVRemove the passphrase you type
cryptsetup open DEV NAME / close NAMEMap and unmap (luksOpen/luksClose are the old names)
cryptsetup open --test-passphrase DEVVerify a passphrase without opening

Opening at boot is controlled by /etc/crypttab, with one line per volume: NAME DEVICE(by UUID) keyfile-or-none options. The mount of the result is a normal /etc/fstab line using /dev/mapper/NAME. If you change either, systemctl daemon-reload and, for the root file system, rebuild the initramfs (dracut -f); see the earlier boot lessons.

Try it

All outputs here are real, from cryptsetup on an image file (a deliberately low iteration count was used to make the test quick; production volumes use the defaults).

The header

cryptsetup luksDump c.img | head -14
LUKS header information
Version:       	2
Epoch:         	3
Metadata area: 	16384 [bytes]
Keyslots area: 	16744448 [bytes]
UUID:          	d8bfff35-9d5c-4876-bd9b-78ded3c47389
Label:         	(no label)
Subsystem:     	(no subsystem)
Flags:       	(no flags)

Data segments:
  0: crypt
	offset: 16777216 [bytes]
	length: (whole device)

The data offset (16 MiB) is the size of the header area. That is also the size of the header backup file:

cryptsetup luksHeaderBackup c.img --header-backup-file hdr.bak
ls -l hdr.bak
-r-------- 1 root root 16777216 Nov  4 17:11 hdr.bak

Destroy and restore the header

dd if=/dev/zero of=c.img bs=1M count=1 conv=notrunc
cryptsetup luksDump c.img | head -3
cryptsetup isLuks c.img; echo isLuks=$?
cryptsetup luksHeaderRestore c.img --header-backup-file hdr.bak -q
cryptsetup isLuks c.img; echo isLuks=$?
Device c.img is not a valid LUKS device.
isLuks=1
isLuks=0

(The dd summary line is omitted; the first command printed no luksDump header because the device is no longer LUKS.) One mebibyte of zeros erased the header and the volume was unreadable until the backup went back. In production, store the backup off the machine, protected as strictly as the passphrase.

Key slots

printf 'labpass\nsecond-pass\n' | cryptsetup luksAddKey c.img
cryptsetup luksDump c.img | grep -E '^\s+[0-9]+: luks2'
  0: luks2
  1: luks2

Two slots now. Test the new passphrase with cryptsetup open --test-passphrase, and only then luksRemoveKey the old one. The order matters: remove first and you may be left with a passphrase you mistyped.

A file system that is “full”

No space left on device has several causes. Check them in this order.

CheckCommandMeaning when exhausted
Blocksdf -h /mnt/tData blocks used up (including reserved blocks for root on ext4)
Inodesdf -i /mnt/tToo many files; blocks may be free
Deleted but open filesls -l /proc/PID/fd, lsof +L1Space is held until the last process closes the file
Quotasrepquota -a, xfs_quota -x -c reportPer-user or per-project limit, the file system itself is not full
Thin poolslvs -o +data_percent,metadata_percentA thin LV looks free but the pool is full

Inodes

Real output: an ext4 file system created with only 208 inodes, after a loop of empty files:

touch m/x
df -h m | tail -1
df -i m
touch: cannot touch 'm/x': No space left on device
/dev/loop0       60M   24K   56M   1% /tmp/fsr/m
Filesystem     Inodes IUsed IFree IUse% Mounted on
/dev/loop0        208   208     0  100% /tmp/fsr/m

The error message is the same as for full blocks, while df -h shows 1 % used. Only df -i shows the cause. Remove the many small files (find the directory: for d in /var/*; do echo "$(find "$d" | wc -l) $d"; done | sort -n | tail). On XFS inodes are allocated dynamically, so this is mostly an ext4 problem, but a fixed-size file system created for millions of small files with the wrong -i option still hits it.

Deleted but still open

A 40 MiB file was written, a process opened it, then the file was deleted:

df -h m | tail -1
ls -l /proc/$P/fd | grep deleted
kill $P
df -h m | tail -1
/dev/loop0       60M   41M   16M  73% /tmp/fsr/m
lr-x------ 1 root root 64 Nov  4 17:11 0 -> /tmp/fsr/m/big (deleted)
/dev/loop0       60M   24K   56M   1% /tmp/fsr/m

du found nothing in the directory, but df still counted 41 MiB, because the process kept the file open. Ending the process (or restarting the service that owns it) released the space immediately. This is a typical result of deleting a huge log file while the daemon still writes to it. Better: truncate in place (: > /var/log/big.log) or use logrotate, which signals the service to reopen its file.

Going further

Why XFS stays quiet about inodes. XFS creates inodes as needed, bounded by imaxpct (a percentage of space). Beyond the exam: LUKS also supports a detached header on separate media, token-based unlocking (TPM2, network-bound with Clevis and Tang), and systemd-cryptenroll. A header backup protects against accidental damage; it does not replace backups of the data itself.

Practice

0 of 3 done

Do these on your own system. They get harder. Check each result, then tick it off.

  1. Warm-up

    Back up the header, then read it

    On a LUKS device (a scratch partition or an image file), print the header, list which key slots are used, and write a header backup to a file outside the encrypted device. Set safe permissions on it.

    Check your result
    • sudo cryptsetup luksDump DEVICE shows Version: 2, a UUID, and a Keyslots: section with 0: luks2.
    • The backup file exists with mode 600 and size equal to the LUKS header (16 MiB for LUKS2 by default).
    Hint

    cryptsetup luksHeaderBackup DEVICE --header-backup-file FILE. The backup is as sensitive as the header: anyone with the backup and a passphrase for that header can open the volume.

    Solution
    truncate -s 64M /root/c.img
    echo -n "labpass" | sudo cryptsetup luksFormat --batch-mode /root/c.img -
    sudo cryptsetup luksDump /root/c.img | sed -n '1,8p;/^Keyslots/,/^Tokens/p'
    sudo cryptsetup luksHeaderBackup /root/c.img --header-backup-file /root/c.hdr
    sudo chmod 600 /root/c.hdr; ls -l /root/c.hdr
  2. Practice

    A destroyed header

    Using the image from the previous exercise: overwrite the first mebibyte with zeros, show that cryptsetup isLuks now fails, restore the header from your backup, and show that it succeeds. Then add a second passphrase, confirm two key slots, and remove the first passphrase only after you are sure the second works.

    Check your result
    • sudo cryptsetup isLuks /root/c.img; echo $? prints 1 after the damage and 0 after the restore.
    • luksDump lists key slots 0 and 1 after luksAddKey; after removal only 1 remains.
    • You tested the second passphrase (for example with cryptsetup open --test-passphrase) before removing the first.
    Hint

    luksHeaderRestore asks for confirmation; -q skips it in scripts. luksAddKey asks for an existing passphrase and then the new one. cryptsetup open --test-passphrase DEVICE checks a passphrase without opening the device.

    Solution
    sudo dd if=/dev/zero of=/root/c.img bs=1M count=1 conv=notrunc
    sudo cryptsetup isLuks /root/c.img; echo "isLuks=$?"
    sudo cryptsetup luksHeaderRestore /root/c.img --header-backup-file /root/c.hdr -q
    sudo cryptsetup isLuks /root/c.img; echo "isLuks=$?"
    sudo cryptsetup luksAddKey /root/c.img
    sudo cryptsetup luksDump /root/c.img | grep -E '^\s+[0-9]+: luks2'
    sudo cryptsetup open --test-passphrase /root/c.img && echo second ok
    sudo cryptsetup luksRemoveKey /root/c.img
  3. Challenge

    df says full. What is really full?

    Setup (on a small scratch file system mounted at /mnt/t, for example an ext4 image with only 200 inodes):

    truncate -s 64M /root/f.img && mkfs.ext4 -q -N 200 /root/f.img
    sudo mkdir -p /mnt/t && sudo mount -o loop /root/f.img /mnt/t
    sudo bash -c 'for i in $(seq 1 300); do : > /mnt/t/f$i 2>/dev/null || break; done'

    A user reports No space left on device, yet df -h shows almost everything free. Show with commands what is exhausted. Then do the second case: fill the file system with one big file, keep a process reading it, delete the file, and show that df still reports it as used while du finds nothing. Free the space without rebooting.

    Check your result
    • Case 1: df -i /mnt/t shows IUse% 100% while df -h /mnt/t shows 1% used.
    • Case 2: ls -l /proc/PID/fd shows (deleted); after you end that process, df -h drops.
    • You can say why du and df differ: du walks directories, df asks the file system.
    Hint

    Three kinds of "full": blocks (df -h), inodes (df -i) and space held by deleted files that a process still has open (lsof +L1 or /proc/PID/fd). Quotas and reserved blocks (tune2fs -l) are the fourth and fifth.

    Solution
    df -h /mnt/t | tail -1; df -i /mnt/t | tail -1
    sudo rm -f /mnt/t/f*
    sudo dd if=/dev/zero of=/mnt/t/big bs=1M count=40
    sleep 1000 < /mnt/t/big & P=$!
    sudo rm /mnt/t/big
    df -h /mnt/t | tail -1
    sudo du -sh /mnt/t
    ls -l /proc/$P/fd | grep deleted
    kill $P; sleep 1; df -h /mnt/t | tail -1