Skip to content

Instantly share code, notes, and snippets.

@tvdstaaij
Last active July 29, 2024 07:40
Show Gist options
  • Save tvdstaaij/53de6743eab05c979a7831c539382632 to your computer and use it in GitHub Desktop.
Save tvdstaaij/53de6743eab05c979a7831c539382632 to your computer and use it in GitHub Desktop.
Raspberry Pi headless full disk encryption with remote unlock
#!/bin/bash
# Based on guide: https://github.com/ViRb3/pi-encrypted-boot-ssh (rev cac7ac5)
#
# Some usage notes:
# - Definitions: "host" means the machine executing this script, "source" means the prebuilt Raspberry Pi OS image used as a base, "target" means the image or disk being provisioned
# - Assumes Debian host system, and execution as root. Running in a VM is highly recommended for safety reasons
# - Only tested with https://raspi.debian.net/tested-images/ Bullseye/Bookworm RPi4 images as source (should also work for RPi3)
# - Assumes target with boot and root partition on one disk
# - Assumes Raspberry Pi is connected with Ethernet and DHCP (i.e. no special network configuration in initramfs)
# - After booting for the first time, you can ssh as `root` using one of the keys authorized for Dropbear
# - During execution of this script Dropbear will complain about an invalid authorized_keys file, this is ok because the file will be copied later in the script
#
# Notable changes/additions/removals compared to ViRb3/pi-encrypted-boot-ssh:
# - Adapted to work with Debian instead of Ubuntu
# - Add discard flags to crypttab and cmdline.txt (for SSDs)
# - Customize cryptsetup and dropbear options
# Some settings you may want to customize. In particular, for RPi3 and/or better performance, consider:
# - Using xchacha12 instead of xchacha20
# - Reducing --pbkdf-parallel to 1
# - Reducing --pbkdf-force-iterations
[[ -z ${LUKS_OPTIONS+x} ]] && LUKS_OPTIONS="-c xchacha20,aes-adiantum-plain64 -s 256 --pbkdf argon2id --pbkdf-parallel 4 --pbkdf-force-iterations 4 --pbkdf-memory 524288"
[[ -z ${DROPBEAR_OPTIONS+x} ]] && DROPBEAR_OPTIONS="-j -k -s -p 22222 -c cryptroot-unlock"
[[ -z ${CRYPTSETUP_TIMEOUT+x} ]] && CRYPTSETUP_TIMEOUT=30
# Override to 0 to inspect incomplete output when script fails
[[ -z ${ERROR_CLEANUP+x} ]] && ERROR_CLEANUP=1
set -Eeuoo pipefail functrace
cleanup() {
umount chroot/boot/firmware
umount chroot/sys
umount chroot/proc
umount chroot/dev/pts
umount chroot/dev
umount chroot
umount rootimg
cryptsetup close cryptroot
[[ -n "${SOURCE_IMAGE}" ]] && kpartx -d "${SOURCE_IMAGE}"
[[ -z "${TARGET_DEV}" ]] && [[ -n "${TARGET_IMAGE}" ]] && kpartx -d "${TARGET_IMAGE}"
rmdir chroot
rmdir rootimg
}
failure() {
set +Eeuo pipefail
local lineno=$2
local fn=$3
local exitstatus=$4
local msg=$5
local lineno_fns=${1% 0}
if [[ "$lineno_fns" != "0" ]] ; then
lineno="${lineno} ${lineno_fns}"
fi
echo "${BASH_SOURCE[1]}:${fn}[${lineno}] Failed with status ${exitstatus}: $msg"
echo "Attempting cleanup..."
[[ ${ERROR_CLEANUP} -eq 1 ]] && cleanup
exit 1
}
trap 'failure "${BASH_LINENO[*]}" "$LINENO" "${FUNCNAME[*]:-script}" "$?" "$BASH_COMMAND"' ERR
[[ -z ${TARGET_DEV+x} ]] && read -p 'Target device (/dev/xyz) (empty to write image): ' TARGET_DEV
[[ -z ${SOURCE_IMAGE+x} ]] && read -p 'Absolute path to OS image: ' SOURCE_IMAGE
[[ -z ${AUTHORIZED_KEYS_FILE+x} ]] && read -p 'Path to authorized_keys file: ' AUTHORIZED_KEYS_FILE
apt update
apt install -y kpartx cryptsetup-bin qemu-user-static rsync
if [[ "${SOURCE_IMAGE}" != /* ]]; then
echo Error: image path must be absolute
exit 1
fi
if [[ ! -f "${SOURCE_IMAGE}" ]]; then
echo Error: ${SOURCE_IMAGE} does not exist
exit 1
fi
if [[ ! -f "${AUTHORIZED_KEYS_FILE}" ]]; then
echo Error: ${AUTHORIZED_KEYS_FILE} does not exist
exit 1
fi
if [[ -z "${TARGET_DEV}" ]]; then
TARGET_IMAGE="${SOURCE_IMAGE}.crypt"
cp "${SOURCE_IMAGE}" "${TARGET_IMAGE}"
TARGET_IMAGE_MAP=$(kpartx -va "${TARGET_IMAGE}")
TARGET_ROOT_DEV="/dev/mapper/$(echo "${TARGET_IMAGE_MAP}" | tail -1 | cut -d ' ' -f 3)"
TARGET_BOOT_DEV="/dev/mapper/$(echo "${TARGET_IMAGE_MAP}" | tail -2 | head -1 | cut -d ' ' -f 3)"
else
read -p "Flashing ${SOURCE_IMAGE} to ${TARGET_DEV}. DATA ON ${TARGET_DEV} WILL BE LOST. Press any key to continue or Ctrl+C to abort."
dd if="${SOURCE_IMAGE}" of="${TARGET_DEV}" bs=4M conv=fdatasync status=progress
sync
TARGET_BOOT_DEV="/dev/$(lsblk -lno name ${TARGET_DEV} | tail -n 2 | head -n 1)"
TARGET_ROOT_DEV="/dev/$(lsblk -lno name ${TARGET_DEV} | tail -n 1)"
read -p "Detected partitions ${TARGET_BOOT_DEV} for boot and ${TARGET_ROOT_DEV} for root. Press any key to continue or Ctrl+C if this is incorrect."
fdisk -l
echo -e "d\n2\nw" | fdisk "${TARGET_DEV}" # Delete partition number 2
read -p "Deleted root partition. Please use fdisk to create a new root partition with number 2. Press any key to start fdisk."
fdisk -l
fdisk "${TARGET_DEV}"
fi
echo "Please initialize encryption:"
cryptsetup luksFormat ${LUKS_OPTIONS} "${TARGET_ROOT_DEV}"
echo "Please unlock the encrypted volume:"
cryptsetup open "${TARGET_ROOT_DEV}" cryptroot
mkfs.ext4 /dev/mapper/cryptroot
mkdir -p chroot
mount /dev/mapper/cryptroot chroot
SOURCE_IMAGE_ROOT_DEV="/dev/mapper/$(kpartx -var "${SOURCE_IMAGE}" | tail -n 1 | cut -d ' ' -f 3)"
mkdir -p rootimg
mount "${SOURCE_IMAGE_ROOT_DEV}" rootimg
rsync --archive --hard-links --acls --xattrs --one-file-system --numeric-ids --info="progress2" rootimg/* chroot/
mkdir -p chroot/boot
mount "${TARGET_BOOT_DEV}" chroot/boot/firmware
mount -t proc none chroot/proc
mount -t sysfs none chroot/sys
mount -o bind /dev chroot/dev
mount -o bind /dev/pts chroot/dev/pts
cp "${AUTHORIZED_KEYS_FILE}" chroot/root/authorized_keys.tmp
cat << EOF | chroot chroot
set -Eeuo pipefail
# Create temporary DNS config
[[ -f /etc/resolv.conf ]] || [[ -L /etc/resolv.conf ]] && mv /etc/resolv.conf /etc/resolv.conf.bak
echo "nameserver 1.1.1.1" > /etc/resolv.conf
# Modify system configuration
PARTITION_REF=\$(blkid -o export ${TARGET_ROOT_DEV} | grep ^UUID)
echo "cryptroot \${PARTITION_REF} none luks,initramfs,discard" > /etc/crypttab
echo "cryptdevice=\${PARTITION_REF}:cryptroot:allow-discards" > /etc/default/raspi-extra-cmdline
sed -i -E 's|ROOTPART=\\S+|ROOTPART=/dev/mapper/cryptroot|' /etc/default/raspi-firmware
sed -i -E 's|root=\\S+|root=/dev/mapper/cryptroot cryptdevice='"\${PARTITION_REF}"':cryptroot:allow-discards|' /boot/firmware/cmdline.txt
sed -i -E 's|rootfstype=\\S+|rootfstype=ext4|' /boot/firmware/cmdline.txt
sed -i -E 's|^\\S+\\s+/\\s+\\S+\\s+\\S+|/dev/mapper/cryptroot / ext4 defaults,relatime,discard,errors=remount-ro|' /etc/fstab
# Install dependencies
apt update
apt install -y busybox cryptsetup dropbear-initramfs openssh-server patch
# Patch cryptsetup unlock timeout
sed -i 's/^TIMEOUT=.*/TIMEOUT=${CRYPTSETUP_TIMEOUT}/g' /usr/share/cryptsetup/initramfs/bin/cryptroot-unlock
# Patch cryptroot hook to prevent incorrect UUID resolution
patch --no-backup-if-mismatch /usr/share/initramfs-tools/hooks/cryptroot << 'CRYPTHOOK'
--- cryptroot
+++ cryptroot
@@ -33,7 +33,7 @@
printf '%s\0' "\$target" >>"\$DESTDIR/cryptroot/targets"
crypttab_find_entry "\$target" || return 1
crypttab_parse_options --missing-path=warn || return 1
- crypttab_print_entry
+ printf '%s %s %s %s\n' "\$_CRYPTTAB_NAME" "\$_CRYPTTAB_SOURCE" "\$_CRYPTTAB_KEY" "\$_CRYPTTAB_OPTIONS" >&3
fi
}
CRYPTHOOK
# Detect Dropbear configuration paths
if [[ -d /etc/dropbear-initramfs ]]; then
# Debian 11
DROPBEAR_CONFIG_FILE=/etc/dropbear-initramfs/config
DROPBEAR_AUTHORIZED_KEYS_FILE=/etc/dropbear-initramfs/authorized_keys
elif [[ -d /etc/dropbear ]]; then
# Debian 12
DROPBEAR_CONFIG_FILE=/etc/dropbear/initramfs/dropbear.conf
DROPBEAR_AUTHORIZED_KEYS_FILE=/etc/dropbear/initramfs/authorized_keys
else
echo Error: dropbear-initramfs configuration directory not found
exit 1
fi
# Configure initramfs
echo >> /etc/cryptsetup-initramfs/conf-hook
echo "CRYPTSETUP=y" >> /etc/cryptsetup-initramfs/conf-hook
echo >> \${DROPBEAR_CONFIG_FILE}
echo "DROPBEAR_OPTIONS=\\"${DROPBEAR_OPTIONS}\\"" >> \${DROPBEAR_CONFIG_FILE}
mv /root/authorized_keys.tmp \${DROPBEAR_AUTHORIZED_KEYS_FILE}
chmod 0600 \${DROPBEAR_AUTHORIZED_KEYS_FILE}
# Update existing initramfs
update-initramfs -u -k all
# Restore DNS config
rm -f /etc/resolv.conf
[[ -f /etc/resolv.conf.bak ]] && mv /etc/resolv.conf.bak /etc/resolv.conf
# Bootstrap SSH (allow initial login as root using the dropbear authorized keys)
mkdir -p /root/.ssh
chmod 700 /root/.ssh
cp \${DROPBEAR_AUTHORIZED_KEYS_FILE} /root/.ssh/authorized_keys
# For some reason, some of the Debian "tested" RPi images have both ssh.service and ssh.socket
# enabled, which are in conflict. Enable only the service.
systemctl is-enabled ssh.socket && systemctl disable ssh.socket
systemctl enable ssh.service
# From https://raspi.debian.net/defaults-and-settings/
# "The image is configured to process the /boot/firmware/sysconf.txt file at boot time; this file documents this functionality can be disabled by issuing:"
systemctl disable rpi-set-sysconf
sync
history -c
EOF
echo "Successfully finished. Cleaning up..."
cleanup
# Note: if image output was used, after flashing the image,
# resize the root partition on the Pi to full size:
# echo -e "d\n2\nn\np\n2\n\n\nw" | fdisk /dev/<maindisk>
# cryptsetup resize cryptroot
# systemctl reboot
@abe-101
Copy link

abe-101 commented Aug 19, 2022

Really neat!
Thanks for sharing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment