Subject: [PATCH alpine-conf v8] Support encrypted root in setup-disk

From: Krystian Chachuła <[email-masked]>
Date: Sat, 14 Aug 2021 21:11:42 +0200

Co-authored-by: Drew DeVault <[email-masked]>
---
Major changes since v7 are:
* setup_grub and setup_root: when using EFI and encryption, the boot
  partition is unencrypted and mounted to /boot instead of /boot/efi
  (like current behavior with syslinux); this solves two issues: setting
  up GRUB with luks2 encrypted /boot is difficult, installing GRUB with
  encrypted /boot needs chroot or a workaround (see v7 discussion)
* install_mounted_root: cryptroot kernel param is now set to UUID
  instead of device name
* init_progs: blkid is explicitly installed, because crypt_required
  needs it

I tested sys and cryptsys installation with syslinux/BIOS,
GRUB/BIOS and GRUB/EFI on x86_64.

See
https://git.sr.ht/~krystianch/alpine-conf/commit/70aaffca3d4685f3a8d78deaca7b479521b13ab8
for full diff between v7 and v8.

I'd appreciate a review / feedback.

 setup-disk.in | 135 +++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 106 insertions(+), 29 deletions(-)

diff --git a/setup-disk.in b/setup-disk.in
index 656b5bc..46c8068 100644
--- a/setup-disk.in
+++ b/setup-disk.in
@@ -82,6 +82,26 @@ enumerate_fstab() {
 		done
 }
 
+# given an fstab on stdin, determine if any of the mountpoints are encrypted
+crypt_required() {
+	while read -r devname mountpoint fstype mntops freq passno; do
+		if [ -z "$devname" ] || [ "${devname###}" != "$devname" ]; then
+			continue
+		fi
+		uuid="${devname##UUID=}"
+		if [ "$uuid" != "$devname" ]; then
+			devname="$(blkid --uuid "$uuid")"
+		fi
+		mapname="${devname##/dev/mapper/}"
+		if [ "$mapname" != "$devname" ]; then
+			if cryptsetup status "$mapname" >&1 >/dev/null; then
+				return 0
+			fi
+		fi
+	done
+	return 1
+}
+
 is_vmware() {
 	grep -q VMware /proc/scsi/scsi 2>/dev/null \
 		|| grep -q VMware /proc/ide/hd*/model 2>/dev/null
@@ -283,19 +303,23 @@ setup_grub() {
 	# install GRUB efi mode
 	if [ -n "$USE_EFI" ]; then
 		local target fwa
+		local efi_directory="$mnt"/boot/efi
 		case "$ARCH" in
 			x86_64)		target=x86_64-efi ; fwa=x64 ;;
 			x86)		target=i386-efi ; fwa=ia32 ;;
 			arm*)		target=arm-efi ; fwa=arm ;;
 			aarch64)	target=arm64-efi ; fwa=aa64 ;;
 		esac
+		if [ -n "$USE_CRYPT" ]; then
+			efi_directory="$mnt"/boot
+		fi
 		# currently disabling nvram so grub doesnt call efibootmgr
 		# installing to alpine directory so other distros dont overwrite it
-		grub-install --target=$target --efi-directory="$mnt"/boot/efi \
+		grub-install --target=$target --efi-directory="$efi_directory" \
 			--bootloader-id=alpine --boot-directory="$mnt"/boot --no-nvram
 		# fallback mode will use boot/boot${fw arch}.efi
-		install -D "$mnt"/boot/efi/EFI/alpine/grub$fwa.efi \
-			"$mnt"/boot/efi/EFI/boot/boot$fwa.efi
+		install -D "$efi_directory"/EFI/alpine/grub$fwa.efi \
+			"$efi_directory"/EFI/boot/boot$fwa.efi
 	# install GRUB for ppc64le
 	elif [ "$ARCH" = "ppc64le" ]; then
 		shift 5
@@ -427,7 +451,7 @@ setup_raspberrypi_bootloader() {
 install_mounted_root() {
 	local mnt="$1"
 	shift 1
-	local disks="${@}" mnt_boot= boot_fs= root_fs=
+	local disks="${@}" mnt_boot= boot_fs= root_fs= use_crypt=
 	local initfs_features="ata base ide scsi usb virtio"
 	local pvs= dev= rootdev= bootdev= extlinux_raidopt= root= modules=
 	local kernel_opts="$KERNELOPTS"
@@ -490,7 +514,6 @@ install_mounted_root() {
 		esac
 	done
 
-
 	if [ -n "$VERBOSE" ]; then
 		echo "Root device:     $rootdev"
 		echo "Root filesystem: $root_fs"
@@ -513,6 +536,28 @@ install_mounted_root() {
 	# we should not try start modloop on sys install
 	rm -f "$mnt"/etc/runlevels/*/modloop
 
+	# generate the fstab
+	if [ -f "$mnt"/etc/fstab ]; then
+		mv "$mnt"/etc/fstab "$mnt"/etc/fstab.old
+	fi
+	enumerate_fstab "$mnt" >> "$mnt"/etc/fstab
+	if [ -n "$SWAP_DEVICES" ]; then
+		local swap_dev
+		for swap_dev in $SWAP_DEVICES; do
+			echo -e "$(uuid_or_device ${swap_dev})\tswap\tswap\tdefaults\t0 0" \
+				>> "$mnt"/etc/fstab
+		done
+	fi
+	cat >>"$mnt"/etc/fstab <<-__EOF__
+		/dev/cdrom	/media/cdrom	iso9660	noauto,ro 0 0
+		/dev/usbdisk	/media/usb	vfat	noauto	0 0
+	__EOF__
+
+	if crypt_required <"$mnt"/etc/fstab; then
+		use_crypt=1
+		initfs_features="$initfs_features cryptsetup"
+	fi
+
 	# generate mkinitfs.conf
 	mkdir -p "$mnt"/etc/mkinitfs/features.d
 	echo "features=\"$initfs_features\"" > "$mnt"/etc/mkinitfs/mkinitfs.conf
@@ -530,24 +575,15 @@ install_mounted_root() {
 	if [ -n "$(get_bootopt nomodeset)" ]; then
 		kernel_opts="nomodeset $kernel_opts"
 	fi
+	if [ "$use_crypt" ] && cryptsetup status "$rootdev" 2>&1 >/dev/null; then
+		# Boot to encrypted root
+		root=$(cryptsetup status "$rootdev" | awk '/device:/ { print $2 }')
+		root=$(uuid_or_device $root)
+		kernel_opts="cryptroot=$root cryptdm=root"
+		root=/dev/mapper/root
+	fi
 	modules="sd-mod,usb-storage,${root_fs}${raidmod}"
 
-	# generate the fstab
-	if [ -f "$mnt"/etc/fstab ]; then
-		mv "$mnt"/etc/fstab "$mnt"/etc/fstab.old
-	fi
-	enumerate_fstab "$mnt" >> "$mnt"/etc/fstab
-	if [ -n "$SWAP_DEVICES" ]; then
-		local swap_dev
-		for swap_dev in $SWAP_DEVICES; do
-			echo -e "$(uuid_or_device ${swap_dev})\tswap\tswap\tdefaults\t0 0" \
-				>> "$mnt"/etc/fstab
-		done
-	fi
-	cat >>"$mnt"/etc/fstab <<-__EOF__
-		/dev/cdrom	/media/cdrom	iso9660	noauto,ro 0 0
-		/dev/usbdisk	/media/usb	vfat	noauto	0 0
-	__EOF__
 	# remove the installed db in case its there so we force re-install
 	rm -f "$mnt"/var/lib/apk/installed "$mnt"/lib/apk/db/installed
 	echo "Installing system on $rootdev:"
@@ -595,6 +631,10 @@ unmount_partitions() {
 
 	# unmount the partitions
 	umount $(awk '{print $2}' /proc/mounts | egrep "^$mnt(/|\$)" | sort -r)
+
+	if [ "$USE_CRYPT" ]; then
+		cryptsetup close /dev/mapper/root
+	fi
 }
 
 # figure out decent default swap size in mega bytes
@@ -697,9 +737,10 @@ select_bootloader_pkg() {
 
 # install needed programs
 init_progs() {
-	local raidpkg= lvmpkg= fs= fstools= grub=
+	local raidpkg= lvmpkg= cryptpkg= fs= fstools= grub=
 	[ -n "$USE_RAID" ] && raidpkg="mdadm"
 	[ -n "$USE_LVM" ] && lvmpkg="lvm2"
+	[ -n "$USE_CRYPT" ] && cryptpkg="cryptsetup blkid"
 	for fs in $BOOTFS $ROOTFS $VARFS; do
 		# we need load btrfs module early to avoid the error message:
 		# 'failed to open /dev/btrfs-control'
@@ -714,7 +755,7 @@ init_progs() {
 		vfat) fstools="$fstools dosfstools";;
 		esac
 	done
-	apk add --quiet sfdisk $lvmpkg $raidpkg $fstools $@
+	apk add --quiet sfdisk $cryptpkg $lvmpkg $raidpkg $fstools $@
 }
 
 show_disk_info() {
@@ -1018,11 +1059,11 @@ setup_root() {
 	mkfs.$ROOTFS $MKFS_OPTS_ROOT $mkfs_args "$root_dev"
 	mkdir -p "$SYSROOT"
 	mount -t $ROOTFS $root_dev "$SYSROOT" || return 1
-	if [ -n "$boot_dev" ] && [ -z "$USE_EFI" ]; then
+	if [ -n "$boot_dev" ] && ([ -z "$USE_EFI" ] || [ -n "$USE_CRYPT" ]); then
 		mkdir -p "$SYSROOT"/boot
 		mount -t $BOOTFS $boot_dev "$SYSROOT"/boot || return 1
 	fi
-	if [ -n "$boot_dev" ] && [ -n "$USE_EFI" ]; then
+	if [ -n "$boot_dev" ] && [ -n "$USE_EFI" ] && [ -z "$USE_CRYPT" ]; then
 		mkdir -p "$SYSROOT"/boot/efi
 		mount -t $BOOTFS $boot_dev "$SYSROOT"/boot/efi || return 1
 	fi
@@ -1082,6 +1123,18 @@ native_disk_install_lvm() {
 	setup_root $root_dev $BOOT_DEV
 }
 
+setup_crypt() {
+	local dev="$1" local dmname="$2"
+	mkdir -p /run/cryptsetup
+	echo "Preparing root partition for encryption." >&2
+	echo "You will be prompted for your password at boot." >&2
+	echo "If you forget your password, your data will be lost." >&2
+	cryptsetup luksFormat --type luks2 "$dev" >&2
+	echo "Enter password again to unlock disk for installation." >&2
+	cryptsetup open "$dev" "$dmname" >&2
+	echo "/dev/mapper/$dmname"
+}
+
 native_disk_install() {
 	local prep_part_type=$(partition_id prep)
 	local root_part_type=$(partition_id linux)
@@ -1156,6 +1209,10 @@ native_disk_install() {
 		root_dev=$(find_nth_non_boot_parts $index "$root_part_type" $@)
 	fi
 
+	if [ "$USE_CRYPT" ]; then
+		root_dev=$(setup_crypt $root_dev root)
+	fi
+
 	[ $SWAP_SIZE -gt 0 ] && setup_swap_dev $swap_dev
 	setup_root $root_dev $BOOT_DEV $@
 }
@@ -1176,7 +1233,7 @@ diskselect_help() {
 diskmode_help() {
 	cat <<-__EOF__
 
-		You can select between 'sys', 'data', 'lvm', 'lvmsys' or 'lvmdata'.
+		You can select between 'sys', 'cryptsys', 'data', 'lvm', 'lvmsys' or 'lvmdata'.
 
 		sys:
 		  This mode is a traditional disk install. The following partitions will be
@@ -1184,6 +1241,12 @@ diskmode_help() {
 
 		  This mode may be used for development boxes, desktops, virtual servers, etc.
 
+		cryptsys:
+		  This mode is equivalent to sys, except that the root filesystem will be
+		  encrypted with cryptsetup. You will be prompted to enter a decryption
+		  password, and will need to use this password to boot up the operating
+		  system after installation.
+
 		data:
 		  This mode uses your disk(s) for data storage, not for the operating system.
 		  The system itself will run from tmpfs (RAM).
@@ -1233,7 +1296,7 @@ ask_disk() {
 
 usage() {
 	cat <<-__EOF__
-		usage: setup-disk [-hLqrv] [-k kernelflavor] [-m MODE] [-o apkovl] [-s SWAPSIZE]
+		usage: setup-disk [-hLqrve] [-k kernelflavor] [-m MODE] [-o apkovl] [-s SWAPSIZE]
 		                  [MOUNTPOINT | DISKDEV...]
 
 		Install alpine on harddisk.
@@ -1247,6 +1310,7 @@ usage() {
 
 		options:
 		 -h  Show this help
+		 -e  Encrypt disk
 		 -m  Use disk for MODE without asking, where MODE is either 'data' or 'sys'
 		 -o  Restore system from given apkovl file
 		 -k  Use kernelflavor instead of $KERNEL_FLAVOR
@@ -1286,11 +1350,13 @@ case $kver in
 	*) KERNEL_FLAVOR=lts;;
 esac
 
+USE_CRYPT=
 DISK_MODE=
 USE_LVM=
 # Parse args
-while getopts "hk:Lm:o:qrs:v" opt; do
+while getopts "hek:Lm:o:qrs:v" opt; do
 	case $opt in
+		e) USE_CRYPT=1;;
 		m) DISK_MODE="$OPTARG";;
 		k) KERNEL_FLAVOR="$OPTARG";;
 		L) USE_LVM="_lvm";;
@@ -1319,6 +1385,12 @@ fi
 reset_var
 swapoff -a
 
+if [ $USE_CRYPT -eq 1 ] && [ -n "$USE_LVM" ]; then
+	echo "cryptsys and lvm are currently mutually incompatible."
+	echo "Please run $0 again with only one of these options selected."
+	exit 1
+fi
+
 # stop all volume groups in use
 vgchange --ignorelockingfailure -a n >/dev/null 2>&1
 
@@ -1381,9 +1453,14 @@ if [ -n "$diskdevs" ] && [ -z "$DISK_MODE" ]; then
 		echo "The following $disk_is_or_disks_are selected${USE_LVM:+ (with LVM)}:"
 		show_disk_info $diskdevs
 		_lvm=${USE_LVM:-", 'lvm'"}
-		ask "How would you like to use $it_them? ('sys', 'data'${_lvm#_lvm} or '?' for help)" "?"
+		ask "How would you like to use $it_them? ('sys', 'cryptsys', 'data'${_lvm#_lvm} or '?' for help)" "?"
 		case "$resp" in
 		'?') diskmode_help;;
+		cryptsys)
+			resp=sys
+			USE_CRYPT=1
+			break
+			;;
 		sys|data) break;;
 		lvm) USE_LVM="_lvm" ;;
 		nolvm) USE_LVM="";;
-- 
2.32.0