Alpine Linux
Alpine Linux

I recently built a file server. For the operating system, I decided to use Alpine Linux. As with most of my Linux systems, I wanted to utilize full disk encryption. The following guide is mostly based off the documentation on the Alpine Wiki, and goes through the installation of Alpine on a modern UEFI system, with LUKS full disk encryption.

After booting from the Alpine USB installation media, I used ifconfig eth0 up and udhcpc eth0 to bring up a network connection. I used setup-apkrepos to configure the package manager. I then installed the following tools, needed for setting up encrypted disks and logical volumes.

apk update
apk add gptfdisk cryptsetup lvm2 e2fsprogs util-linux dosfstools

Next I partitioned the disk. If your system is using an NVME device, the primary device will look like /dev/nvme0n1 and the individual partitions will be /dev/nvme0n1p1 and /dev/nvme0n1p2. For systems using SATA devices, your primary device will likely be /dev/sda and the partitions will be /dev/sda1 and /dev/sda2. For the rest of this guide, I assumed an NVME device as the primary boot drive. If you’re using SATA devices, adjust the device names appropriately. To create a GPT partition, I used the gdisk command installed from the gptfdisk package.

# gdisk /dev/nvme0n1

Command (? for help): p
Disk /dev/nvme0n1: 1000215216 sectors, 476.9 GiB
Model: SK hynix PC300 HFS512GD9MND-5510A
Sector size (logical/physical): 512/512 bytes
Disk identifier (GUID): 1C949023-866B-4764-B397-252D5125277C
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 1000215182
Partitions will be aligned on 2048-sector boundaries
Total free space is 2014 sectors (1007.0 KiB)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048          104447   50.0 MiB    EF00  EFI system partition
   2          104448      1000215182   476.9 GiB   8300  Linux filesystem

I created a small EFI System Partition (ESP) of 50MB. This doesn’t need to be very large, as it will only hold the GRUB EFI executable used to boot the system (and possible other boot loaders as well if you choose to dual boot Windows or another operating system). Be careful not to make the partition too small or else you could get a WARNING: Not enough clusters for a 32 bit FAT! error1 when attempting to format it.

Other than the GRUB boot loader, everything else on this system will be fully encrypted. GRUB2 allows for unlocking an LUKS encrypted system at boot, but only the LUKS 1 format. GRUB has added support for LUKS 22, but that support hasn’t made it into a stable release version yet. I used the following cryptsetup commands to created the encrypted partition. To fully understand what these options mean, I suggest reading the Arch Wiki documentation on LUKS encryption modes and options. Learning what these options mean, and deciding which ones to use, is vital to ensuring the confidentiality of your system.

cryptsetup luksFormat --type luks1 -c aes-xts-plain64 -s 512 --iter-time 5000 --hash sha512 /dev/nvme0n1p2
cryptsetup luksOpen /dev/nvme0n1p2 enc_root

Next, I formatted my ESP partition as FAT32, and setup two logical volumes within the encrypted partition. One of the volumes was 16GB in size and formatted as swap, and the second filled the remaining space and was formated as ext4.

mkfs.fat -F32 /dev/nvme0n1p1

pvcreate /dev/mapper/enc_root
vgcreate lvm /dev/mapper/enc_root

lvcreate -L 16G lvm -n swap
lvcreate -l 100%FREE lvm -n root

mkswap /dev/lvm/swap
mkfs.ext4 /dev/lvm/root

Next, I mounted the root and ESP file systems.

mount /dev/lvm/root /mnt -t ext4
mkdir /mnt/boot/efi -p
mount /dev/nvme0n1p1 /mnt/boot/efi/

Take note of the UUID for the encrypted partition using the blkid command. It will be needed for the GRUB configuration later.

blkid -s UUID -o value /dev/nvme0n1p2
/dev/nvme0n1p2: UUID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx" TYPE="crypto_LUKS"

I installed the Alpine base system using the setup-disk command on the mounted volume.

setup-disk -m sys /mnt
Installing system on /dev/lvm/root:
/mnt/boot is device /dev/lvm/root
100% #########==> initramfs: creating /boot/initramfs-lts
/boot is device /dev/lvm/root

To finish the install, I chrooted into the new Alpine environment. Before doing so, I needed to bind the virtual proc, sys and dev file systems into the new environment.

mount -t proc /proc /mnt/proc
mount --rbind /dev /mnt/dev
mount --make-rslave /mnt/dev
mount --rbind /sys /mnt/sys

Then I used chroot on the environment, and changed my prompt as well to make it clear I was in the new root.

chroot /mnt
source /etc/profile
export PS1="(chroot) $PS1"

I needed to install grub-efi and efibootmgr for my boot loader. Since I’m using UEFI and not BIOS/legacy, I removed syslinux.

apk add grub-efi efibootmgr
apk del syslinux

To get grub to work correctly with full disk encryption and my logical volumes, I needed to edit /etc/default/grub, and add in the following options. Be sure to set the UUID to the one returned by blkid above (the UUID of your LUKS encrypted partition).

# /etc/default/grub
GRUB_DISTRIBUTOR="Alpine"
GRUB_TIMEOUT=2
GRUB_DISABLE_SUBMENU=y
GRUB_DISABLE_RECOVERY=true
GRUB_ENABLE_CRYPTODISK=y
GRUB_CMDLINE_LINUX_DEFAULT="cryptroot=UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx cryptdm=lvmcrypt cryptkey"
GRUB_PRELOAD_MODULES="luks cryptodisk part_gpt lvm"

The arguments in the GRUB_CMDLINE_LINUX_DEFAULT listed above are not for the kernel but for the initial ram disk (initrd) created using mkinitfs. The cryptroot argument specifies which device to unlock on boot and cryptkey indicates that a key exists at /crypto_keyfile.bin on the root file system that should be included in the initrd. The cryptdm argument indicates the name of the encrypted disk mapping, which doesn’t really matter as the initrd will map the logical volume names for the rest of the boot process.

The GRUB_PRELOAD_MODULES argument indicates which modules are baked into the GRUB unencrypted executable on the ESP. These modules allow GRUB to unlock an encrypted disk, read GPT partitions and load logical volumes, all of which are needed to boot this particular encrypted setup. A full list of modules can be found in /usr/lib/grub/x86_64-efi/.

Next, I generated a random key and added it to my encrypted volume. The initial ram disk will be on the fully encrypted root volume, and GRUB will prompt me to unlock that volume on boot. Without adding this key to the LUKS volume and initrd, I would have to type in my password twice, once for GRUB to load the initrd and then the initrd would prompt me for it again.

dd bs=512 count=4 if=/dev/urandom of=/crypto_keyfile.bin
chmod 000 /crypto_keyfile.bin
cryptsetup luksAddKey /dev/nvme0n1p2 /crypto_keyfile.bin

I setup my initial ram disk. The cryptsetup and cryptkey features were added to /etc/mkinitfs/mkinitfs.conf for unlocking the encrypted disk on boot. I’ve also added nvme since I’ve booting from an NVME drive3, but it can be left out for systems with SATA system drives.

# /etc/mkinitfs/mkinitfs.conf
features="ata base ide scsi usb virtio ext4 lvm cryptsetup cryptkey nvme"

Next, I made the initramfs and installed grub. I used $(ls /lib/modules/) to specify the version of the kernel that is installed to the system disk, as it may be different from the kernel version used on the USB boot media. Some memory leak warnings may appear. These can be safely ignored.

mkinitfs -c /etc/mkinitfs/mkinitfs.conf $(ls /lib/modules/)
grub-install --target=x86_64-efi --efi-directory=/boot/efi
grub-mkconfig -o /boot/grub/grub.cfg

Before rebooting, I needed to run passwd to set a password for the root user. Afterwards, I unmounted all the file systems and rebooted. If you get an error about -R being an unrecognized option, it’s because you didn’t install util-linux earlier on in this tutorial. The busybox umount command doesn’t support recursive unmounting, which is needed for rbind mounts.

passwd
Changing password for root
New password:
Retype password:
passwd: password for root changed by root

exit
cd /
umount -R /mnt/proc /mnt/dev /mnt/sys /mnt/boot/efi
umount /mnt
reboot
GRUB LUKS Encryption Unlock
GRUB LUKS Encryption Unlock
GRUB Alpine boot menu
GRUB Alpine boot menu

Post Install

Since I skipped the typical setup-alpine installation method, and called setup-disk to install the system partition, there are many parts of the system that did not get configured. At this point you can run setup-alpine and skip the disk installation step, or call the individual setup tools such as setup-timezone, setup-keymap, setup-hostname, setup-ntp, and others. I do like how Alpine separates out each individual setup step into self-contained commands.

You should also setup swap by adding adding the line /dev/mapper/lvm-swap none swap sw 0 0 to /etc/fstab. You can also use the swap volume’s UUID instead of the lvm device node. After setting up swap, run /etc/init.d/swap start to activate it and rc-update add swap boot to ensure swap is activated when the system starts.

Troubleshooting

The Alpine initramfs isn’t very well documented. If you run into issues, the boot script for it can be located at /usr/share/mkinitfs/initramfs-init. Reading through it is how I discovered where it expects the location for the crypto_keyfile.bin file.

If GRUB fails to boot and you need to return to the installation USB to debug what’s happening, the command vgchange -a y will load all the logical volumes once you’ve unlocked the encrypted LUKS volume. The efibootmgr tool is also useful when trying to diagnose EFI boot order issues.

Sometimes it will take a few reboots to fully diagnose an issue, so I typically keep around all the commands I need to get me back into the chroot environment of the new install.

ifconfig eth0 up
udhcpc eth0

setup-apkrepos -r
apk update
apk add gptfdisk cryptsetup lvm2 e2fsprogs util-linux

cryptsetup luksOpen /dev/nvme0n1p2 enc_root
vgchange -a y

mount /dev/mapper/lvm-root /mnt
mount /dev/nvme0n1p1 /mnt/boot/efi/

mount -t proc /proc /mnt/proc
mount --rbind /dev /mnt/dev
mount --make-rslave /mnt/dev
mount --rbind /sys /mnt/sys

chroot /mnt
source /etc/profile
export PS1="(chroot) $PS1"

Here are some common questions you should yourself in you run into issues:

  • Does efibootmgr -v show the correct device and boot loader?
  • Did GRUB not install correctly? (rerun grub-install in the chroot environment)
  • Did you forget to configure GRUB with grub-mkconfig -o /boot/grub/grub.cfg?
  • Does the initial ram disk have all the modules/features it needs? (Run mkinitfs -L for a list)
  • Is the ESP partition not formatted in a way the UEFI/BIOS can recognize it (did you remember to use -F32? Maybe try FAT16 instead?)

Most Dell BIOS screens have a UI for configuring UEFI executables, which can be helpful for manually finding and loading the GRUB boot loader. Some motherboards have a UEFI diagnostic shell, but for the vast majority of systems I’ve seen, there are few if any diagnostic tools for UEFI and boot issues.

Final Thoughts

Installation with full disk encryption for Linux distributions such as Gentoo, Void and Alpine Linux, isn’t very straight forward. It typically involves going past the default installation instructions or setup tools, and requires knowledge about how Linux file systems and boot loaders are setup and configured. It does take some additional work, but having a core understanding of how Linux disk partitions, encryption, and boot loaders are setup, are valuable skills that carry over to other aspects of development, operations and Linux system administration.

  1. WARNING: Not enough clusters for a 32 bit FAT! - SOLVED. Installation - Arch Linux Forums. Retrieved 13 October 2020. 

  2. GNU GRUB - Bugs: bug #55093, Add LUKS2 support. 25 November 2018. dllud. GNU GRUB. Retrieved 14 October 2020. 

  3. NVME - Alpine Linux. Alpine Linux wiki. Retrieved 13 October 2020.