How to automatically decrypt a LUKS LVM setup on boot with a USB

How to automatically decrypt a LUKS LVM setup on boot with a USB cover image

Written by Filis Futsarov
on June 25 of 2025

GNU/Linux Security

This guide walks you through a robust procedure to auto-decrypt a LUKS-on-LVM setup at boot with a USB key. It assumes you already have your system set up with LUKS encryption on LVM.

The core idea is simple: keep a dedicated USB stick at home to unlock your system effortlessly, and leave it behind whenever you head out, so your machine stays securely encrypted on the go.

I extensively tested the procedure in multiple VMs and on real-world installs of Ubuntu 24.04 and Pop!_OS 22.04. Until reaching that point, I repeatedly broke my VM while trying outdated guides, AI-generated suggestions, and other unreliable sources—until I found my own way and then passed it to real setups. Here’s what I tried (and what finally worked with some customization):

This solution relies on the keyscript option supported by initramfs-tools (not by cryptsetup itself),which allows delegating the decryption process to a custom script in a higher-level and safer way than using low-level initramfs hooks — although the option is defined in /etc/crypttab and passed to the initramfs layer, cryptsetup itself only accepts raw key data.

Internally, initramfs-tools handles keyscript by executing the specified script and piping its output to cryptsetup, effectively feeding the keyfile through standard input.

Most scripts I came across failed to handle this process in a fault-tolerant way: instead of falling back to the familiar "Enter password" screen when any kind of error happens, they leave you with a broken system.

That’s why I wrote a script that does things properly. It mounts your USB, attempts to load the keyfile up to three times, and if it still fails, gracefully falls back to the standard password prompt you're used to.

Before proceeding, ⚠️ make a full disk backup using Clonezilla (not just the partition) and test this solution in a VM running the same Ubuntu version as yours. This ensures you're familiar with the process and minimizes risk — I personally broke my VM several times during testing for writing this post, but snapshots made rollback easy.

The only requirement for this solution to work is support for the keyscript parameter, which has been available in initramfs-tools since at least Ubuntu 16.04.

✅ Tested on:

These cover cryptsetup versions from 2.4.3 to 2.7.0. You can verify your version with:

cryptsetup --version

1. Creating a secure keyfile

This command creates a 4MB keyfile filled with cryptographically secure binary data (non-ASCII characters), providing high entropy per byte — making brute-force attacks significantly harder.

💡 Tip: Save the keyfile directly to the USB drive that you'll use to decrypt your setup.

dd if=/dev/urandom of=/media/usb/secure.key bs=1M count=4 iflag=fullblock

2. Downloading the unlock script

We'll need the unlock script for the keyscript parameter in the crypttab entry.

In systemd-based setups (e.g. Pop!_OS),the keyscript must be placed inside /lib/cryptsetup/scripts. In non-systemd setups (e.g. Ubuntu),it can technically reside anywhere — but placing it in /lib/cryptsetup/scripts ensures compatibility across systems.

sudo wget -O /lib/cryptsetup/scripts/keyscript.sh \
https://raw.githubusercontent.com/filisko/cryptsetup-usb-keyscript/main/src/keyscript.sh

⚠️ It's critical that the script is owned by root and has execution permissions:

sudo chown root:root /lib/cryptsetup/scripts/keyscript.sh
sudo chmod 755 /lib/cryptsetup/scripts/keyscript.sh

3. Finding your USB's UUID

We need USB's UUID to tell cryptsetup where to find the keyfile.

sudo blkid

There will be many lines like the following one. We're interested in the UUID="81D6-413D" part.

/dev/sda1: UUID="81D6-413D" BLOCK_SIZE="512" TYPE="vfat" PARTUUID="c371356e-01"

3. Updating /etc/crypttab entries

To enable automatic decryption, we need to edit /etc/crypttab and provide:

This tells the system where to find the keyfile and how to decrypt at boot.

sudo nano /etc/crypttab

It’s very likely that your system has an entry like this by default (note luks):

dm_crypt-0 UUID=XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX none luks

I suggest that you save it somewhere or comment it putting # at the beginning of the line.

The first thing that you see in the line is LUKS' device name that we will also need later to add the keyfile inside, so also save it. Common names are:

⚠️ It's crucial to preserve the original device name (e.g.: dm_crypt-0),UUID, and exact field spacing to avoid boot issues.

Replace (not add, duplicate UUIDs or device names ºare not allowed) your entry to match the changes of this entry:

dm_crypt-0 UUID=XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX /dev/disk/by-uuid/81D6-413D:/secure.key luks,discard,cipher=aes-xts-plain64,size=256,hash=sha1,keyscript=/lib/cryptsetup/scripts/keyscript.sh,tries=4

⚠️ The tries=4 option is mandatory — it ensures the system runs the script 3 times and gives it a chance to gracefully fall back on the 4th try. That 4th attempt is where the script prompts the user to manually enter the password, in case:

Double-check the USB UUID and keyscript path, especially if you changed the script location.

