This is a step-by-step guide for making your Raspberry Pi 5 more secure and powerful by using encrypted rootfs and btrfs. It is highly recommended to use NVME drive as a boot device for this setup to work properly. Installing NVME drive as a boot partition and enabling SSH autostart is outside of the scope of this guide. This guide is inspired by this AskUbuntu! answer and this btrfs guide.
This method of installation is completely headless, but can also be performed on the device itself.
All commands in this guide are executed from root account (aka with sudo).
Don't forget to make a backup of your system BEFORE attempting any changes. It is highly recommented to do this conversion after the initial install of Raspberry Pi OS, before adding any new data to the system, in case something goes horribly wrong.
- A Raspberry Pi 5, connected to the local network via Ethernet cable, and receiving the IP address via DHCP.
- Another machine, that will be used for remote SSH connection into Pi.
- A hour or two of time (depends on the boot drive size).
- Fresh installation of latest Raspberry Pi OS (as of time of writing, based on Debian 12 Bookworm).
Most of the steps will be executed in the initramfs environment (which is convenient for modifying the rootfs partition). We need to add some modules and utilites to the initramfs image before proceeding.
- Update your system:
# apt update && apt upgrade
- Reboot your system to make sure you're using the latest kernel:
# reboot
- Reconnect to your system, and install required packages:
# apt install dropbear-initramfs busybox btrfs-progs
- Add new modules to initramfs by adding these lines to the end of
/etc/initramfs-tools/modulesfile. Use your preferred text editor to do so (nano, vim, mcedit, you name it).
algif_skcipher
aes_arm64
aes_ce_blk
aes_ce_ccm
aes_ce_cipher
sha256_arm64
cbc
dm-crypt
btrfs
- Make sure dropbear has network. Add the following line to the end of
/etc/initramfs-tools/initramfs.conffile, replacing the<hostname>part with your hostname of choice.
IP=::::<hostname>::dhcp:::
- Make sure dropbear SSH uses the same host key as your main system (to avoid messing around with your local connection not working because of mismatching keys):
for type in ecdsa ed25519 rsa ; do
cp /etc/ssh/ssh_host_${type}_key /tmp/openssh.key
ssh-keygen -p -N "" -m PEM -f /tmp/openssh.key
dropbearconvert openssh dropbear \
/tmp/openssh.key \
/etc/dropbear/initramfs/dropbear_${type}_host_key
done
-
Add your SSH key to login: copy and paste it into
/etc/dropbear/initramfs/authorized_keys. -
Add the tools for working with filesystems to initramfs by creating /etc/initramfs-tools/hooks/fs_hooks with the following content:
#!/bin/sh -e
PREREQS=""
case $1 in
prereqs) echo "${PREREQS}"; exit 0;;
esac
. /usr/share/initramfs-tools/hook-functions
copy_exec /sbin/e2fsck /sbin
copy_exec /sbin/resize2fs /sbin
copy_exec /sbin/fdisk /sbin
copy_exec /sbin/cryptsetup /sbin
copy_exec /usr/bin/btrfs /sbin
copy_exec /usr/bin/btrfsck /sbin
copy_exec /usr/bin/btrfs-convert /sbin
copy_exec /usr/bin/btrfs-find-root /sbin
copy_exec /usr/bin/btrfs-image /sbin
copy_exec /usr/bin/btrfs-map-logical /sbin
copy_exec /usr/bin/btrfs-select-super /sbin
copy_exec /usr/bin/btrfstune /sbin
- Make new initramfs hook executable:
# chmod +x /etc/initramfs-tools/hooks/fs_hooks
- Update initramfs to apply all these changes:
# update-initramfs -u
- Verify that initramfs has all the modules and binaries we need. Replace
<suffix>in these commands with the suffix of your initramfs file name (it depends on your kernel and memory page size mode):
# lsinitramfs /boot/initrd.img-<suffix> | grep -P "sbin/(cryptsetup|resize2fs|fdisk|e2fsck|btrfs)"
The command above should print:
usr/sbin/btrfs
usr/sbin/btrfs-convert
usr/sbin/btrfs-find-root
usr/sbin/btrfs-image
usr/sbin/btrfs-map-logical
usr/sbin/btrfs-select-super
usr/sbin/btrfsck
usr/sbin/btrfstune
usr/sbin/cryptsetup
usr/sbin/e2fsck
usr/sbin/resize2fs
usr/sbin/fdisk
To verify modules inside initramfs, run:
# lsinitramfs /boot/initrd.img-<kernel version> | grep -P "(algif_skcipher|aes-arm64|sha256-arm64|cbc|dm-crypt|btrfs)"
It should print something along the lines of:
scripts/local-premount/btrfs
usr/bin/btrfs
usr/lib/modules/<kernel version>/kernel/arch/arm64/crypto/aes-arm64.ko
usr/lib/modules/<kernel version>/kernel/arch/arm64/crypto/sha256-arm64.ko
usr/lib/modules/<kernel version>/kernel/crypto/algif_skcipher.ko
usr/lib/modules/<kernel version>/kernel/crypto/cbc.ko
usr/lib/modules/<kernel version>/kernel/crypto/xcbc.ko
usr/lib/modules/<kernel version>/kernel/drivers/md/dm-crypt.ko
usr/lib/modules/<kernel version>/kernel/fs/btrfs
usr/lib/modules/<kernel version>/kernel/fs/btrfs/btrfs.ko
usr/lib/udev/rules.d/64-btrfs-dm.rules
usr/lib/udev/rules.d/64-btrfs.rules
usr/sbin/btrfs
usr/sbin/btrfs-convert
usr/sbin/btrfs-find-root
usr/sbin/btrfs-image
usr/sbin/btrfs-map-logical
usr/sbin/btrfs-select-super
usr/sbin/btrfsck
usr/sbin/btrfstune
- Edit
/etc/fstaband replace the current root partition mount with a new one (this filesystem does not exist yet, but don't worry, we will fix it soon):
/dev/mapper/rootfs / btrfs rw,relatime,compress=zstd:3,space_cache=v2,subvol=@ 0 0
Add the following lines below it:
/dev/mapper/rootfs /home btrfs rw,relatime,compress=zstd:3,space_cache=v2,subvol=@home 0 0
/dev/mapper/rootfs /var/log btrfs rw,relatime,compress=zstd:3,space_cache=v2,subvol=@var_log 0 0
/dev/mapper/rootfs /var/lib/docker btrfs rw,relatime,compress=zstd:3,space_cache=v2,subvol=@var_lib_docker 0 0
/dev/mapper/rootfs /.snapshots btrfs rw,relatime,compress=zstd:3,space_cache=v2,subvol=@snapshots/root 0 0
/dev/mapper/rootfs /home/.snapshots btrfs rw,relatime,compress=zstd:3,space_cache=v2,subvol=@snapshots/home 0 0
/dev/mapper/rootfs /var/lib/docker/.snapshots btrfs rw,relatime,compress=zstd:3,space_cache=v2,subvol=@snapshots/docker 0 0
Note, that the lines above are representing the subvolumes structure used in this guide. If you prefer to have different
btrfs subvolumes structure, change your /etc/fstab settings accordingly!
- Update initramfs, ignoring its warning about missing root partition
# update-initramfs -u
-
Edit
/boot/firmware/cmdline.txt:- Change
root=LABEL=writabletoroot=/dev/mapper/rootfs - Add
rootflags=rw,relatime,compress=zstd:3,space_cache=v2,subvol=@ cryptdevice=/dev/nvme0n1p2:rootfsto the end of the line
- Change
-
Cross fingers and reboot:
# reboot
Now Raspberry Pi reboots into your new initramfs environment. As soon as its IP address is pingable, login via SSH
using root account. If everything is done correctly, you must end up in a Busybox shell.
- Run
e2fsckon your root FS:
# e2fsck -f /dev/nvme0n1p2
- Shrink rootfs to make some space for LUKS header:
# resize2fs -M /dev/nvme0n1p2
- Encrypt rootfs in-place by using
cryptsetup reencrypt. It will take some time depending on the speed and size of your NVME drive (for me it took 45 minutes on 512GB drive):
# cryptsetup reencrypt --new --reduce-device-size=16M --type=luks2 -c aes-xts-plain64 -s 256 -h sha256 --use-urandom /dev/nvme0n1p2
-
Make some coffee and relax.
-
Once the encryption is done, open newly encrypted partition:
# cryptsetup luksOpen /dev/nvme0n1p2 rootfs
- Expand existing ext4 filesystem to use all available space:
# resize2fs /dev/mapper/rootfs
- Mount newly created encrypted root and check its contents to make sure encryption went fine:
# mkdir -p /mnt/root
# mount /dev/mapper/rootfs /mnt/root
# cd /mnt/root
# ls -la
# umount /mnt/root
At this point we have encrypted ext4 partition that we want to convert to btrfs (and make some subvolumes). My layout of subvolumes is kinda opinionated, your mileage may vary: if you know what you're doing, you can make your own subvolumes structure as you please.
- Convert your ext4 filesystem to btrfs. It will take some time (on my 512GB drive it took five minutes)
# btrfs-convert /dev/mapper/rootfs
-
Make some coffee and relax.
-
Mount new btrfs partition to work with it
# mount -o rw,relatime,compress=zstd:3,space_cache=v2 /dev/mapper/rootfs /mnt/root/
- Create subvolumes.
In my case, I use the following subvolumes:
@for/@homefor/home@var_logfor/var/log@var_lib_dockerfor/var/lib/docker@snapshots/{root,home,docker}for/.snapshots,/home/.snapshotsand/var/lib/docker/.snapshotsrespectively.
# cd /mnt/root
# btrfs subvolume create @
# btrfs subvolume create @home
# btrfs subvolume create @var_log
# btrfs subvolume create @var_lib_docker
# mkdir "@snapshots"
# btrfs subvolume create @snapshots/root
# btrfs subvolume create @snapshots/home
# btrfs subvolume create @snapshots/var_lib_docker
- Move everything to subvolumes.
Note, that your structure of subvolumes may differ, but if it is, you know what you're doing anyway.
# mv var/lib/docker/* @var_lib_docker/
# rmdir var/lib/docker
# mv var/log/* @var_log
# rmdir var/log
# mv home/* @home/
# rmdir home
# for i in var boot etc opt media mnt srv tmp tmpdmesg usr root bin lib sbin dev proc run sys; do mv $i @/$i; done
# mkdir @/home
# mkdir @/var/log
# mkdir @/var/lib/docker
# mkdir @/.snapshots
# mkdir @home/.snapshots
# mkdir @var_lib_docker/.snapshots
# rm -rf lost+found
- Make sure everything is moved to snapshots:
# ls -lah
The result must contain only your new created subvolumes and a special subvolume named ext2_saved, which was created
by btrfs-convert for rollback purposes.
- Delete
ext2_savedsubvolume:
# btrfs subvolume delete ext2_saved
- Finally, it's time to reboot!
# reboot -f
Since now we've setup the encryption, each time you reboot or power on your system it will be booted into initramfs. To unlock your system and continue boot, you must execute this command:
# cryptroot-unlock
It will ask you for your password for encrypted partition and then will close your connection to the system. In a few moments you may reconnect using your standard SSH login.
If you don't want to enter cryprsetup-unlock command each time you boot (or don't want to have the ability to mess
around with your system during initramfs stage), you can edit /etc/dropbear/initramfs/authorized_key file and
prepend the line with your SSH key with command="/usr/bin/cryptroot-unlock", so it will look like this:
command="/usr/bin/cryptroot-unlock" ssh-rsa AAA...<your very long and secure key>
After updating /etc/dropbear/initramfs/authorized_key, run update-initramfs -u to apply your changes and then
reboot.
I highly recommend to have two SSH keys at your disposal: one which will be restricted to use cryptroot-unlock only
and another one without restrictions. It will allow you to unlock remotely your Raspberry Pi conveniently and also have
the ability to boot into a rescue environment in the headless mode in case anything gets wrong in the future and you
can't boot into your system anymore.
I hope you will find this guide useful. Don't forget to star it and share with your curious homelab running friends!
- Vladimir Hodakov
Last update: 22.07.2025
Thank you so much, kept trying to figure this out with pre-creating an encrypted volume on an SD card (for my intended use case I consider that to be an acceptable rootfs storage medium) and
rsyncing the (mounted by way of a loop device in between) image contents into that volume, but could never get it to boot; knew the in-place reencrypt was possible from here butsdm-cryptconfigin particular didn't work for whatever reason so... regardless, I got it working now.I didn't do the btrfs part since I don't see a need for snapshots or any of the other nice stuff it has for what I wanted to do with the Pi in question (offsite and in and of itself dispensable
zfs sendtarget for eventual 3-2-1, or actually 3-2-2, backup scheme, with spinning drives attached to it somehow which I'm still not entirely decided on), and in my case it worked without the initramfs-tools hooks part at all (just installingcryptsetup-initramfs dropbear-initramfs cryptsetup busyboxmade everything needed for me to make it work show up in the image), and made it work on Armbian instead of RPi OS (Armbian's build system technically has an option in its build system to generate an encrypted rootfs image, but I could never get it to work on a Pi... bug report-worthy possibly?), will probably attempt to write either an Ansible role/playbook or bash script that automates doing this remotely at some point.FYI it's possible to specify port options and other stuff (including the command for it to run upon login which I find easier to do than the described authorized_keys
command=solution) in/etc/dropbear/initramfs/dropbear.conflike this:DROPBEAR_OPTIONS="-I 180 -j -k -p 2222 -s -c cryptroot-unlock"(options taken from and described here which I found when setting this up on a Proxmox host a while back, and then took this from my notes when doing this stuff)