Skip to main content

Command Palette

Search for a command to run...

Using LUKS2 for Encrypted Application Storage on Linux

Updated
•7 min read•View as Markdown

Recently I am asked to add one more layer of protection of disk encryption to our data installed on a cloud development instance.

I decided to use LUKS2, the goals include:

  • Mount the decrypted filesystem under /var/backend

  • Move MongoDB data and middleware onto the encrypted volume

  • Measure performance impact

  • Understand how to expand the encrypted disk later

This article documents the complete process and some practical considerations I encountered along the way.

1. Disk layout

The additional data disk attached to the cloud instance is /dev/vdb:

First, create a GPT partition table and a partition covering almost the entire disk:

sudo parted /dev/vdb --script mklabel gpt mkpart primary 1MiB 100%

Resulting in partition of /dev/vdb1

The storage stack will eventually look like this:

/dev/vdb
  └── /dev/vdb1
        └── LUKS2
              └── /dev/mapper/reframe_crypto
                    └── XFS/ext4
                          └── /var/backend
                                └── mongo/

2. Generate a temporary encryption key

I generated a random 256-bit value with OpenSSL:

KEYHEX=$(openssl rand -hex 32) \
  && printf "%s" "$KEYHEX" | sudo tee /run/luks.key >/dev/null

Why /run?

/run is normally backed by tmpfs, so its contents live in volatile memory and disappear after reboot. This is preferable to casually leaving an encryption key somewhere on the persistent root filesystem.

3. Create the LUKS2 container

Install cryptsetup:

sudo dnf install cryptsetup

Then initialize the partition as LUKS2:

sudo cryptsetup luksFormat \
  --type luks2 \
  --pbkdf argon2id \
  /dev/vdb1 \
  --key-file /run/luks.key

I explicitly selected:

LUKS2
PBKDF: Argon2id

LUKS itself manages the encrypted container and its key slots. Argon2id is the password-based key derivation function used here to make brute-force attacks against passphrases or key material more expensive.

4. Back up the LUKS header

Before going further, I backed up the LUKS header:

sudo cryptsetup luksHeaderBackup \
  /dev/vdb1 \
  --header-backup-file /run/reframe.luks.hdr

The LUKS header contains critical metadata required to unlock the encrypted volume. Damage to the header can make the data inaccessible.

Keep the backup to a a secure location separate from the encrypted disk.

5. Open the encrypted volume

Open the LUKS container and create a device-mapper mapping called reframe_crypto:

sudo cryptsetup open \
  /dev/vdb1 \
  reframe_crypto \
  --key-file /run/luks.key

After the volume is successfully opened, the temporary key can be removed:

sudo rm -f /run/luks.key

The decrypted block device is now available as:

/dev/mapper/reframe_crypto

We can verify it with:

sudo cryptsetup status reframe_crypto

For example:

/dev/mapper/reframe_crypto is active and is in use.
type:    LUKS2
cipher:  aes-xts-plain64
keysize: 512 [bits]
device:  /dev/vdb1
mode:    read/write

A potentially confusing detail is:

cipher:  aes-xts-plain64
keysize: 512 bits

This is not AES-512 because AES-XTS uses two AES keys. A 512-bit XTS key corresponds to two 256-bit AES keys.

6. Create the filesystem

Initially I formatted the decrypted device as ext4:

sudo mkfs.ext4 /dev/mapper/reframe_crypto

Then create the mount point:

sudo mkdir -p /var/backend

and mount it:

sudo mount /dev/mapper/reframe_crypto /var/backend

Later I decided to reformat it as XFS, which recommended by Mongo. Do notice this is reformat, Any existing data will be cleared.

First unmount it:

sudo umount /var/backend

Then create the XFS filesystem:

sudo mkfs.xfs -f /dev/mapper/reframe_crypto

and mount it again:

sudo mount /dev/mapper/reframe_crypto /var/backend

7. Quick sequential I/O test with dd

Before moving the application data, I did some simple disk tests.

For the root filesystem:

dd if=/dev/zero of=/tmp/testfile \
  bs=1G count=1 oflag=direct

For the encrypted filesystem:

dd if=/dev/zero of=/var/backend/testfile \
  bs=1G count=1 oflag=direct

Read tests:

dd if=/tmp/testfile of=/dev/null \
  bs=1G iflag=direct

and:

dd if=/var/backend/testfile of=/dev/null \
  bs=1G iflag=direct

Using direct I/O helps reduce the influence of the Linux page cache, although dd should still be considered a quick sanity check rather than a comprehensive storage benchmark.

