DEV Community

Cover image for Stop Blindly Mounting Untrusted Images: Practical dm-verity with veritysetup on Linux
Lyra
Lyra

Posted on

Stop Blindly Mounting Untrusted Images: Practical dm-verity with veritysetup on Linux

Stop Blindly Mounting Untrusted Images: Practical dm-verity with veritysetup on Linux

Encryption answers who can read this. It does not answer did anyone change a block since I published it. A LUKS volume with a weak or leaked key still mounts whatever is on disk. A golden appliance image, an immutable /usr, a signed container rootfs export, or a lab “known-good” loop image needs a different property: read-only, block-level integrity that fails closed when a single sector is flipped.

That is what dm-verity does. The kernel device-mapper verity target verifies every data block against a Merkle hash tree. Userspace builds and activates that tree with veritysetup (from the cryptsetup project). systemd can bring devices up at boot via /etc/veritytab and systemd-veritysetup-generator.

This guide is an operator walkthrough: format a data/hash pair, activate a verified mapper device, prove corruption is blocked, optionally add FEC, wire veritytab, and understand root-hash trust—including boot-time roothash= for OS images. It is not LUKS confidentiality, AIDE/debsums file inventories, Btrfs scrub, Secure Boot key enrollment, or dm-integrity authenticated encryption. Those solve adjacent problems. Here the unit of work is a read-only verified block device.

What you are actually building

From the kernel dm-verity docs and veritysetup(8):

Piece Role
Data device Read-only payload (partition, loop file, or image). Applications never mount this raw if you want verification.
Hash device Stores the Merkle tree (and optional on-disk superblock). Can be a second partition/file, or the same device after a hash offset.
Root hash Cryptographic digest of the tree root (+ salt). This is the trust anchor. Anyone who can change the root hash can fake the tree.
dm-verity mapping Kernel target that serves verified blocks under /dev/mapper/<name>. I/O fails (or restarts/panics, if configured) on mismatch.
Optional FEC Reed-Solomon parity so some corrupt/unreadable blocks can be recovered, then re-checked against the hash.

Theory of operation (kernel docs, condensed):

  • On read, the kernel hashes the data block and walks the tree up to the trusted root hash.
  • Mismatch → default behavior is I/O error for that read (fail closed).
  • The target is read-only. You do not “write through verity” and rehash live; you rebuild offline and publish a new root hash.
  • Format version 1 is the modern on-disk layout (salt prepended; use it for new devices). Version 0 is the old Chrome OS layout.

Important mental model: dm-verity is not a backup, not a filesystem, and not encryption. It proves the bytes match a published root hash. Confidentiality still needs LUKS/dm-crypt (often stacked under or beside verity in image pipelines). Writable integrity needs different tools (integritysetup / dm-integrity, filesystem checksums, etc.).

Prerequisites

  • Kernel with dm-verity (stock on modern distros).
  • cryptsetup package that ships veritysetup (Debian/Ubuntu: cryptsetup / cryptsetup-bin; Fedora/RHEL: cryptsetup).
  • Optional: systemd ≥ 248 for full veritytab option set; generator support for root/usr verity goes back further (roothash= since v233 era).
  • Root shell and a scratch directory. Prefer loop files for the lab so you never touch real disks.
# Debian/Ubuntu
sudo apt-get update
sudo apt-get install -y cryptsetup

# Fedora
sudo dnf install -y cryptsetup

veritysetup --version
# Expect veritysetup from cryptsetup 2.x+
Enter fullscreen mode Exit fullscreen mode

Confirm the module path exists on systems where it is not built-in:

# On systems where pstore-style live introspection is available:
lsmod | grep verity || modprobe dm-verity
cat /sys/module/dm_verity/parameters/use_bh_bytes 2>/dev/null || true
Enter fullscreen mode Exit fullscreen mode

1) Build a tiny verified image lab

Create a data image, put a filesystem on it, freeze content, then format hashes into a separate hash image.

LAB=/var/tmp/verity-lab
sudo mkdir -p "$LAB"
cd "$LAB"

# 64 MiB data image + empty hash image (veritysetup can create the hash file)
sudo dd if=/dev/zero of=data.img bs=1M count=64 status=none
sudo dd if=/dev/zero of=hash.img bs=1M count=8 status=none

