Debian Root on Btrfs with LUKS encryption

by Maurice Zhou, posted in Linux

Caution

Requirements

Encryption

This guide will use LUKS1 for disk encryption. As of Debian bullseye, GRUB bootloader only supports LUKS1 on boot partition. Btrfs native encryption is still under development.

Encrypted:

Not encrypted:

Step 1: Prepare the install environment

  1. Boot the Live CD. Press Ctrl-Alt-T to open a terminal.
  2. Setup repositories, install OpenSSH server and set a password for live user.
sudo nano /etc/apt/sources.list
# append `contrib` after `main`
sudo apt update
sudo apt install --yes openssh-server nano
passwd
  1. Optional: access the live system with SSH
whoami
# find out the current user name
ip -4 addr show scope global
# find out the IPv4 address
ssh user@IP
# on another computer
  1. Switch to root account
sudo -i
  1. Install system installation tools in the Live system
apt install --yes gdisk btrfs-progs dosfstools cryptsetup arch-install-scripts debootstrap

Step 2: Disk formatting

  1. For single disk installation, set a disk variable. Set a username and hostname as well.
d=/dev/disk/by-id/nvme-PM951_NVMe_SAMSUNG_256GB__S29NNX0H516379
username='YOUR USER NAME'
hostname_target='YOUR HOSTNAME'
pw='YOUR LUKS DISK ENCRYPTION PASSWORD'
DISTRO='debian'
  1. Clear the disk and create partitions
blkdiscard $d # for SSD with TRIM support, add `-f` for newer versions
sgdisk --zap-all $d # clear partition table
sgdisk -n1:0:256M $d # create 256MB EFI partition
sgdisk -n2:0:0 $d # create system partition with remaining space
  1. Set variables for LUKS mapper name
partuuid=`blkid -s PARTUUID -o value $d-part2`
mapper_name=luks1-partuuid-$partuuid
mapper_path=/dev/mapper/$mapper_name
  1. Format the system partition with cryptsetup
echo -n $pw | cryptsetup luksFormat --type luks1 $d-part2 -
echo -n $pw | cryptsetup open $d-part2 $mapper_name -
  1. Format the EFI boot partition and system partition
mkfs.vfat $d-part1
mkfs.btrfs $mapper_path
mount $mapper_path /mnt
  1. Create btrfs subvolumes. First, create a subvolume named debian. Then, create @, which is a shorthand for root directory. @/0 is the first snapshot of the root directory andwill be mounted at /. Other snapshots in the future will be located at debian/@/1, debian/@/2, etc. To exclude other directories from the root snapshot, other subvolumes need to be created and mounted separately. Here, according to OpenSuse exclude /home, /tmp, /var/, /opt, /root, /srv, /usr/local and a subvolume for swap file.
# https://en.opensuse.org/SDB:BTRFS
cd /mnt
btrfs subvolume create $DISTRO
btrfs subvolume create $DISTRO/@
btrfs subvolume create $DISTRO/@/0
btrfs subvolume create $DISTRO/@home
btrfs subvolume create $DISTRO/@opt
btrfs subvolume create $DISTRO/@root
btrfs subvolume create $DISTRO/@srv
btrfs subvolume create $DISTRO/@tmp
btrfs subvolume create $DISTRO/@usr
btrfs subvolume create $DISTRO/@usr/local
btrfs subvolume create $DISTRO/@var
btrfs subvolume create $DISTRO/@swap
cd ~
umount /mnt
  1. Mount Btrfs subvolumes
mount $mapper_path /mnt -o subvol=/$DISTRO/@/0
mkdir -p /mnt/.snapshots
mkdir -p /mnt/home
mkdir -p /mnt/opt
mkdir -p /mnt/root
mkdir -p /mnt/srv
mkdir -p /mnt/tmp
mkdir -p /mnt/usr/local
mkdir -p /mnt/var
mkdir -p /mnt/swap
mount $mapper_path /mnt/.snapshots/ -o subvol=$DISTRO/@,compress=zstd
mount $mapper_path /mnt/home/ -o subvol=$DISTRO/@home,compress=zstd
mount $mapper_path /mnt/opt/ -o subvol=$DISTRO/@opt,compress=zstd
mount $mapper_path /mnt/root/ -o subvol=$DISTRO/@root,compress=zstd
mount $mapper_path /mnt/srv/ -o subvol=$DISTRO/@srv,compress=zstd
#uncomment to use btrfs for /tmp. default to tmpfs mounted by systemd
#mount $mapper_path /mnt/tmp -o subvol=$DISTRO/@tmp,compress=zstd
mount $mapper_path /mnt/usr/local -o subvol=$DISTRO/@usr/local,compress=zstd
mount $mapper_path /mnt/var/ -o subvol=$DISTRO/@var,compress=zstd
mount $mapper_path /mnt/swap -o subvol=$DISTRO/@swap
  1. Mount EFI boot partition
