[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

[stretch] RAID + LUKS + PATCHed cryptoroot script to use USB-stick keyfile via passdev with fallback to askpass + silenced systemd



Hi List!

I spent sereval hours today figuring out how to do an useful RAID with
full-disk encryption for headless servers that will use an usb stick to
read the keyfile from and falling back to askpass to enter the pw from
console if no stick is found and also managed to silence systemd.

This post has the intention of being a reference post of the type "Im
finally done after a day of doing stuff in a vm and hopefully this will
save someone a few hours to get a similar setup set-up."

Also included: a patch for cryptoroot to make it fall back to askpass,
if no keyfile can be found on usb-sticks.

There are many outdated and incomplete guides, mostly written for
oldoldold-stable, which will make you do more than necessary. :P
The fewer changes to a system, the better, IMO.


So...

I ran the default netinstall iso and partitioned the VM as follows:
/dev/sda1: /boot
/dev/sda2: RAID0: md0
/dev/md0: crypto_LUKS: md0_crypt (defaults + passphrase)
/dev/mapper/md0_crypt: ext4: /

Ill test a more useful raid level on the live setup, once the hardware
is assembled, but I expect it to behave identically.

After the installer finished, and rebooted the VM, I got to enter the
passphrase for the first time, it booted fine. An encrypted root-fs on a
raid seems to work out of the box by now - no need to fiddle with grub
as some guides suggest!


The next step was to get the passwordless usb-stick (/dev/sdb)
auto-decryption working.
(Again: it works nearly out of the boy by now, there are MANY outdated
guides out there suggesting you to download some unnecessary scripts)

It all boiled down to:

# mkfs.ext4 -L keys /dev/sdb
# mount LABEL=keys /mnt
# echo -n "asdf" > /mnt/root.keys
# cryptsetup luksAddKey /dev/md0 /mnt/root.key
# umount /mnt

Seting the following line in /etc/crypttab
---
md0_crypt UUID=[...] /dev/disk/by-label/keys:/root.key:5
luks,initramfs,keyscript=/lib/cryptsetup/scripts/passdev,tries=2
---

# update-initramfs -tuck all
# update-grub
# reboot

Now we are in a state that debian will boot automatically, unlocking the
encrypted root filesystem on a raid without any user-interaction.
Sweet?
Somehow... systemd has a tendency to complain and slow down the boot
process by 1:30 min because it fails to handle the new crypttab entry,
but after the timeout, you get presented with a login.

To silence systemd:
# touch "/etc/systemd/system/systemd-cryptsetup@md0_crypt.service"
# reboot

This will override the generated systemd-cryptsetup services.

After the reboot, we get booted instantly, without systemd complaining
about anything.

Now what happens, if you dont have your usb-stick connected to the
machine when rebooting?
Well... passdev will wait for 5 sec, then fail, retry, fail again, ...
There doesnt seem to be any kind of fallback to console-pw-entry.
This should not really matter, but what if for some reason the usb
controller or usb-stick fails and renders the machine unbootable,
because you dont have a replacement at hand?
That would suck, wouldnt it?

Therefore I extended/hacked the original
/usr/share/initramfs-tools/scripts/local-top/cryptroot script a little bit.

# cp /usr/share/initramfs-tools/scripts/local-top/cryptroot
/etc/initramfs-tools/scripts/local-top/cryptroot

*edit stuff* The patch is attached.

The intention was to try to use the usb stick and if passdev times out,
fall back to passphrase-entry in the console via askpass.

I added the option "cryptaskpassfallback" that should have been settable
via /etc/crypttab as another option, but for some reason it was not
passed from /etc/crypttab and by that time, my motivation wasnt really
that high anyomre to investigate it further, and i simply hardcoded the
default to ="yes" instead of ="".

The actual fallback feels a little hacky too and im not really happy
about it, as Im not really sure, if I didnt create any unwanted
side-effects, by calling setup_mapping() once more.

It would be awesome, if some maintainer of the corresponding packages
would consider including this functionality, maybe a little less hacky. :)

Anyway. After applying the patch, all seems done.

# update-initramfs -tuck all
# reboot

Will either insta-boot with the usb stick accessible, or ask for a
passphrase, and systemd wont complain.
Thats what I wanted for myself and it seems to work as expected...

