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
sudoprivileges. - 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.
WarningFormatting a device with LUKS destroys all data on it. Double check the device name in Step 2 before running
luksFormat.
Choosing where to encrypt
There are several layers where you can encrypt data at rest. They protect against different things:
| Layer | Tool | Protects against | Does not protect against |
|---|---|---|---|
| Block device | LUKS / dm-crypt | Lost or reused disks, leaked volume snapshots | Anyone with access to the running, unlocked server |
| Directory | fscrypt (ext4) | Other users reading a locked directory | Root on the running server |
| Database | MySQL/MariaDB/PostgreSQL encryption features | Copies of raw database files | Database users with valid credentials |
| Application | Encrypting fields before storing them | Database dumps and DB administrators | A compromised application server |
| Files and backups | GnuPG, age, restic | Backups copied off the server | Whoever 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
NoteDeleting files does not reliably erase them from the underlying storage, especially on SSDs. Blocks from the old, unencrypted data directory may remain on the root disk. For data that must never exist unencrypted, create the encrypted volume before loading the data, or restore it from an encrypted dump directly onto the volume.
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.
