Tech Articles

Setting up LUKS on a new SSD

I’m not a linux OS expert even though I’ve been using linux for over twenty years (that said, I’d say that most anyone who installs and uses linux has to have a good amount of technical knowledge). It wasn’t until somewhat recently that I realized I really should have Full Disk Encryption (FDE) on the NVMe SSD drives that I’ve been installing on the last few computers builds I’ve done (and, for sure, it’s wise to use some encryption if there’s any private or important information on a drive–a simple delete of files doesn’t actually overwrite the data on the disk of deleted files usually; this advice applies regardless of NVMe or SATA form-factor). So, as I’ve been building newer systems and adding drives, I’ve been setting them up with with FDE using LUKS (Linux Unified Key Setup), and have been quite happy with the simplicity and the security of it all.

This article goes over the steps needed to set up LUKS on a new, non-boot SSD drive–in this case it’s a new 1TB Crucial MX500 2.5″ SATA drive.

I’m going to label Step 0 as “install the physical SSD drive in the computer” and not go into any details at all there. For my setup, I just plugged it into a SATA connector and confirmed via UEFI setup screens that it’s detectable at the hardware level.

Step 1 is to locate the block device in linux itself. While a non-root user can do a few of these commands, you really need to be root, and I tend to just do a simple sudo bash and then do all the commands while in the root shell. So open up a shell:

[scott] $ sudo bash
[root] # 

and list block devices:

[root] # lsblk
NAME                                          MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
sda                                             8:0    0 931.5G  0 disk  
sr0                                            11:0    1  1024M  0 rom   
zram0                                         252:0    0     8G  0 disk  [SWAP]
nvme0n1                                       259:0    0   1.8T  0 disk  
├─nvme0n1p1                                   259:1    0   600M  0 part  /boot/efi
├─nvme0n1p2                                   259:2    0     1G  0 part  /boot
└─nvme0n1p3                                   259:3    0   1.8T  0 part  
  └─luks-822c7f1b-7d42-48ac-b2f6-3758ceecc5b3 253:0    0   1.8T  0 crypt /home
                                                                         /
nvme1n1                                       259:4    0 931.5G  0 disk  
└─nvme1n1p1                                   259:5    0 931.5G  0 part  
  └─crucialx                                  253:1    0 931.5G  0 crypt /crucial

You can see the already-setup FDE on crucialx and luks-822c7f1b-7d42-48ac-b2f6-3758ceecc5b3–both have type crypt. The new MX500 is a SATA drive and it shows up as sda–not even partitioned, so the next step is to partition it.

Note also that there’s another way to list the new device: fdisk -l, one advantage of which is that it also displays the full /dev path and the drive model info:

[root] # fdisk -l
Disk /dev/sda: 931.51 GiB, 1000204886016 bytes, 1953525168 sectors
Disk model: CT1000MX500SSD1 
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
...

(output has been truncated to show just the new drive).

Partitioning can be done with fdisk (or, if the device is more than 2TB, the GNU utility parted needs to be used) and the steps for this are to create a new primary partition (assuming all of the drive is to be part of the FDE process), write out the partition table, and then make sure the OS uses this new information:

[root@titan scott]# fdisk /dev/sda

Welcome to fdisk (util-linux 2.38).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Device does not contain a recognized partition table.
Created a new DOS disklabel with disk identifier 0x0cbc7bb1.


Command (m for help): n
Partition type
   p   primary (0 primary, 0 extended, 4 free)
   e   extended (container for logical partitions)
Select (default p): p
Partition number (1-4, default 1): 
First sector (2048-1953525167, default 2048): 
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-1953525167, default 1953525167): 

Created a new partition 1 of type 'Linux' and of size 931.5 GiB.

Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.


[root] # partprobe

Use lsblk to see the new partition:

[root@titan scott]# lsblk
NAME                                          MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
sda                                             8:0    0 931.5G  0 disk  
└─sda1                                          8:1    0 931.5G  0 part  

The new partition is /dev/sda1. Now it’s time for LUKS setup.

LUKS setup really requires that you understand a bit of the overall model of it before jumping in. The high level idea of how it operates here is that LUKS sits between the filesystem (such as btrfs or ext4) and the raw block device (like /dev/sda1) doing the encryption of data written and the decryption of data read. The LUKS command that manages this in-between part is cryptsetup and the basic operations needed for this management are as follows:

  • cryptsetup luksFormat: formats a raw block device so it can be used with the LUKS system
  • cryptsetup luksOpen: opens up a formatted LUKS block device and assigns a logical ID to id
  • cryptsetup luksClose: closes a device opened up with luksOpen
  • cryptsetup luksAddKey: adds a new encryption key to the device
  • cryptsetup luksDump: lists the encryption key info on the device

As LUKS uses AES256 encryption, the question of how the keys are stored comes up. It turns out that there’s a single AES256 master key and it’s encrypted by secondary keys. LUKS provides 8 “slots” (slot storage is created during the luksFormat operation and are filled via the luksAddKey command) to store the keys used to decrypt the master key. The keys stored in these 8 slots can be created via a passphrase that’s stretched into the required number of bits (LUKS version 2 uses Argon2 for that), or via a good secure random number (bit) generator. Both of these methods will be used so that the FDE is automounted during bootup.

