-
-
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/ Bookworm/Trixie 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 | |
| sed -i -E 's|^MODULES=.*|MODULES=most|' /etc/initramfs-tools/initramfs.conf | |
| # Configure dropbear | |
| 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 |
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.
Sharp observation. I believe you are referring to /usr/share/initramfs-tools/hooks/cryptroot and /usr/share/cryptsetup/initramfs/bin/cryptroot-unlock.
As for hooks/cryptroot, I believe this patch is only needed to prevent confusion about devices while in the chroot, i.e. while running this setup script. When the initramfs is recreated on the target during an apt update or some such, it shouldn't matter whether the hook is patched or not. From a cleanliness perspective it would perhaps be good to revert the patch near the end of the script, but I wanted to keep it as short and simple as possible.
The cryptroot-unlock patch however is indeed a bit tricky and it has bothered me before. I don't think I've actually seen this go wrong in practice during the years I've been using this setup, but in theory some update could indeed overwrite the timeout, leading to an unbootable system. It would be a highly recoverable situation, albeit with some hassle (you'd need to mount the rootfs, edit the config file and recreate the initramfs in a chroot on a development system).
Long story short, if your system boots with the Debian default timeout and doesn't really need the TIMEOUT= patch, I wouldn't bother with the hold. But otherwise I think it's worth considering, especially if you dread the possibility of having to manually fix the system in a chroot at some point.
Interestingly I've never had this issue on my RPi4b, the MODULES setting is already set to most in my case. Maybe it's because of base image differences? I always use the images from https://raspi.debian.net/tested-images/
I used the official Raspberry Pi OS images from their website (https://www.raspberrypi.com/software/operating-systems/) when I did this. I don't recall trying the Debian Raspberry Pi images. I must have skipped that detail. From what I can tell I don't think these are the same. Raspberry Pi OS is based on Debian and the link you provided is to a Debian release for the Raspberry Pi. Is there a reason to use plain Debian when Raspberry Pi OS exists?
I used the official Raspberry Pi OS images from their website (https://www.raspberrypi.com/software/operating-systems/) when I did this. I don't recall trying the Debian Raspberry Pi images. I must have skipped that detail. From what I can tell I don't think these are the same. Raspberry Pi OS is based on Debian and the link you provided is to a Debian release for the Raspberry Pi.
Ah, that explains it. As mentioned in the comments at the top of the script, it was originally made to automate the steps in an existing guide for Ubuntu, and then modified to work with Debian (because they have some notable differences in filesystem layout etc). I never tested it with Raspberry Pi OS.
Technically Raspberry Pi OS is a separate distribution, though I'd guess it doesn't deviate from vanilla Debian too much. If the script also works with RPi OS now, all the better.
Is there a reason to use plain Debian when Raspberry Pi OS exists?
RPi OS used to be 32-bit only, even when 64-bit Pi models weren't brand new anymore, and quite a few popular applications had already dropped 32-bit support. So historically, it wasn't a viable choice if you needed such applications. These days RPi OS has a 64-bit version however, so that's no longer an argument.
So now it comes down to personal preference. What I personally like about vanilla Debian:
- It's quite consistent across all of my Debian systems regardless of hardware platform
- I trust their release process
- RPi OS releases lag behind Debian releases (I don't know by how much, but from my experiences in the past I recall that the Raspberry team doesn't tend to be very fast with software)
- No bloat by default
I don't know if there are any arguments in favor of RPi OS, because I'm satisfied with Debian and haven't looked into the differences.
By the way, the reason for this is so that if you are behind a NAT/firewall, you could forward/allow connections to the unlock port but not to the main SSH port. This makes it possible to remotely unlock the device in case of a reboot, power failure, etc., without exposing the booted device to SSH attacks from the internet.
Note that you can add a separate entry to your
~/.ssh/configfor easy unlocking:Then just do
ssh rpi-unlock. This also helps to preventknown_hostsconflicts.However, if this split port approach is not useful for a particular setup, you can just change the port to 22 in
DROPBEAR_OPTIONS.