mkdir -p /mnt/boot/efi
mount $d-part1 /mnt/boot/efi/
  1. Create a LUKS key file and add it to the encrypted partition. Initramfs won’t be able to unlock the partition automatically without this step.
mkdir -p /mnt/lukskey
dd bs=512 count=8 if=/dev/urandom of=/mnt/lukskey/crypto_keyfile.bin
echo -n $pw | cryptsetup luksAddKey $d-part2 /mnt/lukskey/crypto_keyfile.bin -
  1. Install base system
debootstrap bullseye /mnt https://mirrors.example.com/debian

Step 3: System Configuration

  1. Generate /etc/fstab
genfstab -U /mnt >> /mnt/etc/fstab
  1. Configure hostname
echo $hostname_target > /mnt/etc/hostname
  1. Configure repository.
tee /mnt/etc/apt/sources.list << EOF
deb https://mirrors.example.com/debian bullseye main contrib
EOF
  1. Optional: don’t let apt install recommended packages
echo 'apt::install-recommends "false";' >> /mnt/etc/apt/apt.conf
  1. Enter the new system with arch-chroot
arch-chroot /mnt /usr/bin/env d=$d username=$username DISTRO=$DISTRO bash --login
  1. Set variables for LUKS
partuuid=`blkid -s PARTUUID -o value $d-part2`
mapper_name=luks1-partuuid-$partuuid
mapper_path=/dev/mapper/$mapper_name
cryptkey=/lukskey/crypto_keyfile.bin
echo "$mapper_name PARTUUID=$partuuid $cryptkey luks,discard" >> /etc/crypttab
  1. Add a new user
useradd -s /bin/bash -U -G sudo,video -m $username
# interactively set user password
passwd $username
  1. Setup repositories and upgrade system
apt update
apt upgrade --yes

Step 3.1: Packages

apt install --yes grub-efi-amd64-signed linux-image-amd64 shim-signed btrfs-progs dosfstools cryptsetup cryptsetup-initramfs snapper apt-transport-https ca-certificates sudo locales
# system utilities
apt install --yes network-manager gdisk arch-install-scripts debootstrap man nano
# SwayWM
apt install --yes sway swayidle swaylock grim wdisplays wofi i3status kitty
# Qt5 Wayland support
apt install --yes qt5ct qtwayland5
# GNOME icons and GNOME Qt theme
apt install --yes gnome-themes-extra adwaita-qt
# pulseaudio
apt install --yes pulseaudio pavucontrol-qt
# internal display brightness
apt install --yes brightnessctl brightness-udev
# Intel VAAPI driver
apt install --yes intel-media-va-driver
# Desktop applications
apt install --yes keepassxc firefox-esr mpv zathura lximage-qt pcmanfm-qt featherpad gvfs-backends gvfs-fuse libglib2.0-bin
# xarchiver and its dependencies
apt install --yes xarchiver p7zip-full unzip arj lhasa lrzip lzip lzop ncompress unrar-free unar zip zstd
# for xorg support, install xwayland
apt install --yes xwayland
# for Chinese input support, install fcitx5 (runs under xwayland)
apt install --yes fcitx5 fcitx5-frontend-gtk3 fcitx5-frontend-qt5 fcitx5-module-dbus fcitx5-module-wayland fcitx5-module-xorg fcitx5-config-qt 
# pinyin support in a large package
apt install --yes fcitx5-pinyin
# watch youtube and write blog
apt install --yes proxychains4 youtube-dl hugo git
# fonts
apt install --yes fonts-noto-cjk fonts-freefont-ttf

Step 3.2: GRUB and initramfs

echo "GRUB_ENABLE_CRYPTODISK=y" >> /etc/default/grub
echo "GRUB_CMDLINE_LINUX=\"cryptdevice=PARTUUID=$partuuid:$mapper_name root=$mapper_path cryptkey=rootfs:$cryptkey\"" >> /etc/default/grub
echo 'UMASK=0077' >> /etc/initramfs-tools/initramfs.conf
echo "KEYFILE_PATTERN=/lukskey/*.bin" >> /etc/cryptsetup-initramfs/conf-hook
chmod 600 /lukskey/*
update-initramfs -u -k all
grub-install --bootloader-id=$DISTRO
grub-mkconfig -o /boot/grub/grub.cfg
chmod 600 /boot/initrd*

Step 3.3: Create swap file

touch /swap/swapfile
truncate -s 0 /swap/swapfile
chattr +C /swap/swapfile
btrfs property set /swap/swapfile compression none
dd if=/dev/zero of=/swap/swapfile bs=1M count=8192 status=progress
chmod 600 /swap/swapfile
mkswap /swap/swapfile
swapon /swap/swapfile
echo "/swap/swapfile none swap defaults 0 0" >> /etc/fstab

Step 3.4: Enable NetworkManager

systemctl enable NetworkManager.service

Step 3.5: Power button press to suspend

echo "HandlePowerKey=suspend" >> /etc/systemd/logind.conf

Step 3.6: Locales, timezone

dpkg-reconfigure tzdata
dpkg-reconfigure locales

Step 3.7: Mount tmpfs to /tmp

cp /usr/share/systemd/tmp.mount /etc/systemd/system/
systemctl enable tmp.mount

Step 3.8: After reboot: enable btrfs snapshot

umount /.snapshots/
rmdir /.snapshots/
snapper -c root create-config /
rmdir /.snapshots/
mkdir /.snapshots/
mount /.snapshots/
snapper -c home create-config /home/
systemctl enable --now /lib/systemd/system/snapper-*

Troubleshooting

  1. Boot from a Live CD
  2. Install btrfs-progs and arch-install-scripts
  3. Set variable for the target disk, user name and LUKS password
d=/dev/disk/by-id/nvme-PM951_NVMe_SAMSUNG_256GB__S29NNX0H516379
pw='LUKS password'
DISTRO='debian'
  1. Unlock and map the LUKS disk
partuuid=`blkid -s PARTUUID -o value $d-part2`
mapper_name=luks1-partuuid-$partuuid
mapper_path=/dev/mapper/$mapper_name
echo -n $pw | cryptsetup open $d-part2 $mapper_name -
  1. Mount Btrfs subvolumes
# change root subvolume accordingly, if you have rolled back any snpshot
mount $mapper_path /mnt/ -o subvol=$DISTRO/@/0
mount $mapper_path /mnt/.snapshots/ -o subvol=$DISTRO/@
mount $mapper_path /mnt/home/ -o subvol=$DISTRO/@home
mount $mapper_path /mnt/opt/ -o subvol=$DISTRO/@opt
mount $mapper_path /mnt/root/ -o subvol=$DISTRO/@root
mount $mapper_path /mnt/srv/ -o subvol=$DISTRO/@srv
mount $mapper_path /mnt/tmp -o subvol=$DISTRO/@tmp
mount $mapper_path /mnt/usr/local -o subvol=$DISTRO/@usr/local
mount $mapper_path /mnt/var/ -o subvol=$DISTRO/@var
mount $mapper_path /mnt/swap -o subvol=$DISTRO/@swap
  1. Mount EFI boot partition
mount $d-part1 /mnt/boot/efi/
  1. Enter the system with arch-chroot and fix the system
arch-chroot /mnt /usr/bin/env d=$d bash --login
  1. When done, unmount the partition
exit
cd ~
mount | grep '/mnt/' | tac | awk '/\/mnt/ {print $3}' |     xargs -i{} umount -lf {}
umount /mnt
cryptsetup close $mapper_name

Rollback a root snapshot

  1. Check the snapshot number that you want to rollback to.
# snapper -c root list
   # | Type   | Pre # | Date                            | User | Cleanup  | Description     | Userdata     
-----+--------+-------+---------------------------------+------+----------+-----------------+--------------
  0  | single |       |                                 | root |          | current         |              
  2  | single |       | Fri 06 Nov 2020 07:13:40 PM CST | root | timeline | timeline        |              
  6  | pre    |       | Fri 06 Nov 2020 08:01:06 PM CST | root | number   | apt             |              
  7  | post   |     6 | Fri 06 Nov 2020 08:01:24 PM CST | root | number   | apt             |              
  1. Rollback the snapshot
# snapper --ambit classic rollback 6
Creating read-only snapshot of current system. (Snapshot 129.)
Creating read-write snapshot of snapshot 130. (Snapshot 130.)
Setting default subvolume to snapshot 130.
  1. Write down the default subvolume number.
130
  1. Comment out the line for system root in /etc/fstab
# comment it out
# UUID=fbe161e1-fcac-4995-a886-8daf2f654fdd	/         	btrfs     	rw,relatime,ssd,space_cache,subvolid=258,subvol=/archlinux/@/0,subvol=archlinux/@/0,compress=zstd	0 0
  1. Snapper will set the default subvolume to #130, but this does not have effect on GRUB. The root subvolume is hard-coded in grub.cfg, therefore the computer will still boot from the broken one. We will need to change that manually on next boot within GRUB menu.

  2. Reboot. In GRUB menu, press e to edit the boot entry. Replace every occurence of debian/@/0 with debian/@/130/snapshot. Press Ctrl+X to boot.

  3. After entering the system, update GRUB configuration to redetect the root subvolume.

update-grub
grub-mkconfig -o /boot/grub/grub.cfg
# somehow grub.cfg in EFI partition still contains reference to the broken subvolume
# delete and regenerate it with grub-install
rm /boot/efi/EFI/debian/grub.cfg
grub-install --bootloader-id=debian
  1. GRUB should boot from #130 on next reboot automatically.

Credits

edited: