A simple tutorial on installing Debian on an encrypted LVM with btrfs and subvolumes :-)

There are plenty tutorials out there already and I used to mix several methods to create my way of the process. This blog post is a kind note for myself. My recipe for something that fits and that I can read and use when I need it – but shared so that others can use it as wels :-)

A brief roadmap:

I’ll use a Debian LiveDVD for a command line install using debootstrap. /boot will be an unencrypted partition and on it’s own – separate from the rest. The same applies for the bios boot partition to support non-UEFI systems, too.

I’m doing all the following on a local virtual machine and I prefer to work with my terminal emulator, so I’ll set a password for the user user and install ssh to be able to connect.

sudo su -
passwd user
apt update
apt install arch-install-scripts ssh cryptsetup lvm2 debootstrap vim gdisk btrfs-progs

After I connected via ssh I’m setting up the disk with three partitions: one for “/boot“, one for the encrypted LVM with two logical volumes for the system and swap and one for the bios boot partition. I use gdisk for partitioning – but of course you can use whatever you prefer.

gdisk /dev/vda
n <ENTER>
(Partition number) <ENTER>
(First sector) <ENTER>
(Last sector) +2G <ENTER>
(Partition type) 8300 <ENTER>

n <ENTER>
(Partition number) 128 <ENTER>
(First sector) -2M <ENTER>
(Last sector) <ENTER>
(Partition type) ef02 <ENTER>

n <ENTER>
(Partition number) <ENTER>
(First sector) <ENTER>
(Last sector) <ENTER>
(Partition type) 8309 <ENTER>

p <ENTER>
w <ENTER>

It’s just a habit of mine to place the bios boot partition to the very end of the disk so I set its partition number to 128. Now it’s time to set up the LUKS container and open it:

cryptsetup luksFormat --type luks2 --cipher aes-xts-plain64 --hash sha256 --iter-time 2000 --key-size 256 --pbkdf argon2id --use-random --verify-passphrase /dev/vda2

WARNING!
========
This will overwrite data on /dev/vda2 irrevocably.

Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for /dev/vda2: 
Verify passphrase:

cryptsetup luksOpen /dev/vda2 crypt
Enter passphrase for /dev/vda2:

You can choose whatever name you like for the container at the end of the luksOpen command. It’s only mapping name.

Now I’m setting up the LVM:

pvcreate /dev/mapper/crypt 
  Physical volume "/dev/mapper/crypt" successfully created.

vgcreate vg-zebra /dev/mapper/crypt
  Volume group "vg--zebra" successfully created

lvcreate -L 4G -n Swap vg-zebra
  Logical volume "Swap" created.

lvcreate -l 100%FREE -n Root vg-zebra
  Logical volume "Root" created.

I used a dash (a.k.a. hyphen) in the volume group’s name. This is ok, but… if you’re working on the LVM-level you have to use the name exactly as used. But later, you’re going to see /dev/mapper/vg–zebra-Root – so with two dashes. This is because the device mapper has its own conventions and the dash is used to separate volume group names and logical volume names, so if you use a dash in the volume group name it puts another dash in front of it to mask it and make it part of the name 🙂

Now I’m setting up the filesystems:

mkfs.ext2 /dev/vda1

mkswap -L swap /dev/mapper/vg--zebra-Swap

mkfs.btrfs -L btrfs-pool /dev/mapper/vg--zebra-Root

mount /dev/mapper/vg--zebra-Root /mnt

for S in rootfs home var var@lib; do btrfs subvolume create /mnt/@${S}; done

umount /mnt

Next step is to mount the filesystems and to create mountpoints:

mount -t btrfs -o subvol=@rootfs /dev/mapper/vg--zebra-Root /mnt
mkdir /mnt/{boot,home,var}
mount /dev/vda1 /mnt/boot/
mount -t btrfs -o subvol=@home /dev/mapper/vg--zebra-Root /mnt/home
mount -t btrfs -o subvol=@var /dev/mapper/vg--zebra-Root /mnt/var
mkdir /mnt/var/lib
mount -t btrfs -o subvol=@var@lib /dev/mapper/vg--zebra-Root /mnt/var/lib

swapon /dev/mapper/vg--zebra-Swap 

