Skip to content

Instantly share code, notes, and snippets.

@ysegorov
Last active February 5, 2024 10:13
Show Gist options
  • Save ysegorov/23a71d7009da96dc661a774fed1c4b50 to your computer and use it in GitHub Desktop.
Save ysegorov/23a71d7009da96dc661a774fed1c4b50 to your computer and use it in GitHub Desktop.
Void Linux Full Disk Encryption with detached header, remote unlock and /boot on USB stick

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

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
  • 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

Link sshd and dhcpcd-eth0 services to default runlevel to start at boot

[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

Install mkinitcpio and related packages

[xchroot /mnt] # xbps-install mkinitcpio mkinitcpio-encrypt mkinitcpio-lvm2
[xchroot /mnt] # xbps-install mkinitcpio-encryptssh

Switch from dracut to mkinitcpio

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

Copy encryptssh to encryptssh2 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/encryptssh2
[xchroot /mnt] # cp /usr/lib/initcpio/install/encryptssh /etc/initcpio/install/encryptssh2
[xchroot /mnt] # nvi /etc/initcpio/hooks/encryptssh2
diff --git a/encryptssh2 b/encryptssh2
index f15eb32..b4ad06f 100644
--- a/encryptssh2
+++ b/encryptssh2
@@ -54,6 +54,8 @@ run_hook ()
 
         OLDIFS="${IFS}"
         IFS=","
+        cryptheader="--header /boot/header.img"
+        cryptargs="${cryptargs} ${cryptheader}"
         for cryptopt in ${cryptoptions}; do
             case ${cryptopt} in
                 allow-discards)
@@ -74,7 +76,7 @@ run_hook ()
 
             echo ${resolved} > /.cryptdev
 
-            if /sbin/cryptsetup isLuks ${resolved} >/dev/null 2>&1; then
+            if /sbin/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 encryptssh2 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.

#!/usr/bin/ash
run_hook ()
{
/sbin/modprobe -a -q dm-crypt >/dev/null 2>&1
if [ -e "/sys/class/misc/device-mapper" ]; then
if [ ! -e "/dev/mapper/control" ]; then
mkdir /dev/mapper
mknod "/dev/mapper/control" c $(cat /sys/class/misc/device-mapper/dev | sed 's|:| |')
fi
[ "${quiet}" = "y" ] && CSQUIET=">/dev/null"
# Get keyfile if specified
ckeyfile="/crypto_keyfile.bin"
if [ "x${cryptkey}" != "x" ]; then
ckdev="$(echo "${cryptkey}" | cut -d: -f1)"
ckarg1="$(echo "${cryptkey}" | cut -d: -f2)"
ckarg2="$(echo "${cryptkey}" | cut -d: -f3)"
if poll_device "${ckdev}" ${rootdelay}; then
case ${ckarg1} in
*[!0-9]*)
# Use a file on the device
# ckarg1 is not numeric: ckarg1=filesystem, ckarg2=path
mkdir /ckey
mount -r -t ${ckarg1} ${ckdev} /ckey
dd if=/ckey/${ckarg2} of=${ckeyfile} >/dev/null 2>&1
umount /ckey
;;
*)
# Read raw data from the block device
# ckarg1 is numeric: ckarg1=offset, ckarg2=length
dd if=${ckdev} of=${ckeyfile} bs=1 skip=${ckarg1} count=${ckarg2} >/dev/null 2>&1
;;
esac
fi
[ ! -f ${ckeyfile} ] && echo "Keyfile could not be opened. Reverting to passphrase."
fi
if [ -n "${cryptdevice}" ]; then
DEPRECATED_CRYPT=0
cryptdev="$(echo "${cryptdevice}" | cut -d: -f1)"
cryptname="$(echo "${cryptdevice}" | cut -d: -f2)"
cryptoptions="$(echo "${cryptdevice}" | cut -d: -f3)"
else
DEPRECATED_CRYPT=1
cryptdev="${root}"
cryptname="root"
fi
warn_deprecated() {
echo "The syntax 'root=${root}' where '${root}' is an encrypted volume is deprecated"
echo "Use 'cryptdevice=${root}:root root=/dev/mapper/root' instead."
}
OLDIFS="${IFS}"
IFS=","
cryptheader="--header /boot/header.img"
cryptargs="${cryptargs} ${cryptheader}"
for cryptopt in ${cryptoptions}; do
case ${cryptopt} in
allow-discards)
echo "Enabling TRIM/discard support."
cryptargs="${cryptargs} --allow-discards"
;;
*)
echo "Encryption option '${cryptopt}' not known, ignoring." >&2
;;
esac
done
IFS="${OLDIFS}"
echo ${cryptname} > /.cryptname
echo ${cryptargs} > /.cryptargs
if resolved=$(resolve_device "${cryptdev}" ${rootdelay}); then
echo ${resolved} > /.cryptdev
if /sbin/cryptsetup isLuks ${cryptheader} ${resolved} >/dev/null 2>&1; then
[ ${DEPRECATED_CRYPT} -eq 1 ] && warn_deprecated
dopassphrase=1
# If keyfile exists, try to use that
if [ -f ${ckeyfile} ]; then
if eval /sbin/cryptsetup --key-file ${ckeyfile} luksOpen ${resolved} ${cryptname} ${cryptargs} ${CSQUIET}; then
dopassphrase=0
else
echo "Invalid keyfile. Reverting to passphrase."
fi
fi
# Ask for a passphrase
if [ ${dopassphrase} -gt 0 ]; then
echo ""
echo "A password is required to access the ${cryptname} volume:"
#loop until we get a real password
while ! eval /sbin/cryptsetup luksOpen ${resolved} ${cryptname} ${cryptargs} ${CSQUIET}; do
if [ -f /.done ]; then
break
fi
sleep 2;
done
if [ -f /.done ]; then
rm /.done
fi
if [ -f /.cryptdev ]; then
rm /.cryptdev
fi
if [ -f /.cryptname ]; then
rm /.cryptname
fi
if [ -f /.cryptargs ]; then
rm /.cryptargs
fi
fi
if [ -e "/dev/mapper/${cryptname}" ]; then
if [ ${DEPRECATED_CRYPT} -eq 1 ]; then
export root="/dev/mapper/root"
fi
else
err "Password succeeded, but ${cryptname} creation failed, aborting..."
exit 1
fi
elif [ -n "${crypto}" ]; then
[ ${DEPRECATED_CRYPT} -eq 1 ] && warn_deprecated
msg "Non-LUKS encrypted device found..."
if [ $# -ne 5 ]; then
err "Verify parameter format: crypto=hash:cipher:keysize:offset:skip"
err "Non-LUKS decryption not attempted..."
return 1
fi
exe="/sbin/cryptsetup create ${cryptname} ${cryptdev} ${cryptargs}"
tmp=$(echo "${crypto}" | cut -d: -f1)
[ -n "${tmp}" ] && exe="${exe} --hash \"${tmp}\""
tmp=$(echo "${crypto}" | cut -d: -f2)
[ -n "${tmp}" ] && exe="${exe} --cipher \"${tmp}\""
tmp=$(echo "${crypto}" | cut -d: -f3)
[ -n "${tmp}" ] && exe="${exe} --key-size \"${tmp}\""
tmp=$(echo "${crypto}" | cut -d: -f4)
[ -n "${tmp}" ] && exe="${exe} --offset \"${tmp}\""
tmp=$(echo "${crypto}" | cut -d: -f5)
[ -n "${tmp}" ] && exe="${exe} --skip \"${tmp}\""
if [ -f ${ckeyfile} ]; then
exe="${exe} --key-file ${ckeyfile}"
else
exe="${exe} --verify-passphrase"
echo ""
echo "A password is required to access the ${cryptname} volume:"
fi
eval "${exe} ${CSQUIET}"
if [ $? -ne 0 ]; then
err "Non-LUKS device decryption failed. verify format: "
err " crypto=hash:cipher:keysize:offset:skip"
exit 1
fi
if [ -e "/dev/mapper/${cryptname}" ]; then
if [ ${DEPRECATED_CRYPT} -eq 1 ]; then
export root="/dev/mapper/root"
fi
else
err "Password succeeded, but ${cryptname} creation failed, aborting..."
exit 1
fi
else
err "Failed to open encryption mapping: The device ${cryptdev} is not a LUKS volume and the crypto= paramater was not specified."
fi
fi
rm -f ${ckeyfile}
fi
}
#
# Configuration file for GRUB.
#
GRUB_DEFAULT=0
#GRUB_HIDDEN_TIMEOUT=0
#GRUB_HIDDEN_TIMEOUT_QUIET=false
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR="Void"
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"
# Uncomment to use basic console
#GRUB_TERMINAL_INPUT="console"
# Uncomment to disable graphical terminal
#GRUB_TERMINAL_OUTPUT=console
#GRUB_BACKGROUND=/usr/share/void-artwork/splash.png
#GRUB_GFXMODE=1920x1080x32
#GRUB_DISABLE_LINUX_UUID=true
#GRUB_DISABLE_RECOVERY=true
# Uncomment and set to the desired menu colors. Used by normal and wallpaper
# modes only. Entries specified as foreground/background.
#GRUB_COLOR_NORMAL="light-blue/black"
#GRUB_COLOR_HIGHLIGHT="light-cyan/blue"
# vim:set ft=sh
# MODULES
# The following modules are loaded before any boot hooks are
# run. Advanced users may wish to specify all system modules
# in this array. For instance:
# MODULES=(usbhid xhci_hcd)
MODULES=(loop)
# BINARIES
# This setting includes any additional binaries a given user may
# wish into the CPIO image. This is run last, so it may be used to
# override the actual binaries included by a given hook
# BINARIES are dependency parsed, so you may safely ignore libraries
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=(/boot/header.img)
# HOOKS
# This is the most important setting in this file. The HOOKS control the
# modules and scripts added to the image, and what happens at boot time.
# Order is important, and it is recommended that you do not change the
# order in which HOOKS are added. Run 'mkinitcpio -H <hook name>' for
# help on a given hook.
# 'base' is _required_ unless you know precisely what you are doing.
# 'udev' is _required_ in order to automatically load modules
# 'filesystems' is _required_ unless you specify your fs modules in MODULES
# Examples:
## This setup specifies all modules in the MODULES setting above.
## No RAID, lvm2, or encrypted root is needed.
# HOOKS=(base)
#
## This setup will autodetect all modules for your system and should
## work as a sane default
# HOOKS=(base udev autodetect modconf block filesystems fsck)
#
## This setup will generate a 'full' image which supports most systems.
## No autodetection is done.
# HOOKS=(base udev modconf block filesystems fsck)
#
## This setup assembles a mdadm array with an encrypted root file system.
## Note: See 'mkinitcpio -H mdadm_udev' for more information on RAID devices.
# HOOKS=(base udev modconf keyboard keymap consolefont block mdadm_udev encrypt filesystems fsck)
#
## This setup loads an lvm2 volume group.
# HOOKS=(base udev modconf block lvm2 filesystems fsck)
#
## 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 netconf dropbear encryptssh2 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="gzip"
#COMPRESSION="bzip2"
#COMPRESSION="lzma"
#COMPRESSION="xz"
#COMPRESSION="lzop"
#COMPRESSION="lz4"
# COMPRESSION_OPTIONS
# Additional options for the compressor
#COMPRESSION_OPTIONS=()
# MODULES_DECOMPRESS
# Decompress kernel modules during initramfs creation.
# Enable to speedup boot process, disable to save RAM
# during early userspace. Switch (yes/no).
#MODULES_DECOMPRESS="yes"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment