Preface

So I started running home assistant at home on a raspberry PI 5 machine and I just installed HAOS on an SD. I then started growing deeply uncomfortable on storing credentials in the HA filesystem in clear text (any obfuscation is not enough).

Considering configuring an encrypted root with HAOS is simply not possible without forking it, and also considering that dedicating a RPI5 entirely to HAOS is a waste of resources, I decided to add an SSD to the Pi, boot it with raspbian and then run HAOS inside a VM.

This way, I can have an encrypted root on the main host, thus encrypting the entire HAOS VM.

Furthermore I can now snapshot the entire HAOS VM and I have much more flexibility in managing it. Last but not least, I can also use the remaining RPI CPU and RAM for something else.

Requirements

First, a big thank you to this post that gave me the initial pointers on how to set this up. But that 2021 post is now slightly outdated, and many steps are no longer necessary.

  • An RPI5 with debian 13 or newer
  • A decent and reliable USB stick that can be fully erased
  • A keyboard and monitor directly connected to the RPI

Overview

The overall idea is:

  • We prepare an initrd that contains the resize2fs tool that allows shrinking and enlarging ext4 filesystems
  • We configure the system to boot off an encrypted root that does not exist yet, thus forcing the system to fall into the initrd.
  • While in initrd, we shrink the root filesystem to the smallest possible size
  • We copy the root filesystem from the cleartext device over to the USB stick
  • We create the encrypted device using LUKS
  • We copy back the root filesystem from the USB stick back to the encrypted device
  • We extend the root filesystem to the maximum size
  • We configure SSH in the initrd so that you can unlock it even after you’ve deployed your raspberry pi in a location without a keyboard or screen.

Ready? Set, go!

Preparing the initramfs

We want to tell the firmware to load an initramfs, that’s a RAM disk with a minimal Linux environment that has the necessary pieces to open an encrypted device and unlock it - provided the user inserts the right password.

By default, the firmware loads the kernel_2712.img kernel on the raspberry pi 5, as that is the optimised one.

If the auto_initramfs option is set to 1, then the firmware will also load the corresponding initramfs_2712 image in memory and pass its address to the kernel.

When creating a new initramfs, the /etc/initramfs/post-update.d/z50-raspi-firmware script ensures to place a copy of the most recent initramfs in the firmware directory, and name it as initramfs_2712. So all the heavy lifting is already done for us!

1. Ensure the firmware loads the initrd

Ensure you have in /boot/firmware/config.txt:

auto_initramfs=1

but this should already be the case.

2. Tell the kernel to mount root from the encrypted device

Create a backup of your current cmdline.txt, should we need to restore it later:

> sudo cp /boot/firmware/cmdline.txt /boot/firmware/cmdline.txt.bak

Then edit /boot/firmware/cmdline.txt and replace any root=XXX setting with root=/dev/mapper/cryptroot.

For example, my cmdline.txt looks like this:

console=tty1 root=/dev/mapper/cryptroot rootfstype=ext4 fsck.repair=yes rootwait

3. Install cryptsetup, initramfs tools and busybox

Run:

> sudo apt install cryptsetup initramfs-tools busybox

If some of these are already installed, that’s perfectly fine!

Verify that your PI AES hardware acceleration is working as expected:

> cryptsetup benchmark -c aes-xts
# Tests are approximate using memory only (no storage IO).
# Algorithm |       Key |      Encryption |      Decryption
    aes-xts        256b      1736.6 MiB/s      1804.6 MiB/s

you should get similar values.

4. Configure the initramfs to include resize2fs

Create a new /etc/initramfs-tools/hooks/luks_hooks file, containing:

#!/bin/sh -e

case $1 in
        prereqs) echo ""; exit 0;;
esac

. /usr/share/initramfs-tools/hook-functions

copy_exec /sbin/resize2fs /sbin

and making it executable:

> sudo chmod +x /etc/initramfs-tools/hooks/luks_hooks

5. Add disk encryption kernel modules to the initramfs

Add the following lines at the bottom of /etc/initramfs-tools/modules:

aes_ce_blk
sha2_ce
dm_crypt

6. Ask the initramfs to include most modules

The initramfs tools try to miminize the number of modules installed in the RAM disk, doing some fancy detection of the hardware and the needed ones. However this may fail and you may end up with a system that just hangs in the initramfs.

So let’s edit /etc/initramfs-tools/initramfs.conf and ensure that the MODULES line reads:

MODULES=most

7. Regenerate the initramfs

Let’s now run this command to generate the initramfs for all installed kernels:

> sudo update-initramfs -u
update-initramfs: Generating /boot/initrd.img-6.12.62+rpt-rpi-2712
'/boot/initrd.img-6.12.62+rpt-rpi-2712' -> '/boot/firmware/initramfs_2712'

Let’s check we have everything we need:

> lsinitramfs /boot/firmware/initramfs_2712 |grep -E 'sbin/(cryptsetup|resize2fs|fdisk)'
usr/sbin/cryptsetup
usr/sbin/resize2fs
usr/sbin/fdisk

and:

> lsinitramfs /boot/firmware/initramfs_2712 |grep -E 'aes.ce.blk|sha2.ce|dm.crypt'
usr/lib/modules/6.12.62+rpt-rpi-2712/kernel/arch/arm64/crypto/aes-ce-blk.ko.xz
usr/lib/modules/6.12.62+rpt-rpi-2712/kernel/arch/arm64/crypto/sha2-ce.ko.xz
usr/lib/modules/6.12.62+rpt-rpi-2712/kernel/drivers/md/dm-crypt.ko.xz

It is now time to reboot and get dropped into the (initramfs) shell! Connect your disposable USB stick to the pi and:

> sudo reboot

Tweaking the root disk in the initramfs shell

After a few attempts to mount /dev/mapper/cryptroot, the initramfs will give up and drop you to a shell whose prompt is just (initramfs) . In this shell we have root access and we can manipulate the block devices connected to the pi.

1. Ensure that you can access all needed devices

We need to access both the USB stick and your root device. Let’s check it by issuing lsblk.

You should see a /dev/sda device that’s your USB stick, and a /dev/mmcblk0 device if you are using an SD card, or a /dev/nvme0n1 device if you’re using an NVME drive.

2. Ensure that we can use the encryption algorithms

Let’s re-run the benchmarks:

(initramfs) cryptsetup benchmark -c aes-xts
# Tests are approximate using memory only (no storage IO).
# Algorithm |       Key |      Encryption |      Decryption
    aes-xts        256b      1761.5 MiB/s      1813.9 MiB/s

If all is good, We can now proceed with our plan: shrink the root filesystem, copy it over to the USB stick, create a new encrypted device and move the root filesystem back to the encrypted device.

3. Check the root filesystem integrity

Run:

e2fsck -f /dev/nvme0n1p2

If e2fsck said that the filesystem is OK, proceed to the next step! Otherwise, if there are some serious errors, it may be best to stop and undo the changes done so far to avoid serious data corruption. It’s time to restore the cmdline.txt backup we did earlier.

Use lsblk to find the device name of the the 512M partition - that’d be /dev/mmcblk0p1 if using an SD card or /dev/nvme0n1p1 if using an NVME drive.

Then, let’s mount it, restore the former cmdline.txt and reboot:

mkdir -p /foo
mount /dev/YOURPARTITION /foo
mv /foo/cmdline.txt.bak /foo/cmdline.txt
umount /foo
reboot

That’ll undoes the boot from the encrypted root, and the pi will boot back from the cleartext root. Having a non-repairable filesystem is dangerous, so seek some advice :-).

4. Resize the root file system to the smallest possible size

Run:

resize2fs -fM -p /dev/nvme0n1p2

That’ll output something like:

Resizing the filesystem on /dev/nvme0n1p2 to 2345678 (4k) blocks.

The number of blocks (2345678 in this example) is the number to take note of.

We’re now going to extract a checksum of it, so to ensure that after we copy it around and back no data corruption took place.

5. Compute the checksum of your root filesystem

Run:

dd bs=4k count=XXXXX if=/dev/nvme0n1p2 | sha1sum

and take note of the resulting checksum. Taking a picture of the screen works :)

6. Copy the root filesystem over to the USB stick

If your USB stick is /dev/sda (likely), then we can copy the shrunk root filesystem to it with:

dd bs=4k count=XXXXX if=/dev/nvme0n1p2 of=/dev/sda

This will take some time, so please be patient and wait.

7. Check copy integrity

Let’s now calculate the checksum of what we copied so far to the USB stick, to ensure it was stored properly:

dd bs=4k count=XXXXX if=/dev/sda | sha1sum

This should perfectly match with the checksum we found at step 5. If this is not the case, you should use a different USB stick. If you don’t have another one, then you can undo the changes so far by looking at step 3 in this section, and reboot your pi.

8. Create the encrypted device

This is the command I settled on:

cryptsetup --type luks2 --cipher aes-xts-plain64 --hash sha256 -–keysize 256 luksFormat /dev/nvme0n1p2

The defaults nowadays are to use argon2id as Password-Base Key Derivation Function, and the PI5 is fast enough to use the default 2000ms iteration time.

This command will ask for a passphrase, that’s what we’ll need every time the system boots to unlock the root disk and let the system continue booting.

