User:Yolk/LUKS Root LUKS boot on Btrfs

From ArchWiki

TODO

I've forked this from https://wiki.archlinux.org/title/User:M0p/LUKS_Root_on_Btrfs and will be updating to include my full install

Notes

Fully encrypted Arch Linux setup. Everything except EFI system partition (contains a single grubx64.efi file) and BIOS boot sector is encrypted with LUKS 1.

Only single disk installation is supported. Consider Root on ZFS if you want to use a multi-disk setup.

Btrfs also has quite some pitfalls, gotchas and limitations. See [1], [2] and search reddit for posts involving crashed btrfs filesystems. If uncertain, use Root on ZFS. I use Root on ZFS on my laptop.

Existing data on target disk will be destroyed.

Password MUST BE interactively entered at boot and can not be automated. Efforts to automate this for the cloud exist, see [3].

For support and questions, leave a message on User_talk:M0p/LUKS_Root_on_Btrfs.

Data integrity

It should be noted that, with only one disk, data are not redundant and not protected from bitrot.

For best data integrity and redundancy, you can either:

  • minimum 2 disks: use Root on ZFS with mirror/raidz2/raidz3, or
  • minimum 3 disks: use single disk Root on Btrfs, but store important data on ZFS with mirror/raidz2/raidz3

Preinstallation

Follow official installation guide up to Installation_guide#Boot_the_live_environment.

Connect to the internet. If the target computer aquires IP address with DHCP, no further steps need to be taken. Otherwise, refer to Network Configuration wiki page.

SSH server

Interactively set root password with:

passwd

Start SSH server:

systemctl start sshd

Find the IP address of the target computer:

ip -4 address show scope global

On another computer, connect to the target computer with:

ssh root@192.168.1.10

Enter a bash shell:

bash

Select mirror

Kill reflector:

killall -9 reflector

Edit the following files:

/etc/pacman.d/mirrorlist

Uncomment and move mirrors to the beginning of the file.

Tools

pacman -Sy --needed cryptsetup btrfs-progs gdisk

Variables

In this part, we will set some variables to configure the system.

Timezone

List the available timezones with:

ls /usr/share/zoneinfo/

Store the target timezone in a variable:

INST_TZ=/usr/share/zoneinfo/Asia/Irkutsk

Host name

Store the host name in a variable:

INST_HOST='localhost'

Kernel variant

Store the kernel variant in a variable. Available variants in official repo are:

  • linux
  • linux-lts
  • linux-zen
  • linux-hardened
INST_LINVAR='linux'

Target disk

List the available disks with:

ls -d /dev/disk/by-id/* | grep -v part

If the disk is not in the command output, use /dev/disk/by-path.

Store the target disk in a variable:

DISK=/dev/disk/by-id/nvme-foo_NVMe_bar_512GB

Mountpoint

INST_MNT=$(mktemp -d)

Format and Partition

Clear the partition table:

sgdisk --zap-all $DISK

Create EFI system partition (for use now or in the future):

sgdisk -n1:1M:+1G -t1:EF00 $DISK

Create BIOS boot partition, skip if you don't use this:

sgdisk -a1 -n5:24K:+1000K -t5:EF02 $DISK

Create main partition:

sgdisk -n2:0:0   $DISK

cryptsetup mapper

This naming scheme is taken from Debian installer

boot_partuuid=`blkid -s PARTUUID -o value $DISK-part2`
boot_mapper_name=cryptroot-luks1-partuuid-$boot_partuuid
boot_mapper_path=/dev/mapper/$boot_mapper_name

Format and open LUKS container

cryptsetup luksFormat --type luks1 $DISK-part2
cryptsetup open $DISK-part2 $boot_mapper_name

Format the LUKS container as Btrfs

mkfs.btrfs $boot_mapper_path
mount $boot_mapper_path $INST_MNT

Create subvolumes

The idea is to separate persistent data from root file system rollbacks.

System is installed to snapshot 0.

cd $INST_MNT

btrfs subvolume create @
mkdir @/0
btrfs subvolume create @/0/snapshot

for i in {home,root,srv,usr,usr/local,swap,var};
do btrfs subvolume create @$i;
done

# exclude these dirs under /var from system snapshot
for i in {tmp,spool,log};
do btrfs subvolume create @var/$i;
done

Mount subvolumes

cd ~
umount $INST_MNT
mount $boot_mapper_path $INST_MNT -o subvol=/@/0/snapshot,compress-force=zstd,noatime,space_cache=v2

mkdir -p $INST_MNT/{.snapshots,home,root,srv,tmp,usr/local,swap}

mkdir -p $INST_MNT/var/{tmp,spool,log}
mount $boot_mapper_path $INST_MNT/.snapshots/ -o subvol=@,compress-force=zstd,noatime,space_cache=v2

# mount subvolumes
# separate /{home,root,srv,swap,usr/local} from root filesystem
for i in {home,root,srv,swap,usr/local};
do mount $boot_mapper_path $INST_MNT/$i -o subvol=@$i,compress-force=zstd,noatime,space_cache=v2;
done

# separate /var/{tmp,spool,log} from root filesystem
for i in {tmp,spool,log};
do mount $boot_mapper_path $INST_MNT/var/$i -o subvol=@var/$i,compress-force=zstd,noatime,space_cache=v2;
done

Disable Copy-on-Write

for i in {swap,};
do chattr +C $INST_MNT/$i;
done

Format and mount EFI partition

mkfs.vfat -n EFI $DISK-part1
mkdir -p $INST_MNT/boot/efi
mount $DISK-part1 $INST_MNT/boot/efi

Packages

Only essential packages given here

pacstrap $INST_MNT base vi mandoc grub cryptsetup btrfs-progs snapper snap-pac grub grub-btrfs
chmod 750 $INST_MNT/root
chmod 1777 $INST_MNT/var/tmp/

Install kernel

pacstrap $INST_MNT $INST_LINVAR

If your computer has hardware that requires firmware to run:

pacstrap $INST_MNT linux-firmware

If you boot your computer with EFI:

pacstrap $INST_MNT dosfstools efibootmgr

Microcode:

  • pacstrap $INST_MNT amd-ucode
  • pacstrap $INST_MNT intel-ucode

For other optional packages, see ArchWiki.

System Configuration

First, generate fstab

genfstab -U $INST_MNT >> $INST_MNT/etc/fstab

Remove hard-coded system subvolume. If not removed, system will ignore btrfs default-id setting, which is used by snapper when rolling back.

sed -i 's|,subvolid=258,subvol=/@/0/snapshot,subvol=@/0/snapshot||g' $INST_MNT/etc/fstab

Create LUKS key for initramfs. Without this, you will need to enter the password twice: once in GRUB, once in initramfs.

mkdir -p $INST_MNT/lukskey
dd bs=512 count=8 if=/dev/urandom of=$INST_MNT/lukskey/crypto_keyfile.bin
chmod 600 $INST_MNT/lukskey/crypto_keyfile.bin
cryptsetup luksAddKey $DISK-part2 $INST_MNT/lukskey/crypto_keyfile.bin
chmod 700 $INST_MNT/lukskey

Configure initramfs.

cryptkey=/lukskey/crypto_keyfile.bin
mv $INST_MNT/etc/mkinitcpio.conf $INST_MNT/etc/mkinitcpio.conf.original
tee $INST_MNT/etc/mkinitcpio.conf << EOF
BINARIES=(/usr/bin/btrfs)
FILES=($cryptkey)
HOOKS=(base udev autodetect modconf block encrypt filesystems keyboard fsck grub-btrfs-overlayfs)
EOF

Edit /etc/default/grub

  • Enable cryptodisk for GRUB
  • Add kernel command line to decrypt and map LUKS
  • Specify LUKS key
  • Specify root device
echo "GRUB_ENABLE_CRYPTODISK=y" >> $INST_MNT/etc/default/grub
echo "GRUB_CMDLINE_LINUX=\"cryptdevice=PARTUUID=$boot_partuuid:$boot_mapper_name root=$boot_mapper_path cryptkey=rootfs:$cryptkey\"" >> $INST_MNT/etc/default/grub

Optional: Create swapfile. Adjust the file size if needed.

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

Host name:

echo $INST_HOST > $INST_MNT/etc/hostname

Configure the network interface: Find the interface name:

ip link

Store it in a variable:

INET=enp1s0

Create network configuration:

tee $INST_MNT/etc/systemd/network/20-default.network <<EOF

[Match]
Name=$INET

[Network]
DHCP=yes
EOF

Customize this file if the system is not a DHCP client. See Network Configuration.

Timezone:

ln -sf $INST_TZ $INST_MNT/etc/localtime
hwclock --systohc

Locale:

echo "en_US.UTF-8 UTF-8" >> $INST_MNT/etc/locale.gen
echo "LANG=en_US.UTF-8" >> $INST_MNT/etc/locale.conf

Other locales should be added after reboot.

Chroot:

arch-chroot $INST_MNT /usr/bin/env  DISK=$DISK \
  INST_UUID=$INST_UUID bash --login

Apply locales:

locale-gen

Enable networking:

systemctl enable systemd-networkd systemd-resolved

Set root password:

passwd

Generate initramfs:

mkinitcpio -P

Enable btrfs service

systemctl enable grub-btrfs.path

Enable snapper

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

Optionally add a normal user, use --btrfs-subvolume-home:

useradd -s /bin/bash -U -G wheel,video -m --btrfs-subvolume-home myuser
snapper --no-dbus -c myuser create-config /home/myuser

GRUB installation

EFI

grub-install

Some motherboards does not properly recognize GRUB boot entry, to ensure that your computer will boot, also install GRUB to fallback location with:

grub-install --removable

BIOS

grub-install $DISK

Generate GRUB menu

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

Finish Installation

exit
mount | grep "$INST_MNT/" | tac | cut -d' ' -f3 | xargs -i{} umount -lf {}
umount $INST_MNT
cryptsetup close $boot_mapper_name
reboot

Create root filesystem snapshot

snapper -c root create

Additional options for create, such as --description, see snapper help.

Rollback root filesystem

If the system is broken, reboot, select a bootable entry in GRUB snapshot list. Your computer will then boot with a read-only root filesystem with a writable OverlayFS on it.

Run snapper -c root list to find out which snapshot you want to rollback to.

 # | Type   | Pre # | Date                            | User | Cleanup | Description        | Userdata
---+--------+-------+---------------------------------+------+---------+--------------------+---------
0- | single |       |                                 | root |         | current            |
1  | single |       | Tue 09 Feb 2021 09:44:55 PM +08 | root | number  | boot               |
2  | single |       | Tue 09 Feb 2021 09:49:30 PM +08 | root | number  | boot               |
3  | pre    |       | Tue 09 Feb 2021 09:49:51 PM +08 | root | number  | pacman -S dropbear |
4  | post   |     3 | Tue 09 Feb 2021 09:49:52 PM +08 | root | number  | dropbear           |

If we want to rollback to 3, run snapper --ambit classic rollback 3 to rollback.

Ambit is classic.
Creating read-only snapshot of current system. (Snapshot 5.)
Creating read-write snapshot of snapshot 3. (Snapshot 6.)
Setting default subvolume to snapshot 6.

Remember the new default subvolume number 6. This will be the new / the computer will boot into. Now run grub-mkconfig -o /boot/grub/grub.cfg to let GRUB know about the new snapshots. Reboot, select snapshot 6 from GRUB menu.

After reboot, run

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

to make snapshot 6 the default boot volume. Then you won't need to manually select it again on next reboot.