lsblk
NAME                 MAJ:MIN RM  SIZE RO TYPE  MOUNTPOINTS
loop0                  7:0    0  1.2G  1 loop  /run/live/rootfs/filesystem.squashfs
sr0                   11:0    1  1.8G  0 rom   /run/live/medium
vda                  254:0    0   40G  0 disk  
├─vda1               254:1    0    2G  0 part  /mnt/boot
├─vda2               254:2    0   38G  0 part  
│ └─crypt            252:0    0   38G  0 crypt 
│   ├─vg--zebra-Swap 252:1    0    4G  0 lvm   [SWAP]
│   └─vg--zebra-Root 252:2    0   34G  0 lvm   /mnt/var/lib
│                                              /mnt/var
│                                              /mnt/home
│                                              /mnt
└─vda128             259:1    0    2M  0 part  

Now I’m running debootstrap and creating the /mnt/etc/apt/sources.list file and /mnt/etc/fstab afterwards:

debootstrap trixie /mnt

echo '## If you want access to contrib and non-free components,
## add " contrib non-free" after every "non-free-firmware" in this file:
deb https://deb.debian.org/debian bookworm main non-free-firmware
deb-src https://deb.debian.org/debian bookworm main non-free-firmware

deb https://security.debian.org/debian-security bookworm-security main non-free-firmware
deb-src https://security.debian.org/debian-security bookworm-security main non-free-firmware

deb https://deb.debian.org/debian bookworm-updates main non-free-firmware
deb-src https://deb.debian.org/debian bookworm-updates main non-free-firmware' | sed 's/bookworm/trixie/g' | sed 's/\(http\)s/\1/' > /mnt/etc/apt/sources.list

genfstab -U /mnt >> /mnt/etc/fstab

The bookworm sources list is from the Debian-Wiki and to be able to install anything https needs to be swapped by http at this point.

Now I’m entering the chroot environment to install the missing components. You may wish to add more, swap some of mine by some of yours or skip certain stuff – this is what I did and need:

arch-chroot /mnt

export PS1="(- chroot -) "

passwd

apt install apt-transport-https locales
apt modernize-sources

dpkg-reconfigure locales
dpkg-reconfigure tzdata

apt install linux-image-amd64 initramfs-tools sudo chrony ssh vim git tmux grub-pc btrfs-progs gdisk wget curl net-tools lvm2 cryptsetup cryptsetup-initramfs man manpages

echo 'lvm2
dm-mod
dm-crypt
btrfs' >> /etc/initramfs-tools/modules

Forgot to install cryptsetup-initramfs. Yeah, I know sounds obvious, but I tend to forget that little package even though cryptsetup is already installed. So the initrd didn’t contain it and LUKS unlock didn’t happen. I repeated every single step to be sure, but still nothing. It took me quite a while to remember! 🙂 This package installs quite a few things – you can check with dpkg -L cryptsetup-initramfs and I decided not go deeper into its magic. Install it and it works! 🙂

Now I create /etc/crypttab and modify the GRUB_CMDLINE_LINUX in /etc/default/grub:

MY_UUID=$(cryptsetup luksDump /dev/vda2 | sed -n '/UUID/s/UUID:[^0-9a-z]*//p')

echo "crypt UUID=${MY_UUID} none luks" >> /etc/crypttab

sed -i 's/^\(GRUB_CMDLINE_LINUX="\)"/\1cryptdevice=UUID='$(echo ${MY_UUID})':crypt root=\/dev\/mapper\/vg--zebra-Root"/' /etc/default/grub

Now I rebuild the initrd image and verify everything we need got placed into it:

update-initramfs -u -k all

lsinitramfs /boot/initrd.img-6.12.43+deb13-amd64 | grep btrfs

lsinitramfs /boot/initrd.img-6.12.43+deb13-amd64 | grep lvm

lsinitramfs /boot/initrd.img-6.12.43+deb13-amd64 | grep cryptsetup

Next step is to install grub and create its config file

grub-install /dev/vda

grub-mkconfig -o /boot/grub/grub.cfg

# The following is optional and only needed if you want a rough verification that everything is fine

grep rootfs /boot/grub/grub.cfg

And that’s it. Exit the chroot by firing up exit and reboot your machine.