How to load LUKS passphrase from USB, falling back to keyboard?

24.3k Views Asked by At

I want to set up a headless Linux (Debian Wheezy) PC with whole disk encryption, with the ability to unlock the disk either with a USB drive, or by entering a passphrase by keyboard. My starting point is a fresh install using the basic whole disk encryption option in the Debian Installer, which manages everything besides /boot as a LUKS-encrypted logical volume group and gives me the keyboard option. I will describe my current solution in an answer, in hopes that it will be useful and that others can improve on it.

Here are some of the issues I had:

  • Setting up a passphrase and putting it on the USB drive.

  • Loading the USB modules in time.

  • Waiting for the USB drive to recognized by Linux before trying to read from it.

  • Identifying the correct USB drive (not some other drive that happens to be inserted).

  • Writing a "keyscript" to pull a passphrase off the USB drive.

  • Ensuring that the fall-back to keyboard kicks in in all USB failure cases.

I will accept an answer with significant improvements and upvote answers that offer contributions.

6

There are 6 best solutions below

0
On

despite the great answer from @Andrew which works in previous versions. The solution actually is outdated and needs lots of tuning for ubuntu 18.04 and 19.10. So I want to share my research on this.

There are several catches about crypttab. The sepcs actually changed a lot from 14.04 to 18.04 and to 19.10. It starts to support more parameters for cryptsetup. For example keyfile-offset, keyfile-size, etc. Some of the options e.g. nobootwait are gone. Some parameters supported in other distro already but is not supported in ubuntu yet (for example very nice parameter keyfile-timeout. This can eliminate the entire keyscript since it will automatically fallback to keyboard input after the keyfile-timeout.)

The major pit-fall for crypttab on ubuntu is that it actually processed by 2 different processes. One is the traditionally initramfs and another is the modern systemd. Systemd is supposed to be more advanced and flexiable in many aspect. However, systemd has poor support for crypptab, there are many options such as keyscript just silently ignored. so I have no idea what is going on, until I spotted this post. Almost all the posts online about crypttab settings is for initramfs not for systemd. So we need to add initramfs to all the entries in crypttab to avoid problems.

I also discovered a nice way to debug our keyscript and crypttab without VM or repeatedly rebooting. It is cryptdisks_start. Before we actually propagate our changes to initramfs, we should always test it with this nice command. Otherwise, you have to end-up locked out from your system and can only recover it through chroot environment.

@andrew posted a nice way to use data hide in the raw area of a file system. However, I found it is very annoying when we want to automatically create partitions and dd the raw data to lots of usbkeys, we have to calculate the offset for all different file systems and different partition sizes. Moreover, if a user accidentally write onto the FS, there is some risk that the key got overritten. A raw partition without any FS on it makes more sense in this case. However raw partition does not have UUID which is not very useful for automatic unlocking. Thus, I would like introduce a way just use normal passphrase files on the usbkey filesystem. The major issue of passdev is it does not seek/stop during reading the file. Thus we cannot use the keyfile-offset and keyfile-size option when we want to fallback to keyboard input. Because cryptsetup will actually try to skip in the input content and if the content is shorter than keyfile-size, it raises an error. This also means if there is large offset, passdev can be very slow since it always read from beginning. However, there is no point to implement offset and keyfile size for a actual file on file system. I believe those are created for raw device.

The crypttab

luks-part UUID="<uuid>" /dev/disk/by-uuid/<keyfile FS uuid>:/<keyfile path relative to usbkey root>:<timeout in sec> luks,keyfile-offset=<seek to the key>,keyfile-size=<>,keyscript=/bin/passphrase-from-usbfs.sh,tries=<number of times to try>,initramfs

the keyscript passphrase-from-usbfs.sh utilized the /lib/cryptsetup/scripts/passdev which will wait the usb device and mount the fs then pipe out the file content. It supports the CRYPTTAB_KEY in format of /device-path/<keyfile FS uuid>:/<keyfile path relative to usbkey root>:<timeout in sec>.

#!/bin/sh
#all message need to echo to stderr, the stdout is used for passphrase
# TODO: we may need to do something about the plymouth
echo "CRYPTTAB_KEY=$CRYPTTAB_KEY" >&2
echo "CRYPTTAB_OPTION_keyfile_offset=$CRYPTTAB_OPTION_keyfile_offset" >&2
#set your offset and file size here if your system does not support those paramters
#CRYPTTAB_OPTION_keyfile_offset=
#CRYPTTAB_OPTION_keyfile_size=
echo "timeout=$CRYPTTAB_OPTION_keyfile_timeout" >&2
CRYPTTAB_OPTION_keyfile_timeout=10 # keyfile-timeout is not supported yet 
pass=$(/lib/cryptsetup/scripts/passdev $CRYPTTAB_KEY)
rc=$?
if ! [ $rc -eq 0 ]; then
    echo "Can't find $CRYPTTAB_KEY; USB stick not present?" >&2
    /lib/cryptsetup/askpass "Unlocking the disk $CRYPTTAB_SOURCE ($CRYPTTAB_NAME) Enter passphrase: "
else
    echo "successfully load passphrase." >&2
    echo -n $pass
fi

The hook tell update-initramfs to copy our scripts.

#!/bin/sh

PREREQ=""

prereqs() {
        echo "$PREREQ"
}

case "$1" in
        prereqs)
                prereqs
                exit 0
        ;;
esac

. "${CONFDIR}/initramfs.conf"
. /usr/share/initramfs-tools/hook-functions
copy_exec /bin/passphrase-from-usbfs.sh
copy_exec /bin/passphrase-from-usb.sh
#when using passdev we need to hook additionaly FS and binary
copy_exec /lib/cryptsetup/scripts/passdev
manual_add_modules ext4 ext3 ext2 vfat btrfs reiserfs xfs jfs ntfs iso9660 udf

Finally I posted the updated version of passphrase-from-usb.sh which can use the new parameters in crypttab:

7
On

A lot of my solution is derived from the post, Using A USB Key For The LUKS Passphrase.

  1. Create a random passphrase:

     dd if=/dev/urandom bs=1 count=256 > passphrase
    
  2. Insert a USB drive. dmesg output will show the device name; assume /dev/sdd. Figure out its size:

     blockdev --getsize64 /dev/sdd
    
  3. I decided to install the passphrase at the end of the raw device, figuring it might survive any accidental use of the USB drive.

     dd if=passphrase of=/dev/sdd bs=1 seek=<size-256>
    
  4. Add the passphrase to the LUKS volume:

     cryptsetup luksAddKey /dev/sda5 passphrase
    

    This does not affect the existing hand-entered passphrase from the installer. The passphrase file can be deleted:

     rm passphrase
    
  5. Find a unique name for the USB stick, so we can identify it when present:

     ls -l /dev/disk/by-id | grep -w sdd
    

    You should see one symlink. I will call it /dev/disk/by-id/<ID>.

  6. Edit /etc/crypttab. You should see a line like:

     sdc5_crypt UUID=b9570e0f-3bd3-40b0-801f-ee20ac460207 none luks
    

    Modify it to:

     sdc5_crypt UUID=b9570e0f-3bd3-40b0-801f-ee20ac460207 /dev/disk/by-id/<ID> luks,keyscript=/bin/passphrase-from-usb
    
  7. The keyscript referred to above will need to read the passphrase from the USB device. However, it needs to do more than that. To understand how it is used, check /usr/share/initramfs-tools/scripts/local-top/cryptroot, the script that runs at boot time to unlock the root device. Note when a keyscript is set, it is simply run and the output piped to luksOpen with no other checking. There is no way to signal an error (USB drive not present) or fall back to keyboard input. If the passphrase fails, the keyscript is run again in a loop, up to some number of times; however we are not told which iteration we are on. Also, we have no control over when the keyscript is run, so we can't be sure Linux has recognized the USB drive.

    I addressed this with some hacks:

    1. Poll on the USB drive and wait 3 seconds for it to appear. This works for me, but I would love to know a better way.

    2. Create a dummy file /passphrase-from-usb-tried on first run to indicate that we have been run at least once.

    3. If we have been run at least once, or the USB drive cannot be found, run the askpass program used by cryptroot for keyboard input.

    The final script:

    #!/bin/sh
    
    set -e
    
    if ! [ -e /passphrase-from-usb-tried ]; then
        touch /passphrase-from-usb-tried
        if ! [ -e "$CRYPTTAB_KEY" ]; then
            echo "Waiting for USB stick to be recognized..." >&2
            sleep 3
        fi
        if [ -e "$CRYPTTAB_KEY" ]; then
            echo "Unlocking the disk $CRYPTTAB_SOURCE ($CRYPTTAB_NAME) from USB key" >&2
            dd if="$CRYPTTAB_KEY" bs=1 skip=129498880 count=256 2>/dev/null
            exit
        else
            echo "Can't find $CRYPTTAB_KEY; USB stick not present?" >&2
        fi
    fi
    
    /lib/cryptsetup/askpass "Unlocking the disk $CRYPTTAB_SOURCE ($CRYPTTAB_NAME)\nEnter passphrase: "
    

    Finally, we need to ensure that this script is available in the initramfs. Create /etc/initramfs-tools/hooks/passphrase-from-usb containing:

    #!/bin/sh
    
    PREREQ=""
    
    prereqs() {
            echo "$PREREQ"
    }
    
    case "$1" in
            prereqs)
                    prereqs
                    exit 0
            ;;
    esac
    
    . "${CONFDIR}/initramfs.conf"
    . /usr/share/initramfs-tools/hook-functions
    
    copy_exec /bin/passphrase-from-usb /bin
    
  8. The USB drivers were not present in my initramfs. (It appears they are by default in later versions of Debian.) I had to add them by adding to /etc/initramfs-tools/modules:

     uhci_hcd
     ehci_hcd
     usb_storage
    
  9. When all is done, update the initramfs:

     update-initramfs -u
    
0
On

To accompany excellent answers above please see C routines you could use to write/generate and read raw block device key. The "readkey.c" extracts key of given size from block device and "writekey.c" can generate or write existing key to raw device. The "readkey.c" once compiled can be used in custom script to extract key of known size from raw block device like so:

readkey </path/to/device> <keysize>

To see usage for "writekey", after compiled run it with no flags.
To compile just use:

gcc readkey.c -o readkey

gcc writekey.c -o writekey

I tested both on Verbatim 16GB USB 2.0 USB flash drive with custom "keyscript=" in crypttab also published below. The idea for "crypto-usb.sh" is from "debian etch" cryptsetup guide.

crypto-usb.sh:

#!/bin/sh
echo ">>> Trying to get the key from agreed space <<<" >&2
modprobe usb-storage >/dev/null 2>&1
sleep 4
OPENED=0
disk="/sys/block/sdb"
boot_dir="/boot"
readkey="/boot/key/readkey"
echo ">>> Trying device: $disk <<<" >&2
F=$disk/dev
if [ 0`cat $disk/removable` -eq 1 -a -f $F ]; then
    mkdir -p $boot_dir
    mount /dev/sda1 $boot_dir -t ext2 >&2
    echo ">>> Attempting key extraction <<<" >&2
    if [ -f $readkey ]; then
        # prints key array to the caller
        $readkey /dev/sdb 4096
        OPENED=1
    fi
    umount $boot_dir >&2
fi


if [ $OPENED -eq 0 ]; then
    echo "!!! FAILED to find suitable key !!!" >&2
    echo -n ">>> Try to enter your password: " >&2
    read -s -r A
    echo -n "$A"
else
    echo ">>> Success loading key <<<" >&2
fi

When generating the key size of the key has to be provided, generated key is saved to ".tmpckey" file with file permissions 0600 for later use. When writing existing key, size is determined by measuring the existing key size. This looks like complex approach however once compiled with simple "gcc" it can provide easy way of manipulating the raw key content.

readkey.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main(int argc, char *argv[])
{
    int blockSize = 512;
    int keySize = 2048; 

    FILE *device;       

    if (  argc == 3 
           && (sizeof(argv[1]) / sizeof(char)) > 1
           && (sizeof(argv[2]) / sizeof(char)) > 1
       && (atoi(argv[2]) % 512) == 0
       ) {
        device = fopen(argv[1], "r");
        if(device == NULL) { 
            printf("\nI got trouble opening the device %s\n", argv[1]);
            exit(EXIT_FAILURE);
        }
        keySize = atoi(argv[2]);        
    }
    else if (  argc == 2 
            && (sizeof(argv[1]) / sizeof(char)) > 1
        ) {
        device = fopen(argv[1], "r");
        if(device == NULL) { 
            printf("\nI got trouble opening the device %s\n", argv[1]);
            exit(EXIT_FAILURE);
        }

    }
    else {

        printf("\nUsage: \n");
        printf("\nKey Size Provided: \n");
        printf("\n\t\treadkey </path/to/device> <keysize> \n");
        printf("\nDefault key size: %d\n", keySize);
        printf("\n\t\treadkey </path/to/device>\n");
        exit(1);
    }

    int count;

    char *block;

    /* Verify if key is multiple of blocks */
    int numBlocks = 0;
    if (keySize % 512 != 0) {
       printf("\nSory but key size is not multiple of block size, try again. TA.\n");
       exit(1);
    }

    /* Seek till the end to get disk size and position to start */
    fseek(device, 0, SEEK_END);

    /* Determine where is the end */
    long endOfDisk = ftell(device);

    /* Make sure we start again */
    rewind(device); // Do I need it ???

    /* Get the required amount minus block size */
    long startFrom = endOfDisk - blockSize - keySize;

    /* Allocate space for bloc */
    block = calloc(keySize, sizeof(char));

    /* Start reading from specified block */
    fseek(device, startFrom, SEEK_SET);
    fread(block, 1, keySize, device);

    /* Do something with the data */
    for(count = 0; count < keySize/*sizeof(block)*/; count++){
        printf("%c", block[count]);
    }

    /* Close file */
    fclose(device);

    /* Make sure freed array is zeroed */
    memset(block, 0, keySize);
    free(block);
}

