When running servers I want to encrypt the data stored on them. The problem you then pretty quickly run into is that it’s hard to actually boot with an encrypted root. I’ve solved this problem in the past by having a tinysshd in my initramfs which prompts me for a password to unlock the volumes. Though this works, it’s annoying in that the server isn’t able to boot at all, causing any additional monitoring I have to not work. There are also some services on machines that don’t need any encrypted storage and that I’d be happy to have start unattended.
It turns out though, systemd provides us all the tools we need to achieve this kind of split boot.
Setting up the encrypted disks
In my servers there’s usually 2 disks, with a bunch of partitions including a data partition. The data partition is first ecnrypted with LUKS and then turned into a btrfs RAID1.
As a consequence, I have something along the following in /etc/crypttab
:
data1 UUID=<UUID> none luks,noauto
data2 UUID=<UUID> none luks,noauto
The noauto
is very important here, we don’t want to attempt to unlock these
disks at boot. The UUID
comes from blkid /dev/nvme0n1pX
etc.
This also provides us with our first building block. For every of these disks,
systemd will automatically generate a service,
systemd-cryptsetup@<NAME>.service
. This is done automatically for you by
systemd-cryptsetup-generator
.
Having these services is rather handy. You can start them by hand using
systemctl
, and systemd will prompt you for the password on the TTY.
Mounting the encrypted disks
Having these cryptsetup services is rather handy, but we need more than that, we need them mounted. Without them being mounted, we won’t be able to use the data stored on them.
Filesystem mounts are defined in /etc/fstab
, and I have something like this
in it:
UUID=<UUID> /var/lib/docker btrfs defaults,noauto,noatime,ssd,subvolid=256,subvol=/docker,x-mount.mkdir,x-systemd.requires=systemd-cryptsetup@data1.service,x-systemd.requires=systemd-cryptsetup@data2.service 0 2
UUID=<UUID> /data btrfs defaults,noauto,noatime,ssd,subvolid=259,subvol=/data,x-mount.mkdir,x-systemd.automount,x-systemd.requires=systemd-cryptsetup@data1.service,x-systemd.requires=systemd-cryptsetup@data2.service 0 2
UUID=<UUID> /var/cache/pacman/pkg btrfs defaults,noauto,noatime,ssd,subvolid=260,subvol=/pkgcache,x-mount.mkdir,x-systemd.automount,x-systemd.requires=systemd-cryptsetup@data1.service,x-systemd.requires=systemd-cryptsetup@data2.service 0 2
Make sure that the UUID
here is that of blkid /dev/mapper/<NAME>
, using the
names you used for devices in /etc/crypttab
.
Much like in /etc/crypttab
, we need to pass noauto
to our mount options,
to ensure we don’t attempt to mount these filesystems on boot.
The big trick here is that we can declare dependencies using x-systemd.requires
as part of our mount options. /etc/fstab
in turn is parsed by
systemd-fstab-generator
,
yielding .mount
units. Their names is the mount point, which each /
replaced by -
, so for example var-lib-docker.mount
. I strongly suggest
you take a stroll through systemd.mount
to understand all the options at your disposal.
The x-mount.mkdir
option will automatically create the target directory
for us if it doesn’t exist, prior to mounting. This avoids silly scenarios
like a mount failing because we forgot to create /data
.
Don’t worry about the x-systemd.automount
option, we’ll discuss that a bit
later.
Depending on the encrypted mount
Next up, we’ll need to update a service. What we want to do is ensure that a service is only started after that encrypted filesystem has been mounted. If the disk is already unlocked this will be a no-op, if not, it should trigger the system to prompt us for the password and mount the filesystem.
We achieve this through the Requires
and After
of a service unit. In them
we specify additional dependencies on mount units (and anythign else you’d
like). You can edit one using systemctl edit
, lets try with docker.service
:
[Unit]
Requires=var-lib-docker.mount
After=var-lib-docker.mount
All together now
With all of this in place, when you systemctl start docker.service
systemd
will now try to start var-lib-docker.mount
. var-lib-docker.mount
is
generated by systemd-fstab-generator
. We’ve specified through
x-systemd.requires
that in order to start var-lib-docker.mount
, we need
systemd-cryptsetup@<NAME>
services started. This in turn will cause systemd
to prompt you for the decryption passwords on the TTY. Once the filesystems
have been unlocked and successfully mounted our docker.service
will start.
Since the disks are now decrypted, any other .mount
units won’t prompt you
for a password and will just mount instead.
Isn’t that lovely?
Be careful not to start these services at boot, i.e don’t systemctl enable
them. If you don’t want to have to start all services individually, make a
custom .target
that you then start, and override the WantedBy
of the
units so they don’t get started as part of multi-user.target
. Only once
you’ve done that should you systemctl enable
the services.
Bonus section
Now one last thing, that x-systemd.automount
thing. automount
is another
type of unit. systemd effectively watches for file access to the mount point,
and when it happens it will automatically start the associated mount unit,
causing the cryptsetup services to start making systemd prompt you for the
password. This means that if you were to cd /data
based on the setup I’ve
showed you here, and the disks aren’t decrypted and mounted yet, you’ll be
prompted for you decryption passwords at that point.
You can see automount units in the mount
output, as there will be an
additional entry for them:
systemd-1 on /data type autofs (rw,relatime,fd=46,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=18402)
/dev/mapper/data1 on /data type btrfs (rw,noatime,ssd,space_cache,subvolid=259,subvol=/data)
The second mount, the type btrfs
one, only exists after data.mount
has
started, but the autofs
one exists on boot.
Automounts are usually only used together with network mounted filesystems, but it provides a nice little usability improvement in our case too.
Now you might be wondering, why don’t you have the automount
option on
/var/lib/docker
? Well, unfortunately because it trips up Docker. I run
Docker with the btrfs
volume driver, but unfortunately at startup Docker
only notices the autofs
mount and concludes it shouldn’t load the btrfs
snapshotter plugin. In order to avoid that, and since realistically Docker
is the only thing that should be doing stuff under /var/lib/docker
, I omit
the x-systemd.automount
.