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

---

From: "Drew DeVault" <[email-masked]>
Date: Fri, 01 Oct 2021 19:31:12 +0200

Some feedback from ncopa:

From: Natanael Copa <[email-masked]>
To: Krystian Chachu*a <[email-masked]>
Cc: ~alpine/[email-masked], Drew DeVault <[email-masked]>
Subject: Re: [PATCH alpine-conf v8] Support encrypted root in setup-disk
Date: Fri, 1 Oct 2021 18:40:51 +0200
X-Mailer: Claws Mail 4.0.0 (GTK+ 3.24.30; x86_64-alpine-linux-musl)

Hi!

Thank you for working on this.

On Sat, 14 Aug 2021 21:11:42 +0200

>   encrypted /boot needs chroot or a workaround (see v7 discussion)  

I think it would be nice with an explanation in the commit message why
it is done this way.

> GRUB/BIOS and GRUB/EFI on x86_64.  

great! thanks!
> +	cryptsetup open "$dev" "$dmname" >&2  

What happens if user provides wrong password to `cryptsetup open` in
setup_crypt? Shouldn't we handle error here?

> +		root_dev=$(setup_crypt $root_dev root)  

What should happen here if `cryptsetup open` failed?

> +	echo "Please run $0 again with only one of these options selected."

I wonder how much extra work it would be to add support for encrypted
disk under lvm?

> +		ask "How would you like to use $it_them? ('sys', 'cryptsys', 'data'${_lvm#_lvm} or '?' for help)" "?"

What would be needed to be able to select 'crypt' and then select
either 'sys' or 'data'? similar to what we do with lvm. So we could
have data on cryptsetup as well.

---

From: Krystian Chachuła <[email-masked]>
Date: Sun, 10 Oct 2021 19:55:32 +0200

Thank you Natanael for the review.

> I think it would be nice with an explanation in the commit message why

Ok, I'll address it in v9.

> setup_crypt? Shouldn't we handle error here?

I'd add `--tries 1` to make `cryptsetup open` exit on wrong password and prompt
user what to do:

Could not unlock disk: [REASON FROM EXIT CODE]. What would you like to do?
('retry', 'reformat', 'skip' or '?' for help)

Help would say:

You can select between 'retry', 'reformat', or 'skip'.

retry:
  Try unlocking disk again.

reformat:
  Discard current password and set a new one.

skip:
  Continue without encrypting the root partition.

> What should happen here if `cryptsetup open` failed?

I'd make `setup_crypt` echo nothing when user skips encryption and, if it
happens, skip overwriting `root_dev` here.

> I wonder how much extra work it would be to add support for encrypted

I'd say not that much. For LVM on LUKS:

(1) `native_disk_install_lvm` would need to call `setup_crypt` to create
	`/dev/mapper/cryptlvm` before `setup_lvm_volume_group` and
(2) `setup_lvm_volume_group` would need to set up LVM on `/dev/mapper/cryptlvm`.

If this looks ok I'll implement this in v9.

> have data on cryptsetup as well.

I think we need an init script that runs before localmount and opens the
encrypted var partition by asking for password interactively.