Just wanted to share this. Maybe this will help someone in the future. :)

br,
Jan
--
I only read plaintext emails.
#!/bin/sh

PREREQ="cryptroot-prepare"

#
# Standard initramfs preamble
#
prereqs()
{
	# Make sure that cryptroot is run last in local-top
	for req in $(dirname $0)/*; do
		script=${req##*/}
		if [ $script != cryptroot ]; then
			echo $script
		fi
	done
}

case $1 in
prereqs)
	prereqs
	exit 0
	;;
esac

# source for log_*_msg() functions, see LP: #272301
. /scripts/functions

# define $askpass
askpass="/lib/cryptsetup/askpass"

#
# Helper functions
#
message()
{
	if [ -x /bin/plymouth ] && plymouth --ping; then
		plymouth message --text="$@"
	else
		echo "$@" >&2
	fi
	return 0
}

udev_settle()
{
	# Wait for udev to be ready, see https://launchpad.net/bugs/85640
	if command -v udevadm >/dev/null 2>&1; then
		udevadm settle --timeout=30
	elif command -v udevsettle >/dev/null 2>&1; then
		udevsettle --timeout=30
	fi
	return 0
}

parse_options()
{
	local cryptopts
	cryptopts="$1"

	if [ -z "$cryptopts" ]; then
		return 1
	fi

	# Defaults
	cryptcipher=aes-cbc-essiv:sha256
	cryptsize=256
	crypthash=ripemd160
	crypttarget=cryptroot
	cryptsource=""
	cryptheader=""
	cryptlvm=""
	cryptkeyscript=""
	cryptkey="" # This is only used as an argument to an eventual keyscript
	cryptkeyslot=""
	crypttries=3
	crypttcrypt=""
	cryptveracrypt=""
	cryptrootdev=""
	cryptdiscard=""
	cryptaskpassfallback="yes"
	CRYPTTAB_OPTIONS=""

	local IFS=" ,"
	for x in $cryptopts; do
		case $x in
		hash=*)
			crypthash=${x#hash=}
			;;
		size=*)
			cryptsize=${x#size=}
			;;
		cipher=*)
			cryptcipher=${x#cipher=}
			;;
		target=*)
			crypttarget=${x#target=}
			export CRYPTTAB_NAME="$crypttarget"
			;;
		source=*)
			cryptsource=${x#source=}
			if [ ${cryptsource#UUID=} != $cryptsource ]; then
				cryptsource="/dev/disk/by-uuid/${cryptsource#UUID=}"
			elif [ ${cryptsource#LABEL=} != $cryptsource ]; then
				cryptsource="/dev/disk/by-label/$(printf '%s' "${cryptsource#LABEL=}" | sed 's,/,\\x2f,g')"
			fi
			export CRYPTTAB_SOURCE="$cryptsource"
			;;
		header=*)
			cryptheader=${x#header=}
			if [ ! -e "$cryptheader" ] && [ -e "/conf/conf.d/cryptheader/$cryptheader" ]; then
				cryptheader="/conf/conf.d/cryptheader/$cryptheader"
			fi
			export CRYPTTAB_HEADER="$cryptheader"
			;;
		lvm=*)
			cryptlvm=${x#lvm=}
			;;
		keyscript=*)
			cryptkeyscript=${x#keyscript=}
			;;
		key=*)
			if [ "${x#key=}" != "none" ]; then
				cryptkey=${x#key=}
			fi
			export CRYPTTAB_KEY="$cryptkey"
			;;
		keyslot=*)
			cryptkeyslot=${x#keyslot=}
			;;
		tries=*)
			crypttries="${x#tries=}"
			case "$crypttries" in
			  *[![:digit:].]*)
				crypttries=3
				;;
			esac
			;;
		tcrypt)
			crypttcrypt="yes"
			;;
		veracrypt)
			cryptveracrypt="--veracrypt"
			;;
		rootdev)
			cryptrootdev="yes"
			;;
		discard)
			cryptdiscard="yes"
			;;
		askpassfallback)
			cryptaskpassfallback="yes"
			;;
		esac
		PARAM="${x%=*}"
		if [ "$PARAM" = "$x" ]; then
			VALUE="yes"
		else
			VALUE="${x#*=}"
		fi
		CRYPTTAB_OPTIONS="$CRYPTTAB_OPTIONS $PARAM"
		eval export CRYPTTAB_OPTION_$PARAM="\"$VALUE\""
	done
	export CRYPTTAB_OPTIONS

	if [ -z "$cryptsource" ]; then
		message "cryptsetup ($crypttarget): source parameter missing"
		return 1
	fi
	return 0
}