8. Random I/O benchmark with fio

For a more realistic random I/O comparison, I installed fio:

sudo dnf install fio

I tested the normal filesystem first:

fio \
  --name=randrw \
  --ioengine=libaio \
  --rw=randrw \
  --bs=4k \
  --size=1G \
  --numjobs=4 \
  --runtime=60 \
  --group_reporting \
  --directory=/tmp

Result:

READ:  bw=4873KiB/s (4990kB/s)
WRITE: bw=4897KiB/s (5015kB/s)

Then I ran the same workload against the encrypted filesystem:

fio \
  --name=randrw \
  --ioengine=libaio \
  --rw=randrw \
  --bs=4k \
  --size=1G \
  --numjobs=4 \
  --runtime=60 \
  --group_reporting \
  --directory=/var/backend

Result:

READ:  bw=4359KiB/s (4463kB/s)
WRITE: bw=4385KiB/s (4490kB/s)

In this particular test, the encrypted filesystem delivered roughly 10% lower bandwidth.

I also checked whether the CPU include AES acceleration ability:

grep aes /proc/cpuinfo

Modern CPUs mostly com with hardware AES acceleration.

9. Move MongoDB onto the encrypted volume

Next I created a dedicated MongoDB directory:

sudo mkdir -p /var/backend/mongo
sudo chown mongod:mongod /var/backend/mongo

Then I changed MongoDB's storage path in:

/etc/mongod.conf

to

storage:
  dbPath: /var/backend/mongo

After migrating the existing MongoDB data as required, MongoDB can be restarted:

sudo systemctl restart mongod && sleep 3 && sudo systemctl status mongod

10. Prevent services from starting before the disk is unlocked

This was an important operational consideration.

After a reboot, the LUKS volume is locked:

/dev/vdb1
      ↓
   [LOCKED]

Therefore:

/var/backend

is not yet backed by the encrypted filesystem.

If MongoDB starts automatically before the LUKS volume is opened and mounted, it may see the ordinary /var/backend directory from the root filesystem instead.

That can lead to a particularly confusing situation:

/var/backend

appears to contain one set of files before the mount and another set after the encrypted filesystem is mounted.

Nothing has magically reappeared or disappeared because a Linux mount simply hides the contents of the original mount-point directory while another filesystem is mounted over it.

For this reason I disabled automatic MongoDB and PM2 auto startup in this setup:

sudo systemctl disable mongod
systemctl disable pm2-root.service

The intended boot sequence became:

Boot
  ↓
Obtain LUKS key
  ↓
cryptsetup open
  ↓
mount /var/backend
  ↓
start MongoDB
  ↓
start application processes

For example:

sudo cryptsetup open \
  /dev/vdb1 \
  reframe_crypto \
  --key-file /run/luks.key

sudo rm -f /run/luks.key

sudo mount /dev/mapper/reframe_crypto /var/backend

sudo systemctl start mongod

pm2 resurrect

For a fully automated production system, I would prefer expressing these dependencies through crypttab, fstab, and systemd dependencies rather than relying on manual command ordering.

The key point is that the application must never start under the assumption that /var/backend is mounted when it is not.

another way is add "RequiresMountsFor" to mongo systemd unit file

[Unit]
RequiresMountsFor=/var/backend

11. Expanding the encrypted disk

Disk expansion with LUKS involves multiple layers.

Conceptually:

Physical/virtual disk
        ↓
Partition
        ↓
LUKS mapping
        ↓
Filesystem

Each relevant layer needs to know about the additional space.

First expand partition 1:

sudo growpart /dev/vdb 1

Then resize the active LUKS mapping:

sudo cryptsetup resize reframe_crypto

Finally, expand the filesystem.

For ext4:

sudo resize2fs /dev/mapper/reframe_crypto

For XFS, the command is different.

Because XFS grows while mounted, use:

sudo xfs_growfs /var/backend

While resize2fs which applies to ext filesystems, after reformatting the volume as XFS, xfs_growfs is the appropriate filesystem-level expansion command.

So for my final XFS setup the expansion sequence is:

sudo growpart /dev/vdb 1

sudo cryptsetup resize reframe_crypto

sudo xfs_growfs /var/backend

12. What I learned

For me, this is a good hands-on experience of implementing LUKS in a real application environment, integrating the encrypted storage cleanly into the application's boot sequence, data layout, backup strategy, and operational procedures. For real production environment, there will be more factors like how and where to keep the LUKS decryption key to think about, in the end, LUKS is more for keeping data at rest against offline access (like being cloned in EBS snapshot service) not online usage after unlocked.

4 views