writekey.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    int blockSize = 512;
    int keySize = 2048;

    int count;

    unsigned char *block;

    /*
        Thing to always remember that argv starts from 0 - the name of the program, and argc starts from 1 i.e. 1 is the name of the program.
    */
    if ( argc == 3 
       && strcmp(argv[1], "genwrite") != 0
       && (sizeof(argv[2]) / sizeof(char)) > 2
       ) {
        char ch;
        FILE *keyF;
        keyF = fopen(argv[1], "r");
        if (keyF == NULL) exit(EXIT_FAILURE);

        /* Tell key Size */
        fseek(keyF, 0, SEEK_END);
        keySize = ftell(keyF);
        rewind(keyF);
        printf("\nKey Size: %d\n", keySize);

        block = calloc(keySize, sizeof(char));
        printf("\n-- Start Key --:\n");
                for(count = 0; count < keySize/*sizeof(block)*/; count++){
            char ch = fgetc(keyF);
                        block[count] = ch;
            /*
              Uncomment below to see your key on screen
            */
            // printf("%c",ch);

                }
        printf("\n-- End Key --:\n");
        fclose(keyF);
    }
    else if (  argc == 3 
        && strcmp(argv[1], "genwrite") == 0 
        && (sizeof(argv[2]) / sizeof(char)) > 2
        ) 
        {
        printf("\n-- Attempting to create random key(ish --) of size: %d\n", keySize);
        block = calloc(keySize, sizeof(char));
        int count;
        for(count = 0; count < keySize/*sizeof(block)*/; count++){
            block[count] = (char) rand();
        }
        FILE *tmpfile;
        tmpfile = fopen(".tmpckey", "w");
        if(tmpfile == NULL) exit(EXIT_FAILURE);
        fwrite(block, 1, keySize, tmpfile);
        fclose(tmpfile);
        chmod(".tmpckey", 0600);
    }
    else if (  argc == 4 
        && strcmp(argv[1], "genwrite") == 0
        && (sizeof(argv[2]) / sizeof(char)) > 2
        && ((atoi(argv[3]) % 512) == 0)
        ) 
        {
        keySize = atoi(argv[3]);
        printf("\n-- Attempting to create random key(ish --) of size: %d\n", keySize);
        block = calloc(keySize, sizeof(char));
        int count;
        for(count = 0; count < keySize/*sizeof(block)*/; count++){
            block[count] = (char) rand();
        }
        FILE *tmpfile;
        tmpfile = fopen(".tmpckey", "w");
        if(tmpfile == NULL) exit(EXIT_FAILURE);
        fwrite(block, 1, keySize, tmpfile);
        fclose(tmpfile);
        chmod(".tmpckey", 0600);
    }   
    else {
        printf("\n");
        printf("################################################################################\n");
        printf("#                                                                              #\n");
        printf("#                              Usage:                                          #\n");
        printf("#                                                                              #\n");
        printf("################################################################################\n");
        printf("#> To write existing key to device:                                            #\n");
        printf("#                                                                              #\n");
        printf("#     writekey </path/to/keyfile> </path/to/removable/sd*>                     #\n");
        printf("#                                                                              #\n");
        printf("#> To generate and write pseudo random key,                                    #\n");
        printf("#> key will be saved to temporary file .tmpckey                                #\n");
        printf("#                                                                              #\n");
        printf("#     writekey genwrite </path/to/removable/sd*> <keysize in multiples of 512> #\n");
        printf("#                                                                              #\n");
        printf("#> When keysize is not provided default size is set to %d.                     #\n", keySize);
        printf("#                                                                              #\n");
        printf("################################################################################\n");
        exit(1);
    }

    /*
        Some printf debugging below, uncomment when needed to see what is going on.
    */
    /*
    printf("\nNumber of Args: %d\n", argc);
    printf("\nCurrently block array contains: \n");
    for(count = 0; count < keySize; count++){
        printf("%c", block[count]);
    }
    printf("\n-- End block -- \n");
    */
    /* Open Device itp... */
    FILE *device = fopen(argv[2], "a");
    if(device == NULL) exit(EXIT_FAILURE);

    printf("\nDevice to write: %s\n", argv[2]);

    fseek(device, 0, SEEK_END);

    /* Determine where is the end */
    long endOfDisk = ftell(device);
    printf("\nDevice Size: %ld\n", endOfDisk);

    /* Verify if key is multiple of blocks */
    int numBlocks = 0;
    if (keySize % 512 != 0 || endOfDisk < (blockSize + keySize) ) {
            printf("\nSorry but key size is not multiple of block size or device you trying to write to is too small, try again. TA.\n");
        fclose(device);
            exit(1);
    }



    /* Make sure we start again */
    rewind(device);

    /* Get the required amount sunbstracting block size */
    long startFrom = endOfDisk - blockSize - keySize;

    /* Write some data to the disk */
    printf("\nWriting data starting from: %ld\n", startFrom);
    fseek(device, startFrom, SEEK_SET);
    fwrite(block, 1, keySize, device);
    printf("\nBlock Position after data write procedure : %ld\n", ftell(device));

    /*
        Below is just for convenience, to read what was written,
        can aid in debugging hence left commented for later.
    */
    /*
    printf("\nAmount of Data written : %ld\n", ftell(device) - startFrom);

    // Start reading from specified block 
    printf("\n>>>>>>>> DEBUGGING SECTION <<<<<<<<<\n");
    rewind(device); //
    fseek(device, startFrom, SEEK_SET);
    printf("\nBlock Position before read attempted: %d\n", ftell(device));
    printf("\nKey size: %d\n", keySize);
    fread(block, 1, keySize, device);

    // Do something with the data
    printf("\nBlock Position startFrom: %ld\n", startFrom);
    printf("\nBlock Position after read: %d\n", ftell(device));
    printf("\n-- Buffer Read: --\n");
    for(count = 0; count < keySize; count++){
        printf("%c", block[count]);
    }
    printf("\n-- End block -- \n");
    printf("\n--  -- \n");
    printf("\n--  -- \n");
    */

    /* Close file */
    fclose(device);

    /* Make sure freed array is zeroed */
    memset(block, 0, keySize);
    free(block);

/* Return success, might change it to be useful return not place holder */
return 0;
}

To verify key written to raw device is the same as the one in file(below will output nothing if keys are identical):

diff -B <(./readkey </path/to/device> 4096) <(cat .tmpckey)

Or for existing key generated using own means:

diff -B <(./readkey </path/to/device> <generated elsewhere key size>) <(cat </path/to/keyfile>)

Thank You

0
On

Here is a solution similar to the one by Andrew, but

  • using CRYPTTAB_TRIED described in the Debian crypttab man page to distinguish tries, and

  • calling the existing standard keyscript /lib/cryptsetup/scripts/passdev on the first try.

  1. Create your keyfile or keypartition as usual for the passdev script.

  2. Create the following file /usr/local/bin/key-from-usb and make it executable.

    #!/bin/sh
    set -e
    if [ $CRYPTTAB_TRIED -ge 1 ]; then
      /lib/cryptsetup/askpass "Second try to unlock $CRYPTTAB_SOURCE ($CRYPTTAB_NAME). Please enter passphrase: "
    else
      /lib/cryptsetup/scripts/passdev $CRYPTTAB_KEY
    fi
    
  3. In /etc/crypttab use the parameter keyscript=/usr/local/bin/key-from-usb.

  4. Create /etc/initramfs-tools/hooks/key-from-usb with this content:

    #!/bin/sh
    
    PREREQ=""
    
    prereqs() {
            echo "$PREREQ"
    }
    
    case "$1" in
             prereqs)
                     prereqs
                     exit 0
             ;;
    esac
    
    . "${CONFDIR}/initramfs.conf"
    . /usr/share/initramfs-tools/hook-functions
    
    manual_add_modules vfat
    
    copy_exec /usr/lib/cryptsetup/scripts/passdev /usr/lib/cryptsetup/scripts/passdev
    
    copy_exec /usr/local/bin/key-from-usb /usr/local/bin/key-from-usb
    

    The first copy_exec line here is needed because passdev is not copied if it is not mentioned in crypttab. Similarly, manual_add_modules vfat will ensure that a vfat usb disk can still be used.

Hint: Use lsinitramfs /boot/initrd.img-... and diff/compare the results to check that the script and all its dependencies are included.

1
On

It would be ideal to me if I could simply have a small USB stick containing a passphrase that will unlock the disk. Not only would that be handy for servers (where you could leave the USB stick in the server - the goal is to be able to return broken harddisks without having to worry about confidential data), it would also be great for my laptop: Insert the USB stick when booting and remove it after unlocking the cryptodisk.

I have now written a patch that will search the root dir of all devices for the file 'cryptkey.txt' and try decrypting with each line as a key. If that fails: Revert to typing in the pass phrase.

It does mean the key cannot contain \n, but that would apply to any typed in key, too. The good part is that you can use the same USB disk to store the key for multiple machines: You do not need a separate USB disk for each. So if you have a USB drive in your physical key ring, you can use the same drive for all the machines you boot when being physically close.

You add the key with:

cryptsetup luksAddKey /dev/sda5

And then put the same key as a line in a file on the USB/MMC disk called 'cryptkey.txt'. The patch is here:

https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=864647

If the USB drivers, MMC drivers or the filesystems are not present in your initramfs, you need to add them by adding to /etc/initramfs-tools/modules:

uhci_hcd
ehci_hcd
usb_storage
nls_utf8
nls_cp437
vfat
fat
sd_mod
mmc_block
tifm_sd
tifm_core
mmc_core
tifm_7xx1
sdhci
sdhci_pci

When all is done, update the initramfs:

update-initramfs -u

It can be found as patch and file at: https://gitlab.com/ole.tange/tangetools/tree/master/decrypt-root-with-usb

0
On

I used a different solution without a script (running Fedora 39, KDE spin, Linux 6.7.4):

  1. Display the UUIDs of the LUKS device and the USB key. In this example, /dev/sda3 and /dev/sdb1, respectively:

    $ sudo lsblk --fs
    NAME                                              FSTYPE      FSVER LABEL  UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
    sda                                                                                                                            
    ├─sda1                                            vfat        FAT32        0000-0000                             579.8M     3% /boot/efi
    ├─sda2                                            ext4        1.0          11111111-1111-1111-1111-111111111111  614.9M    30% /boot
    └─sda3                                            crypto_LUKS 2            22222222-2222-2222-2222-222222222222                
      └─luks-22222222-2222-2222-2222-222222222222     btrfs             fedora 33333333-3333-3333-3333-333333333333  467.1G     1% /home
    sdb                                                                                                                            
    └─sdb1                                            vfat        FAT32        4444-4444                              29.3G     0% /media/usb
    zram0                                                                                                                          [SWAP]
    
    
  2. Auto-mount the USB key by adding a line to /etc/fstab:

    UUID=4444-4444 /media/usb vfat rw,x-systemd.automount,nosuid,nodev,nofail,uid=0,gid=0,umask=0077,dmask=0077,fmask=0077 0 0

  3. Update systemd mounts:

    sudo systemctl daemon-reload

  4. Mount the USB key:

    sudo mount -a

  5. Write a random private key to the USB key:

    sudo dd if=/dev/urandom of=/media/usb/keyfile bs=4096 count=1

  6. Add the private key to LUKS:

    sudo cryptsetup luksAddKey /dev/sda3 /media/usb/keyfile

  7. Comment out the existing line in /etc/crypttab and add a line (not sure if needed, but I added _usb to the key):

    luks-22222222-2222-2222-2222-222222222222_usb UUID=22222222-2222-2222-2222-222222222222 keyfile:UUID=4444-4444 discard,keyfile-timeout=5s

  8. Comment out the existing GRUB_CMDLINE_LINUX line in /etc/default/grub and add:

    GRUB_CMDLINE_LINUX="rd.luks.uuid=luks-22222222-2222-2222-2222-222222222222 cryptkey=UUID=4444-4444:vfat:/keyfile rhgb"

  9. Update initramfs

    sudo dracut -f

  10. Generate GRUB2 config:

    grub2-mkconfig -o /boot/efi/EFI/fedora/grub.cfg

  11. Reboot

    sudo reboot

If it doesn't work, review journalctl -b -p warning. In my case, without adding the cryptkey option in step 8, I received:

Feb 21 07:23:55 myhost systemd-cryptsetup[1955]: Cannot use device /dev/disk/by-uuid/22222222-2222-2222-2222-222222222222 which is in use (already mapped or mounted).

Feb 21 07:23:55 myhost systemd-cryptsetup[1955]: Failed to activate with key file '/run/systemd/cryptsetup/keydev-luks-22222222-2222-2222-2222-222222222222_usb/keyfile': Device or resource busy