% Full disk encryption, including `/boot`: Unlocking LUKS devices from GRUB

Introduction
============

So called “full disk encryption” is often a misnomer, because there is
typically a separate plaintext partition holding `/boot`.  For instance
the Debian Installer does this in its “encrypted LVM” partitioning method.
Since not all bootloaders are able to unlock LUKS devices, a plaintext
`/boot` is the only solution that works for all of them.

However, GRUB2 is (since Jessie) able to unlock LUKS devices with its
[`cryptomount`](https://www.gnu.org/software/grub/manual/grub/html_node/cryptomount.html)
command, which therefore enables encryption of the `/boot` partition as
well: using that feature reduces the amount of plaintext data written to
disk.  It is especially interesting when GRUB is installed to a read-only
media, for instance as [coreboot payload](https://doc.coreboot.org/payloads.html#grub2)
flashed to a write-protected chip.  On the other hand, it is *incompatible*
with some other features that only enabled later at initramfs stage, such
as splash screens or remote unlocking.

Since enabling unlocking LUKS devices from GRUB [isn't exposed to the d-i
interface](https://bugs.debian.org/814798) (as of Buster), people have
come up with various custom workarounds.  But as of Buster [`cryptsetup`(8)]
defaults to a new [LUKS header format version](https://gitlab.com/cryptsetup/LUKS2-docs),
which isn't supported by GRUB as of 2.04.  **Hence the pre-Buster
workarounds won't work anymore**.  Until LUKS *version 2* support is
[added to GRUB2](https://savannah.gnu.org/bugs/?55093), the device(s)
holding `/boot` needs to be in *LUKS format version 1* to be unlocked from
the boot loader.

This document describes a generic way to unlock LUKS devices from GRUB
for Debian Buster.


Encrypting the device holding `/boot`
=====================================

There are two alternatives here:

  * Either format an existing `/boot` partition to LUKS1; or
  * Move `/boot` to the root file system.  The root device(s) needs to
    use LUKS version 1, but existing LUKS2 devices can be *converted*
    (in-place) to LUKS1.

These two alternatives are described in the two following sub-sections.

We assume the system resides on a single drive `/dev/sda`, partitioned
with d-i's “encrypted LVM” scheme:

    root@debian:~# lsblk -o NAME,FSTYPE,MOUNTPOINT /dev/sda
    NAME                    FSTYPE      MOUNTPOINT
    sda
    ├─sda1                  ext2        /boot
    ├─sda2
    └─sda5                  crypto_LUKS
      └─sda5_crypt          LVM2_member
        ├─debian--vg-root   ext4        /
        └─debian--vg-swap_1 swap        [SWAP]

*Note*: The partition layout of your system may differ.


Formatting the existing `/boot` partition to LUKS1
--------------------------------------------------

Since the installer creates a separate (plaintext) `/boot` partition by
default in its “encrypted LVM” partitioning method, the simplest
solution is arguably to re-format it as LUKS1, especially if the root
device is in LUKS2 format.

That way other partitions, including the one holding the root file
system, can remain in LUKS2 format and benefit from the *stronger
security guaranties* and *convenience features* of the newer version:
more secure (memory-hard) Key Derivation Function, backup header,
ability to offload the volume key to the kernel keyring (thus preventing
access from userspace), custom sector size, persistent flags, unattended
unlocking via kernel keyring tokens, etc.

Furthermore every command in this sub-section can be run from the main
system: no need to reboot into a live CD or an initramfs shell.

 1. Before copying content of the `/boot` directory, remount it read-only
    to make sure data is not modified while it's being copied.

        root@debian:~# mount -oremount,ro /boot

 2. Archive the directory elsewhere (on another device), and unmount it
    afterwards.

        root@debian:~# install -m0600 /dev/null /tmp/boot.tar
    <!-- -->
        root@debian:~# tar -C /boot --acls --xattrs --one-file-system -cf /tmp/boot.tar .
    <!-- -->
        root@debian:~# umount /boot

    (If `/boot` has sub-mountpoints, like `/boot/efi`, you'll need to
    unmount them as well.)

 3. Optionally, wipe out the underlying block device (assumed to be
    `/dev/sda1` in the rest of this sub-section).

        root@debian:~# dd if=/dev/urandom of=/dev/sda1 bs=1M status=none
        dd: error writing '/dev/sda1': No space left on device

 4. Format the underlying block device to LUKS1.  (Note the `--type luks1`
    in the command below, as Buster's [`cryptsetup`(8)] defaults to LUKS
    version 2 for `luksFormat`.)

        root@debian:~# cryptsetup luksFormat --type luks1 /dev/sda1

        WARNING!
        ========
        This will overwrite data on /dev/sda1 irrevocably.

        Are you sure? (Type uppercase yes): YES
        Enter passphrase for /dev/sda1:
        Verify passphrase:

 5. Add a corresponding entry to [`crypttab`(5)] with mapped device name
    `boot_crypt`, and open it afterwards.

        root@debian:~# uuid="$(blkid -o value -s UUID /dev/sda1)"
    <!-- -->
        root@debian:~# echo "boot_crypt UUID=$uuid none luks" | tee -a /etc/crypttab
    <!-- -->
        root@debian:~# cryptdisks_start boot_crypt
        Starting crypto disk...boot_crypt (starting)...
        Please unlock disk boot_crypt:  ********
        boot_crypt (started)...done.

 6. Create a file system on the mapped device.  Assuming source device for
    `/boot` is specified by its UUID in the [`fstab`(5)] -- which the
    Debian Installer does by default -- reusing the old UUID avoids
    editing the file.

        root@debian:~# grep /boot /etc/fstab
        # /boot was on /dev/sda1 during installation
        UUID=c104749f-a0fa-406c-9e9a-3fc01f8e2f78 /boot           ext2    defaults        0       2
    <!-- -->
        root@debian:~# mkfs.ext2 -m0 -U c104749f-a0fa-406c-9e9a-3fc01f8e2f78 /dev/mapper/boot_crypt
        mke2fs 1.44.5 (15-Dec-2018)
        Creating filesystem with 246784 1k blocks and 61752 inodes
        Filesystem UUID: c104749f-a0fa-406c-9e9a-3fc01f8e2f78
        […]

 7. Finally, mount `/boot` again from [`fstab`(5)], and copy the saved
    tarball to the new (and now encrypted) file system.

        root@debian:~# mount -v /boot
        mount: /dev/mapper/boot_crypt mounted on /boot.
    <!-- -->
        root@debian:~# tar -C /boot --acls --xattrs -xf /tmp/boot.tar

    (If `/boot` had sub-mountpoints, like `/boot/efi`, you'll need to
    mount them back as well.)

You can skip the next sub-section and go directly to [Enabling
`cryptomount` in GRUB2].  Note that `init`(1) needs to unlock the
`/boot` partition *again* during the boot process.  See [Avoiding the
extra password prompt] for details and a proposed workaround.  (You'll
need to substitute `/` resp. `sda5` with `/boot` resp. `sda1` in that
section, however only steps 1-3 are relevant here: no need to copy the
key file to the initramfs image since `/boot` can be unlocked and
mounted later during the boot process.)


Moving `/boot` to the root file system
--------------------------------------

The [previous sub-section][Formatting the existing `/boot` partition to LUKS1]
described how to to re-format the `/boot` partition as LUKS1.
Alternatively, it can be moved to the root file system, assuming the
latter is not held by any LUKS2 device.  (As shown below, LUKS2 devices
created with default parameters can be “downgraded” to LUKS1.)

The advantage of this method is that the original `/boot` partition can
be preserved and used in case of *disaster recovery* (if for some reason
the GRUB image is lacking the `cryptodisk` module and the original
plaintext `/boot` partition is lost, you'd need to reboot into a live CD
to recover).  Moreover increasing the number of partitions *increases
usage pattern visibility*: a separate `/boot` partition, even encrypted,
will likely leak the fact that a kernel update took place to an attacker
with access to both pre- and post-update snapshots.

On the other hand, the downside of that method is that the root file
system can't benefit from the nice LUKS2 improvements over LUKS1, some
of which were listed above.  Another (minor) downside is that space
occupied by the former `/boot` partition (typically 256MiB) becomes
unused and can't easily be reclaimed by the root file system.

### Downgrading LUKS2 to LUKS1 ###

Check the LUKS format version on the root device (assumed to be
`/dev/sda5` in the rest of this sub-section):

    root@debian:~# cryptsetup luksDump /dev/sda5 | grep -A1 "^LUKS"
    LUKS header information
    Version:        2

Here the LUKS format version is 2, so the device needs to be *converted*
to LUKS *version 1* to be able to unlock from GRUB.  Unlike the rest of
this document, conversion can't be done on an open device, so you'll
need reboot into a live CD or an [initramfs shell].  (The `(initramfs)`
prompt strings in this sub-section indicates commands that are executed
from an initramfs shell.)  Also, if you have valuable data in the root
partition, then *make sure you have a backup* (at least of the LUKS
header)!

[initramfs shell]: https://wiki.debian.org/InitramfsDebug#Rescue_shell_.28also_known_as_initramfs_shell.29

Run `cryptsetup convert --type luks1 DEVICE` to downgrade.  However if
the device was created with the default parameters then in-place
conversion will fail:

    (initramfs) cryptsetup convert --type luks1 /dev/sda5

    WARNING!
    ========
    This operation will convert /dev/sda5 to LUKS1 format.


    Are you sure? (Type uppercase yes): YES
    Cannot convert to LUKS1 format - keyslot 0 is not LUKS1 compatible.

This is because its first key slot uses Argon2 as Password-Based Key
Derivation Function (PBKDF) algorithm:

    (initramfs) cryptsetup luksDump /dev/sda5 | grep "PBKDF:"
            PBKDF:      argon2i

Argon2 is a *memory-hard* function that was selected as the winner of
the Password-Hashing Competition; LUKS2 devices use it by default for
key slots, but LUKS1's only supported PBKDF algorithm is PBKDF2.  Hence
the key slot has to be converted to PBKDF2 prior to LUKS format version
downgrade.

    (initramfs) cryptsetup luksConvertKey --pbkdf pbkdf2 /dev/sda5
    Enter passphrase for keyslot to be converted:

Now that all key slots use the PBKDF2 algorithm, the device shouldn't
have any LUKS2-only features left, and can be converted to LUKS1.

    (initramfs) cryptsetup luksDump /dev/sda5 | grep "PBKDF:"
            PBKDF:      pbkdf2
<!-- -->
    (initramfs) cryptsetup convert --type luks1 /dev/sda5

    WARNING!
    ========
    This operation will convert /dev/sda5 to LUKS1 format.


    Are you sure? (Type uppercase yes): YES
<!-- -->
    (initramfs) cryptsetup luksDump /dev/sda5 | grep -A1 "^LUKS"
    LUKS header information

### Moving `/boot` to the root file system ###

(The moving operation can be done from the normal system.  No need to
reboot into a live CD or an initramfs shell if the root file system
resides in a LUKS1 device.)

 1. To ensure data is not modified while it's being copied, remount
    `/boot` read-only.

        root@debian:~# mount -oremount,ro /boot

 2. Recursively copy the directory to the root file system, and replace
    the old `/boot` mountpoint with the new directory.

    <!-- -->
        root@debian:~# cp -axT /boot /boot.tmp
    <!-- -->
        root@debian:~# umount /boot
    <!-- -->
        root@debian:~# rmdir /boot
    <!-- -->
        root@debian:~# mv -T /boot.tmp /boot

    (If `/boot` has sub-mountpoints, like `/boot/efi`, you'll need to
    unmount them first, and then remount them once `/boot` has been
    moved to the root file system.)

 3. Comment out the [`fstab`(5)] entry for the `/boot` mountpoint.
    Otherwise at reboot `init`(1) will mount it and therefore shadow data
    in the new `/boot` directory with data from the old plaintext
    partition.

        root@debian:~# grep /boot /etc/fstab
        ## /boot was on /dev/sda1 during installation
        #UUID=c104749f-a0fa-406c-9e9a-3fc01f8e2f78 /boot           ext2    defaults        0       2


Enabling `cryptomount` in GRUB2
===============================

Enable the feature and update the GRUB image:

    root@debian:~# echo "GRUB_ENABLE_CRYPTODISK=y" >>/etc/default/grub
<!-- -->
    root@debian:~# update-grub
<!-- -->
    root@debian:~# grub-install /dev/sda

If everything went well, `/boot/grub/grub.cfg` should contain `insmod
cryptodisk` (and also `insmod lvm` if `/boot` is on a Logical Volume).

*Note*: The PBKDF parameters are determined via benchmark upon key slot
creation (or update).  Thus they only makes sense if the environment in
which the LUKS device is open matches (same CPU, same RAM size, etc.)
the one in which it's been formatted.  Unlocking from GRUB does count as
an environment mismatch, because GRUB operates under tighter memory
constraints and doesn't take advantage of all crypto-related CPU
instructions.  Concretely, that means unlocking a LUKS device from GRUB
might take *a lot* longer than doing it from the normal system.  Since
GRUB's LUKS implementation isn't able to benchmark, you'll need to do it
manually.  It's easier for PBKDF2 as there is a single parameter to play
with (iteration count) — while Argon2 has two (iteration count and
memory) — and changing it affects the unlocking time linearly: for
instance halving the iteration count would speed up unlocking by a
factor of two.  (And of course, making low entropy passphrases twice as
easy to brute-force.  There is a trade-off to be made here.  Balancing
convenience and security is the whole point of running PBKDF
benchmarks.)

    root@debian:~# cryptsetup luksDump /dev/sda1 | grep -B1 "Iterations:"
    Key Slot 0: ENABLED
        Iterations:             1000000
<!-- -->
    root@debian:~# cryptsetup luksChangeKey --pbkdf-force-iterations 500000 /dev/sda1
    Enter passphrase to be changed:
    Enter new passphrase:
    Verify passphrase:

(You can reuse the existing passphrase in the above prompts.  Replace
`/dev/sda1` with the LUKS1 volume holding `/boot`; in this document
that's `/dev/sda1` if `/boot` resides on a separated encrypted
partition, or `/dev/sda5` if `/boot` was moved to the root file system.)

*Note*: `cryptomount` lacks an option to specify the key slot index to
open.  All active key slots are tried sequentially until a match is
found.  Running the PBKDF algorithm is a slow operation, so to speed up
things you'll want the key slot to unlock at GRUB stage to be the first
active one.  Run the following command to discover its index.

    root@debian:~# cryptsetup luksOpen --test-passphrase --verbose /dev/sda5
    Enter passphrase for /dev/sda5:
    Key slot 0 unlocked.
    Command successful.


Avoiding the extra password prompt
==================================

The device holding the kernel (and the initramfs image) is unlocked by
GRUB, but the root device needs to be *unlocked again* at initramfs
stage, regardless whether it's the same device or not.  This is because
GRUB boots with the given `vmlinuz` and initramfs images, but there is
currently no way to securely pass cryptographic material (or Device
Mapper information) to the kernel.  Hence the Device Mapper table is
initially empty at initramfs stage; in other words, all devices are
locked, and the root device needs to be unlocked again.

To avoid extra passphrase prompts at initramfs stage, a workaround is
to *unlock via key files stored into the initramfs image*.  Since the
initramfs image now resides on an encrypted device, this still provides
protection for data at rest.  After all for LUK1 the volume key can
already be found by userspace in the Device Mapper table, so one could
argue that including key files to the initramfs image -- created with
restrictive permissions -- doesn't change the threat model for LUKS1
devices.  Please note however that for LUKS2 the volume key is normally
*offloaded to the kernel keyring* (hence no longer readable by
userspace), while key files lying on disk are of course readable by
userspace.

 1. Generate the shared secret (here with 512 bits of entropy as it's also
    the size of the volume key) inside a new file.

        root@debian:~# mkdir -m0700 /etc/keys
    <!-- -->
        root@debian:~# ( umask 0077 && dd if=/dev/urandom bs=1 count=64 of=/etc/keys/root.key conv=excl,fsync )
        64+0 records in
        64+0 records out
        64 bytes copied, 0.000698363 s, 91.6 kB/s

 2. Create a new key slot with that key file.

        root@debian:~# cryptsetup luksAddKey /dev/sda5 /etc/keys/root.key
        Enter any existing passphrase:
    <!-- -->
        root@debian:~# cryptsetup luksDump /dev/sda5 | grep "^Key Slot"
        Key Slot 0: ENABLED
        Key Slot 1: ENABLED
        Key Slot 2: DISABLED
        Key Slot 3: DISABLED
        Key Slot 4: DISABLED
        Key Slot 5: DISABLED
        Key Slot 6: DISABLED
        Key Slot 7: DISABLED

 3. Edit the [`crypttab`(5)] and set the third column to the key file path
    for the root device entry.

        root@debian:~# cat /etc/crypttab
        root_crypt UUID=… /etc/keys/root.key luks,discard,key-slot=1

    The unlock logic normally runs the PBKDF algorithm through each key
    slot sequentially until a match is found.  Since the key file is
    explicitly targeting the second key slot, its index is specified with
    `key-slot=1` in the [`crypttab`(5)] to save useless expensive PBKDF
    computations and *reduce boot time*.

 4. In `/etc/cryptsetup-initramfs/conf-hook`, set `KEYFILE_PATTERN` to a
    `glob`(7) expanding to the key path names to include to the initramfs
    image.

        root@debian:~# echo "KEYFILE_PATTERN=\"/etc/keys/*.key\"" >>/etc/cryptsetup-initramfs/conf-hook

 5. In `/etc/initramfs-tools/initramfs.conf`, set `UMASK` to a restrictive
    value to avoid leaking key material.  See [`initramfs.conf`(5)] for
    details.

        root@debian:~# echo UMASK=0077 >>/etc/initramfs-tools/initramfs.conf

 6. Finally re-generate the initramfs image, and double-check that it
    1/ has restrictive permissions; and 2/ includes the key.

        root@debian:~# update-initramfs -u
        update-initramfs: Generating /boot/initrd.img-4.19.0-4-amd64
    <!-- -->
        root@debian:~# stat -L -c "%A  %n" /initrd.img
        -rw-------  /initrd.img
    <!-- -->
        root@debian:~# lsinitramfs /initrd.img | grep "^cryptroot/keyfiles/"
        cryptroot/keyfiles/root_crypt.key

    (`cryptsetup-initramfs` normalises and renames key files inside the
    initramfs, hence the new file name.)

Should be safe to reboot now :-)  If all went well you should see a
single passphrase prompt.


Using a custom keyboard layout
==============================

GRUB uses the US keyboard layout by default.  Alternative layouts for
the LUKS passphrase prompts can't be loaded from `/boot` or the root
file system, as the underlying devices haven't been mapped yet at that
stage.  If you require another layout to type in your passphrase, then
you'll need to manually generate the core image using
[`grub-mkimage`(1)].  A possible solution is to embed a memdisk
containing the keymap inside the core image.

 1. Create a memdisk (in GNU tar format) with the desired keymap, for
    instance dvorak's.  (The XKB keyboard layout and variant passed to
    `grub-kbdcomp`(1) are described in the [`setxkbmap`(1)] manual.)

        root@debian:~# memdisk="$(mktemp --tmpdir --directory)"
    <!-- -->
        root@debian:~# grub-kbdcomp -o "$memdisk/keymap.gkb" us dvorak
    <!-- -->
        root@debian:~# tar -C "$memdisk" -cf /boot/grub/memdisk.tar .

 2. Generate an early configuration file to embed inside the image.

        root@debian:~# uuid="$(blkid -o value -s UUID /dev/sda1)"
    <!-- -->
        root@debian:~# cat >/etc/early-grub.cfg <<-EOF
			terminal_input --append at_keyboard
			keymap (memdisk)/keymap.gkb
			cryptomount -u ${uuid//-/}

			set root=(cryptouuid/${uuid//-/})
			set prefix=/grub
			configfile grub.cfg
		EOF

    *Note*: This is for the case of a separate `/boot` partition.  If
    `/boot` resides on the root file system, then replace `/dev/sda1`
    with `/dev/sda5` (the LUKS device holding the root file system) and
    set `prefix=/boot/grub`; if it's in a logical volume you'll also
    [need to set][GRUB device syntax] `root=(lvm/DMNAME)`.

    *Note*: You might need to remove the first line if you use a USB
    keyboard, or tweak it if GRUB doesn't see any PC/AT keyboard among its
    available terminal input devices.  Start by specifing `terminal_input`
    in an interactive GRUB shell in order to determine the suitable input
    device.  (Choosing an incorrect device might prevent unlocking if no
    input can be be entered.)

 3. Finally, manually create and install the GRUB image.  Don't use
    `grub-install`(1) here, as we need to pass an early configuration
    and a ramdisk.  Instead, use [`grub-mkimage`(1)] with suitable image
    file name, format, and module list.

        root@debian:~# grub-mkimage \
            -c /etc/early-grub.cfg -m /boot/grub/memdisk.tar \
            -o "$IMAGE" -O "$FORMAT" \
            diskfilter cryptodisk luks gcry_rijndael gcry_sha256 \
            memdisk tar keylayouts configfile \
            at_keyboard usb_keyboard uhci ehci \
            ahci part_msdos part_gpt lvm ext2

    (Replace with `ahci` with a suitable module if the drive holding
    `/boot` isn't a SATA drive supporting AHCI.  Also, replace `ext2`
    with a file system driver suitable for `/boot` if the file system
    isn't ext2, ext3 or ext4.)

    The value of `IMAGE` and `FORMAT` depend on whether GRUB is in EFI
    or BIOS mode.

     a. For EFI mode: `IMAGE="/boot/efi/EFI/debian/grubx64.efi"` and
        `FORMAT="x86_64-efi"`.

     b. For BIOS mode: `IMAGE="/boot/grub/i386-pc/core.img"`,
        `FORMAT="i386-pc"` and set up the image as follows:

            root@debian:~# grub-bios-setup -d /boot/grub/i386-pc /dev/sda

    You can now delete the memdisk and the early GRUB configuration
    file, but note that subquent runs of `grub-install`(1) will override
    these changes.


[`cryptsetup`(8)]: https://manpages.debian.org/cryptsetup.8.en.html
[`crypttab`(5)]: https://manpages.debian.org/crypttab.5.en.html
[`fstab`(5)]: https://manpages.debian.org/fstab.5.en.html
[`initramfs.conf`(5)]: https://manpages.debian.org/initramfs.conf.5.en.html
[`grub-mkimage`(1)]: https://manpages.debian.org/grub-mkimage.1.en.html
[`setxkbmap`(1)]: https://manpages.debian.org/setxkbmap.1.en.html
[GRUB device syntax]: https://www.gnu.org/software/grub/manual/grub/grub.html#Device-syntax

 -- Guilhem Moulin <guilhem@debian.org>, Sun, 09 Jun 2019 16:35:20 +0200