The usefulness of the 8 slots is that you can specify a passphrase (or two) while having another secure random key of 256 bits that can be used for scripts or during bootup, all of which will allow decryption of the master key that is used for actual encryption and decryption. (And, having multiple slots allows multiple people to unlock, say, a boot drive that has FDE without sharing passwords.)

Before jumping into the actual operations to use LUKS, it’s important to understand the logical ID that is required for some operations: this logical ID is the “handle” to a block device/partition that’s been opened by LUKS and it shows up in the file system (under /dev/mapper/). It must be unique across mapped/opened block devices and I’ve found it useful to have a convention of appending x to a useful name to indicate that it’s encrypted, like mx500x.

Step 2 is to put it all together:

[root]# cryptsetup luksFormat /dev/sda1

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

Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for /dev/sda1: 
Verify passphrase: 

[root]# cryptsetup luksOpen /dev/sda1 mx500x
Enter passphrase for /dev/sda1: 
[root]# ls -l /dev/mapper/
total 0
crw-------. 1 root root 10, 236 Mar  5 06:50 control
lrwxrwxrwx. 1 root root       7 Mar  5 06:50 crucialx -> ../dm-1
lrwxrwxrwx. 1 root root       7 Mar  5 06:50 luks-822c7f1b-7d42-48ac-b2f6-3758ceecc5b3 -> ../dm-0
lrwxrwxrwx. 1 root root       7 Mar  5 07:53 mx500x -> ../dm-2
[root]# 

For the passphrase, choose a secure (long) one and do not forget it as you will lose access to all data on the FDE drive (well, not 100% true if you set things up for automount by using the secure random key generated during that process). And to show the effect of closing a logical ID:

[root]# cryptsetup luksClose mx500x
[root]# ls -l /dev/mapper/
total 0
crw-------. 1 root root 10, 236 Mar  5 06:50 control
lrwxrwxrwx. 1 root root       7 Mar  5 06:50 crucialx -> ../dm-1
lrwxrwxrwx. 1 root root       7 Mar  5 06:50 luks-822c7f1b-7d42-48ac-b2f6-3758ceecc5b3 -> ../dm-0
[root]# 

At this point all the LUKS setup has been done and it’s time for Step 4 where the drive is formatted. The device does need to be opened (via cryptsetup luksOpen) and then it’s easily formatted; I have recently been using btrfs instead of ext4, so the following is what I did:

[root]# cryptsetup luksOpen /dev/sda1 mx500x
Enter passphrase for /dev/sda1: 
[root]# mkfs.btrfs /dev/mapper/mx500x 
btrfs-progs v6.1.3
See http://btrfs.wiki.kernel.org for more information.

NOTE: several default settings have changed in version 5.15, please make sure
      this does not affect your deployments:
      - DUP for metadata (-m dup)
      - enabled no-holes (-O no-holes)
      - enabled free-space-tree (-R free-space-tree)

Label:              (null)
UUID:               8bc72d72-09f9-478a-b21e-dc0a76bf7eda
Node size:          16384
Sector size:        4096
Filesystem size:    931.50GiB
Block group profiles:
  Data:             single            8.00MiB
  Metadata:         DUP               1.00GiB
  System:           DUP               8.00MiB
SSD detected:       yes
Zoned device:       no
Incompat features:  extref, skinny-metadata, no-holes
Runtime features:   free-space-tree
Checksum:           crc32c
Number of devices:  1
Devices:
   ID        SIZE  PATH
    1   931.50GiB  /dev/mapper/mx500x

and then to mount it:

[root]# mkdir /mx500
[root]# mount /dev/mapper/mx500x /mx500/
[root]# df -h
Filesystem            Size  Used Avail Use% Mounted on
devtmpfs              4.0M     0  4.0M   0% /dev
tmpfs                  31G  736M   31G   3% /dev/shm
tmpfs                  13G  2.4M   13G   1% /run
/dev/dm-0             1.9T  267G  1.6T  15% /
tmpfs                  31G   45M   31G   1% /tmp
/dev/dm-0             1.9T  267G  1.6T  15% /home
/dev/nvme0n1p2        974M  318M  589M  36% /boot
/dev/nvme0n1p1        599M   18M  582M   3% /boot/efi
/dev/mapper/crucialx  932G  367G  565G  40% /crucial
tmpfs                 6.2G  2.4M  6.2G   1% /run/user/1000
/dev/mapper/mx500x    932G  3.8M  930G   1% /mx500

At this point /mx500 is fully usable and all data written to it will be encrypted.

For convenience I (strongly) suggest setting it up so that the FDE drive is automounted during OS bootup. This can be a bit more complicated than desired if you have more than one NVMe drive, as the assignment of /dev drive is dependent on the scan done during bootup and isn’t 100% predictable (as I found out later). If you do have more than one NVMe drive, read the section lower down also.

