Automated LUKS Unlock on OpenMediaVault 8 via LAN Key Service

debian openmediavault security

This post is a companion to the full disk encryption setup on OpenMediaVault 8. The previous step introduced LUKS-based encryption for both the operating system and data layers. The remaining usability problem was boot-time unlocking without requiring manual passphrase entry on every reboot.

To solve this, I implemented a network-based key retrieval mechanism inspired by Aterfax: link

Overview

At a high level, the system retrieves the LUKS decryption key during early boot from a trusted web service running on the local network. If the key is successfully retrieved, the root filesystem is unlocked automatically and the system continues booting normally.

If the system is not able to reach the local network service, boot stops at the LUKS prompt, effectively preserving the physical security model.

How it works

The implementation modifies the early boot process (initramfs stage) to include a small unlock routine.

During boot:

Installation Script

The implementation in my script can be found here: link

Key service dependency

The system depends on a trusted service running inside the local network that serves the decryption key over HTTPS. The key is stored encrypted on the webserver and is only decrypted as part of the retrieval process during boot.

The service is assumed to be reachable only when the machine is physically inside the trusted environment. In this setup, the host is assigned a stable address using a DHCP reservation to ensure predictable reachability during early boot.

The decryption key is also stored locally under /boot. This means an attacker with physical access to the disk would be able to retrieve it in cleartext. This is an accepted tradeoff in this design, since the primary security boundary is the ability to boot and access the system without being on the trusted network rather than protecting against offline disk inspection in isolation.

Fallback behavior

If automatic key retrieval fails, boot does not permanently fail. The system falls back to standard LUKS unlock options. The operator can unlock the system manually either via the local console or over SSH once networking is available.

This preserves recoverability in cases where the key service is unavailable while maintaining the primary automated path for normal operation.

Threat model

The security assumption remains focused on physical theft.

If the machine is powered on outside of the trusted network, or removed from the LAN entirely, it will not be able to retrieve the LUKS key. In that state, the system remains encrypted and unusable.

This does not protect against an attacker who already has access to the unlocked system or the internal network where the key service is reachable.

Design tradeoff

This approach intentionally trades strict boot-time security for operational convenience in a home lab environment.

Full disk encryption is preserved, but unattended boot becomes possible only within a controlled network boundary.


#!/bin/bash
set -e

################ Configuration ################

MAPPER_NAME=tardis
# update url  /bin/luksunlockhttps and update-initramfs -u -k all
WEBSERVERURL="https://..."
HTTPS_VERIFY=true
SSH_PUBKEY="ssh-ed25519..."
DROPBEAR_PORT=2222
NETWORK_MODE=dhcp        # dhcp or static
NETWORK_INTERFACE=enp3s0
NETWORK_IP=              # static only
NETWORK_NETMASK=         # static only
NETWORK_GATEWAY=         # static only, leave empty for no gateway
NETWORK_DNS=             # static only, leave empty for no dns

################ No changes below this line ################

# Validate config
if [ -z "$SSH_PUBKEY" ] || [ "$SSH_PUBKEY" = "ssh-rsa AAAA..." ]; then
    echo "Error: SSH_PUBKEY not set. Aborting."
    exit 1
fi

if [ -z "$WEBSERVERURL" ]; then
    echo "Error: WEBSERVERURL not set. Aborting."
    exit 1
fi

if [ "$NETWORK_MODE" = "static" ]; then
    if [ -z "$NETWORK_IP" ] || [ -z "$NETWORK_NETMASK" ] || [ -z "$NETWORK_INTERFACE" ]; then
        echo "Error: Static network mode requires NETWORK_IP, NETWORK_NETMASK and NETWORK_INTERFACE. Aborting."
        exit 1
    fi
fi

echo "=== Configuring dropbear authorized_keys ==="
mkdir -p /etc/dropbear/initramfs
echo "$SSH_PUBKEY" > /etc/dropbear/initramfs/authorized_keys
chmod 600 /etc/dropbear/initramfs/authorized_keys

echo "=== Installing dependencies ==="
apt install -y dropbear-initramfs curl openssl uuid pwgen

echo "=== Configuring dropbear options ==="
cat > /etc/dropbear/initramfs/dropbear.conf << EOF
DROPBEAR_OPTIONS="-p ${DROPBEAR_PORT} -s -j -k -I 60 -E"
EOF

echo "=== Configuring initramfs networking ==="
if [ "$NETWORK_MODE" = "dhcp" ]; then
    cat >> /etc/initramfs-tools/initramfs.conf << EOF

DEVICE=${NETWORK_INTERFACE}
IP=dhcp
EOF
else
    HOSTNAME=$(hostname)
    IP_PARAM="${NETWORK_IP}::${NETWORK_GATEWAY}:${NETWORK_NETMASK}:${HOSTNAME}:${NETWORK_INTERFACE}:off"
    if [ -n "$NETWORK_DNS" ]; then
        IP_PARAM="${IP_PARAM}:${NETWORK_DNS}"
    fi
    cat >> /etc/initramfs-tools/initramfs.conf << EOF

