Replacing Debian keyscript with systemd socket activation for encrypted volumes
The feature is already here and it works
May 21, 2026
In Debian (and Ubuntu), when using a LUKS encrypted volume with cryptsetup, there is a documented option in the manpage for /etc/crypttab called keyscript: it should allow the init system to launch a program and have its standard output used as a key to unlock a volume. But there is a catch: this option does not work, and even the manpage says as much:
WARNING: With systemd as init system, this option might be ignored. At the time this is written (December 2016), the systemd cryptsetup helper doesn't support the keyscript option to /etc/crypttab.
Only problem, in the 11 years since Debian integrated systemd, nothing changed on this front: systemd still does not support keyscript=. If you look at the bug reports from back then, you might see some suggestions of writing a systemd password agent, but I'm here to tell you there is no need, systemd has something even better: just use socket activation.
Since systemd 248 (March 2021), you can use a unix socket as the keyfile of a volume in /etc/crypttab; systemd will then socket-activated any related service and fetch the key from the socket. This is even documented in the systemd crypttab manpage. Unfortunately, neither Debian nor Ubuntu ship this manpage, as they prefer their own obsolete one for some reason.
Let's go through a full example with a service I named crypthome (I'll let you guess why). Here is crypthome@.socket:
[Unit]
Description="decrypt home socket integration"
DefaultDependencies=no
Before=sockets.target
[Socket]
ListenStream=/run/crypthome.sock
SocketMode=0640
Accept=true
[Install]
WantedBy=multi-user.target
A few comments on this socket file:
- quite important: because the unit uses
Accept=true, this must be a template unit, and this is why it's named with an@before.socket. (I did not lose more than one hour because I skipped reading this part of the manpage). ListenStream=points to the unix socket we want to be created- here I used
DefaultDependencies=nobecause my script depends on the network (as you'll see below), and I did not want the socket to depend onsysinit.target, which is before the network is available. I keepsockets.targetwhich is normally part of the default set.
Then the associated service:
[Unit]
Description="decrypt home password"
After=network-online.target
[Service]
ExecStart=-/usr/local/bin/key.sh
StandardError=journal
StandardInput=socket
Type=oneshot
- I am not sure about this (comments welcome!): to prevent a failure of the script to block the retrying the socket activations, the
ExecStartcommand is prefixed with"-". - stdout is inherited from stdin by default, so it will also be on the socket
- errors will be on the journal.
- this particular script depends on the network hence why the ordering with
network-online.target.
And that's it!
Over-engineered
Editing three files instead of one, isn't that over-engineered? Well, this often comes-up with systemd. But there a few things to note: first, the socket activation is much more powerful and used pervasively with systemd. This a standard mechanism, not a one-off script; systemd can use this socket integration to fetch keys from hardware security modules in the case of crypttab for example. The script is debuggable as a normal systemd service. And it's all declarative.
Then, systemd provides a thing for free here: dependency on the network. With just a script it's almost impossible to get right. But after switching Debian to use systemd-networkd, I now get perfect network timing when the script is started, with any sleep or ad-hoc network detection.
Going further: what is the volume?
In my case I did not need to know the volume name, but systemd provides a way to know for which volume a key is requested over a socket:
The source socket name is chosen according to the following format:
NUL RANDOM /cryptsetup/ VOLUME[...] Services listening on the AF_UNIX stream socket may query the source socket name withgetpeername(2), and use this to determine which key to send, allowing a single listening socket to serve keys for multiple volumes.
So, imagine you have a minimal Debian install, on which you use this method to decrypt a volume with an arbitrary systemd service running a script. How to fetch the remote socket name from the script, without adding any new package dependency to the system (no compiler for example)? I won't bore with all the details, but one way to do this is to use Perl, which still ships by default in Debian:
#!/usr/bin/env perl
use IO::Socket::UNIX;
my $sock = IO::Socket::UNIX->new_from_fd(0, "r+");
print $sock->peerpath()
 (Rev 1).3-S-3-s-60-S-320-actual.png)
 (Rev 1).3-S-3-s-60-S-320-target.png)