-
-
Save tvdstaaij/53de6743eab05c979a7831c539382632 to your computer and use it in GitHub Desktop.
#!/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 |
If anyone runs into the following error while running the script,
mount: /home/user/Downloads/rootimg: unknown filesystem type 'crypto_LUKS'.
dmesg(1) may have more information after failed mount system call.
./rpi-install-cryptroot.sh:script[114] Failed with status 32: mount "${SOURCE_IMAGE_ROOT_DEV}" rootimg
try using a shorter name for the input image instead of the longer default name of 2024-11-19-raspios-bookworm-arm64-lite.img
. When writing to an image, the second kpartx command seems to behave as if the given file name is the same as what was passed to the first command, and returns the same mappings as the first. This doesn't happen when a shorter name is used or the order of the two invocations is swapped (i.e. calling kpartx with SOURCE_IMAGE much earlier), strangely enough. This happened on Debian Bookworm.
A few more gotchas:
If you are trying to set this up on the Pi 5 and get this error message when trying to unlock the disk at boot
No usable keyslot is available
swap xchacha20,aes-adiantum-plain64
for aes-xts-plain64
in the LUKS_OPTIONS
definition. The Pi 5 has hardware accelerated cryptography extensions so Adiantum isn't needed anyway.
Also, when SSHing in to unlock the disk, note that the daemon is running on a non-standard port (22222), so be sure to modify the SSH command to use that port. e.g. ssh -p 22222 root@IPADDR
.
It's probably also a good idea to scrub the target root partition before encrypting it, otherwise the previous unencrypted data may remain. I did this with shred -n 1 -v "${TARGET_ROOT_DEV}"
before the cryptsetup luksFormat
line.
Since the script also patches some installed files, it's probably a good idea to hold the associated package back from being updated lest the changes be lost. This can be done with sudo apt-mark hold cryptsetup-initramfs
, since both patches apply to files owned by this package.
@ronandalton I am getting the No usable keyslot is available
error for a 32-bit install on an old Raspberry Pi. I see that the adiantum
kernel module is loaded, but xchacha20,aes-adiantum-plain64
still does not seem to be available - confirmed by running cryptsetup benchmark xchacha20,aes-adiantum-plain64
and getting an error to the effect.
Do you have any suggestions for how I could get xchacha20,aes-adiantum-plain64
working? My old Pi is very slow on aes-xts-plain64
, about 10 MiB/s according to the benchmark, so I would prefer to use a better optimised cipher for the hardware if possible.
I'm not sure sorry. Running cryptsetup benchmark xchacha20,aes-adiantum-plain64
works for me, but I'm unable to test on an older Pi model because I don't have one. What error do you get when you run that? Which model Pi is it? Is it possible to open a device encrypted with that cipher on the Raspberry Pi once booted up, i.e. not in from the initramfs (could be testable using a flash drive?)? Presumably this worked at some point, perhaps with an older cryptsetup or kernel version...
Thank you. This is a Raspberry Pi 2 Model B running under kernel 6.12 with Raspberry Pi OS (32-bit) Lite (from https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2025-05-13/2025-05-13-raspios-bookworm-armhf-lite.img.xz)
When running cryptsetup benchmark xchacha20,aes-adiantum-plain64
from within initramfs I get
Cipher xchacha20,aes-adiantum-plain64 (with 256 bits key) is not available.
Notably the initramfs7
file itself generated (7750189 bytes) was several megabytes less than the original from the base image (10898398 bytes), and the crypto modules present were different:
Original (via lsinitramfs
):
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/arch/arm/crypto
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/arch/arm/crypto/chacha-neon.ko
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/arch/arm/crypto/curve25519-neon.ko
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/arch/arm/crypto/poly1305-arm.ko
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/crypto
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/crypto/blake2b_generic.ko
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/crypto/md5.ko
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/crypto/xor.ko
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/lib/crypto
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/lib/crypto/libarc4.ko
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/lib/crypto/libchacha20poly1305.ko
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/lib/crypto/libcurve25519-generic.ko
As built via the script OR via the guide at https://github.com/ViRb3/pi-encrypted-boot-ssh:
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/crypto
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/crypto/adiantum.ko
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/crypto/af_alg.ko
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/crypto/algif_skcipher.ko
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/lib/crypto
usr/lib/modules/6.12.25+rpt-rpi-v7/kernel/lib/crypto/libpoly1305.ko
When trying to unlock in the initramfs, the specific error message is
Cannot use xchacha20,aes-adiantum-plain64 cipher for keyslot encryption.
No usable keyslot is available.
cryptsetup: ERROR: cryptroot: cryptsetup failed, bad password or options?
After installing the base Raspberry Pi image and then installing cryptsetup on top of it, cryptsetup benchmark -c xchacha20,aes-adiantum-plain64
reports that it's now available:
xchacha20,aes-adiantum 256b 36.8 MiB/s 44.1 MiB/s
So for some reason the kernel module is not being included in the initramfs.
I also have this issue on raspberry pi 4b
I suggest considering wifi connections too (wpa-supplicant etc...). Example: https://gist.github.com/telenieko/d17544fc7e4b347beffa87252393384c
Really neat!
Thanks for sharing