Automating LUKS Decryption with KeePassXC Secret Service
Managing encrypted external drives often requires a trade-off between security and convenience. By combining udisksctl with the Freedesktop Secret Service API, you can securely store your drive passphrases inside a KeePassXC database and allow local scripts to handle decryption instantly without manual typing.
🔑 Prerequisites
Before the script can look up keys, your password manager must be configured to act as a system keyring daemon.
🛠️ Step 1: Enable Secret Service Integration
Open KeePassXC and navigate to Tools > Settings > General > Secret Service Integration. Check the box for Enable KeePassXC Freedesktop Secret Service integration. If you want to isolate your infrastructure keys, you can optionally choose to expose only a specific database group.
🔍 Step 2: Extract the Drive UUID
Because traditional Linux device letters (like /dev/sdb) can shift depending on the order devices are connected, you must target the drive using its unique, immutable hardware UUID. Run the following command in your terminal to fetch it:
lsblk -dno UUID /dev/sdb
📝 Step 3: Configure the Database Entry
Create a new entry inside your exposed KeePassXC group. Set the Title of the entry to be the exact UUID string returned by the lsblk command above. Place your raw LUKS encryption passphrase in the Password field and save your database.
💻 The Complete Deployment Script
Save the following code block to /home/simon/.local/bin/usb-unlock.sh and ensure it is marked as executable (chmod +x). It features a hybrid execution flow: passing a direct block device path skips straight to background decryption (ideal for bar scripts), while invoking it with list spawns an interactive visual picker.
#!/bin/bash
set -e
ACTION=$1 # list, unlock, mount, unmount, poweroff
DEV=$2 # e.g., /dev/sdb
MAPPER=$3 # e.g., /dev/dm-1
case "$ACTION" in
list)
devices=()
while read -r line; do
NAME="" FSTYPE="" SIZE="" MODEL=""
eval "$line"
if [ "$FSTYPE" = "crypto_LUKS" ]; then
MAP_COUNT=$(lsblk -pno NAME "$NAME" | wc -l)
if [ "$MAP_COUNT" -eq 1 ]; then
# State 1: Completely locked
devices+=("$NAME" "$SIZE" "[Locked] ${MODEL:-Raw LUKS Drive}")
else
# State 2: Unlocked, but check if it's missing a mountpoint
CHILD_MAPPER=$(lsblk -pnro NAME "$NAME" | tail -n 1)
if [ -z "$(lsblk -dno MOUNTPOINTS "$CHILD_MAPPER")" ]; then
devices+=("$NAME" "$SIZE" "[Unlocked] ${MODEL:-Raw LUKS Drive}")
fi
fi
fi
done < <(lsblk -Pp -o NAME,FSTYPE,SIZE,MODEL)
if [ ${#devices[@]} -eq 0 ]; then
zenity --info --title="Unlock Drive" --text="No action required. All encrypted drives are either locked or already mounted." --width=400 2>/dev/null
exit 0
fi
CHOICE=$(zenity --list --title="Select Encrypted Drive" \
--text="Select a drive to process:" \
--column="Device" --column="Size" --column="Status / Model" \
--width=550 --height=350 \
"${devices[@]}" 2>/dev/null || true)
if [ -n "$CHOICE" ]; then
"$0" unlock "$CHOICE"
fi
;;
unlock)
MAP_COUNT=$(lsblk -pno NAME "$DEV" | wc -l)
# If already unlocked, bypass passphrase step and just mount it
if [ "$MAP_COUNT" -gt 1 ]; then
CHILD_MAPPER=$(lsblk -pnro NAME "$DEV" | tail -n 1)
if [ -z "$(lsblk -dno MOUNTPOINTS "$CHILD_MAPPER")" ]; then
udisksctl mount -b "$CHILD_MAPPER"
fi
else
# Truly locked -> pull password and decrypt
UUID=$(lsblk -dno UUID "$DEV")
PASS=""
if command -v secret-tool &>/dev/null; then
PASS=$(secret-tool lookup Title "$UUID" 2>/dev/null)
fi
if [ -z "$PASS" ]; then
PASS=$(kdialog --title "Unlock Drive" --password "KeePassXC lookup failed. Enter passphrase for $DEV" 2>/dev/null)
fi
if [ -n "$PASS" ]; then
if udisksctl unlock -b "$DEV" --key-file=<(printf "%s" "$PASS"); then
sleep 0.4
CHILD_MAPPER=$(lsblk -pnro NAME "$DEV" | tail -n 1)
udisksctl mount -b "$CHILD_MAPPER"
fi
fi
fi
;;
mount)
TARGET="${MAPPER:-$DEV}"
udisksctl mount -b "$TARGET"
;;
unmount)
TARGET="${MAPPER:-$DEV}"
udisksctl unmount -b "$TARGET"
;;
poweroff)
if [ -n "$MAPPER" ]; then
udisksctl unmount -b "$MAPPER" 2>/dev/null || true
else
udisksctl unmount -b "$DEV" 2>/dev/null || true
fi
udisksctl power-off -b "$DEV"
;;
esac
Posted
11:47 AM May 25, 2026