activate_vg()
{
	# Sanity checks
	if [ ! -x /sbin/lvm ]; then
		message "cryptsetup ($crypttarget): lvm is not available"
		return 1
	fi

	# Detect and activate available volume groups
	/sbin/lvm vgscan
	/sbin/lvm vgchange -a y --sysinit
	return $?
}

setup_mapping()
{
	local opts count cryptopen cryptremove NEWROOT
	opts="$1"

	if [ -z "$opts" ]; then
		return 0
	fi

	parse_options "$opts" || return 1

	# disable cryptkeyscript - fall back to askpass.
	if [ -n "$do_fallback" ]; then
		cryptkeyscript=""
	fi

	if [ -z "$cryptkeyscript" ]; then
		if [ ${cryptsource#/dev/disk/by-uuid/} != $cryptsource ]; then
			# UUIDs are not very helpful
			diskname="$crypttarget"
		else
			diskname="$cryptsource ($crypttarget)"
		fi
		cryptkeyscript=$askpass
		cryptkey="Please unlock disk $diskname: "
	elif ! type "$cryptkeyscript" >/dev/null; then
		message "cryptsetup ($crypttarget): error - script \"$cryptkeyscript\" missing"
		return 1
	fi

	if [ "$cryptkeyscript" = "cat" ] && [ "${cryptkey#/root/}" != "$cryptkey" ]; then
		# skip the mapping if the root FS is not mounted yet
		sed -rn 's/^\s*[^#]\S*\s+(\S+)\s.*/\1/p' /proc/mounts | grep -Fxq "$rootmnt" || return 1
		# substitute the "/root" prefix by the real root FS mountpoint otherwise
		cryptkey="${rootmnt}/${cryptkey#/root/}"
	fi

	if [ -n "$cryptheader" ] && ! type "$cryptheader" >/dev/null; then
		message "cryptsetup ($crypttarget): error - LUKS header \"$cryptheader\" missing"
		return 1
	fi

	# The same target can be specified multiple times
	# e.g. root and resume lvs-on-lvm-on-crypto
	if [ -e "/dev/mapper/$crypttarget" ]; then
		return 0
	fi

	modprobe -q dm_crypt

	# Make sure the cryptsource device is available
	if [ ! -e $cryptsource ]; then
		activate_vg
	fi

	# If the encrypted source device hasn't shown up yet, give it a
	# little while to deal with removable devices

	# the following lines below have been taken from
	# /usr/share/initramfs-tools/scripts/local, as suggested per
	# https://launchpad.net/bugs/164044
	if [ ! -e "$cryptsource" ]; then
		log_begin_msg "Waiting for encrypted source device..."

		# Default delay is 180s
		if [ -z "${ROOTDELAY}" ]; then
			slumber=180
		else
			slumber=${ROOTDELAY}
		fi

		slumber=$(( ${slumber} * 10 ))
		while [ ! -e "$cryptsource" ]; do
			# retry for LVM devices every 10 seconds
			if [ ${slumber} -eq $(( ${slumber}/100*100 )) ]; then
				activate_vg
			fi

			/bin/sleep 0.1
			slumber=$(( ${slumber} - 1 ))
			[ ${slumber} -gt 0 ] || break
		done

		if [ ${slumber} -gt 0 ]; then
			log_end_msg 0
		else
			log_end_msg 1 || true
		fi
 	fi
	udev_settle

	# We've given up, but we'll let the user fix matters if they can
	if [ ! -e "${cryptsource}" ]; then
		
		echo "  ALERT! ${cryptsource} does not exist."
		echo "	Check cryptopts=source= bootarg: cat /proc/cmdline"
		echo "	or missing modules, devices: cat /proc/modules; ls /dev"
		panic -r "Dropping to a shell. Will skip ${cryptsource} if you can't fix."
	fi

	if [ ! -e "${cryptsource}" ]; then
		return 1
	fi


	# Prepare commands
	cryptopen="/sbin/cryptsetup -T 1"
	if [ "$cryptdiscard" = "yes" ]; then
		cryptopen="$cryptopen --allow-discards"
	fi
	if [ -n "$cryptheader" ]; then
		cryptopen="$cryptopen --header=$cryptheader"
	fi
	if [ -n "$cryptkeyslot" ]; then
		cryptopen="$cryptopen --key-slot=$cryptkeyslot"
	fi
	if /sbin/cryptsetup isLuks ${cryptheader:-$cryptsource} >/dev/null 2>&1; then
		cryptopen="$cryptopen open --type luks $cryptsource $crypttarget --key-file=-"
	elif [ "$crypttcrypt" = "yes" ]; then
		cryptopen="$cryptopen open --type tcrypt $cryptveracrypt $cryptsource $crypttarget"
	else
		cryptopen="$cryptopen -c $cryptcipher -s $cryptsize -h $crypthash open --type plain $cryptsource $crypttarget --key-file=-"
	fi
	cryptremove="/sbin/cryptsetup remove $crypttarget"
	NEWROOT="/dev/mapper/$crypttarget"

	# Try to get a satisfactory password $crypttries times
	count=0
	while [ $crypttries -le 0 ] || [ $count -lt $crypttries ]; do
		export CRYPTTAB_TRIED="$count"
		count=$(( $count + 1 ))

		if [ ! -e "$NEWROOT" ]; then
			if ! crypttarget="$crypttarget" cryptsource="$cryptsource" \
			     $cryptkeyscript "$cryptkey" | $cryptopen; then
				message "cryptsetup ($crypttarget): cryptsetup failed, bad password or options?"

				# if not askpass, fall back to askpass on fail.
				if [ -z "$cryptaskpassfallback" ]; then
					continue
				elif [ "$cryptkeyscript" = "$askpass" ]; then
					continue
				else
					export do_fallback="$cryptaskpassfallback"
					setup_mapping "$1"
					return
				fi
			fi
		fi

		if [ ! -e "$NEWROOT" ]; then
			message "cryptsetup ($crypttarget): unknown error setting up device mapping"
			return 1
		fi

		#FSTYPE=''
		#eval $(fstype < "$NEWROOT")
		FSTYPE="$(/sbin/blkid -s TYPE -o value "$NEWROOT")"

		# See if we need to setup lvm on the crypto device
		#if [ "$FSTYPE" = "lvm" ] || [ "$FSTYPE" = "lvm2" ]; then
		if [ "$FSTYPE" = "LVM_member" ] || [ "$FSTYPE" = "LVM2_member" ]; then
			if [ -z "$cryptlvm" ]; then
				message "cryptsetup ($crypttarget): lvm fs found but no lvm configured"
				return 1
			elif ! activate_vg; then
				# disable error message, LP: #151532
				#message "cryptsetup ($crypttarget): failed to setup lvm device"
				return 1
			fi

			# Apparently ROOT is already set in /conf/param.conf for
			# flashed kernels at least. See bugreport #759720.
			if [ -f /conf/param.conf ] && grep -q "^ROOT=" /conf/param.conf; then
				NEWROOT=$(sed -n 's/^ROOT=//p' /conf/param.conf)
			else
				NEWROOT=${cmdline_root:-/dev/mapper/$cryptlvm}
				if [ "$cryptrootdev" = "yes" ]; then
					# required for lilo to find the root device
					echo "ROOT=$NEWROOT" >>/conf/param.conf
				fi
			fi
			#eval $(fstype < "$NEWROOT")
			FSTYPE="$(/sbin/blkid -s TYPE -o value "$NEWROOT")"
		fi

		#if [ -z "$FSTYPE" ] || [ "$FSTYPE" = "unknown" ]; then
		if [ -z "$FSTYPE" ]; then
			message "cryptsetup ($crypttarget): unknown fstype, bad password or options?"
			udev_settle
			$cryptremove
			continue
		fi

		# decrease $count by 1, apparently last try was successful.
		count=$(( $count - 1 ))

		message "cryptsetup ($crypttarget): set up successfully"
		break
	done

	failsleep=60 # make configurable later?

	if [ "$cryptrootdev" = "yes" ] && [ $crypttries -gt 0 ] && [ $count -ge $crypttries ]; then
		message "cryptsetup ($crypttarget): maximum number of tries exceeded"
		message "cryptsetup: going to sleep for $failsleep seconds..."
		sleep $failsleep
		exit 1
	fi

	udev_settle
	return 0
}

#
# Begin real processing
#

# Do we have any kernel boot arguments?
cmdline_cryptopts=''
unset cmdline_root
for opt in $(cat /proc/cmdline); do
	case $opt in
	cryptopts=*)
		opt="${opt#cryptopts=}"
		if [ -n "$opt" ]; then
			if [ -n "$cmdline_cryptopts" ]; then
				cmdline_cryptopts="$cmdline_cryptopts $opt"
			else
				cmdline_cryptopts="$opt"
			fi
		fi
		;;
	root=*)
		opt="${opt#root=}"
		case $opt in
		/*) # Absolute path given. Not lilo major/minor number.
			cmdline_root=$opt
			;;
		*) # lilo major/minor number (See #398957). Ignore
		esac
		;;
	esac
done

if [ -n "$cmdline_cryptopts" ]; then
	# Call setup_mapping separately for each possible cryptopts= setting
	for cryptopt in $cmdline_cryptopts; do
		setup_mapping "$cryptopt"
	done
	exit 0
fi

# Do we have any settings from the /conf/conf.d/cryptroot file?
if [ -r /conf/conf.d/cryptroot ]; then
	while read mapping <&3; do
		setup_mapping "$mapping" 3<&-
	done 3< /conf/conf.d/cryptroot
fi

exit 0
--- /usr/share/initramfs-tools/scripts/local-top/cryptroot	2017-05-09 13:50:59.000000000 +0200
+++ /mnt/cryptroot	2017-12-16 02:26:34.401812974 +0100
@@ -26,6 +26,9 @@
 # source for log_*_msg() functions, see LP: #272301
 . /scripts/functions
 
+# define $askpass
+askpass="/lib/cryptsetup/askpass"
+
 #
 # Helper functions
 #
@@ -75,6 +78,7 @@
 	cryptveracrypt=""
 	cryptrootdev=""
 	cryptdiscard=""
+	cryptaskpassfallback="yes"
 	CRYPTTAB_OPTIONS=""
 
 	local IFS=" ,"
@@ -144,6 +148,9 @@
 		discard)
 			cryptdiscard="yes"
 			;;
+		askpassfallback)
+			cryptaskpassfallback="yes"
+			;;
 		esac
 		PARAM="${x%=*}"
 		if [ "$PARAM" = "$x" ]; then
@@ -188,6 +195,11 @@
 
 	parse_options "$opts" || return 1
 
+	# disable cryptkeyscript - fall back to askpass.
+	if [ -n "$do_fallback" ]; then
+		cryptkeyscript=""
+	fi
+
 	if [ -z "$cryptkeyscript" ]; then
 		if [ ${cryptsource#/dev/disk/by-uuid/} != $cryptsource ]; then
 			# UUIDs are not very helpful
@@ -195,7 +207,7 @@
 		else
 			diskname="$cryptsource ($crypttarget)"
 		fi
-		cryptkeyscript="/lib/cryptsetup/askpass"
+		cryptkeyscript=$askpass
 		cryptkey="Please unlock disk $diskname: "
 	elif ! type "$cryptkeyscript" >/dev/null; then
 		message "cryptsetup ($crypttarget): error - script \"$cryptkeyscript\" missing"
@@ -308,7 +320,17 @@
 			if ! crypttarget="$crypttarget" cryptsource="$cryptsource" \
 			     $cryptkeyscript "$cryptkey" | $cryptopen; then
 				message "cryptsetup ($crypttarget): cryptsetup failed, bad password or options?"
-				continue
+
+				# if not askpass, fall back to askpass on fail.
+				if [ -z "$cryptaskpassfallback" ]; then
+					continue
+				elif [ "$cryptkeyscript" = "$askpass" ]; then
+					continue
+				else
+					export do_fallback="$cryptaskpassfallback"
+					setup_mapping "$1"
+					return
+				fi
 			fi
 		fi
 

Attachment: signature.asc
Description: OpenPGP digital signature


Reply to: