#!/bin/bash
# start-up a remote machine which has cryptroot and dropbear, for more info run with -h
VERSION="0.9 [12 Sep 2025]"
THIS=$(basename "$0")
COLUMNS=$(stty size 2>/dev/null||echo 80); COLUMNS=${COLUMNS##* }
REMOTEIP=192.168.100.143
PORT=22
HIDEPWD="-s"
set -o pipefail

# process command line options
while getopts ":dfhi:lp:s:tvw" optname; do
    case "$optname" in
		"d")	DEBUG="-d";;
		"h")	HELP="y";;
		"i")	IDFILE="$OPTARG"; IDOPT="-i";;
		"l")	CHANGELOG="y";;
		"p")	PORT=$OPTARG;;
		"s")	TEST="y"; STATUSFILE="$OPTARG";;
		"t")	TEST="y";;
		"v")	unset HIDEPWD;;
		"w")	COLUMNS=30000;;
		"?")	echo "Unknown option $OPTARG">&2; exit 1;;
		":")	echo "No argument value for option $OPTARG">&2; exit 1;;
		*)	# Should not occur
			echo "Unknown error while processing options">&2; exit 1;;
    esac
done
shift $((OPTIND-1))

[[ -z $TEST ]] && echo -e "\n$THIS v$VERSION - by Dominic (-h for help)\n${THIS//?/=}\n"
# show help and/or changelog
[[ -z $1 && -z $CHANGELOG ]] && HELP="y"
if [ -n "$HELP" ]; then
	echo -e "This utility provides an easy way to enter the decrypt passphrase on a remote machine which has root encryption with dm-crypt+LUKS (e.g. as set up at \
Debian or Ubuntu installation if you select 'encrypted LVM') - so that local access is not required when booting the machine.

When the encrypted machine boots it loads an initramfs image from a small unencrypted boot partition and then waits for the encryption \
passphrase, without which it cannot read the main partition. If it has previously been suitably modified (see below) \
you can reach it remotely (by ssh) at this boot stage and enter the passphrase, which allows booting to complete.

From a remote client you can use $THIS to enter the passphrase, provided your public key (i.e. matching the private key \
used for ssh connection by $THIS) has previously been added to the encrypted machine's /etc/dropbear/initramfs/authorized_keys file \
and its initramfs has then been updated (see below). Please note this means there are two requirements for ability to remote boot the encrypted \
machine: you must know the passphrase *and* your public key must be pre-loaded on the encrypted machine.

You can also use $THIS in test mode (-t) in a cron job to monitor the encrypted machine and warn you if it ceases to be fully available: \
if all is well then running $THIS -t generates no text output, otherwise it will show an appropriate message.

Usage: ./$THIS [options] ip.address.of.remote.encrypted.machine

Options   : -d      - debug mode (implementation may vary)
            -h      - show this help and exit
            -i file - specify private key identity file (default: selected automatically by ssh)
            -l      - show changelog and exit
            -p n    - where 'n' is the ssh port used by dropbear in initramfs on the encrypted machine (default: 22)
            -s file - test status of remote machine and output text if status has changed since the preceding run of '$THIS -s' to the specified 'file'
            -t      - test status of remote machine and exit with code - silent if running normally
            -v      - show passphrase on console as you enter it

Exit Codes: 0 - remote machine is running normally
            1 - some error occurred or remote machine is off/unresponsive
            2 - remote machine is still awaiting passphrase

Prerequisites: \
$THIS is designed for a remote machine that has dm-crypt + LUKS on the root system so that it cannot be started up without the pre-set passphrase being entered. \
(The process of setting up a machine for dm-crypt + LUKS is not covered here, but it can most easily be done on Debian or Ubuntu if you use the alternate \
or netboot installer [file: mini.iso] and select 'Guided - use entire disk and set up encrypted LVM'.) By default, booting such an encrypted machine requires local \
access in order to enter the passphrase, but remote access at this stage is possible by setting up the encrypted machine thus (tested under Ubuntu 22.04; for 20.04 and earlier use /etc/dropbear-initramfs/authorized_keys instead of /etc/dropbear/initramfs/authorized_keys):
    sudo -i # become root (if not already)
    apt-get install dropbear-initramfs # check/install necessary software
    # add public keys for remote users who could run $THIS here, one per line:
    nano /etc/dropbear/initramfs/authorized_keys
    sed -i -e '\$a\' /etc/dropbear/initramfs/authorized_keys # ensure file ends with EOL
    update-initramfs -u -k all # update boot-time filesystem
    hostname -I # note the ip address, please ensure it won't change on reboot
Dependencies: bash grep sed ssh

Notes: If you do not know the passphrase, or if you do not have a private key \
that matches a public key previously set up for the encrypted machine's initramfs, then $THIS cannot help you; \
you can get remote access only to the initial boot stage of the encrypted machine and it will be impossible to access the main system or data. \
If you have the passphrase but not a suitable private key, you will require local access to the encrypted machine in order to start it up fully.

More information about remote booting with dmcrypt + LUKS can be found at:
https://web.archive.org/web/20250710180620/http://blog.neutrino.es/2011/unlocking-a-luks-encrypted-root-partition-remotely-via-ssh/
https://web.archive.org/web/20170508133453/https://www.adfinis-sygroup.ch/blog/en/decrypt-luks-devices-remotely-via-dropbear-ssh/

For a tool for converting an existing unencrypted partition to dm-crypt+LUKS (must be offline) see:
http://johndoe31415.github.io/luksipc

You can test a passphrase on an *already-mounted* dm-crypt + LUKS partition thus (a non-zero \
exit code indicates a wrong passphrase):
    cryptsetup open \$(blkid|grep crypto_LUKS|cut -d: -f1|head -n1) x --test-passphrase --tries 1; echo \$?
Depending on the remote machine's network configuration and resulting state \
(a) during booting and \
(b) after booting has completed, these two states may have different ips or \
accept connections from different ports; \
if so, after you have successfully entered the passphrase $THIS will report \
that it was unable to connect and ask you to check if the remote machine is \
switched on - the remote machine may be working fine but with \
a different ip address or port. So try to ensure that the same ip address \
is allocated when booting (see below) as when fully booted (e.g. per \
/etc/network/interfaces), and is allocated in the same way.

To specify ip parameters at boot time (i.e. running from initramfs) set 'ip=' in variable GRUB_CMDLINE_LINUX in \
/etc/default/grub and then run update-grub. The parameters are ip=client-ip:[server-ip]:gateway-ip:netmask:[hostname]:device:autoconf \
- for more info see https://www.kernel.org/doc/Documentation/filesystems/nfs/nfsroot.txt. \
Never specify server-ip; and do \
not specify a hostname because $THIS depends on the hostname during booting being '(none)'. Examples:
    GRUB_CMDLINE_LINUX=\"ip=:::::eth0:dhcp\"
    GRUB_CMDLINE_LINUX=\"ip=192.0.2.62::192.0.2.1:255.255.255.192::eth0:none\"
And afterwards run
    update-grub

To specify a non-standard ssh port at boot time (i.e. not 22), add a line in /etc/dropbear/initramfs/dropbear.conf (in Ubuntu 20.04 and earlier: /etc/dropbear-initramfs/config) like
    DROPBEAR_OPTIONS="-p 4748"
And then update initramfs
    update-initramfs -u -k all

Cryptroot-unlock: Whereas $THIS runs under bash on the client machine, \
cryptroot-unlock resides by default in the \
initramfs package on the encrypted machine and can be run using SSH \
from a client with any OS. cryptroot-unlock has the same prerequisites \
(see above) as $THIS. Subject to that, here are \
examples of how, instead of using $THIS, cryptroot-unlock can \
be run from a remote client logging in with SSH:

One-line example to run remotely (i.e. on client machine) to remote machine 192.168.20.88 under Linux or Cygwin or WSL:
    ssh -t -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@192.168.20.88 cryptroot-unlock
Another one-line example but using a non-default private key file:
    ssh -ti /path/to/private_key_file -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@192.168.20.88 cryptroot-unlock
or under Windows using plink:
    plink.exe -t -i C:\path\private_key_file.ppk root@192.168.20.88 cryptroot-unlock

