dm-verity

From ArchWiki

This article contains information and instructions to implement dm-verity integrity checking on the root filesystem using systemd. Dm-verity uses a tree of sha256 hashes to verify blocks as they are read from a block device. Consequently, this ensures files have not changed between reboots or during runtime. This is useful for mitigating zero days and unauthorized changes to root, as well as enforcing security policies and encryption. Verity devices are regular block devices which can be accessed in /dev/mapper.

Dm-Verity is part of Device Mapper in the kernel.

Components

A dm-verity root setup setup consists of the following:

  1. a / root filesystem image or partition,
  2. verity hash tree verity.bin,
  3. the root hash of the verity tree roothash.txt,
  4. systemd-veritysetup.generator,
  5. systemd-veritysetup.service,
  6. verity kernel command line options,
  7. veritysetup (part of cryptsetup),
  8. a unified kernel image which contains a stub EFI loader, kernel, initramfs, kernel command line, and microcode: kernel.efi,
  9. Secure Boot.

The unified kernel image and Secure Boot are recommended but not required. Verity is intended to be used as one of the last steps in a boot process that protects the OS and the kernel from changes. It is easily defeated without Secure Boot and unified kernel images.

Installation

To enable dm-verity, you must have a working system already installed and configured. See Installation Guide for the details.

Typically it is necessary to have a separate partition or logical volume to store the verity hash data. Next, some files and programs require special consideration. Finally, pacman's default behavior should be modified slightly.

The recommended disk layout is as follows:

GPT disk layout

Two to three partitions minimum:

  1. an EFI System Partition for the bootloader (LABEL=EFI);
  2. a systemd XBOOTLDR partition (LABEL=XBOOT)
    Note: Using systemd's XBOOTLDR partition type allows you to keep the bootloader and kernels separate. This can be useful for creating images used to install or update embedded systems and servers, but it requires you to use systemd-boot. It is recommended to also put the Type 1 bootloader entries into this partition. Also, if you attempt to boot an XBOOTLDR partition from Virtualbox, it will not be able to find the partition.
  3. a LUKS partition (LABEL=OS).

The LUKS partition will be used as a LVM physical volume for the OS volume group

OS volume group

Create at least the first two logical volumes.

  1. Root (formatted as ext4, f2fs, erofs LABEL=Root)
  2. Verity (LABEL=Verity)
  3. Home (optional if you want write access for a user)
  4. Var (many programs will not run if /var is not writable)

/home and /var should be writeable filesystems. On a server that just has one purpose this may be optional, since e.g. a wireguard server needs no write access to the disk.

Possible issues with boot and runtime

Any files that need to be written to during init or changed during runtime must be made writable by some method otherwise the program will not function as expected.

For example, to setup connections with NetworkManager, you need write access to /etc/NetworkManager/system-connections. One way to do this is to move the system-connections folder to /var/etc/system-connections and symlink to it on the root filesystem. Some programs will expect these folders to still exist (even read-only) on the root filesystem for early init. For instance, systemd-journald will break if /etc/machine-id does not exist or is a symlink.

You can also use a separate /etc partition but this will make all of these configuration files writable.

Note: Some programs expect files to exist in /var during boot even if you have a separate /var

One way to find out which files will change when the system is running is to enable the dracut-overlayroot module, use the system, and check the files in /run/overlayroot/u to see what you may need to address. Any files in this folder were written to the tmpfs overlaid on top of root. Place the module into /usr/lib/dracut/modules.d/, add overlayroot to the dracut modules list, and overlayroot=1 to your kernel command line and regenerate the initramfs. The module can be found at https://github.com/TylerHelt0/dracut-overlayroot.

Pacman

Since the root filesystem will be mounted read-only and /var should be mounted read-write in most cases, the path to the pacman database should be changed to /usr/lib/pacman. This will ensure the rootfs always has the correct list of installed packages.

  1. cp /var/lib/pacman /usr/lib
  2. Edit /etc/pacman.conf and set DBPath = /usr/lib/pacman
  3. To be able to sync lists and check updates, move /usr/lib/pacman/cache to /var/lib/pacman and symlink it.
  4. If you wish to be able to change mirrorlist without modifying the root file system, move it to /var/etc and symlink it as well.

Setting up verity

  1. Boot from a live medium
  2. Mount your root filesystem as read-only
  3. Make sure all your changes are perfect
  4. do veritysetup format /dev/OS/Root /dev/OS/Verity | grep Root | cut -f2 >> roothash.txt

You will now have the rootfs, the verity hash tree, and the roothash. Alternatively you can save the hashes to a file by replacing the /dev/OS/Verity path and write it to the device later.

To test it you can use veritysetup open /dev/OS/Root root /dev/OS/Verity $(cat roothash.txt). The verity device can be mounted from /dev/mapper/root.

Configuring the kernel command line

Add the following options to your kernel command line:

  1. lsm=lockdown
  2. lockdown=confidentiality optional but helps keep kernel and userspace seperate
  3. ro to prevent changes to root if not using erofs
  4. systemd.verity=1
  5. roothash=contents_of_roothash.txt
  6. systemd.verity_root_options=restart-on-corruption or panic-on-corruption (the default behavior will just print an error to dmesg and will not prevent untrusted code from running)
  7. systemd.verity_root_data=/dev/OS/Root
  8. systemd.verity_root_hash=/dev/OS/Verity
  9. rd.emergency=reboot to prevents access to a shell if the root is corrupt
  10. rd.shell=0 to prevents access to a shell if boot fails

Do not forget to regenerate the initramfs anytime the roothash changes.

Secure Boot

It is recommended to enable Secure Boot after verity is setup.

Verity protection is useless if a virus or attacker can replace the kernel.efi containing the embedded roothash which would allow any root filesystem to be booted. Signing the kernel image for Secure Boot will prevent the kernel image from being replaced and ensure integrity of the root filesystem as long as the firmware is secure.

One must create a custom kernel to enable signing and loading of kernel and DKMS modules in lockdown mode and create keys as described in Secure Boot. It is recommended to use sbupdate-gitAUR to maintain your Unified kernel images and keep your bootloader signed. sbupdate-gitAUR will also handle your kernel command line. sbctl can be used to create Secure Boot keys.

TPM

A TPM2.0 can be used to protect encryption keys for the LUKS device containing root. After Secure Boot is enabled, you can use systemd-cryptenroll to bind keys to PCRs. Recommended PCRs are 0,1,5,7. This will stop decryption if the firmware, firmware options, secure boot state, or GPT layout is changed.

You must pass this command line option:

rd.luks.options=UUID_of_LUKS=tpm2-device=auto

You may also need to add tpm2 support to your initramfs or include the module if using dracut.

systemd-boot

If you use systemd-boot as your bootloader, it will measure the kernel.efi into PCR 4. This can be used to prevent decryption of root if the kernel image, initramfs, or kernel command line is changed.

Devices other than root

The use of dm-verity is not limited to the root device. Other devices that need to be verified at boot can be put into /etc/veritytab and will be assembled by systemd-veritysetup.service.

Security Considerations

Although the verity root device will be tamper-resistant, it provides no confidentiality. It could be located on an unencrypted partition if it contains no secret data. If the kernel is protected by Secure Boot, it would be impossible to replace the data in the root or verity devices without replacing the kernel.

The verity root device can be used to unlock other encrypted devices. If done with keyfiles, the verity root should be encrypted. If using a TPM and systemd-cryptenroll to store keys, the verity root could be unencrypted.

During runtime, methods such as OverlayFS, tmpfs, and bind mounts can still be used to get write access on the folders within root. For this reason it is important to still harden the OS. Apparmor, SELinux and other access control mechanisms are useful for this.

See also