Ensure to choose a decently sized passphrase, and check out this page if you need advice.

9. Copy back the root filesystem to the encrypted device

Let’s open our encrypted root:

cryptsetup luksOpen /dev/nvme0n1p2 cryptroot

key in the passphrase that you inserted at step 8. This will make available a ned /dev/mapper/cryptroot device node, on which we can now copy back our root filesystem:

dd bs=4k count=XXXXX if=/dev/sda of=/dev/mapper/cryptroot

and let’s calculate the checksum of the copy so to ensure that what we’ve copied back matched what’s on the USB stick, that we found at step 7:

dd bs=4k count=XXXXX if=/dev/mapper/cryptroot | sha1sum

if there’s a mismatch, ouch - your root filesystem should still be safe on the USB stick, but at this point this means either the USB stick is faulty or the target storage is broken. Either case it’s not pleasant, and I’d suggest to find out who’s at fault and replace it. But for now we should carry on.

10. Check the root filesystem and resize it back

Let’s ensure the filesystem is still sane:

e2fsck /dev/mapper/cryptroot

and let’s resize it back to the maximum size of the underlying device:

resize2fs -f /dev/mapper/sdcard

at this point, we can exit from the initramfs shell and the boot process will continue normally.

exit

Making changes permanent

So far, we’d need to open the encrypted partition every time by hand in the initramfs shell, and that’s quite uncomfortable.

Furthermore, we’d always need a keyboard and screen hooked up to the pi to insert the password, and that’s also pretty ugly.

So we now instruct the initramfs tools to attempt opening the encrypted root automatically and wait for a user to insert the password.

We also set up an SSH server in the initramfs that allows a remote user to connect and unlock the root device remotely.

1. Set up the encrypted partition in /etc/crypttab

Your /etc/crypttab should look like the following:

# <target name> <source device>         <key file>      <options>
cryptroot       /dev/nvme0n1p2          none            luks

2. Set up the encrypted root in /etc/fstab

The device for the root filesystem should be changed to the /dev/mapper/cryptroot encrypted device.

It is likely that your root device would be specified with a PARTUUID= line. These are displayed in lsblk output if you run lsblk -o NAME,TYPE,MOUNTPOINT,PTUUID.

Anyway, you need to change the line for the / partition to:

/dev/mapper/cryptroot /               ext4    defaults,noatime  0       1

3. Add initramfs SSH

Let’s install the necessary packages:

> sudo apt install dropbear-initramfs

This will install the necessary hooks to add the dropbear SSH daemon in the initramfs, and generate all the host keys.

This setup only allows SSH public key authentication, so you need to add your own SSH key to the /etc/dropbear/initramfs/authorized_keys file.

Then, the host keys generated for the initramfs are different from the ones in the host. This means that your SSH client will complain every time it connects to the SSH daemon in the initramfs as it’ll see a key mismatch with the host.

So, we can convert the ones in the host to the initramfs:

> cd /etc/ssh
> dropbearconvert openssh dropbear ssh_host_ecdsa_key /etc/dropbear/initramfs/dropbear_ecdsa_host_key
> dropbearconvert openssh dropbear ssh_host_ed25519_key /etc/dropbear/initramfs/dropbear_ed25519_host_key
> dropbearconvert openssh dropbear ssh_host_rsa_key /etc/dropbear/initramfs/dropbear_rsa_host_key

and now let’s generate the corresponding public keys:

> cd /etc/dropbear/initramfs
> dropbearkey -y -f dropbear_ecdsa_host_key | grep ^ecdsa > dropbear_ecdsa_host_key.pub
> dropbearkey -y -f dropbear_ed25519_host_key | grep ^ssh > dropbear_ed25519_host_key.pub
> dropbearkey -y -f dropbear_rsa_host_key | grep ^ssh > dropbear_rsa_host_key.pub

And we’re good!

4. Regenerate the initramfs

As done previously – however now that we’ve changed crypttab and fstab, initramfs will run all the necessary plumbing to try to open the encrypted root.

> sudo update-initramfs -u
update-initramfs: Generating /boot/initrd.img-6.12.62+rpt-rpi-2712
'/boot/initrd.img-6.12.62+rpt-rpi-2712' -> '/boot/firmware/initramfs_2712'

Given we’ve also added dropbear, the initramfs will also spawn the SSH daemon when waiting for the root to be unlocked.

Did it blend?

Of course it did! If you’ve made this far, congratulations! You can now enjoy your mini Linux server with encrypted root. Upon reboot, just connect over SSH to it, and run:

cryptroot-unlock

And after inserting your encrypted device password, the connection will close and your server will boot.

Happy hacking!