4. Get LUKS' device path

With LUKS' device name (e.g.: dm_crypt-0) from the crypttab entry in previous step, we will get its device path to add the keyfile into it.

sudo cryptsetup status dm_crypt-0

The line that we need from the output is: device: /dev/nvme0n1p3 where /dev/nvme0n1p3 is the device path that we need.

/dev/mapper/dm_crypt-0 is active and is in use.
type: LUKS2
cipher: aes-xts-plain64
keysize: 512 bits
key location: keyring
device: /dev/nvme0n1p3
sector size: 512
offset: 32768 sectors
size: 1993975808 sectors
mode: read/write

5. Add the keyfile to LUKS device

Here we need to set LUKS' device path that we got in the previous step and specify the keyfile that we generated at the very beginning, in step 1.

This will require an existing passphrase to add the new keyfile:

sudo cryptsetup luksAddKey /dev/nvme0n1p3 /media/usb/secure.key

If "nothing happens" (it actually returns a shell success code) then it worked.

6. Test the keyfile

To test that the previous step was done correctly, we can do the following:

sudo cryptsetup luksOpen --test-passphrase --key-file /media/usb/secure.key /dev/nvme0n1p3

If it works, it won't show anything (only return success in the terminal). If it fails, it will show:

No key available with this passphrase.

7. Update initramfs

The last step to set everything up is updating initramfs. Initramfs is a temporary file system with a mini-Linux inside used only during boot. After boot everything is deleted.

Updating initramfs will add to the initramfs image the updated version of /etc/crypttab together with the unlock script that we've previously downloaded inside /lib/cryptsetup.

Warning: If you see any errors while running the command, it's better to revert to your original /etc/crypttab entry and post a comment in the comments section below this post.

This may take around 1 min:

sudo update-initramfs -u

To check that our script was added inside initramfs we can run:

sudo lsinitramfs /boot/initrd.img-XXXXXXXX-generic | grep 'keyscript.sh'

lsinitramfs lists the files of initramfs' compressed image. So all the files of the mini-Linux will be listed there.

And something like this should be printed in our console (you can Ctrl-C to cancel the grep after finding it):

usr/lib/cryptsetup/scripts/keyscript.sh

8. Reboot

This is the last necessary step.

Simply reboot the USB and your setup should be automatically decrypted! 🎉🥳

sudo reboot

If all went well, please comment below with your Ubuntu (or other distro) version and your cryptsetup version.

9. Logs

Something cool that I figured out is a way to log messages inside initramfs. Usually this is almost impossible because, as I said earlier, initramfs (a temporary filesystem) is cleaned up right after boot, so anything written anywhere will be removed.

I realized that logs can be sent directly to the kernel using /dev/kmsg, so that later on, after boot, you can grep logs doing the following:

sudo dmesg | grep 'keyscript.sh'

This is pretty cool because either on success or failure (you had to manually enter the password) you can check the logs.

Logs when using the USB key normally:

[ 8.199775] keyscript.sh: Attempt #1
[ 8.213481] keyscript.sh: Using keyfile: /mnt/unlock-usb/secure.key

Logs when USB device was not found or simply choosing to manually introduce the password

[ 3.258277] keyscript.sh: Attempt #1
[ 6.271922] keyscript.sh: Device for decryption not found: /dev/disk/by-uuid/81D6-413D
[ 6.941412] keyscript.sh: Proceeding to ask for manually entering the password

Logs when plugging in a USB without the keyfile

[ 3.234567] keyscript.sh: Attempt #1
[ 6.274412] keyscript.sh: Keyfile not found at: /mnt/unlock-usb/secure.key
[ 6.954763] keyscript.sh: Proceeding to ask for manually entering the password

Logs when the USB could not be mounted on boot

[ 3.112245] keyscript.sh: Attempt #1
[ 6.514122] keyscript.sh: Failed to mount device: /dev/disk/by-uuid/81D6-413D at /mnt/unlock-usb
[ 6.613212] keyscript.sh: Proceeding to ask for manually entering the password

Logs when the keyfile is located but incorrect

[ 8.082461] keyscript.sh: Attempt #1
[ 8.096964] keyscript.sh: Using keyfile: /mnt/unlock-usb/secure.key
[ 13.501285] keyscript.sh: Attempt #2
[ 13.511812] keyscript.sh: Using keyfile: /mnt/unlock-usb/secure.key
[ 18.349567] keyscript.sh: Attempt #3
[ 18.361704] keyscript.sh: Using keyfile: /mnt/unlock-usb/secure.key
[ 23.205031] keyscript.sh: Max retries (3) reached. Proceeding to ask for manually entering the password.

Share thoughts!

Would it be interesting to consider any USB as valid? Or it's better to restrict it by UUID as we're doing already?

Special thanks to Guilhem Moulin, from the official Cryptsetup Team at Debian, for helping me clear up some doubts about the behavior of cryptsetup.

Talk back!

Say thanks if you made it, or help me keep this post updated with new versions of Ubuntu or cryptsetup.