DEVICE=${NETWORK_INTERFACE}
IP=${IP_PARAM}
EOF
fi

echo "=== Generating LUKS keyfile ==="
WORKDIR="/tmp/lukskeys"
mkdir -p ${WORKDIR}
chown root:root ${WORKDIR}
chmod 700 ${WORKDIR}
cd ${WORKDIR}

UUID=$(uuid)
PW=$(pwgen -Bcnsy 256 -1 | tr -dc 'A-Za-z0-9-_@#$%^&*()')

touch ${UUID}.lek ${UUID}.lek.enc
chmod 600 ${UUID}.lek ${UUID}.lek.enc

dd if=/dev/urandom bs=1 count=256 > ${UUID}.lek
openssl enc -aes-256-cbc -pbkdf2 -salt -a -in ${UUID}.lek -out ${UUID}.lek.enc -pass pass:"${PW}"

echo "=== Adding keyfile to LUKS ==="
LUKS_DEVICE=$(cryptsetup status ${MAPPER_NAME} | awk '/device:/{print $2}')
if [ -z "$LUKS_DEVICE" ]; then
    echo "Error: Could not find LUKS device for mapper ${MAPPER_NAME}. Is it unlocked and running?"
    exit 1
fi
echo "LUKS device: ${LUKS_DEVICE}"
cryptsetup luksAddKey ${LUKS_DEVICE} ${WORKDIR}/${UUID}.lek

echo "=== Creating luksunlockhttps script ==="
if [ "$HTTPS_VERIFY" = "false" ]; then
    CURL_OPTS="-fsS -k --retry-connrefused --retry 5"
else
    CURL_OPTS="-fsS --retry-connrefused --retry 5"
fi

cat > /bin/luksunlockhttps << EOF
#!/bin/sh -e

if [ \$CRYPTTAB_TRIED -eq "0" ]; then
    sleep 10
fi

DECRYPT_PASSWORD='${PW}'
UUID='${UUID}'
WEBSERVERURL='${WEBSERVERURL}'
ENCKEYFILENAME="\${UUID}.lek.enc"

touch /run/luks.key.enc
chmod 700 /run/luks.key.enc

if curl ${CURL_OPTS} \${WEBSERVERURL}/\${ENCKEYFILENAME} -o /run/luks.key.enc; then
    touch /run/luks.key
    chmod 700 /run/luks.key
    openssl enc -d -aes-256-cbc -pbkdf2 -a -in /run/luks.key.enc -out /run/luks.key -pass pass:"\${DECRYPT_PASSWORD}" >/dev/null 2>&1
    cat /run/luks.key
    rm /run/luks.key /run/luks.key.enc
    exit
fi

/lib/cryptsetup/askpass "Enter password and press ENTER: "
EOF

chmod 700 /bin/luksunlockhttps

echo "=== Installing initramfs hooks ==="
cat > /etc/initramfs-tools/hooks/luksunlockhttps << 'HOOK'
#!/bin/sh
PREREQ=""
prereqs() { echo "$PREREQ"; }
case "$1" in
    prereqs) prereqs; exit 0 ;;
esac

. /usr/share/initramfs-tools/hook-functions

copy_exec /bin/luksunlockhttps /bin/luksunlockhttps
copy_exec /usr/bin/curl /bin/curl
copy_exec /usr/bin/openssl /bin/openssl
copy_exec /bin/chmod /bin/chmod
copy_exec /bin/cat /bin/cat
copy_exec /bin/rm /bin/rm
HOOK

chmod +x /etc/initramfs-tools/hooks/luksunlockhttps

echo "=== Updating crypttab ==="
LUKS_UUID=$(blkid -s UUID -o value ${LUKS_DEVICE})
sed -i "s|${MAPPER_NAME} UUID=${LUKS_UUID} none luks$|${MAPPER_NAME} UUID=${LUKS_UUID} none luks,keyscript=/bin/luksunlockhttps|" /etc/crypttab
cat /etc/crypttab

echo "=== Updating initramfs ==="
update-initramfs -u -k all

echo "=== Done ==="
echo
echo "Copy the encrypted key file to your webserver:"
echo "  File: ${WORKDIR}/${UUID}.lek.enc"
echo
echo "  contents for copy/paste:"
cat ${WORKDIR}/${UUID}.lek.enc
echo
echo "  or rsync directly:"
echo "  rsync ${WORKDIR}/${UUID}.lek.enc user@keyserver:/path/to/webroot/"
echo
echo "Reboot to test. Dropbear listening on port ${DROPBEAR_PORT} as fallback."
echo "If HTTPS unlock fails, SSH in with: ssh -p ${DROPBEAR_PORT} root@<ip>"
echo "Then run: cryptroot-unlock"