Skip to content

Instantly share code, notes, and snippets.

@fat0troll
Last active November 21, 2025 19:25
Show Gist options
  • Select an option

  • Save fat0troll/888e29e8d13991646c12d17c4ee3f119 to your computer and use it in GitHub Desktop.

Select an option

Save fat0troll/888e29e8d13991646c12d17c4ee3f119 to your computer and use it in GitHub Desktop.
Raspberry Pi Encrypted Root Partition + btrfs Headless Setup Guide

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.

Prerequisites

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

Preparing the initramfs

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.

  1. Update your system:
# apt update && apt upgrade
  1. Reboot your system to make sure you're using the latest kernel:
# reboot
  1. Reconnect to your system, and install required packages:
# apt install dropbear-initramfs busybox btrfs-progs
  1. Add new modules to initramfs by adding these lines to the end of /etc/initramfs-tools/modules file. 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
  1. Make sure dropbear has network. Add the following line to the end of /etc/initramfs-tools/initramfs.conf file, replacing the <hostname> part with your hostname of choice.
IP=::::<hostname>::dhcp:::
  1. 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
  1. Add your SSH key to login: copy and paste it into /etc/dropbear/initramfs/authorized_keys.

  2. 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
  1. Make new initramfs hook executable:
# chmod +x /etc/initramfs-tools/hooks/fs_hooks
  1. Update initramfs to apply all these changes:
# update-initramfs -u
  1. 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

Preparing encrypted boot

  1. Edit /etc/fstab and 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!

  1. Update initramfs, ignoring its warning about missing root partition
# update-initramfs -u
  1. Edit /boot/firmware/cmdline.txt:

    • Change root=LABEL=writable to root=/dev/mapper/rootfs
    • Add rootflags=rw,relatime,compress=zstd:3,space_cache=v2,subvol=@ cryptdevice=/dev/nvme0n1p2:rootfs to the end of the line
  2. Cross fingers and reboot:

# reboot

Encrypting the root filesystem

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.

  1. Run e2fsck on your root FS:
# e2fsck -f /dev/nvme0n1p2
  1. Shrink rootfs to make some space for LUKS header:
# resize2fs -M /dev/nvme0n1p2
  1. 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
  1. Make some coffee and relax.

  2. Once the encryption is done, open newly encrypted partition:

# cryptsetup luksOpen /dev/nvme0n1p2 rootfs
  1. Expand existing ext4 filesystem to use all available space:
# resize2fs /dev/mapper/rootfs
  1. 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

Converting to btrfs and setting up subvolumes

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.

  1. Convert your ext4 filesystem to btrfs. It will take some time (on my 512GB drive it took five minutes)
# btrfs-convert /dev/mapper/rootfs
  1. Make some coffee and relax.

  2. Mount new btrfs partition to work with it

# mount -o rw,relatime,compress=zstd:3,space_cache=v2 /dev/mapper/rootfs /mnt/root/
  1. Create subvolumes.

In my case, I use the following subvolumes:

  • @ for /
  • @home for /home
  • @var_log for /var/log
  • @var_lib_docker for /var/lib/docker
  • @snapshots/{root,home,docker} for /.snapshots, /home/.snapshots and /var/lib/docker/.snapshots respectively.
# 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
  1. 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
  1. 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.

  1. Delete ext2_saved subvolume:
# btrfs subvolume delete ext2_saved
  1. Finally, it's time to reboot!
# reboot -f

Booting into system and hardening boot sequence

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

@generic-internet-user
Copy link

generic-internet-user commented Oct 18, 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 but sdm-cryptconfig in 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 send target 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 installing cryptsetup-initramfs dropbear-initramfs cryptsetup busybox made 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.conf like 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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment