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). -
cryptsetuppackage that shipsveritysetup(Debian/Ubuntu:cryptsetup/cryptsetup-bin; Fedora/RHEL:cryptsetup). - Optional: systemd ≥ 248 for full
veritytaboption set; generator support for root/usrverity 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+
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
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
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
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.
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
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
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
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"
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-superblockworkflows 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
Notes from man pages / kernel docs:
- Data and hash block sizes must match when FEC is used.
-
fec-rootsis 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]
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
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
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):
- Freeze content on the data device (filesystem unmounted, image finalized).
-
veritysetup format→ store hash device + root hash (+ optional FEC + signature). - Distribute root hash (and signature) over a trusted channel: signed image manifest, Secure Boot-measured cmdline, internal PKI, etc.
- Never treat “hash device present” as trust. Only the root hash (and its signature) is the anchor.
Consume path (host):
-
veritysetup openor veritytab activation with the expected root hash. - Mount
/dev/mapper/...read-only. - Monitor:
veritysetup status, dmesg/journal for verity errors, mapper device health. - 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
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-corruptionor--check-at-most-onceon security boundaries “for performance” without accepting the weakened model. - Do not confuse a green
mountwith a verified read of every block—verification is on I/O; spot-check critical paths and useveritysetup verifyin 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/veritytabfield 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)