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.
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.
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.
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:
--new
: initialize and run in-place encryption.--reduce-device-size 16M
: make space for the LUKS1 header--type=luks1
: use LUKS1 encryptionThis 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
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.
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.
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.
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:
And then your system should load in.