Disk encryption and boot integrity
LUKS2, unlocking, Secure Boot and lockdown.
Disk encryption answers a threat the controls so far do not touch: someone who has the disk itself, not a login. A stolen laptop, a decommissioned drive that skipped wiping, a cloud volume snapshot, a backup tape. None of your file permissions or service sandboxes matter once an attacker reads the blocks directly. Be precise about the scope, though: this lesson encrypts a secondary data volume while the root filesystem stays unencrypted, so /etc/shadow, the SSH host keys, the systemd credential key and the logs are still readable from a stolen root disk. Encrypting the root filesystem is an install-time choice. In a cloud the provider usually encrypts volumes at rest already; LUKS adds protection against a stolen snapshot only if its key is outside that cloud account's reach. On Linux the standard answer is LUKS, and boot integrity (Secure Boot, kernel lockdown, signed modules) is the matching control for tampering with the code that runs before anything is unlocked. In this lesson you build a LUKS2 volume, add and remove keys, arrange for it to unlock at boot, and read the Secure Boot and lockdown state from the machine rather than assuming it. Everything runs against a file, so no real disk is touched.
How LUKS protects data
LUKS (Linux Unified Key Setup) encrypts a whole block device. The important design point is that your passphrase never encrypts the data directly. The data is encrypted with a single random volume key that is generated once and never changes. That volume key is itself encrypted and stored in one of several keyslots, each unlocked by a different passphrase or key. Type a passphrase, LUKS uses it to decrypt the volume key from a slot, and the volume key decrypts the data.
This is why rotating a passphrase is instant: you add a slot with the new passphrase and remove the old slot, and the terabytes of data underneath are untouched. It is also why losing every keyslot means losing the data outright. There is no reset.
Build a LUKS2 volume
Work on a file with a loop device instead of a real partition. cryptsetup luksFormat writes a fresh LUKS2 header and its first keyslot. The passphrase here comes from a file so the lab is unattended; on a real system you type it. Keep any such key file tight, mode 600 and owned by you, or cryptsetup and systemd will warn.
luksDump reads the header back. The default on cryptsetup 2.8 is LUKS2 with the argon2id key-derivation function, the current recommendation because it is deliberately slow and memory-hard: the header records a time cost and a memory cost that an attacker must spend on every guess. Here the dump shows Time cost: 29 and Memory: 257170 (KiB, about 250 MiB). These are not fixed defaults: cryptsetup benchmarks the machine when it creates a keyslot and picks costs that make one unlock take about two seconds there, and a benchmarked memory cost always lies between 64 MiB and 1 GiB (cryptsetup-luksFormat(8)). The result depends on the machine and on how busy it is at that moment, so each keyslot records its own values; slot 2 below, added later on the same busy lab VM, got 184274 KiB. That memory hardness is what makes a stolen header expensive to brute-force.
open decrypts the volume key with the passphrase and creates a decrypted view of the device under /dev/mapper. From there it is an ordinary block device: put a filesystem on it, mount it, use it. cryptsetup status confirms the cipher and that the key lives in the kernel keyring.
The encryption is transparent above /dev/mapper and total below it. Write a recognisable string through the mapper, then search for it: it is there in the decrypted view and absent from the backing file, where only ciphertext lives.
grep finds one match through the mapping and none in the raw image (its exit status 1 means "no match", which is the point). Closing the mapping removes the decrypted view; the data is inaccessible again until the next open.
Keyslots: recovery keys and rotation
A single passphrase is a single point of failure. Add a second credential so a forgotten passphrase is not a lost disk. systemd-cryptenroll --recovery-key generates a high-entropy key, prints it once, and stores it in a new slot. Write it down offline; it is never shown again.
Rotating a passphrase is add-then-remove. luksAddKey puts the new passphrase in a free slot, then luksKillSlot destroys the old one. Do it in that order, and verify the new one works before killing the old, so a typo never locks you out.
The slot list shows the result: slot 0, the old passphrase, is gone, and slots 1 (recovery) and 2 (the new passphrase) remain. The old passphrase is now refused, and the new one works. cryptsetup returns exit status 2 for a passphrase that matches no slot.
cryptsetup luksHeaderBackup saves the header and keyslots to a file, which is worth having because a corrupted header makes the data unrecoverable. But that backup plus a passphrase valid at backup time can decrypt the data even after you change or remove that passphrase on the disk. Store header backups as carefully as the disk itself, and remember that wiping the on-disk header no longer guarantees the data is unreadable if a backup survives.Unlocking at boot
A server reboots unattended, so something must supply a key without a person at the console. The options trade convenience against where the trust sits. A key file on the root filesystem (referenced from /etc/crypttab) unlocks a secondary volume automatically, but it only protects that volume against theft of the volume alone, since the key travels with the root disk.
A TPM (a chip on the mainboard) can hold the key instead, but only the options decide what it checks. systemd-cryptenroll --tpm2-device=auto on its own binds the key to no boot measurements at all; systemd-cryptenroll(1) states that no PCRs is the default. The TPM then releases the key to anything that boots on that board, a USB stick included, and protects only against the disk leaving the machine. Add --tpm2-pcrs=7 (plus 14 when shim's Machine Owner Keys are used), as the man page suggests, to bind the key to the Secure Boot state and certificates. The cost is operational: PCR 7 changes when the firmware's certificate lists change, a Secure Boot dbx update included, and a headless server then stops at a passphrase prompt. So enrol a recovery key and make sure you have console access before you enrol a TPM, and treat firmware and bootloader updates as a re-enrolment event.
On RHEL the Clevis/Tang model (NBDE, network-bound disk encryption) releases the key only when the machine can reach a Tang server on the trusted network. If that server is down, no host bound to it can boot unattended, so run at least two Tang servers and bind with Clevis's sss (secret sharing) policy. This lab VM has no TPM, so these bindings are described, not demonstrated.
The key-file case is worth doing because it shows the mechanism and its cost. Enrol a random key file, store it root-owned and mode 400 in /etc/cryptsetup-keys.d, and add a crypttab line. A 512-byte random key has all the strength it needs in the key itself, so its slot does not need argon2id's memory cost, which would spend seconds of CPU and hundreds of MiB of memory at every boot (and can fail in a small initramfs). --pbkdf pbkdf2 --pbkdf-force-iterations 1000 gives it a cheap slot; systemd-cryptenroll made the same choice for the recovery key (slot 1 shows pbkdf2 with 1000 iterations in the dump). Mark the slot prefer with cryptsetup config as well: cryptsetup-config(8) says preferred slots are tried before normal ones. Without that, an unlock can try an argon2id slot first and pay its full cost before it reaches the cheap one. noauto keeps the line out of the boot sequence for the lab; a real secondary volume omits it so systemd unlocks it at boot.
# name backing device key file optionshard_diskcrypt /var/tmp/hard-diskcrypt.img /etc/cryptsetup-keys.d/hard_diskcrypt.key luks,noauto
systemd turns each crypttab line into a systemd-cryptsetup@NAME service. Starting it unlocks the volume exactly as boot would; with the cheap, preferred slot the status shows CPU: 231ms and Mem peak: 2.4M for the whole unlock.
To roll the setup back, stop the unit (which closes the mapping), delete the crypttab line and the key file, and reload systemd so the generated unit disappears. Remove the key file's slot as well with cryptsetup luksKillSlot if the volume stays in use. (An image in /var/tmp, as here, is also removed by systemd-tmpfiles after 30 days unused, so a real volume never belongs there.)
The operational reality is the point of this section. A LUKS volume that needs a typed passphrase cannot come back from a reboot on its own, so a headless server needs the TPM, Tang or key-file path, each of which weakens the stolen-disk protection in some way. And key loss is data loss: no keyslot, no recovery key, no header backup means the volume is gone. Plan the recovery path before you encrypt anything you cannot afford to lose.
Boot integrity: read it, do not assume it
Encryption protects data at rest; it does nothing about tampering with the code that runs before the disk is unlocked. Two kernel features raise the cost of that. Secure Boot checks that the bootloader and kernel carry a trusted signature, and kernel lockdown then restricts even root from routes that would read or rewrite the running kernel (/dev/mem, unsigned modules, kexec). Both must be read from the machine, because their state depends on the firmware, not on the distribution.
On this VM the firmware reports no Secure Boot, and /sys/kernel/security/lockdown shows [none]: lockdown is compiled in but not engaged, so nothing above is enforced yet. On a physical server with Secure Boot on, lockdown moves to integrity automatically. mokutil --sb-state (with the shim bootloader that Ubuntu and RHEL use for Secure Boot) reports the state and manages the Machine Owner Keys that let you trust modules you build yourself.
Module signing is the related control the modules lesson builds on. Ubuntu's kernel is built with CONFIG_MODULE_SIG=y and its modules are signed, but MODULE_SIG_FORCE is not set, so an unsigned module still loads unless enforcement is switched on: by lockdown (engaged through Secure Boot) or by module.sig_enforce=1 on the kernel command line (the kernel's module-signing documentation). You can confirm a shipped module is signed:
So the accurate summary for a default cloud image is: signing infrastructure present, enforcement off until Secure Boot engages lockdown or the kernel command line sets module.sig_enforce=1. Turning that on is a firmware and bootloader exercise beyond one host's config file, but knowing how to read the state is the first step, and it is what an auditor asks for.
On RHEL 10
cryptsetup and systemd-cryptenroll work the same way, and LUKS2 with argon2id is the same default. The differences are in unattended unlock and packaging: RHEL's preferred network unlock is Clevis with Tang, which binds a keyslot to a policy such as "this Tang server is reachable", so a disk stolen off the network stays locked. On Rocky 10.2 the packages come from these repositories:
TPM2 enrollment through systemd-cryptenroll is available on both. Secure Boot and lockdown are read exactly as above; mokutil is packaged on RHEL too.
Try this
On a lab machine, create a 128 MiB file, sudo cryptsetup luksFormat it with a passphrase (interactively it asks you to type YES in capitals, then the passphrase twice), and luksDump it to find the argon2id line and its memory cost. Open it, put an ext4 filesystem on it, mount it, write a file, then unmount and cryptsetup close. Prove the encryption two ways: sudo grep -a for your text finds it through /dev/mapper and not in the backing file. Then add a recovery key with systemd-cryptenroll --recovery-key, list slots with systemd-cryptenroll <file>, and confirm both the passphrase and the recovery key open the volume. Finish by reading /sys/kernel/security/lockdown and mokutil --sb-state on the same machine and noting what each reports.
Takeaway
LUKS encrypts data with a volume key that many keyslots can unlock, so add a recovery key, rotate by add-then-remove, and treat header backups as copies of the lock. Unattended unlock (TPM2, Tang, or a key file) always trades some of the stolen-disk protection for the ability to reboot headless, and key loss is data loss. Read Secure Boot and lockdown state from the machine; do not assume it.