Setting up automount during bootup requires the following:

  • create a secure random key, saved to a protected file
  • edit /etc/crypttab to add a line that will do an automatic open on the device during bootup
  • edit /etc/fstab to add a line that will do an automatic mount of the opened device

I’ve put the the secure random key file under /root directory as that’s protected and isn’t likely to be mistakenly deleted or modified. It can be created like this:

[root]# dd if=/dev/random bs=32 count=1 of=/root/lukskey.mx500x
1+0 records in
1+0 records out
32 bytes copied, 5.4303e-05 s, 589 kB/s

If you haven’t used dd (“data duplicator”) before, what that command is doing is copying 32 bytes (bs=32 count=1: 1 block of size 32) from /dev/random to /root/lukskey.mx500x. It’s a very useful command (but can be dangerous if you don’t know what you’re doing as it can and will write to raw block devices (and totally destroy the data on them).

Now the new 32-byte key must be added to one of the 8 slots:

[root] cryptsetup luksAddKey /dev/sda1 /root/lukskey.mx500x
Enter any existing passphrase: 
[root]

Once you’ve created the key file, add a line like the following to /etc/crypttab (which automates the luksOpen done manually above):

mx500x   /dev/sda1  /root/lukskey.mx500x

and finally a line like the following can be added to /etc/fstab (which automates the mount done manually above):

/dev/mapper/mx500x   /mx500   btrfs   defaults  0 0

And that’s all the setup needed if you don’t have more than one NVMe drive. At this point, double-check paths that were entered into /etc/*tab files as errors here will cause bootup problems. Reboot the computer to test it all out to make sure everything gets automounted.

If you have more than one NVMe drive, then it’s necessary to use the UUID of the partition as the actual drive number (e.g., the 0 in /dev/nvme0n1) is assigned during a boot-time scan of devices. And there are actually two UUIDs that need to be used: one for use in /etc/crypttab and one for use in /etc/fstab. In order to get the UUIDs, use the following command (the important UUIDs are in bold):

$ sudo lsblk -o +name,mountpoint,uuid
[root@titan scott]# lsblk -o +name,uuid
NAME                                          MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS NAME                  UUID
sda                                             8:0    0 931.5G  0 disk              sda                   
└─sda1                                          8:1    0 931.5G  0 part              sda1                  561e1265-0150-437c-b979-4bf4a5a1ff49
  └─mx1000x                                   253:1    0 931.5G  0 crypt /mx1000     mx1000x               8bc72d72-09f9-478a-b21e-dc0a76bf7eda
sdb                                             8:16   0   3.6T  0 disk              sdb                   
└─sdb1                                          8:17   0   3.6T  0 part              sdb1                  d8347463-e812-4293-ac9b-c9dae64d2992
  └─mx4000x                                   253:2    0   3.6T  0 crypt /mx4000     mx4000x               331eef2c-4ef9-47d7-a856-8ef28e928a90
zram0                                         252:0    0     8G  0 disk  [SWAP]      zram0                 
nvme0n1                                       259:0    0   1.8T  0 disk              nvme0n1               
├─nvme0n1p1                                   259:1    0   600M  0 part  /boot/efi   nvme0n1p1             B4CD-57EE
├─nvme0n1p2                                   259:2    0     1G  0 part  /boot       nvme0n1p2             c0d6df9c-9664-4174-99a3-4c207a52f318
└─nvme0n1p3                                   259:3    0   1.8T  0 part              nvme0n1p3             822c7f1b-7d42-48ac-b2f6-3758ceecc5b3
  └─luks-822c7f1b-7d42-48ac-b2f6-3758ceecc5b3 253:0    0   1.8T  0 crypt /home       luks-822c7f1b-7d42-48ac-b2f6-3758ceecc5b3
                                                                                                           66cead1d-0664-4f46-99a8-0b3ddb8891f5
                                                                         /                                 
nvme1n1                                       259:4    0 931.5G  0 disk              nvme1n1               
└─nvme1n1p1                                   259:5    0 931.5G  0 part              nvme1n1p1             40818881-4df8-45a2-8c34-390e1faaca5d
  └─luks-40818881-4df8-45a2-8c34-390e1faaca5d 253:3    0 931.5G  0 crypt /crucial    luks-40818881-4df8-45a2-8c34-390e1faaca5d
                                                                                                           2686e2f5-4a18-4be3-9340-79296349a7af

The first UUID (that’s associated with the crypt type partition of the device) is used in /etc/crypttab like so:

luks-40818881-4df8-45a2-8c34-390e1faaca5d UUID=40818881-4df8-45a2-8c34-390e1faaca5d /root/lukskey.crucialx

and the second UUID (that’s associated with the mounted partition) is used in /etc/fstab like so:

UUID=2686e2f5-4a18-4be3-9340-79296349a7af /crucial                btrfs   defaults        0 0

(I haven’t verified the following but I think that it’s necessary to do a luksOpen and mount of the partition to get the second UUID.)