newline

Table of Contents

  1. Why?
  2. How?
  3. Encryption
  4. Post-encryption setup
    1. Creating the keyfile
    2. Adding the keyfile to the initial root file system image
    3. Setting up GRUB for an encrypted disk

Encrypting an existing Linux system with LUKS

Guide, Shell

December 08, 2024

I have an existing disk partition with a GNU/Linux system, formatted as ext4, which is unencrypted. I would like to encrypt this, without having to entirely reformat the partition (which would delete my data). Here’s how I did it.

Why?

Because if you store data on an unencrypted disk, it’s game over as soon as someone has physical access. If your computer gets stolen, your data is readable by anyone. Or someone can just plug a USB into your computer, mount your disk, and read whatever you have on there.

If you encrypt your disk, the data on there is protected at rest. That means when your computer is off, it’s unreadable without a key.

How?

We’ll use LUKS, which is part of the Linux kernel in the dm-crypt implementation, to encrypt the data on the disk. With LUKS, the disk is encrypted with a master key, and the master key is encrypted with each user key (you can have multiple keys, up to 8 in LUKS1). On the disk, there’s a LUKS header which contains several key slots. Each of the slots contains the master key, encrypted with a key derived from input data (such as a passphrase). When you boot the system, you enter a passphrase; that passphrase is passed through a key derivation function (PBKDF2 in LUKS1) to create user key, and that user key is used to decrypt the master key in one of the key slots. The decrypted master key is then used to encrypt and decrypt the actual data on the disk. One advantage is that changing a user key is inexpensive – you don’t have to re-encrypt all of the data.

I omitted some details in this explanation, but it should be enough for an overview of what we’re doing here.

There are many possible disk and encryption configurations, so here’s what my goal is. I have an Ubuntu GNU/Linux system on a single unencrypted ext4-formatted partition, with disk size a bit under 400 GB, and an EFI partition. Because the OS is on one partition, I will be using an encrypted boot configuration. I’m using GRUB, so I will encrypt it with LUKS1, because at the time when I’m writing this, GRUB only has partial support for LUKS2 and doesn’t support Argon 2 (1, 2). If your setup is different, these steps may or may not work in your case (e.g. if you use a different filesystem or partition layout). You’re on your own to find substitutions and changes for your particular case – the Arch Wiki and Gentoo Wiki are good resources.

To follow this post, you need a live Linux environment separate from your main installation (I used a USB with SystemRescueCD, a great distribution with many data recovery and disk manipulation utilities pre-installed), and about 4-5 hours of time. Also, you should have a reliable backup of your data in case something goes wrong. I’ll be assuming you are comfortable working on the command line.

Encryption

First, install the necessary tools – on Ubuntu, this means apt install cryptsetup cryptsetup-bin cryptsetup-initramfs.

Boot into the live environment, and open a terminal as root. Check what partitions you have (you might want to alias this command):

lsblk -o name,label,size,type,fstype,ro,uuid,mountpoints
NAME   LABEL                             SIZE TYPE FSTYPE   RO UUID                                 MOUNTPOINTS
...
└─sda2                                   385G part ext4      0 0795d750-b882-4fb0-afe9-65e8206f3ce7

Note the name of the partition you want to encrypt (in my case /dev/sda2, we’ll call it /dev/system-partition) and the name of the EFI partition (we’ll call it /dev/efi-partition).

The first step is to run a filesystem check and repair as needed:

e2fsck -f /dev/system-partition

Run that command until the output does not say that something has changed.

Next, we want to shrink the filesystem to make space for the LUKS header; this shrinks it down to its minimum possible size (this command took me about 45 minutes to run):

resize2fs -M /dev/system-partition

When that finishes, we’re ready to encrypt the partition:

cryptsetup-reencrypt /dev/system-partition --new --reduce-device-size 16M --type=luks1

Explanation of flags:

This will ask you for a passphrase (what you will use to unlock the partition), and then encrypt your data. For me, it took maybe 2.5 hours to finish.

Then, unlock the partition:

crypsetup open /dev/system-partition rootfs

This will ask you to enter your passphrase, and then it’ll create a mapped device under /dev/mapper/rootfs.

Check what block devices you have available:

lsblk -o name,label,size,type,fstype,ro,uuid,mountpoints

You should see the top-level encrypted partition, and the unlocked rootfs partition below it; note the UUIDs of both:

...
└─sda2                                       385G part  crypto_LUKS  0 8a0bab7e-b245-459a-b564-ec75320b956c
  └─rootfs                                   385G crypt ext4         0 0795d750-b882-4fb0-afe9-65e8206f3ce7 /

Next, resize the filesystem back to its maximum possible size:

resize2fs /dev/mapper/rootfs

Post-encryption setup

Now we’re done with encryption, but if we added this encrypted partition to GRUB, you’d be prompted for your passphrase twice. When Linux boots, it loads the first stage of GRUB, which is unencrypted. That then loads the second stage of GRUB, which however is encrypted (on the boot ‘partition’, which is on the /dev/system-partition partition in my case), so it asks you to decrypt it. Then the second stage loads the kernel and the initramfs, but because there’s currently no way to pass cryptographic material to the kernel, the root partition has to be unlocked again (by the kernel), so you’re prompted for a password again. We have to set up a way for that second decryption to happen automatically.

One solution, and the one I went with, is to use a keyfile: a file, stored in the initramfs image, which acts as another LUKS user key and whose contents are used as the passphrase to unlock the encrypted volume. During the startup process, the bootloader (GRUB) loads the kernel and initial root file system image (initramfs) into memory (RAM), and then starts the kernel, passing it the memory address of the initramfs image. We can put this keyfile into the initramfs image, so the kernel can access it and use it to decrypt data on disk. The initramfs image is stored on the encrypted partition itself (because /boot is encrypted as part of that partition), so it’s safe at rest, which means it’s fine to put the key there. If you’re not using an encrypted /boot, this is not secure and your key will be exposed. The keyfile is owned and only readable by root (permissions 0400), so if someone gets access to it on a running system, you have bigger problems anyway. For LUKS1, this approach doesn’t change the threat model; for LUKS2, it can be argued that it does.

Creating the keyfile

Let’s mount the unlocked partition under /mnt and chroot into it:

# Mount the unlocked encrypted partition
mount /dev/mapper/rootfs /mnt

# Mount the EFI partition
mount /dev/system-partition-efi /mnt/boot/efi

# Mount things required for the chroot
for i in /dev /dev/pts /proc /sys /sys/firmware/efi/efivars /run; do
    mount --bind $i /mnt$i;
done

# Chroot to the unlocked encrypted partition
chroot /mnt

We’ll create a 4096-bit random key:

mkdir /etc/cryptsetup-keys.d
dd if=/dev/urandom bs=4096 count=1 of=/etc/cryptsetup-keys.d/boot_os.key

Restrict permissions on it:

chmod u=rx,go-rwx /etc/cryptsetup-keys.d
chmod u=r,go-rwx /etc/cryptsetup-keys.d/boot_os.key

And add it to LUKS for the partition:

cryptsetup luksAddKey /dev/system-partition /etc/cryptsetup-keys.d/boot_os.key

Now, LUKS knows about the key, but we still need to integrate it into the boot process, by adding it to the initramfs image.

Adding the keyfile to the initial root file system image

Ensure that your key gets copied into the initial ramdisk by putting this line in /etc/cryptsetup-initramfs/conf-hook:

KEYFILE_PATTERN="/etc/cryptsetup-keys.d/*.key"

And set the umask in /etc/initramfs-tools/initramfs.conf to avoid leaking key data:

UMASK=0077

This means that the permissions of the generated initramfs file will be masked by 0077; for example, if you generate the file with permissions 777, the mask will subtract 0077, leaving you with permissions 0700 (read-write-execute only by the owner – root).

And to specify where the key is used (so that it gets included when generating the initramfs image), add this to /etc/crypttab:

rootfs UUID=uuid-of-top-level-encrypted-partition /etc/cryptsetup-keys.d/boot_os.key luks,discard

In my case, uuid-of-top-level-encrypted-partition is 8a0bab7e-b245-459a-b564-ec75320b956c.

Regenerate the initial ramdisk:

update-initramfs -c -u -k all

Flags: -c to create, -u to update, -k all for all kernels.

Now let’s verify that everything was copied correctly. Unpack the image to a temporary directory:

tempd="$(mktemp -d)"
cd $tempd
unmkinitramfs /boot/initrd.img .

Check that ./main/cryptroot/crypttab contains the line referencing rootfs and a key stored inside ./main/cryptroot/keyfiles/. Also check that ./main/cryptroot/keyfiles/ contains the key.

root@ubuntu-server:/tmp/tmp.BHXwpyRa6h# cat main/cryptroot/crypttab
rootfs UUID=8a0bab7e-b245-459a-b564-ec75320b956c /cryptroot/keyfiles/rootfs.key luks,discard
root@ubuntu-server:/tmp/tmp.BHXwpyRa6h# ls main/cryptroot/keyfiles/
rootfs.key

cryptsetup-initramfs normalizes key names inside the initramfs, so that’s why the name and crypttab entry is different.

Setting up GRUB for an encrypted disk

The initramfs image is set up, and now we need to tell GRUB that we’re using an encrypted disk. In /etc/default/grub, remove any references to the root partition from the GRUB_CMDLINE_LINUX variable. Then, make sure the file contains the uncommented line:

GRUB_ENABLE_CRYPTODISK=y

This will instruct grub-mkconfig and grub-install to check for encrypted disks, and generate additional commands to be able to use it.

Update the GRUB config:

update-grub
grub-install /dev/disk-part

disk-part is the root of the disk, e.g. if your encrypted partition is /dev/sda2, then do grub-install /dev/sda.

To verify, check that /boot/grub/grub.cfg has a menuentry with the lines insmod cryptodisk, insmod luks, and a cryptomount instruction referencing your encrypted partition UUID.

Now GRUB can ask you for a passphrase, decrypt the volume, and load the initramfs and kernel. Next, the kernel can decrypt the volume via the key stored in the initramfs image. However, the system might not know how to mount the root filesystem, because it’s now on the encrypted (mapped) volume, so we have to check and potentially modify the fstab. It might be that your fstab is already configured with the correct UUID, so everything will work, but it’s better to verify.

Replace any existing root entry in /etc/fstab with this:

UUID=uuid-of-unlocked-partition / ext4 defaults,errors=remount-ro 0 1

In my case, uuid-of-unlocked-partition is 0795d750-b882-4fb0-afe9-65e8206f3ce7 (from the lsblk above).

Then we can exit the chroot, unmount the filesystem, lock the device, and shut down:

exit
umount -R /mnt
cryptsetup close rootfs
poweroff

Remove whatever device you used for the live environment, then boot up. You should be asked for your passphrase once:

Unlocking the disk at boot

And then your system should load in.