# Filesystem + sample payload (unmounted before format)
sudo losetup -fP --show data.img   # note the loop device, e.g. /dev/loop0
DATA_LOOP=$(losetup -j "$LAB/data.img" | awk -F: 'NR==1{print $1}')
sudo mkfs.ext4 -q -L veritydata "$DATA_LOOP"
sudo mkdir -p /mnt/verity-src
sudo mount "$DATA_LOOP" /mnt/verity-src
echo 'golden payload v1' | sudo tee /mnt/verity-src/README >/dev/null
sudo umount /mnt/verity-src
Enter fullscreen mode Exit fullscreen mode

Format the Merkle tree. Capture the printed Root hash—that string is your trust anchor:

sudo veritysetup format \
  --hash sha256 \
  --data-block-size 4096 \
  --hash-block-size 4096 \
  --root-hash-file "$LAB/root.hash" \
  "$LAB/data.img" "$LAB/hash.img"

sudo cat "$LAB/root.hash"
# Example shape only (yours will differ):
# e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Enter fullscreen mode Exit fullscreen mode

format computes hashes for the data device and permanently stores the tree on the hash device. With --root-hash-file, the root hash is written as hex text (no trailing newline expected by later reads).

Inspect what was written:

sudo veritysetup dump "$LAB/hash.img"
# Shows UUID, hash algorithm, block sizes, data blocks, salt, etc.
Enter fullscreen mode Exit fullscreen mode

2) Activate and mount the verified device

Open a mapping. Applications use /dev/mapper/..., not the raw data image:

ROOT_HASH=$(sudo cat "$LAB/root.hash")

sudo veritysetup open \
  "$LAB/data.img" veritylab \
  "$LAB/hash.img" "$ROOT_HASH"

# Status should report verification mode and devices
sudo veritysetup status veritylab
ls -l /dev/mapper/veritylab

sudo mkdir -p /mnt/verity
sudo mount -o ro /dev/mapper/veritylab /mnt/verity
cat /mnt/verity/README
# golden payload v1
Enter fullscreen mode Exit fullscreen mode

Userspace-only check (no mapper device) is available when you want CI-style validation:

sudo veritysetup verify \
  "$LAB/data.img" "$LAB/hash.img" \
  --root-hash-file "$LAB/root.hash"
echo $?   # 0 on success
Enter fullscreen mode Exit fullscreen mode

3) Prove fail-closed behavior

With the mapping open, corrupt a data block behind the mapper’s back and read again:

# DANGEROUS on real disks — lab images only
sudo umount /mnt/verity

# Flip bytes in the data image while verity is still open or after close+reopen
sudo dd if=/dev/urandom of="$LAB/data.img" bs=4096 seek=10 count=1 conv=notrunc status=none

# Recreate mapping if you closed it
sudo veritysetup close veritylab 2>/dev/null || true
sudo veritysetup open \
  "$LAB/data.img" veritylab \
  "$LAB/hash.img" --root-hash-file "$LAB/root.hash"

# Mount may succeed; the failure is on verified read of the bad block
sudo mount -o ro /dev/mapper/veritylab /mnt/verity
# Force a read that hits the corrupted region (offset depends on FS layout).
# dd through the mapper is a blunt instrument:
sudo dd if=/dev/mapper/veritylab of=/dev/null bs=4096 skip=10 count=1 status=none
# Expect I/O error once the bad block is fetched and checked
Enter fullscreen mode Exit fullscreen mode

Default policy (no extra flags): failed verification → I/O error. That is what you want for golden images.

Corruption knobs from veritysetup open / kernel optional params (use sparingly):

Option Behavior
(default) I/O error on bad block
--ignore-corruption Log and continue (not for production trust boundaries)
--restart-on-corruption Trigger reboot path (need anti-loop strategy)
--panic-on-corruption Kernel panic on bad block
--check-at-most-once Verify each data block only first read — weaker (offline tamper only)
--ignore-zero-blocks Skip expected-zero blocks — special-case only

Kernel status line uses V (valid so far) vs C (corruption seen). FEC-corrected block counts appear when FEC is enabled.

Restore the lab data image from backup, or re-format after restoring known-good bytes, before continuing.

4) Same-device hash offset (single image layout)

You do not always want two files. Hashes can live on the same device after the data area:

# Example pattern from veritysetup(8):
# --data-blocks limits verified data; --hash-offset places the tree later.
# hash-offset is in bytes and must sit past the data region.

sudo veritysetup format \
  --data-blocks 8192 \
  --hash-offset 33554432 \
  --root-hash-file "$LAB/combined.root" \
  "$LAB/combined.img" "$LAB/combined.img"

sudo veritysetup open \
  "$LAB/combined.img" veritycombo \
  "$LAB/combined.img" \
  --data-blocks 8192 \
  --hash-offset 33554432 \
  --root-hash-file "$LAB/combined.root"
Enter fullscreen mode Exit fullscreen mode

Rules of thumb:

  • --data-blocks × data-block-size must end before the hash offset.
  • Activation must pass the same geometry flags you used at format when superblock mode requires it (--no-superblock workflows especially).
  • Prefer separate hash devices in labs; combined layouts are common in appliance partition schemes.

5) Optional FEC: recover then re-verify

dm-verity can attach forward error correction. FEC does not weaken security: recovered blocks are still checked against the Merkle tree before use (kernel docs). FEC runs only when a hash mismatch or read error occurs, so the happy path stays cheap.

sudo dd if=/dev/zero of="$LAB/fec.img" bs=1M count=4 status=none

sudo veritysetup format \
  --hash sha256 \
  --fec-device "$LAB/fec.img" \
  --fec-roots 2 \
  --root-hash-file "$LAB/root-fec.hash" \
  "$LAB/data-good.img" "$LAB/hash-fec.img"

sudo veritysetup open \
  "$LAB/data-good.img" verityfec \
  "$LAB/hash-fec.img" \
  --root-hash-file "$LAB/root-fec.hash" \
  --fec-device "$LAB/fec.img" \
  --fec-roots 2
Enter fullscreen mode Exit fullscreen mode

Notes from man pages / kernel docs:

  • Data and hash block sizes must match when FEC is used.
  • fec-roots is M−N in RS(255, N); supported range 2–24. 2 is the usual recommendation (~0.8% parity overhead class of cost, with interleaving).
  • If the data device is encrypted, encrypt the FEC device too so parity does not leak plaintext structure.
  • FEC helps with storage faults and some corruption bursts; it is not a substitute for a trusted root hash distribution channel.

6) Persist with /etc/veritytab

For non-root volumes you want every boot, use veritytab(5). Field layout:

volume-name  data-device  hash-device  roothash  [options]
Enter fullscreen mode Exit fullscreen mode

Example (paths or UUID/PARTUUID forms):

# /etc/veritytab
# volume  data                         hash                          roothash   options
usrverity UUID=11111111-2222-3333-4444-555555555555 UUID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee 36e3f740ad502e2c25e2a23d9c7c17bf0fdad2300b7580842d4b7ec1fb0fa263 auto
Enter fullscreen mode Exit fullscreen mode

Useful options (systemd veritytab, many added in v248/v254):

  • hash=sha256, data-block-size=4096, hash-block-size=4096
  • hash-offset=…, data-blocks=…, salt=…, superblock=…, format=1
  • ignore-corruption / restart-on-corruption / panic-on-corruption
  • check-at-most-once, ignore-zero-blocks
  • fec-device=, fec-offset=, fec-roots=
  • root-hash-signature=PATH|base64:…|auto (kernel 5.4+ root-hash sig verify)
  • noauto, nofail, _netdev, x-initrd.attach

Generator: systemd-veritysetup-generator turns veritytab (and kernel cmdline) into systemd-veritysetup@.service instances. After editing:

sudo systemctl daemon-reload
# Units are generated; enable path depends on whether something pulls the volume in
# (a mount unit or veritysetup.target membership without noauto).
sudo systemctl start systemd-veritysetup@usrverity.service
systemctl status systemd-veritysetup@usrverity.service
Enter fullscreen mode Exit fullscreen mode

Mount the mapper read-only from fstab as usual, pointing at /dev/mapper/usrverity. If the verity device uses _netdev, mark the mount _netdev too or you can create a dependency loop with local-fs.target.

Initrd / root filesystem case

For OS root (and /usr) verity, prefer the kernel cmdline interface documented in systemd-veritysetup-generator(8):

  • roothash=<hex> — often enough by itself when GPT partition UUIDs are derived from the root hash (first 128 bits → data PARTUUID scheme, last 128 bits → hash PARTUUID scheme).
  • Or explicit: systemd.verity_root_data=, systemd.verity_root_hash=, systemd.verity_root_options=
  • Parallel set for /usr: usrhash=, systemd.verity_usr_data=, systemd.verity_usr_hash=, systemd.verity_usr_options=
  • Toggles: systemd.verity=, rd.systemd.verity=, veritytab=, rd.veritytab=

Use x-initrd.attach in veritytab for devices needed in the initrd so detach ordering during shutdown stays sane.

Root-hash signatures (root-hash-signature=, kernel root_hash_sig_key_desc) bind the root hash to a key in the trusted keyring—important when the cmdline or veritytab itself could be altered. Pair with Secure Boot/UKI policies when you build a real verified-boot chain; verity alone does not protect the bootloader config that supplies the hash.

7) Operational checklist

Publish path (image builder):

  1. Freeze content on the data device (filesystem unmounted, image finalized).
  2. veritysetup format → store hash device + root hash (+ optional FEC + signature).
  3. Distribute root hash (and signature) over a trusted channel: signed image manifest, Secure Boot-measured cmdline, internal PKI, etc.
  4. Never treat “hash device present” as trust. Only the root hash (and its signature) is the anchor.

Consume path (host):

  1. veritysetup open or veritytab activation with the expected root hash.
  2. Mount /dev/mapper/... read-only.
  3. Monitor: veritysetup status, dmesg/journal for verity errors, mapper device health.
  4. On verification failure: treat as incident. Replace data from trusted media; do not “just ignore-corruption” to get the box up unless you are in a deliberate degraded debug mode.

Stacking notes:

  • dm-verity + LUKS: common pattern is encrypt for confidentiality, verity for authenticity of a published image—order depends on design (factory-signed plaintext verity over encrypted at rest, or verity of ciphertext). Be explicit; FEC device encryption should match data encryption guidance in the man page.
  • dm-verity vs dm-integrity: integritysetup/dm-integrity targets writable authenticated disks with per-sector tags and typically a key. Verity is the immutable publish/verify tool.
  • dm-verity vs fs-verity: fs-verity authenticates files inside a filesystem; dm-verity authenticates a block device. Different layers, complementary in image systems.
  • dm-verity vs AIDE/debsums: userspace file inventories drift and race; verity is online in the block I/O path for a frozen image.

8) Rollback and cleanup (lab)

sudo umount /mnt/verity 2>/dev/null || true
sudo veritysetup close veritylab 2>/dev/null || true
sudo veritysetup close veritycombo 2>/dev/null || true
sudo veritysetup close verityfec 2>/dev/null || true
sudo losetup -d "$DATA_LOOP" 2>/dev/null || true
sudo rm -rf /var/tmp/verity-lab
Enter fullscreen mode Exit fullscreen mode

Production rollback is “boot previous signed image + previous root hash,” not “disable verification.” If you must break glass, remove or noauto the veritytab line and reboot from a known recovery path—then fix the trust chain before returning to service.

What not to do

  • Do not mount the raw data device read-write and expect the hash tree to stay valid.
  • Do not pass the root hash from an untrusted HTTP download without signature or out-of-band pin.
  • Do not enable --ignore-corruption or --check-at-most-once on security boundaries “for performance” without accepting the weakened model.
  • Do not confuse a green mount with a verified read of every block—verification is on I/O; spot-check critical paths and use veritysetup verify in pipelines.
  • Do not skip Secure Boot / signed cmdline concerns if the attacker can edit roothash= as easily as the disk.

References

  • Linux kernel docs: dm-verity
  • veritysetup(8) — format/open/verify/close/status/dump, FEC and corruption options (man7 / cryptsetup)
  • veritytab(5)/etc/veritytab field format and options
  • systemd-veritysetup-generator(8)roothash=, usrhash=, cmdline device selection
  • systemd-veritysetup@.service(8) — per-volume activation units
  • cryptsetup project: https://gitlab.com/cryptsetup/cryptsetup

dm-verity turns “I hope nobody touched this image” into “the kernel will not hand me a block that does not match the root hash I trust.” Format offline, distribute the root hash like a secret of equal importance to a signing key, activate through veritysetup or veritytab, and mount only the mapper. That is the whole game—simple, strict, and fail-closed.

Top comments (0)