This article is part of my Self-Hosting with Kicksecure series. It assumes you’ve already set up Etckeeper GitOps on Kicksecure so all configuration changes are tracked from the beginning.

1. Introduction

I needed encrypted storage for my container data. I decided to use a 4TB SSD with BTRFS for system backups and services that don’t require huge amounts of data. For services that require a lot like Nextcloud, I put them on a ZFS RAID 10 array with 4x 8TB HDDs.

My requirements for the BTRFS storage were:

  • Encryption at rest - If someone steals the drive, they can’t read it
  • Automount at boot - Using a keyfile so the server starts unattended
  • Subvolumes - Separate container data for easier snapshots
  • Compression - Save space where possible

2. Identifying the Drive

First, I identified my target drive by reviewing the size column from the lsblk command. In this case, my device was sdb.

Then got its device ID for reliable reference across reboots:

ls -l /dev/disk/by-id/ | grep sdb

I saw entries like:

ata-CT4000BX500SSD1_XXXXXXXXXXXX -> ../../sdb

I used the ata- prefixed ID—it’s more reliable than /dev/sdb since the latter mapping can be subject to change.

3. Creating LUKS Encryption

Create the LUKS container:

cryptsetup luksFormat --type luks2 /dev/sdb

It prompted me to create a passphrase.

Then I unlocked the encrypted container:

cryptsetup open /dev/sdb storage_crypt

This created /dev/mapper/storage_crypt, a path that allows for accessing the unlocked container.

4. Creating BTRFS Filesystem

I created a BTRFS filesystem on the encrypted container:

mkfs.btrfs -L "StorageName" /dev/mapper/storage_crypt

The output showed filesystem details including UUID and features like extref, skinny-metadata, no-holes, and free-space-tree.

5. Creating Subvolumes

I mounted temporarily to create subvolumes:

mkdir /mnt/storage_name
mount /dev/mapper/storage_crypt /mnt/storage_name

Created a subvolume for data:

btrfs subvolume create /mnt/storage_name/@gitea_data

BTRFS allows you to perform rollbacks on subvolumes by creating snapshots. When you snapshot a subvolume, BTRFS doesn’t copy all the data, it only records new changes. You can snapshot terabytes of data in seconds, and the snapshot only takes up space for the actual differences.

By organizing data into separate subvolumes like @jellyfin_data, @nextcloud_data, @homeassistant_data, and @gitea_data, I can make snapshots that act independently for my different podman services. This makes it easy to roll back a single container’s data without affecting others.

I unmounted when I was done with the mount:

umount /mnt/storage_name

6. Keyfile for Automount

I want to unlock the drive automatically at boot rather than always entering the passphrase. I start by creating a keyfile with random data in it:

dd if=/dev/urandom of=/root/storage.key bs=1 count=32
chmod 400 /root/storage.key

I added it to LUKS so that it can be used to unlock:

cryptsetup luksAddKey /dev/sdb /root/storage.key

I entered my original passphrase when prompted.

7. Configuring crypttab

Got the LUKS UUID:

cryptsetup luksUUID /dev/sdb

Backed up the existing crypttab:

cp /etc/crypttab /etc/crypttab.old

Added the encrypted drive to /etc/crypttab:

echo "storage_crypt UUID=YOUR_DISK_UUID /root/storage.key luks,nofail,discard" >> /etc/crypttab

The options explained:

  • luks - Use LUKS encryption
  • nofail - Boot continues if the drive fails to unlock
  • discard - Enable TRIM for SSDs

9. Configuring fstab

Added the BTRFS mount to /etc/fstab:

echo "/dev/mapper/storage_crypt /mnt/storage_name btrfs defaults,nofail,compress=zstd,noatime,subvol=@data 0 0" >> /etc/fstab

The options:

  • defaults - Standard mount options
  • nofail - Boot continues if mount fails
  • compress=zstd - Transparent compression (good balance of speed and ratio)
  • noatime - Don’t update access times (improves performance)
  • subvol=@data - Mount the @data subvolume I created

10. Testing

Reloaded systemd and tested the mount:

systemctl daemon-reload
mount -a

Verified it mounted:

lsblk

I saw my encrypted container mounted at /mnt/storage_name.

11. Permissions

Set ownership for the container user:

chown -R user:user /mnt/storage_name
chmod 770 /mnt/storage_name

12. Creating Additional Subvolumes

As I added more services, I created separate subvolumes:

mount /dev/mapper/storage_crypt /mnt/storage_name
btrfs subvolume create /mnt/storage_name/@gitea_data
btrfs subvolume create /mnt/storage_name/@cache
umount /mnt/storage_name

Then added additional fstab entries or adjusted container volume mounts.

13. BTRFS Snapshots

One advantage of BTRFS is easy snapshots. I create snapshots before upgrades:

btrfs subvolume snapshot /mnt/storage_name/@data /mnt/storage_name/@data-backup-$(date +%Y%m%d)

14. Troubleshooting

Drive Doesn’t Mount at Boot

Check if the keyfile path is correct in /etc/crypttab. It must be accessible during early boot.

Check journal logs:

journalctl -xe

Wrong Subvolume Mounted

If you’re not seeing your data, check that /etc/fstab includes subvol=@data.

Permission Denied for Containers

If containers can’t write, check ownership:

ls -la /mnt/storage_name

For Podman containers, I use podman unshare chown to set container-internal UIDs.

15. What’s Next

Podman Permissions on Kicksecure