Void Linux Full Disk Encryption with detached header, remote unlock and /boot on USB stick

Note

This post was initially published as gist.

This is a short description of steps to have new secure Void Linux installation with following features:

  • /boot partition on USB stick (not encrypted)
  • full disk encryption with detached LUKS header on USB stick
  • LVM on top of the encrypted partition
  • remote unlocking of the encrypted partition using dropbear ssh server
  • mkinitcpio to generate initramfs

This manual is heavily based on Void Linux FDE documentation, Void Linux kernel documentation, Arch Linux LVM on LUKS documentation, Arch Linux detached LUKS header documentation and Arch Linux remote unlocking documentation.

This manual assumptions:

  • /dev/sda - USB stick
  • /dev/nvme0n1 - internal drive
  • wired network connection

Be very careful with drive names! Check command syntax twice before pressing Enter key!

To start prepare Void Linux installation media (must be separate USB drive or whatever else you have to start the process) and boot using it.

You are expected to run all commands below on a target computer running one of live Void Linux installers (some steps can be made beforehand on another computer though).

Prepare USB stick

# dd if=/dev/urandom of=/dev/sda bs=1M oflag=direct status=progress
# fdisk /dev/sda

USB stick must have dos label and two partitions - EFI (to mount as /boot/efi) and Linux (to mount as /boot).

# fdisk -l /dev/sda
Disk /dev/sda: 7.45 GiB, 8002732032 bytes, 15630336 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x0bfa9df7

Device     Boot   Start     End Sectors  Size Id Type
/dev/sda1          2048 1050623 1048576  512M ef EFI (FAT-12/16/32)
/dev/sda2       1050624 3147775 2097152    1G 83 Linux

Format USB stick paritions

# mkfs.vfat /dev/sda1
# mkfs.ext2 /dev/sda2

Temporary mount /dev/sda2 partition to create file to hold LUKS header

# mount /dev/sda2 /mnt
# dd if=/dev/zero of=/mnt/header.img bs=16M count=1

Prepare internal drive

# dd if=/dev/urandom of=/dev/nvme0n1 bs=1M oflag=direct status=progress
# fdisk /dev/nvme0n1

Internal drive must have gpt label and single Linux partition.

# fdisk -l /dev/nvme0n1
Disk /dev/nvme0n1: 931.51 GiB, 1000204886016 bytes, 1953525168 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 45526BA4-E54F-2641-9A90-5735D0C3C772

Device         Start        End    Sectors   Size Type
/dev/nvme0n1p1  2048 1953523711 1953521664 931.5G Linux filesystem

Create and open LUKS conatiner

# cryptsetup luksFormat --header /mnt/header.img /dev/nvme0n1p1
# cryptsetup open --header /mnt/header.img /dev/nvme0n1p1 cryptlvm

Prepare logical volumes

# pvcreate /dev/mapper/cryptlvm
# vgcreate lvgroup /dev/mapper/cryptlvm
# lvcreate -L 8G -n swap lvgroup
# lvcreate -L 32G -n root lvgroup
# lvcreate -l 100%FREE -n home lvgroup
# lvreduce -L -256M lvgroup/home
# mkfs.ext4 /dev/lvgroup/root
# mkfs.ext4 /dev/lvgroup/home
# mkswap /dev/lvgroup/swap

Unmount USB stick

# umount /mnt

Installation

Mount filesystems

# mount /dev/lvgroup/root /mnt
# mkdir /mnt/home
# mount /dev/lvgroup/home /mnt/home

# mkdir /mnt/boot
# mount /dev/sda2 /mnt/boot
# mkdir /mnt/boot/efi
# mount /dev/sda1 /mnt/boot/efi

# swapon /dev/lvgroup/swap

Copy xbps keys and install base system

# mkdir -p /mnt/var/db/xbps/keys
# cp /var/db/xbps/keys/* /mnt/var/db/xbps/keys/

# xbps-install -Sy -R https://repo-default.voidlinux.org/current -r /mnt base-system cryptsetup grub-x86_64-efi lvm2

Chroot into and configure new system

# xchroot /mnt
[xchroot /mnt] # chown root:root /
[xchroot /mnt] # chmod 755 /
[xchroot /mnt] # passwd root
[xchroot /mnt] # echo void > /etc/hostname

[xchroot /mnt] # echo "LANG=en_US.UTF-8" > /etc/locale.conf
[xchroot /mnt] # echo "en_US.UTF-8 UTF-8" >> /etc/default/libc-locales
[xchroot /mnt] # xbps-reconfigure -f glibc-locales

Edit /etc/fstab

Use blkid to find out UUID of USB stick partitions.

# <file system>                             <dir>     <type>   <options>               <dump>  <pass>
tmpfs                                       /tmp      tmpfs    defaults,nosuid,nodev   0       0
/dev/lvgroup/root                           /         ext4     defaults                0       0
/dev/lvgroup/home                           /home     ext4     defaults                0       0
/dev/lvgroup/swap                           swap      swap     defaults                0       0
UUID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" /boot     ext2     defaults                0       0
UUID="xxxx-xxxx"                            /boot/efi vfat     defaults                0       0
[xchroot /mnt] # ln -s /etc/sv/sshd /etc/runit/runsvdir/default/
[xchroot /mnt] # ln -s /etc/sv/dhcpcd-eth0 /etc/runit/runsvdir/default/

Generate openssh host keys (to be used by dropbear too)

[xchroot /mnt] # ssh-keygen -A
[xchroot /mnt] # xbps-install mkinitcpio mkinitcpio-encrypt mkinitcpio-lvm2
[xchroot /mnt] # xbps-install mkinitcpio-encryptssh mkinitcpio-dropbear

Switch from dracut to mkinitcpio

[xchroot /mnt] # xbps-alternatives -s mkinitcpio

Copy encryptssh to encryptssh3 hook and edit it to include LUKS header parameter to cryptsetup commands

[xchroot /mnt] # mkdir -p /etc/initcpio/{hooks,install}
[xchroot /mnt] # cp /usr/lib/initcpio/hooks/encryptssh /etc/initcpio/hooks/encryptssh3
[xchroot /mnt] # cp /usr/lib/initcpio/install/encryptssh /etc/initcpio/install/encryptssh3
[xchroot /mnt] # nvi /etc/initcpio/hooks/encryptssh3

Caution

Do not put quotes around ${cryptheader} in cryptsetup isLuks ... command (see diff below).

$ diff -u /usr/lib/initcpio/hooks/encryptssh /etc/initcpio/hooks/encryptssh3
--- /usr/lib/initcpio/hooks/encryptssh	2026-07-08 20:20:29.000000000 +0000
+++ /etc/initcpio/hooks/encryptssh3	2026-07-12 10:05:13.214090178 +0000
@@ -80,6 +80,9 @@
     IFS="$OLDIFS"
     unset OLDIFS

+    cryptheader="--header /boot/header.img"
+    cryptargs="${cryptargs} ${cryptheader}"
+
     echo "${cryptname}" > /.cryptname
     echo "${cryptargs}" > /.cryptargs

@@ -87,7 +90,7 @@

         echo "${resolved}" > /.cryptdev

-        if cryptsetup isLuks "${resolved}" >/dev/null 2>&1; then
+        if cryptsetup isLuks ${cryptheader} "${resolved}" >/dev/null 2>&1; then
             [ ${DEPRECATED_CRYPT} -eq 1 ] && warn_deprecated
             dopassphrase=1
             # If keyfile exists, try to use that

Edit /etc/mkinitcpio.conf

[xchroot /mnt] # nvi /etc/mkinitcpio.conf
diff --git a/mkinitcpio.conf b/mkinitcpio.conf
index 4a2ce87..143021d 100644
--- a/mkinitcpio.conf
+++ b/mkinitcpio.conf
@@ -4,7 +4,7 @@
 # run.  Advanced users may wish to specify all system modules
 # in this array.  For instance:
 #     MODULES=(usbhid xhci_hcd)
-MODULES=()
+MODULES=(loop)

 # BINARIES
 # This setting includes any additional binaries a given user may
@@ -16,7 +16,7 @@ BINARIES=()
 # FILES
 # This setting is similar to BINARIES above, however, files are added
 # as-is and are not parsed in any way.  This is useful for config files.
-FILES=()
+FILES=(/boot/header.img)

 # HOOKS
 # This is the most important setting in this file.  The HOOKS control the
@@ -49,12 +49,12 @@ FILES=()
 #
 ##   NOTE: If you have /usr on a separate partition, you MUST include the
 #    usr and fsck hooks.
-HOOKS=(base udev autodetect modconf kms keyboard keymap consolefont block filesystems fsck)
+HOOKS=(base udev autodetect modconf kms keyboard keymap consolefont block netconf dropbear encryptssh3 lvm2 filesystems fsck)

 # COMPRESSION
 # Use this to compress the initramfs image. By default, gzip compression
 # is used. Use 'cat' to create an uncompressed image.
-#COMPRESSION="zstd"
+COMPRESSION="zstd"
 #COMPRESSION="gzip"
 #COMPRESSION="bzip2"
 #COMPRESSION="lzma"

Copy your ssh public key to root home directory

[xchroot /mnt] # mkdir /root/.ssh
[xchroot /mnt] # chmod 700 /root/.ssh
[xchroot /mnt] # nvi /root/.ssh/authorized_keys
[xchroot /mnt] # chmod 600 /root/.ssh/authorized_keys

Copy your ssh public key to dropbear configuration to allow remote unlock

Without /etc/dropbear/root_key file and proper public key in it you won't be able to unlock internal drive remotely.

[xchroot /mnt] # cp /root/.ssh/authorized_key /etc/dropbear/root_key

Set proper GRUB_CMDLINE_LINUX_DEFAULT value in /etc/default/grub

Ensure you have proper ip parameters here - host ip address, subnet mask and gateway address.

GRUB_CMDLINE_LINUX_DEFAULT="loglevel=4 cryptdevice=/dev/nvme0n1p1:cryptlvm root=/dev/lvgroup/root net.ifnames=0 ip=192.168.0.55::192.168.0.1:255.255.255.0::eth0:none"

Install bootloader

[xchroot /mnt] # grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id="Void" --removable

Reconfigure the system

This will generate initramfs and bootloader configuration (/boot/grub/grub.cfg file).

[xchroot /mnt] # xbps-reconfigure -fa

Finish and reboot

Don't forget to remove installation media from the box.

[xchroot /mnt] # exit
# umount -R /mnt
# reboot

Connect to your brand new system and unlock the drive

You have to do it from another computer.

$ ssh root@192.168.0.55

Don't forget to backup your /boot/header.img file or full USB stick.

Last modified: 2026-07-27 09:35:00 UTC