License: Copyright © 2025 Dominic Raferd. Licensed under the Apache License, \
Version 2.0 (the \"License\"); you may not use this file except in compliance \
with the License. You may obtain a copy of the License at \
https://www.apache.org/licenses/LICENSE-2.0. Unless required by applicable \
law or agreed to in writing, software distributed under the License is \
distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY \
KIND, either express or implied. See the License for the specific language \
governing permissions and limitations under the License.
"|fold -sw"$COLUMNS"
fi
if [[ -n $CHANGELOG ]]; then
	[[ -n $HELP ]] && echo "Changelog:"
	echo -e "\
0.9 [12 Sep 2025] - update links in help and make shellcheck-compliant
0.8 [14 Nov 2018] - silently correct if user supplies ip address in form name@n.n.n.n
0.7 [09 Nov 2018] - modify instructions to reference cryptroot-unlock, remove references to pass.sh
0.6 [07 Nov 2018] - correct instructions for location of authorized_keys file
0.5 [06 Nov 2018] - bugfix -i option
0.4 [30 May 2017] - bugfix -t option
0.3 [20 Apr 2017] - add -s option, other fixes
0.2 [12 Apr 2017] - updated help, add -i and -t options, several other fixes
0.1 [04 Apr 2017] - initial version
"|fold -sw"$COLUMNS"
fi
[[ -n $HELP$CHANGELOG ]] && exit 0

# main code starts here
REMOTEIP=$1
# if user has inadvertently provided address in the form name@n.n.n.n, strip the 'name@' part
REMOTEIP=${REMOTEIP##*@}
if [[ -n $STATUSFILE ]]; then
	[[ -f "$STATUSFILE" ]] && PRIORSTATUS="$(cat "$STATUSFILE")" || PRIORSTATUS="x"
else
	STATUSFILE=/dev/null
fi

for (( T=1; T<=30; T++ )); do
	# shellcheck disable=SC2086
	REMOTENAME=$(ssh $IDOPT "$IDFILE" -o Port="$PORT" -o ConnectTimeout=6 -o PasswordAuthentication=no -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@"$REMOTEIP" "uname -n" 2>&1|sed '/Permanently added/d;s/\r//')
	SSH_ERR=$?
	[[ -n $DEBUG ]] && echo "\$REMOTENAME: '$REMOTENAME'"
	if [[ $SSH_ERR -gt 0 || $REMOTENAME != "(none)" ]]; then
		echo "$REMOTENAME"|grep -q "Permission denied (publickey,password"
		if [[ $? -eq 0 || $SSH_ERR -eq 0 ]]; then
			# if we gained ssh access (and remote machine is not called '(none)'), or we were explictly denied, then it is ok
			if [[ $T -eq 1 ]]; then
				NEWSTATUS="Remote machine $REMOTEIP is running normally"
				# if testing mode 't', suppress message that all is ok
				[[ -n $TEST && $STATUSFILE == "/dev/null" ]] && PRIORSTATUS="$NEWSTATUS"
				[[ $NEWSTATUS != "$PRIORSTATUS" ]] && echo "$NEWSTATUS" | tee "$STATUSFILE"
			else
				echo -e "\nRemote machine $REMOTEIP has started ok"
			fi
			exit 0
		fi
		if echo "$REMOTENAME"|grep -q "Permission denied (publickey"; then
			# shellcheck disable=SC2089
			NEWSTATUS="${REMOTEIP}'s dropbear rejected your private key."
			if [[ $NEWSTATUS != "$PRIORSTATUS" ]]; then
				echo "$NEWSTATUS" | tee "$STATUSFILE"
				[[ -z $TEST ]] && echo -e "On $REMOTEIP did you previously:\n - install the matching public key at /etc/dropbear/initramfs/authorized_keys?\n - do 'update-initramfs -u -k all'?" >&2
			fi
			exit 1
		fi
		if [[ $T -gt 1 && $CANT_CONNECT_LOOP -le 2 ]]; then
			# we can get a 'can't connect' message in the switchover from dropbear to openssh, so don't worry if it happens a couple of times
			(( CANT_CONNECT_LOOP++ ))
		else
		 	NEWSTATUS="$REMOTENAME (code $SSH_ERR)"
 			if [[ $NEWSTATUS != "$PRIORSTATUS" ]]; then
 				echo "$NEWSTATUS" | tee "$STATUSFILE"
				[[ -z $TEST ]] && echo -e "  - is the address correct?\n  - is the remote machine switched on?" >&2
			fi
			exit 1
		fi
	fi
	[[ -z $TEST ]] || { echo -e "\nRemote machine is booting but requires passphrase"; exit 2; }
	if [[ -z $REPLY ]]; then
		echo "Access to remote machine running dropbear is confirmed"
		# shellcheck disable=SC2229
		read -t60 $HIDEPWD -p"Enter passphrase for remote machine $REMOTEIP, then press ENTER/RETURN: "
		[[ -n $REPLY ]] || { echo "No passphrase entered, please try again" >&2; exit 1; }
		echo -e "[done]\nSending start instruction to remote machine $REMOTEIP"
		# shellcheck disable=SC2086
		ssh $IDOPT "$IDFILE" -o Port="$PORT" -o ConnectTimeout=3 -o PasswordAuthentication=no -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@"$REMOTEIP" "printf \"$REPLY\" >/lib/cryptsetup/passfifo" 2>/dev/null
		echo -n "Please wait"
	fi
	sleep 6s
	echo -n "."
done
echo -e "\nUnable to start remote machine $REMOTEIP\nMaybe try again?" >&2; exit 2
