diff --git a/ChangeLog b/ChangeLog index 28ad2e1..75ee118 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,10 @@ +2021-11-13 Julien Coloos + + * v1.0-1 + Option to use login shell instead of cryptsetup unlocking script + Option to re-enable Wake-on-LAN on network device + + 2021-11-12 Julien Coloos * v0.9-2 diff --git a/PKGBUILD b/PKGBUILD index 0ad59ea..acff4ac 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,20 +1,21 @@ # Maintainer: Julien Coloos pkgname=initrd-ssh-cryptsetup -pkgver=0.9 -pkgrel=2 -pkgdesc="Allows for LUKS-encrypted devices to be unlocked remotely over SSH" +pkgver=1.0 +pkgrel=1 +pkgdesc="Allows to remotely unlock LUKS-encrypted devices over SSH" arch=('any') url="https://github.com/suiryc/archlinux-$pkgname" license=('GPL3') -depends=('dropbear' 'cryptsetup' 'mkinitcpio-nfs-utils' 'iproute2') +depends=('dropbear' 'cryptsetup' 'mkinitcpio-nfs-utils' 'iproute2' 'ethtool') install=$pkgname.install changelog='ChangeLog' source=("http://julien.coloos.free.fr/archlinux/$pkgname-$pkgver.tar.xz" "$pkgname.install") -sha256sums=('c3fa91fc8ba2228b3492d3709231918c8015cc3da49f516c3eacea5c0217536c' +sha256sums=('de6ef287ecfd57614835fec1fcaa01eb3a7f999d42a749e20b6747671320508f' 'b84978b3c2ef32208c2b104ee2d3ce8aaec26da0bd4e9e1c83942f373bbf6285') package() { - install -Dm644 "$srcdir/src/install/ssh-cryptsetup" "$pkgdir/usr/lib/initcpio/install/ssh-cryptsetup" - install -Dm644 "$srcdir/src/hooks/ssh-cryptsetup" "$pkgdir/usr/lib/initcpio/hooks/ssh-cryptsetup" + install -Dm644 "$srcdir/src/install/ssh-cryptsetup" "$pkgdir/usr/lib/initcpio/install/ssh-cryptsetup" + install -Dm644 "$srcdir/src/hooks/ssh-cryptsetup" "$pkgdir/usr/lib/initcpio/hooks/ssh-cryptsetup" + install -Dm644 "$srcdir/src/hooks/ssh-cryptsetup-tools" "$pkgdir/usr/lib/initcpio/hooks/ssh-cryptsetup-tools" } diff --git a/README.md b/README.md index 55bbb81..24e142f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Personal ArchLinux package combining dropbear and cryptsetup in initrd for unlocking LUKS-encrypted devices either locally (boot console) or remotely over SSH. +Personal ArchLinux package combining dropbear and cryptsetup in initramfs for unlocking LUKS-encrypted devices either locally (boot console) or remotely over SSH. The code was reworked from legacy dropbear_initrd_encrypt AUR package. @@ -37,33 +37,43 @@ As explained upon installation, the following things need to be done: The LUKS-encrypted devices to unlock are derived from `/etc/crypttab`. -Some options can be set in `/etc/initcpio/sshcs_env` (file is sourced in initrd shell): +Some options can be set in `/etc/initcpio/sshcs_env` (file is sourced in initramfs shell): * `sshcs_opt_debug`: whether to be more verbose about ongoing actions - - default: 0 + - default: `0` - any non-zero value to enable + * `sshcs_opt_net_wol`: Wake-on-LAN option to set on network device + - default: `g` (MagicPacketâ„¢) + - usually WOL is disabled once in initramfs shell + - set empty to not change network device WOL setting * `sshcs_opt_timeout_ipconfig`: time (in seconds) to configure IP - - default: 10 seconds + - default: `10` * `sshcs_opt_listen`: SSH listening port - - default: 22 + - default: `22` * `sshcs_opt_timeout_poweroff`: time (in seconds) to unlock devices before automatic powering off - - default (and minimum value): 2 minutes + - default (and minimum value): `120` (2 minutes) - negative value to deactivate + * `sshcs_opt_use_shell`: whether to start a full `ash` shell + - default: `0` + - `1` to enable + - when disabled (the default), a script to unlock devices is executed instead For example: sshcs_opt_timeout_ipconfig=30 sshcs_opt_listen=2222 sshcs_opt_timeout_poweroff=-1 + sshcs_opt_use_shell=1 ## Building notes 1. Modify the sources (features in `src`, and/or package building files) 2. If `src` was modified - * archive the `src` folder in `$pkgname-$pkgver.tar.xz` file; e.g.: `tar -cJf initrd-ssh-cryptsetup-0.9.tar.xz src` + * bump `pkgver` in `PKGBUILD` + * archive the `src` folder in `$pkgname-$pkgver.tar.xz` file; e.g.: `tar -cJf initrd-ssh-cryptsetup-$(grep "^pkgver=" PKGBUILD | cut -d'=' -f2).tar.xz src` * upload the archive on the online repository (pointed by `PKGBUILD`) 3. Update ChangeLog 4. Update `PKGBUILD` - * bump `pkgver` if `src` was modified, or `pkgrel` if building files were modified + * bump `pkgrel` if only building files were modified * refresh `sha256sums` with `updpkgsums` if necessary - or manually, based on `sha256sum initrd-ssh-cryptsetup-*.tar.xz initrd-ssh-cryptsetup.install` output 5. Delete generated archive file if any diff --git a/src/hooks/ssh-cryptsetup b/src/hooks/ssh-cryptsetup index c5b55d7..8893472 100644 --- a/src/hooks/ssh-cryptsetup +++ b/src/hooks/ssh-cryptsetup @@ -1,311 +1,324 @@ #!/usr/bin/ash -dbg () { - [ ${sshcs_opt_debug} != 0 ] && echo "$@" -} - -sshcs_env_load() { - local debug_default=0 - local timeout_ipconfig_default=10 - local timeout_poweroff_min=120 - - [ -e "${sshcs_env}" ] && . "${sshcs_env}" - [ -z "${sshcs_opt_debug}" ] && sshcs_opt_debug=${debug_default} - [ -z "${sshcs_opt_timeout_ipconfig}" ] && sshcs_opt_timeout_ipconfig=${timeout_ipconfig_default} - [ -n "${sshcs_opt_listen}" ] && sshcs_opt_listen="-p ${sshcs_opt_listen}" - [ -z "${sshcs_opt_timeout_poweroff}" ] && sshcs_opt_timeout_poweroff=${timeout_poweroff_min} - [ ${sshcs_opt_timeout_poweroff} -ge 0 ] && [ ${sshcs_opt_timeout_poweroff} -lt ${timeout_poweroff_min} ] && sshcs_opt_timeout_poweroff=${timeout_poweroff_min} -} +. "/usr/local/bin/ssh-cryptsetup-tools" sshcs_net_start() { - # we must have an 'ip' setting, and a device in it - [ -z "${ip}" ] && [ -n "${nfsaddrs}" ] && ip="${nfsaddrs}" - [ -z "${ip}" ] && { - dbg "No ip setting to setup network" - return 1 - } + local iparg net_address ipconfig_out net_netmask net_gateway net_dns0 net_dns1 - net_device=$(echo ${ip} | cut -d: -f6) - [ -z "${net_device}" ] && { - dbg "No network device to setup" - return 1 - } + # we must have an 'ip' setting, and a device in it + [ -z "${ip}" ] && [ -n "${nfsaddrs}" ] && ip="${nfsaddrs}" + [ -z "${ip}" ] && { + dbg "No ip setting to setup network" + return 1 + } - # Setup network and save some values - # Note: some useful redirection means ('< <(...)' and '<<< "$(...)"') are - # not supported in the available shell. So we have to write code in a - # temporary file and 'source' it since '... | while read ...' spawns a - # subshell from which outer variables cannot be altered. - : > "${net_env}" + net_device=$(echo ${ip} | cut -d: -f6) + [ -z "${net_device}" ] && { + dbg "No network device to setup" + return 1 + } - echo "" - echo "Configuring IP (timeout = ${sshcs_opt_timeout_ipconfig}s) ..." - # ipconfig manual: https://git.kernel.org/pub/scm/libs/klibc/klibc.git/tree/usr/kinit/ipconfig/README.ipconfig - ipconfig_out=$(ipconfig -t "${sshcs_opt_timeout_ipconfig}" "ip=${ip}") - if [ $? -ne 0 ]; then - err "IP configuration timeout!" - echo "Devices probing:" - ipconfig -n -t 5 -c none all - return 1 - fi + if [ "${sshcs_opt_net_wol:-d}" != "d" ]; then + dbg "Setting network device=${net_device} wol=${sshcs_opt_net_wol}" + ethtool -s "${net_device}" wol "${sshcs_opt_net_wol}" + fi - echo -n "${ipconfig_out}" | while read line; do - [ "${line#"IP-Config:"}" != "${line}" ] && continue + # Setup network and save some values + # Note: some useful redirection means ('< <(...)' and '<<< "$(...)"') are + # not supported in the available shell. So we have to write code in a + # temporary file and 'source' it since '... | while read ...' spawns a + # subshell from which outer variables cannot be altered. + : > "${net_env}" - line="$(echo "${line}" | sed -e 's/ :/:/g;s/: /=/g')" + echo "" + echo "Configuring IP (timeout = ${sshcs_opt_timeout_ipconfig}s) ..." + # ipconfig manual: https://git.kernel.org/pub/scm/libs/klibc/klibc.git/tree/usr/kinit/ipconfig/README.ipconfig + ipconfig_out=$(ipconfig -t "${sshcs_opt_timeout_ipconfig}" "ip=${ip}") + if [ $? -ne 0 ]; then + err "IP configuration timeout!" + echo "Devices probing:" + ipconfig -n -t 5 -c none all + return 1 + fi - for iparg in ${line}; do - case "${iparg}" in - address=*|netmask=*|gateway=*|dns0=*|dns1=*) - echo "net_${iparg}" >> "${net_env}" - ;; - esac - done + echo -n "${ipconfig_out}" | while read line; do + [ "${line#"IP-Config:"}" != "${line}" ] && continue + + line="$(echo "${line}" | sed -e 's/ :/:/g;s/: /=/g')" + + for iparg in ${line}; do + case "${iparg}" in + address=*|netmask=*|gateway=*|dns0=*|dns1=*) + echo "net_${iparg}" >> "${net_env}" + ;; + esac done + done - . "${net_env}" - rm -f "${net_env}" + . "${net_env}" + rm -f "${net_env}" - echo "IP-Config: device=${net_device} ip=${net_address}/${net_netmask} gw=${net_gateway} dns0=${net_dns0} dns1=${net_dns1}" + echo "IP-Config: device=${net_device} ip=${net_address}/${net_netmask} gw=${net_gateway} dns0=${net_dns0} dns1=${net_dns1}" - [ -n "${net_address}" ] + [ -n "${net_address}" ] } sshcs_net_done() { - # we are done with the network - if [ -n "${net_device}" ]; then - dbg "Setting network device=${net_device} down" - ip addr flush dev "${net_device}" - ip link set dev "${net_device}" down - fi + # we are done with the network + if [ -n "${net_device}" ]; then + dbg "Setting network device=${net_device} down" + ip addr flush dev "${net_device}" + ip link set dev "${net_device}" down + fi } sshcs_trapped_timeout() { - err "Timeout reached! Powering off." - poweroff -f - exit + err "Timeout reached! Powering off." + poweroff -f + exit } sshcs_trap_timeout() { - local pid_init=$$ + local pid_init=$$ - if [ ${sshcs_opt_timeout_poweroff} -gt 0 ]; then - echo "" - echo "WARNING! Automatic poweroff will be triggered in ${sshcs_opt_timeout_poweroff}s" - echo "To deactivate, please unlock devices" - echo "" - trap sshcs_trapped_timeout SIGALRM - ( - sleep ${sshcs_opt_timeout_poweroff} - kill -SIGALRM ${pid_init} - # Signal is not processed if cryptsetup is waiting for the password - killall cryptsetup > /dev/null 2>&1 - ) & - pid_timeout=$! - fi + if [ ${sshcs_opt_timeout_poweroff} -gt 0 ]; then + echo "" + echo "WARNING! Automatic poweroff will be triggered in ${sshcs_opt_timeout_poweroff}s" + echo "To deactivate, please unlock devices" + trap sshcs_trapped_timeout SIGALRM + ( + sleep ${sshcs_opt_timeout_poweroff} + kill -SIGALRM ${pid_init} + # Signal is not processed if cryptsetup is waiting for the password + killall cryptsetup > /dev/null 2>&1 + ) & + pid_timeout=$! + fi } sshcs_untrap_timeout() { - [ -z "${pid_timeout}" ] && return 0 - kill ${pid_timeout} - trap - SIGALRM - msg "Timeout cleared." + [ -z "${pid_timeout}" ] && return 0 + # Notes: + # If there was a running SSH shell, it may also try to kill it. + # This only kills the spawned subshell, leaving the 'sleep' command still + # running until done (which is not an issue). + proc_parse_stat ${pid_timeout} && kill ${pid_timeout} + pid_timeout= + trap - SIGALRM + msg "Timeout cleared." } -sshcs_unlock() { - sshcs_trap_timeout +sshcs_shell_run() { + sshcs_trap_timeout - # actual script (shared with SSH login) unlocking encrypted devices - . "${sshcs_cryptsetup_script}" - - sshcs_untrap_timeout + # actual script (shared with SSH login) with which we can unlock devices + sshcs_unlocked_test=0 + . "${sshcs_shell_script}" } -sshcs_dropbear_unlock() { - local pid_timeout= - local dev_pts_mounted=0 - local listen= +sshcs_dropbear_run() { + local pid_timeout= + local dev_pts_mounted=0 + local listen= - # ensure /dev/pts is present - if [ ! -d "/dev/pts" ]; then - mkdir -p "/dev/pts" - mount -t devpts devpts "/dev/pts" - dev_pts_mounted=1 - fi + # ensure /dev/pts is present + if [ ! -d "/dev/pts" ]; then + mkdir -p "/dev/pts" + mount -t devpts devpts "/dev/pts" + dev_pts_mounted=1 + fi - # /etc/passwd file for the root user - echo "root:x:0:0:root:/root:${dropbear_login_shell}" > "/etc/passwd" - echo "${dropbear_login_shell}" > "/etc/shells" - - # root login script - cat < "${dropbear_login_shell}" + if [ ${sshcs_opt_use_shell} -eq 0 ]; then + sshcs_shell_script=${sshcs_cryptsetup_script} + else + cat < "${sshcs_shell_script}" #!/usr/bin/ash -. "/init_functions" +. "/usr/local/bin/ssh-cryptsetup-tools" -if [ ! -f "${sshcs_cryptsetup_script}" ]; then - err "No cryptsetup script present! Please retry." - exit 0 -fi - -if [ -c "/dev/mapper/control" ]; then - CSQUIET= - . "${sshcs_cryptsetup_script}" - - echo "" - echo "cryptsetup succeeded! Boot sequence should go on." - echo "Please wait and retry for standard SSH service." -else - err "Device resources missing! Please retry." -fi echo "" +echo "Call ${sshcs_cryptsetup_script} to try unlocking device(s)" + +# Now give the user its shell +/usr/bin/ash + +# Check whether we are fully done +sshcs_check_done 1 EOF - chmod a+x "${dropbear_login_shell}" + chmod a+x "${sshcs_shell_script}" + fi - [ ! -d "/var/log" ] && mkdir -p "/var/log" - touch "/var/log/lastlog" + # /etc/passwd file for the root user + echo "root:x:0:0:root:/root:${sshcs_shell_script}" > "/etc/passwd" + echo "${sshcs_shell_script}" > "/etc/shells" - msg "Starting dropbear ..." - dropbear -Esgjk -P "${path_dropbear_pid}" ${sshcs_opt_listen} + [ ! -d "/var/log" ] && mkdir -p "/var/log" + touch "/var/log/lastlog" - # Actual unlocking - sshcs_unlock + msg "Starting dropbear ..." + dropbear -Esgjk -P "${path_dropbear_pid}" ${sshcs_opt_listen} - # cleanup dropbear - if [ -f "${path_dropbear_pid}" ]; then - msg "Stopping dropbear ..." - kill $(cat "${path_dropbear_pid}") - rm -f "${path_dropbear_pid}" - fi - rm -f "${sshcs_cryptsetup_script}" "${dropbear_login_shell}" "/etc/passwd" "/etc/shells" "/var/log/lastlog" - - # cleanup /dev/pts if necessary - if [ ${dev_pts_mounted} -ne 0 ]; then - umount "/dev/pts" - rm -R "/dev/pts" - fi + # Actual unlocking shell + sshcs_shell_run } sshcs_cryptpart_process() { - # ensure there is a device (handle 'UUID=' format) - [ -z "${cryptdev}" ] && return 0 - [ "${cryptdev#UUID=}" != "${cryptdev}" ] && cryptdev="/dev/disk/by-uuid/${cryptdev#UUID=}" + local cryptdev_orig cryptopt cryptargs - # get crypt options - cryptargs= - for cryptopt in ${cryptoptions//,/ }; do - case ${cryptopt} in - discard) - cryptargs="${cryptargs} --allow-discards" - ;; + # ensure there is a device (handle 'UUID=' format) + [ -z "${cryptdev}" ] && return 0 + [ "${cryptdev#UUID=}" != "${cryptdev}" ] && cryptdev="/dev/disk/by-uuid/${cryptdev#UUID=}" - luks) - ;; + # get crypt options + cryptargs= + for cryptopt in ${cryptoptions//,/ }; do + case ${cryptopt} in + discard) + cryptargs="${cryptargs} --allow-discards" + ;; - *) - echo "Device ${cryptdev} encryption option '${cryptopt}' not known, ignoring." - ;; - esac - done + luks) + ;; - # ensure device is encrypted and handled - cryptdev_orig=${cryptdev} - if cryptdev=$(resolve_device "${cryptdev_orig}" ${rootdelay}); then - if cryptsetup isLuks "${cryptdev}" >/dev/null 2>&1; then - dbg "Adding crypt device=${cryptdev} type=${crypttype} name=${cryptname} args=<${cryptargs}> in setup script" + *) + echo "Device ${cryptdev} encryption option '${cryptopt}' not known, ignoring." + ;; + esac + done + + # ensure device is encrypted and handled + cryptdev_orig=${cryptdev} + if cryptdev=$(resolve_device "${cryptdev_orig}" ${rootdelay}); then + if cryptsetup isLuks "${cryptdev}" >/dev/null 2>&1; then + dbg "Adding crypt device=${cryptdev} type=${crypttype} name=${cryptname} args=<${cryptargs}> in setup script" + + # update script used to unlock device either in console or SSH + [ -s "${sshcs_cryptsetup_script}" ] || cat <<'EOF' > "${sshcs_cryptsetup_script}" +#!/usr/bin/ash + +. "/usr/local/bin/ssh-cryptsetup-tools" - # update script used to unlock device either in console or SSH - [ -s "${sshcs_cryptsetup_script}" ] || cat < "${sshcs_cryptsetup_script}" cycle_or_retry() { - local res + local res - read -n 1 -s -t 5 -p "Whithin 5s press 'P' to poweroff, 'R' to reboot or any other key to retry. " res - echo "" - if [ "\${res}" = "P" ]; then - poweroff -f - elif [ "\${res}" = "R" ]; then - reboot -f - fi + read -n 1 -s -t 5 -p "Within 5s press 'P' to poweroff, 'R' to reboot or any other key to retry. " res + echo "" + [ "${res}" == "P" ] && poweroff -f + [ "${res}" == "R" ] && reboot -f } + +try_unlock() { EOF - cat <> "${sshcs_cryptsetup_script}" -# loop until device is available -while [ ! -e "/dev/mapper/${cryptname}" ]; do + cat <> "${sshcs_cryptsetup_script}" + # loop until device is available + while [ ! -e "/dev/mapper/${cryptname}" ]; do + if [ \${sshcs_unlocked_test:-0} -eq 1 ]; then + sshcs_unlocked=0 + return + fi if cryptsetup open --type "${crypttype}" "${cryptdev}" "${cryptname}" ${cryptargs} "\${CSQUIET}"; then - if poll_device "/dev/mapper/${cryptname}" ${rootdelay}; then - killall cryptsetup > /dev/null 2>&1 - break - fi - err "Device still not mapped! Please wait or retry." - elif [ ! -e "/dev/mapper/${cryptname}" ]; then - err "cryptsetup failed! Please retry." - else + if poll_device "/dev/mapper/${cryptname}" ${rootdelay}; then + killall cryptsetup > /dev/null 2>&1 break + fi + err "Device still not mapped! Please wait or retry." + elif [ ! -e "/dev/mapper/${cryptname}" ]; then + err "cryptsetup failed! Please retry." + else + break fi cycle_or_retry -done + done EOF - else - err "Failed to manage encrypted device ${cryptdev_orig}: not a LUKS volume." - fi + else + err "Failed to manage encrypted device ${cryptdev_orig}: not a LUKS volume." fi + fi +} + +sshcs_cryptpart_setup() { + local cryptdev crypttype cryptname cryptpass cryptoptions + + # check encrypted devices to handle + cryptdev= + crypttype=luks + while read cryptname cryptdev cryptpass cryptoptions; do + # skip comment lines + [ "${cryptname:0:1}" = "#" ] && continue + # skip devices with given password + [ -n "${cryptpass}" ] && [ "${cryptpass}" != "none" ] && [ "${cryptpass}" != "-" ] && continue + + sshcs_cryptpart_process + done < "${etc_crypttab}" + + # Nothing else to do if there is no device we can unlock + [ -s "${sshcs_cryptsetup_script}" ] || return 0 + + cat <<'EOF' >> "${sshcs_cryptsetup_script}" + # No other device to unlock +} + +if [ -c "/dev/mapper/control" ]; then + CSQUIET= + try_unlock + + [ ${sshcs_unlocked_test:-0} -eq 1 ] && return + sshcs_check_done 0 +else + if [ \${sshcs_unlocked_test:-0} -eq 1 ]; then + sshcs_unlocked=0 + return + fi + echo "" + err "Device resources missing! Please retry." +fi +EOF + chmod a+x "${sshcs_cryptsetup_script}" } run_hook() { - local etc_crypttab="/etc/crypttab" - local sshcs_env="/etc/initcpio/sshcs_env" - local path_dropbear_pid="/.dropbear.pid" - local dropbear_login_shell="/.cryptsetup_shell.sh" - local sshcs_cryptsetup_script="/.cryptsetup_script.sh" - local net_env="/.net_env.sh" - local line iparg net_address net_device ipconfig_out net_netmask net_gateway net_dns0 net_dns1 - local cryptdev cryptdev_orig crypttype cryptname cryptpass cryptoptions cryptopt cryptargs CSQUIET + local etc_crypttab="/etc/crypttab" + local path_dropbear_pid="/.dropbear.pid" + local sshcs_shell_script="/.sshcs_shell.sh" + local net_env="/.sshcs_net_env.sh" + local line net_device + local CSQUIET - # Load our options - sshcs_env_load + # Note: options were loaded already - # sanity check: crypttab should be present - [ ! -e "${etc_crypttab}" ] && { - dbg "No crypttab configuration to process" - return 0 - } + # sanity check: crypttab should be present + [ ! -e "${etc_crypttab}" ] && { + dbg "No crypttab configuration to process" + return 0 + } - modprobe -a -q dm-crypt >/dev/null 2>&1 - [ "${quiet}" = "y" ] && CSQUIET=">/dev/null" + # Initialize random generator ASAP. + # May delay first SSH login by a few seconds otherwise. + (dd if=/dev/urandom of=/dev/null bs=4 count=1 status=none > /dev/null 2>&1) & + (dd if=/dev/random of=/dev/null bs=4 count=1 status=none > /dev/null 2>&1) & - umask 0022 + modprobe -a -q dm-crypt >/dev/null 2>&1 + [ "${quiet}" = "y" ] && CSQUIET=">/dev/null" - # check encrypted devices to handle - cryptdev= - crypttype=luks - while read cryptname cryptdev cryptpass cryptoptions; do - # skip comment lines - [ "${cryptname:0:1}" = "#" ] && continue - # skip devices with given password - [ -n "${cryptpass}" ] && [ "${cryptpass}" != "none" ] && [ "${cryptpass}" != "-" ] && continue + umask 0022 - sshcs_cryptpart_process - done < "${etc_crypttab}" + # Setup script used to unlock device either in console or SSH + sshcs_cryptpart_setup + if [ ! -e "${sshcs_cryptsetup_script}" ]; then + err "No encrypted device found! Skipping crypt remote unlocking." + return 0 + fi - if [ ! -e "${sshcs_cryptsetup_script}" ]; then - err "No encrypted device found! Skipping crypt remote unlocking." - return 0 - fi + # start and check network + if ! sshcs_net_start; then + err "Net interface not available! Skipping crypt remote unlocking." + # We still allow to unlock locally with timeout + sshcs_shell_run + return 0 + fi - # start and check network - if ! sshcs_net_start; then - err "Net interface not available! Skipping crypt remote unlocking." - # We still allow to unlock locally with timeout - sshcs_unlock - # stop the network if possible - sshcs_net_done - return 0 - fi - - # time to unlock (through console or dropbear) - sshcs_dropbear_unlock - # stop the network before going on in boot sequence - sshcs_net_done + # time to unlock (through console or dropbear) + sshcs_dropbear_run } diff --git a/src/hooks/ssh-cryptsetup-tools b/src/hooks/ssh-cryptsetup-tools new file mode 100644 index 0000000..8b9fb1a --- /dev/null +++ b/src/hooks/ssh-cryptsetup-tools @@ -0,0 +1,188 @@ +#!/usr/bin/ash + +. "/init_functions" + +dbg () { + [ ${sshcs_opt_debug} != 0 ] && echo "$@" +} + +sshcs_env_load() { + local sshcs_env="/etc/initcpio/sshcs_env" + + local debug_default=0 + local net_wol_default=g + local timeout_ipconfig_default=10 + local timeout_poweroff_min=120 + local use_shell_default=0 + + [ ${sshcs_env_loaded:-0} -eq 1 ] && return + [ -e "${sshcs_env}" ] && . "${sshcs_env}" + sshcs_env_loaded=1 + [ -z "${sshcs_opt_debug}" ] && sshcs_opt_debug=${debug_default} + [ -z "${sshcs_opt_net_wol}" ] && sshcs_opt_net_wol=${net_wol_default} + [ -z "${sshcs_opt_timeout_ipconfig}" ] && sshcs_opt_timeout_ipconfig=${timeout_ipconfig_default} + [ -n "${sshcs_opt_listen}" ] && sshcs_opt_listen="-p ${sshcs_opt_listen}" + [ -z "${sshcs_opt_timeout_poweroff}" ] && sshcs_opt_timeout_poweroff=${timeout_poweroff_min} + [ -z "${sshcs_opt_use_shell}" ] && sshcs_opt_use_shell=${use_shell_default} + [ ${sshcs_opt_timeout_poweroff} -ge 0 ] && [ ${sshcs_opt_timeout_poweroff} -lt ${timeout_poweroff_min} ] && sshcs_opt_timeout_poweroff=${timeout_poweroff_min} + + sshcs_cryptsetup_script="/.sshcs_cryptsetup_script.sh" +} + +proc_parse_stat() { + local unused + + pid=$1 + cmd= + ppid= + [ ! -e /proc/${pid}/stat ] && return 1 + read unused cmd unused ppid unused < /proc/${pid}/stat +} + +proc_find_parent_cmd() { + pid=$1 + proc_parse_stat ${pid} || return 1 + while [ ${ppid} -gt 1 ] + do + proc_parse_stat ${ppid} || return 1 + if [ "${cmd}" == "($2)" ]; then + ppid=${pid} + pid=$1 + return 0 + fi + pid=${ppid} + done + + return 1 +} + +proc_is_console() { + if [ -z "${is_console:-}" ]; then + # We are in console if we were not forked from dropbear. + is_console=1 + proc_find_parent_cmd $$ "dropbear" && is_console=0 + fi + + [ "${is_console}" -eq 1 ] +} + +sshcs_cleanup() { + local pgrep_output + + # Reminders: + # We are called when devices have been unlocked. + # + # We are only called as part of the main shell (whether on console - the init + # script - or through SSH - as the user login shell), and once we return our + # parent shell will end. + # + # The unlocking script does 'killall cryptsetup' after each successful unlock, + # which is used to wakeup any other running unlocking script. + # Thus as long as all devices have been unlocked, we don't expect any script + # to be stuck on 'cryptsetup' calls. + + if proc_is_console; then + # We are in the console. + # It is time to properly end the processes we started and files we created. + # When using a shell - instead of directly calling the unlocking script - + # we also need to signal any SSH shell so that it is properly cleaned too. + + # cleanup dropbear + if [ -f "${path_dropbear_pid}" ]; then + msg "Stopping dropbear ..." + kill $(cat "${path_dropbear_pid}") + rm -f "${path_dropbear_pid}" + fi + + if [ ${sshcs_opt_use_shell} -eq 1 ]; then + # Find and kill all shells spawned by SSH. + # This is necessary to properly terminate both these shells and the spawned + # SSH processes. + # Reminder: "... | ..." does fork a subshell + dbg "Searching SSH shells ..." + pgrep_output=$(pgrep /usr/bin/ash) + pgrep_output=$(echo "${pgrep_output}" | grep -E -v "^(|1|$$)$") + echo "${pgrep_output}" | while read pid; do + proc_parse_stat ${pid} || continue + proc_find_parent_cmd ${pid} "dropbear" || continue + dbg "Killing SSH shell pid=${pid}" + kill -SIGHUP ${pid} + done + fi + + # cleanup /dev/pts if necessary + if [ ${dev_pts_mounted} -ne 0 ]; then + umount "/dev/pts" + rm -R "/dev/pts" + fi + + # stop the network before going on in boot sequence + sshcs_net_done + + rm -f "${sshcs_cryptsetup_script}" "${sshcs_shell_script}" "/etc/passwd" "/etc/shells" "/var/log/lastlog" + elif [ ${sshcs_opt_use_shell} -eq 1 ]; then + # We are in a SSH shell session. + + # Find and kill console shell. + # This is necessary so that our script launched from the init process can + # finally check that devices have been properly unlocked, properly end and + # let the init process end booting. + # Note: as a side effect, this will also kill the shell that was forked to + # trigger a timeout, which is fine (we want to kill it, either from here or + # from the init shell). + dbg "Searching console shells ..." + pgrep_output=$(pgrep /usr/bin/ash) + pgrep_output=$(echo "${pgrep_output}" | grep -E -v "^(|1|$$)$") + echo "${pgrep_output}" | while read pid; do + proc_find_parent_cmd ${pid} "dropbear" && continue + dbg "Killing console shell pid=${pid}" + kill -SIGHUP ${pid} + done + fi + # else: when in SSH shell script, we have nothing else to do other than exit. +} + +sshcs_check_done() { + # Whether we are called from the main script: that is whether the shell will + # end when we return. + local finalize=$1 + + # This is always the main script when not using shell + [ ${sshcs_opt_use_shell} -eq 0 ] && finalize=1 + + # Reset timeout when applicable: only possible from init script. + proc_is_console && type sshcs_untrap_timeout > /dev/null 2>&1 && sshcs_untrap_timeout + + # Check devices are unlocked. + sshcs_unlocked_test=1 + sshcs_unlocked=1 + . "${sshcs_cryptsetup_script}" + if [ ${sshcs_unlocked} -ne 1 ]; then + echo "" + # When finalizing in console, power off + if [ ${finalize} -eq 1 ] && proc_is_console; then + err "Devices are still locked! Powering off." + poweroff -f + fi + + # When in shell or SSH, let user try again + err "Devices are still locked! Please retry." + return + fi + + if [ ${finalize} -eq 0 ]; then + # Kill our parent (the interactive shell); the script that launched it will + # do finalization. + proc_parse_stat $$ + kill -SIGHUP ${ppid} + exit 0 + fi + + echo "" + echo "cryptsetup succeeded! Boot sequence should go on." + proc_is_console || echo "Please wait and reconnect to nominal SSH service." + + sshcs_cleanup +} + +sshcs_env_load diff --git a/src/install/ssh-cryptsetup b/src/install/ssh-cryptsetup index dd32577..85bbe3c 100644 --- a/src/install/ssh-cryptsetup +++ b/src/install/ssh-cryptsetup @@ -1,109 +1,113 @@ #!/bin/bash sshcs_check_nonempty() { - local filepath="$1" + local filepath="$1" - [ -e "${filepath}" ] && grep -q -v '^\s*\(#\|$\)' "${filepath}" + [ -e "${filepath}" ] && grep -q -v '^\s*\(#\|$\)' "${filepath}" } sshcs_check_keys() { - local dropbear_keyfile - local openssh_keyfile - local fingerprint + local dropbear_keyfile + local openssh_keyfile + local fingerprint - for keytype in "${dropbear_key_types[@]}"; do - dropbear_keyfile=${dropbear_keyfile_prefix}${keytype}${dropbear_keyfile_suffix} - openssh_keyfile=${openssh_keyfile_prefix}${keytype}${openssh_keyfile_suffix} + for keytype in "${dropbear_key_types[@]}"; do + dropbear_keyfile=${dropbear_keyfile_prefix}${keytype}${dropbear_keyfile_suffix} + openssh_keyfile=${openssh_keyfile_prefix}${keytype}${openssh_keyfile_suffix} - # Prefer OpenSSH keys, or generate missing ones - if [ -e "${openssh_keyfile}" ]; then - #echo "Copying OpenSSH ${keytype} host key for dropbear ..." - dropbearconvert openssh dropbear "${openssh_keyfile}" "${dropbear_keyfile}" > /dev/null 2>&1 - elif [ ! -e "${dropbear_keyfile}" ]; then - #echo "Generating ${keytype} host key for dropbear ..." - dropbearkey -t "${keytype}" -f "${dropbear_keyfile}" > /dev/null 2>&1 - fi - fingerprint=$(dropbearkey -y -f "${dropbear_keyfile}" | sed -n '/^Fingerprint:/ {s/Fingerprint: *//; p}') - echo "$(basename "${dropbear_keyfile}") : ${fingerprint}" - done + # Prefer OpenSSH keys, or generate missing ones + if [ -e "${openssh_keyfile}" ]; then + #echo "Copying OpenSSH ${keytype} host key for dropbear ..." + dropbearconvert openssh dropbear "${openssh_keyfile}" "${dropbear_keyfile}" > /dev/null 2>&1 + elif [ ! -e "${dropbear_keyfile}" ]; then + #echo "Generating ${keytype} host key for dropbear ..." + dropbearkey -t "${keytype}" -f "${dropbear_keyfile}" > /dev/null 2>&1 + fi + fingerprint=$(dropbearkey -y -f "${dropbear_keyfile}" | sed -n '/^Fingerprint:/ {s/Fingerprint: *//; p}') + echo "$(basename "${dropbear_keyfile}") : ${fingerprint}" + done } build() { - local etc_crypttab="/etc/crypttab" - local dropbear_authorized_keys="/etc/dropbear/initrd.authorized_keys" - local sshcs_env="/etc/initcpio/sshcs_env" - local dropbear_key_types=( "rsa" "ecdsa" "ed25519" ) - local dropbear_keyfile_prefix="/etc/dropbear/dropbear_" - local dropbear_keyfile_suffix="_host_key" - local openssh_keyfile_prefix="/etc/ssh/ssh_host_" - local openssh_keyfile_suffix="_key" + local etc_crypttab="/etc/crypttab" + local dropbear_authorized_keys="/etc/dropbear/initrd.authorized_keys" + local sshcs_env="/etc/initcpio/sshcs_env" + local dropbear_key_types=( "rsa" "ecdsa" "ed25519" ) + local dropbear_keyfile_prefix="/etc/dropbear/dropbear_" + local dropbear_keyfile_suffix="_host_key" + local openssh_keyfile_prefix="/etc/ssh/ssh_host_" + local openssh_keyfile_suffix="_key" - # Check we are needed - if ! sshcs_check_nonempty "${dropbear_authorized_keys}"; then - echo "There is no root key(s) in ${dropbear_authorized_keys}. Skipping." - return 0 - fi - if ! sshcs_check_nonempty "${etc_crypttab}"; then - echo "There is no device in ${etc_crypttab}. Skipping." - return 0 - fi + # Check we are needed + if ! sshcs_check_nonempty "${dropbear_authorized_keys}"; then + echo "There is no root key(s) in ${dropbear_authorized_keys}. Skipping." + return 0 + fi + if ! sshcs_check_nonempty "${etc_crypttab}"; then + echo "There is no device in ${etc_crypttab}. Skipping." + return 0 + fi - umask 0022 + umask 0022 - sshcs_check_keys + sshcs_check_keys - add_checked_modules "/drivers/net/" - # Note: parts of this script (modules/binaries added) are the same than the - # 'encrypt' install script (/usr/lib/initcpio/install/encrypt) which is the - # nominal one to deal with encrypted volumes at boot time. - add_module dm-crypt - # Note: crypto modules are necessary - if [ -n "${CRYPTO_MODULES}" ]; then - local mod - for mod in ${CRYPTO_MODULES}; do - add_module "${mod}" - done - else - add_all_modules "/crypto/" - fi - - # Note: dmsetup is necessary for device mapper features - add_binary "cryptsetup" - add_binary "dmsetup" - add_binary "dropbear" - add_binary "ip" - add_binary "/usr/lib/initcpio/ipconfig" "/sbin/ipconfig" - - # Our hook files - [ -e "${sshcs_env}" ] && add_file "${sshcs_env}" - - # auth-related files - add_file "/lib/libnss_files.so" - - # SSH-related files - add_file "${dropbear_authorized_keys}" "/root/.ssh/authorized_keys" - for keytype in "${dropbear_key_types[@]}"; do - add_file "${dropbear_keyfile_prefix}${keytype}${dropbear_keyfile_suffix}" + add_checked_modules "/drivers/net/" + # Note: parts of this script (modules/binaries added) are the same than the + # 'encrypt' install script (/usr/lib/initcpio/install/encrypt) which is the + # nominal one to deal with encrypted volumes at boot time. + add_module dm-crypt + # Note: crypto modules are necessary + if [ -n "${CRYPTO_MODULES}" ]; then + local mod + for mod in ${CRYPTO_MODULES}; do + add_module "${mod}" done + else + add_all_modules "/crypto/" + fi - # cryptsetup-related files - add_file "${etc_crypttab}" - add_file "/usr/lib/udev/rules.d/10-dm.rules" - add_file "/usr/lib/udev/rules.d/13-dm-disk.rules" - add_file "/usr/lib/udev/rules.d/95-dm-notify.rules" - add_file "/usr/lib/initcpio/udev/11-dm-initramfs.rules" "/usr/lib/udev/rules.d/11-dm-initramfs.rules" + # Note: dmsetup is necessary for device mapper features + add_binary "cryptsetup" + add_binary "dmsetup" + add_binary "dropbear" + add_binary "ip" + add_binary "/usr/lib/initcpio/ipconfig" "/bin/ipconfig" + add_binary "ethtool" - # At least with LUKS v2 volumes, cryptsetup calls pthread_cancel(), which - # dlopen()s libgcc_s.so.1. - # See the nominal 'encrypt' module, and similar/related bug reports (e.g. - # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=950254). - add_binary "/usr/lib/libgcc_s.so.1" + # Our hook files + [ -e "${sshcs_env}" ] && add_file "${sshcs_env}" + # Note: use /usr/local/bin, even though everything actually points to /usr/bin + # in initramfs. + add_file "/usr/lib/initcpio/hooks/ssh-cryptsetup-tools" "/usr/local/bin/ssh-cryptsetup-tools" - add_runscript + # auth-related files + add_file "/lib/libnss_files.so" + + # SSH-related files + add_file "${dropbear_authorized_keys}" "/root/.ssh/authorized_keys" + for keytype in "${dropbear_key_types[@]}"; do + add_file "${dropbear_keyfile_prefix}${keytype}${dropbear_keyfile_suffix}" + done + + # cryptsetup-related files + add_file "${etc_crypttab}" + add_file "/usr/lib/udev/rules.d/10-dm.rules" + add_file "/usr/lib/udev/rules.d/13-dm-disk.rules" + add_file "/usr/lib/udev/rules.d/95-dm-notify.rules" + add_file "/usr/lib/initcpio/udev/11-dm-initramfs.rules" "/usr/lib/udev/rules.d/11-dm-initramfs.rules" + + # At least with LUKS v2 volumes, cryptsetup calls pthread_cancel(), which + # dlopen()s libgcc_s.so.1. + # See the nominal 'encrypt' module, and similar/related bug reports (e.g. + # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=950254). + add_binary "/usr/lib/libgcc_s.so.1" + + add_runscript } help() { - cat <