Encrypting storage on OpenWRT sounds straightforward — install cryptsetup, run luksOpen, done. Reality is a different story. OpenWRT’s storage model, build system, and boot chain were never designed with encrypted-at-rest workflows in mind. Every step from kernel config to key management requires decisions that upstream tooling simply does not make for you.
Why OpenWRT Is Not a Normal Linux Distribution
Standard Linux distributions store the root filesystem on a block device. systemd-cryptsetup unlocks it at boot via an initrd, Plymouth asks for a passphrase, and LUKS handles the rest. On OpenWRT none of that infrastructure exists.
OpenWRT’s default storage layout is a layered read-only/writable design:
- SquashFS — the read-only base system, compressed and immutable after flashing
- JFFS2 or UBIFS overlay — a writable layer that holds configuration changes and installed packages
- OverlayFS — merges both into a single unified root
The consequence: you cannot encrypt the root filesystem the way you would on a desktop. The base SquashFS lives in flash, the overlay lives in a separate MTD partition or UBI volume, and the bootloader (almost always U-Boot) hands control to a kernel that does not know how to pause and ask for a passphrase before mounting anything.
What You Actually Need: Packages and Kernel Modules
OpenWRT’s default builds strip everything not needed for basic routing. Encryption requires explicitly enabling a chain of kernel modules and userspace tools absent from every stock image.
Userspace package:
CONFIG_PACKAGE_cryptsetup=y
Kernel modules — all must be built together with your kernel or they will be rejected by vermagic validation:
CONFIG_PACKAGE_kmod-dm=y
CONFIG_PACKAGE_kmod-dm-crypt=y
CONFIG_PACKAGE_kmod-crypto-aes=y
CONFIG_PACKAGE_kmod-crypto-xts=y
CONFIG_PACKAGE_kmod-crypto-cbc=y
CONFIG_PACKAGE_kmod-crypto-hash=y
CONFIG_PACKAGE_kmod-crypto-iv=y
CONFIG_PACKAGE_kmod-crypto-manager=y
CONFIG_PACKAGE_kmod-crypto-misc=y
CONFIG_PACKAGE_kmod-crypto-core=y
The critical trap is kmod-dm-crypt. It is not included in CONFIG_ALL_KMODS=y builds by default on all targets. Any attempt to insmod a pre-built kmod against a custom kernel fails immediately — OpenWRT’s vermagic system embeds the exact kernel build hash and rejects mismatches with no useful error message beyond “invalid module format”.
Defining a Custom Target With Encryption Support
The cleanest approach is to define a custom profile in the OpenWRT build system. Profiles live under target/linux/<arch>/ and control which packages and kernel options are baked in. For an x86_64 target with encrypted data storage:
# target/linux/x86/image/Makefile — add a custom profile block
define Profile/encrypted-storage
NAME:=x86 Encrypted Storage Profile
PACKAGES:=
cryptsetup
kmod-dm
kmod-dm-crypt
kmod-crypto-aes
kmod-crypto-xts
kmod-crypto-cbc
kmod-crypto-hash
kmod-crypto-iv
kmod-crypto-manager
block-mount
e2fsprogs
endef
$(eval $(call Profile,encrypted-storage))
On ARM targets (MediaTek, Qualcomm IPQ), kernel config fragments must be placed in target/linux/<arch>/<subarch>/config-*. Add the dm-crypt specific options there:
# config-5.15 (match your target kernel version)
CONFIG_DM_CRYPT=m
CONFIG_CRYPTO_AES=m
CONFIG_CRYPTO_XTS=m
CONFIG_CRYPTO_SHA256=m
CONFIG_BLK_DEV_DM=m
If not modifying the build tree, the Image Builder approach is simpler — pass PACKAGES to add userspace tools, and overlay custom preinit files. The constraint: Image Builder cannot add kernel modules absent from the original build. This is why vermagic is the wall you always hit first.
The Initramfs and Initramdisk Problem
On standard Linux, the initrd is a temporary root that runs before the real root is mounted: it unlocks LUKS, then does a switch_root pivot. OpenWRT’s initramfs exists but serves a different purpose — it is an image for RAM-only deployments, not a pre-mount unlock stage.
OpenWRT’s preinit framework handles what happens just before procd starts. Preinit scripts live in /lib/preinit/ and are sourced in lexicographic order. There is no standardized hook for “pause, unlock block device, then pivot”. You write it yourself:
#!/bin/sh
# /lib/preinit/30_unlock_encrypted_storage
unlock_storage() {
# Load required modules
insmod /lib/modules/$(uname -r)/dm-mod.ko
insmod /lib/modules/$(uname -r)/dm-crypt.ko
insmod /lib/modules/$(uname -r)/aes_generic.ko
insmod /lib/modules/$(uname -r)/xts.ko
# Retrieve passphrase — serial console, USB key, or network call
echo -n "Storage passphrase: "
read -r passphrase
echo "$passphrase" | cryptsetup luksOpen /dev/sda2 encdata
if [ $? -ne 0 ]; then
echo "LUKS unlock failed. Dropping to emergency shell."
/bin/sh
fi
mkdir -p /mnt/encdata
mount /dev/mapper/encdata /mnt/encdata
}
boot_hook_add preinit_main unlock_storage
boot_hook_add preinit_main registers the function to run before overlay assembly. If you need to replace the root entirely (pivot into the decrypted volume rather than mount a secondary partition), the complexity multiplies: you must perform the full switch_root before OpenWRT’s overlay system assembles, which means partially reimplementing the preinit framework itself. No upstream support exists for this. Community patches modify package/base-files/ and none are merged into mainline.
Device Mapper: The dm-crypt Target
Once kmod-dm and kmod-dm-crypt are loaded, the device mapper exposes the dm-crypt target. The raw table format:
# <start> <length> crypt <cipher> <key> <iv_offset> <device> <device_offset>
echo "0 2097152 crypt aes-xts-plain64 <hex-key> 0 /dev/sda2 0" |
dmsetup create encdata
cryptsetup luksOpen builds this table automatically from the LUKS header. The embedded-specific failure mode: device mapper relies on udev to create /dev/mapper/ entries. OpenWRT uses mdev or hotplug2, not udev. Without explicit device node creation after dmsetup create, /dev/mapper/encdata does not appear and every subsequent mount fails silently:
# Create the device node manually when udev is absent
DEVINFO=$(dmsetup info encdata)
MAJOR=$(echo "$DEVINFO" | awk '/Major/ {print $2}')
MINOR=$(echo "$DEVINFO" | awk '/Minor/ {print $2}')
mkdir -p /dev/mapper
mknod /dev/mapper/encdata b $MAJOR $MINOR
Recent cryptsetup versions handle this via --disable-udev or the libdevmapper fallback. Verify which version your build ships — older OpenWRT package feeds lag the upstream releases, and the --disable-udev flag was not always present.
Key Management: The Genuinely Unsolved Problem
This is where OpenWRT encryption becomes a product design question, not just a software one. The standard ecosystem — systemd-cryptenroll for TPM PCR binding, Clevis/Tang for NBDE, FIDO2 keys — is entirely absent.
- Hardcoded passphrase in initramfs — shifts the threat model from “attacker reads the storage” to “attacker reads the firmware”. Only useful against targeted flash chip extraction without the device board.
- Serial console input — requires a human at boot. Viable for gateway hardware in a locked cabinet, impossible for autonomous field devices.
- USB key file — works, but now the key management problem lives in protecting the USB drive. Cryptsetup supports
--key-filedirectly from a mounted USB path. - TPM 2.0 — available via
CONFIG_PACKAGE_tpm2-toolsandkmod-tpm, but consumer router SoCs almost never include a TPM. Industrial ARM SBCs (NXP i.MX8, TI AM64x) sometimes do. PCR-based sealing requires a measured boot chain that U-Boot does not provide without explicit Measured Boot configuration. - Network-bound decryption (NBDE) — a Tang server holds a key fragment; the device reconstructs the key at boot using Clevis over the network. Most operationally scalable for fleet deployments. No official OpenWRT package. Community builds exist, with the fundamental constraint that the network must be reachable before storage is mounted — the reverse of the usual boot order.
The Entropy Problem
LUKS key derivation (PBKDF2 or Argon2) must be slow by design — it resists brute force. On a 600 MHz MIPS or a low-clocked Cortex-A7, this already hurts. The deeper issue is entropy quality.
OpenWRT ships urngd, a timing-jitter entropy daemon. Published research has demonstrated that urngd‘s entropy quality degrades on systems with predictable timing — particularly at cold boot before network interfaces are active. Running cryptsetup luksFormat on a freshly booted device with no I/O may produce a key derived from insufficient entropy, compromising the entire encryption scheme before it starts.
Practical mitigations:
- Load
havegedorrng-toolsbeforeluksFormat; feed from/dev/hwrngif the SoC exposes one - Verify entropy before key generation:
cat /proc/sys/kernel/random/entropy_avail— target above 256 - On platforms with a hardware TRNG (NXP i.MX, Qualcomm IPQ, TI Sitara), ensure the driver is loaded and
/dev/hwrngis active - Pass
--iter-time 1000to reduce PBKDF time on constrained hardware — but acknowledge the brute-force resistance tradeoff explicitly
What a Production Implementation Actually Requires
- A full source build — no Image Builder shortcut when custom kernel modules are required
- A defined target profile with all crypto kmods as required packages, versioned to your kernel
- A preinit hook or dedicated initramfs stage that runs before overlay assembly, loads modules, and unlocks the LUKS container
- A key management architecture decided at device design time — TPM, NBDE, or operational. Retrofitting after production is expensive
- Entropy validation before any
luksFormatinvocation - Encrypting a separate data partition on eMMC or SD, not the JFFS2/UBIFS overlay on NOR/NAND flash, unless you fully understand MTD/UBI wear leveling interactions with a dm-crypt layer
EOTICS specializes in BSP development, OpenWRT customization, and embedded security implementations across Yocto, Buildroot, and OpenWRT targets. If you are working through these problems on a real project, get in touch.