Encryption at rest protects stored data when someone gets hold of the storage itself: a decommissioned disk, a leaked volume snapshot or a stolen backup. On Linux, the standard tool is LUKS (Linux Unified Key Setup), managed with cryptsetup, which encrypts a whole block device so every file system, database and log written to it is encrypted transparently. In this tutorial you will encrypt a secondary data volume on Ubuntu 24.04 with LUKS2, mount it automatically at boot, move a MySQL data directory onto it, and set up the key and header backups you need to avoid losing the data.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • A second, empty disk or volume attached to the server. This guide uses /dev/vdb; your device name may differ.
  • Optionally, MySQL or MariaDB installed, for Step 6.

Choosing where to encrypt

There are several layers where you can encrypt data at rest. They protect against different things:

LayerToolProtects againstDoes not protect against
Block deviceLUKS / dm-cryptLost or reused disks, leaked volume snapshotsAnyone with access to the running, unlocked server
Directoryfscrypt (ext4)Other users reading a locked directoryRoot on the running server
DatabaseMySQL/MariaDB/PostgreSQL encryption featuresCopies of raw database filesDatabase users with valid credentials
ApplicationEncrypting fields before storing themDatabase dumps and DB administratorsA compromised application server
Files and backupsGnuPG, age, resticBackups copied off the serverWhoever holds the key

LUKS is the right base layer for a server: it is built into the kernel, fast on CPUs with AES instructions and requires no application changes. Encrypt backups separately, since they leave the encrypted volume as plain files.

Step 1 - Installing cryptsetup and checking hardware acceleration

Install cryptsetup, which is usually already present on Ubuntu:

sudo apt update
sudo apt install cryptsetup

Check the version and whether your CPU supports AES instructions, which make encryption nearly free in performance terms:

cryptsetup --version
grep -m1 -o -w aes /proc/cpuinfo
cryptsetup 2.7.0
aes

If the second command prints nothing, the CPU (or the virtual CPU model your VPS exposes) does not offer AES-NI. LUKS still works but uses more CPU. You can compare cipher speeds with a quick benchmark:

cryptsetup benchmark --cipher aes-xts-plain64
# Tests are approximate using memory only (no storage IO).
#     Algorithm |       Key |      Encryption |      Decryption
    aes-xts        512b      2412.6 MiB/s      2420.3 MiB/s

Step 2 - Identifying the target device

List block devices with their file systems to find the empty disk:

lsblk -f
NAME    FSTYPE FSVER LABEL UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
vda
├─vda1  ext4   1.0         2b1c6a0e-9f0b-4f8e-8d1f-6c1b8f2a7e11   31.2G    18% /
├─vda14
└─vda15 vfat   FAT32       6A2B-91C4                               98.3M     6% /boot/efi
vdb

vdb has no file system and no mount point, so it is the new volume. If it shows a file system you do not recognize, stop and find out what it contains before continuing.

Step 3 - Creating the LUKS2 volume

Initialize the device as a LUKS2 container. Ubuntu 24.04 defaults to LUKS2 with the aes-xts-plain64 cipher, a 512-bit key (AES-256 in XTS mode) and the memory-hard argon2id key derivation function, so no extra options are needed:

sudo cryptsetup luksFormat /dev/vdb

Type YES in capital letters to confirm, then enter a strong passphrase twice:

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

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

This passphrase is your recovery key. Store it in a password manager; you will use a key file for everyday unlocking in Step 5.

Inspect the new header to confirm the settings:

sudo cryptsetup luksDump /dev/vdb | grep -E "Version|cipher|PBKDF"
Version:       	2
	cipher: aes-xts-plain64
	PBKDF:      argon2id

Step 4 - Opening the volume and creating a file system

Unlock the container. This creates a decrypted device at /dev/mapper/securedata; everything written to it is encrypted before it reaches /dev/vdb:

sudo cryptsetup open /dev/vdb securedata

Enter the passphrase when prompted. Create an ext4 file system on the decrypted device and mount it:

sudo mkfs.ext4 -L securedata /dev/mapper/securedata
sudo mkdir -p /srv/secure
sudo mount /dev/mapper/securedata /srv/secure

Verify the device stack. The crypt layer sits between the disk and the mounted file system:

lsblk -o NAME,TYPE,FSTYPE,SIZE,MOUNTPOINTS /dev/vdb
NAME           TYPE  FSTYPE        SIZE MOUNTPOINTS
vdb            disk  crypto_LUKS    50G
└─securedata   crypt ext4           50G /srv/secure

You can also check the mapping status (output trimmed):

sudo cryptsetup status securedata
/dev/mapper/securedata is active and is in use.
  type:    LUKS2
  cipher:  aes-xts-plain64
  keysize: 512 bits
  device:  /dev/vdb
  mode:    read/write

Step 5 - Unlocking the volume automatically at boot

A remote server usually has nobody available to type a passphrase at boot. The common solution is a key file stored on the root disk and referenced in /etc/crypttab.

Be clear about what this protects. With the key file on the (unencrypted) root disk, anyone who obtains both disks, or root access to the running server, can read the data. It still protects the data volume if it is detached, reused or snapshotted on its own, which is the main risk for a separate volume. If you need protection against someone with the whole server image, keep the volume locked at boot and unlock it manually or from a key server after each reboot.

Create a directory for keys, readable only by root, and generate a random 4096-bit key file:

sudo install -d -m 0700 /etc/cryptsetup-keys.d
sudo dd if=/dev/urandom of=/etc/cryptsetup-keys.d/securedata.key bs=512 count=1
sudo chmod 0400 /etc/cryptsetup-keys.d/securedata.key

Add the key file to a new key slot. You are asked for the existing passphrase to authorize the change:

sudo cryptsetup luksAddKey /dev/vdb /etc/cryptsetup-keys.d/securedata.key

Confirm that two key slots are now in use (slot 0 is the passphrase, slot 1 the key file):

sudo cryptsetup luksDump /dev/vdb | grep -E "^\s+[0-9]+: luks2"
  0: luks2
  1: luks2

Find the UUID of the LUKS device, which is stable across reboots unlike /dev/vdb:

sudo blkid -s UUID -o value /dev/vdb
8f3d2a61-5c47-4b0e-9e21-7d6a4c1f9b30

Open the crypttab file:

sudo nano /etc/crypttab

Add a line with the mapping name, the device UUID, the key file and the options. Replace the UUID with your own:

securedata UUID=8f3d2a61-5c47-4b0e-9e21-7d6a4c1f9b30 /etc/cryptsetup-keys.d/securedata.key luks

Then open the file system table:

sudo nano /etc/fstab

Add a line to mount the decrypted device. nofail lets the server finish booting even if the volume cannot be unlocked, so you can still log in and fix it:

/dev/mapper/securedata  /srv/secure  ext4  defaults,nofail  0  2

Test the whole chain without rebooting. Unmount and close the volume, reload systemd so it reads the new files, and start the generated unlock unit:

sudo umount /srv/secure
sudo cryptsetup close securedata
sudo systemctl daemon-reload
sudo systemctl start [email protected]
sudo mount -a
findmnt /srv/secure
TARGET      SOURCE                 FSTYPE OPTIONS
/srv/secure /dev/mapper/securedata ext4   rw,relatime

The volume was unlocked with the key file, without a passphrase prompt. Reboot once during a maintenance window and run findmnt /srv/secure again to confirm that it also works at boot.

Step 6 - Moving a MySQL data directory onto the encrypted volume

With the volume in place, put your sensitive data on it. A bind mount keeps the original path, so MySQL's configuration and Ubuntu's AppArmor profile keep working unchanged. The same approach works for PostgreSQL (/var/lib/postgresql) or an application's upload directory.

Stop MySQL and copy its data directory, preserving ownership and permissions:

sudo systemctl stop mysql
sudo rsync -a /var/lib/mysql/ /srv/secure/mysql/

Move the original directory aside and create an empty mount point:

sudo mv /var/lib/mysql /var/lib/mysql.old
sudo mkdir /var/lib/mysql

Open /etc/fstab again:

sudo nano /etc/fstab

Add the bind mount below the line you added in Step 5. The x-systemd.requires-mounts-for option makes sure it is mounted only after /srv/secure:

/srv/secure/mysql  /var/lib/mysql  none  bind,nofail,x-systemd.requires-mounts-for=/srv/secure  0  0

MySQL must never start on an empty directory if the volume is missing, because it would create a new, empty database. Make the service depend on the mount with a drop-in:

sudo systemctl edit mysql
[Unit]
RequiresMountsFor=/var/lib/mysql

Mount the directory and start MySQL:

sudo systemctl daemon-reload
sudo mount /var/lib/mysql
sudo systemctl start mysql

Verify that the data directory is on the encrypted device and that your databases are there:

findmnt /var/lib/mysql
sudo mysql -e "SHOW DATABASES;"
TARGET         SOURCE                                FSTYPE OPTIONS
/var/lib/mysql /dev/mapper/securedata[/mysql]        ext4   rw,relatime

Once you have confirmed that the application works, delete the old copy:

sudo rm -rf /var/lib/mysql.old

Step 7 - Backing up the LUKS header and managing keys

The LUKS header at the start of the device holds the encrypted master key. If it is damaged, the data is unrecoverable, even with the right passphrase. Back it up:

sudo cryptsetup luksHeaderBackup /dev/vdb --header-backup-file /root/securedata-luks-header.img
sudo chmod 0400 /root/securedata-luks-header.img

Copy this file off the server to a secure location, together with the passphrase from Step 3, and delete the local copy. Keep in mind that the header backup plus any valid passphrase or key file decrypts the volume, even after you later remove that passphrase from the live header. Protect it like the data itself.

If the header is ever corrupted, restore it with:

sudo cryptsetup luksHeaderRestore /dev/vdb --header-backup-file /path/to/securedata-luks-header.img

To rotate a passphrase, for example when an administrator leaves, add the new one and then remove the old one. luksRemoveKey asks for the passphrase to delete:

sudo cryptsetup luksAddKey /dev/vdb
sudo cryptsetup luksRemoveKey /dev/vdb

This changes who can unlock the volume but not the master key that encrypts the data. To replace the master key itself, LUKS2 can re-encrypt the device online with sudo cryptsetup reencrypt /dev/vdb. It rewrites every block, so take a backup first, run it during a quiet period, and create a new header backup afterwards.

Step 8 - Encrypting backups and exported files

Files copied off the encrypted volume, such as database dumps, are plain text again. Encrypt them before they leave the server. For an occasional file, symmetric encryption with GnuPG is enough:

gpg --symmetric --cipher-algo AES256 your_database.sql

GnuPG asks for a passphrase and writes your_database.sql.gpg. Delete the unencrypted file after checking that the encrypted one decrypts:

gpg --decrypt your_database.sql.gpg | head -n 3
rm your_database.sql

For automated backups, use public key encryption so the server can encrypt but never decrypt, or a backup tool that encrypts by default, such as restic or BorgBackup.

Troubleshooting

[email protected] fails at boot. Check the error with journalctl -b -u [email protected]. The usual causes are a wrong UUID in /etc/crypttab (compare with sudo blkid /dev/vdb) or a key file path typo. Because of nofail, the server still boots and you can log in to fix it.

No key available with this passphrase. The passphrase or key file does not match any key slot. Try the recovery passphrase from Step 3. If none work and the header was overwritten, restore it from the backup made in Step 7.

MySQL does not start after a reboot. Run findmnt /var/lib/mysql. If nothing is mounted, the encrypted volume did not unlock; fix that first, then run sudo mount -a and sudo systemctl start mysql. The RequiresMountsFor drop-in is what prevented MySQL from starting on an empty directory.

Conclusion

You encrypted a data volume with LUKS2, configured automatic unlocking with a key file through crypttab, moved a MySQL data directory onto it with a bind mount, and backed up the LUKS header and passphrase needed for recovery. Your stored data is now protected against lost disks and leaked snapshots. As next steps, encrypt your offsite backups with public key encryption, restrict and audit who can log in to the unlocked server, and document your key management procedure for compliance frameworks such as GDPR or PCI DSS.