Securely running dehydrated with systemd

Dehydrated is a light-weight client to obtain SSL certificates from letsencrypt. It comes packaged in Debian, but it's left up to you to configure your system so that it runs regularly and refreshes your certificates before they expire.

So after configuring dehydrated itself, you can use the following instructions to create a locked-down systemd service and an accompanying timer unit.

I'd rather avoid running dehydrated as root (even given the systemd lockdown shown below), as it doesn't need to be root to do its job. As dehydrated needs to share data with the webserver, I'm not going to use systemd's DynamicUser feature, but use a dedicated system user. The Debian package does not create a dedicated user for dehydrated, so let's create one (including a dedicatd group) ourselves, and adjust its home directory's ownership accordingly:

adduser --system --no-create-home --home /var/lib/dehydrated \
        --shell /usr/bin/nologin --disabled-login \
        --gecos "dehydrated ACME client,,," --group dehydrated
chown -R dehydrated: /var/lib/dehydrated

Note that I did not need to make the certs subdirectory readable by nginx, as it will be started as root, and reads the certificate files before dropping privileges. If your server is started unprivileged, you are advised to use hooks (see this github issue).

Note that your WELLKNOWN directory must be writable by the dehydrated user as well, and this directory must be readable by the unprivileged web server worker processes. In my case, the WELLKNOWN directory is /srv/www/.acme-challenges:

mkdir /srv/www/.acme-challenges
chown dehydrated:www-data /srv/www/.acme-challenges
chmod u=rwx,go=rx /srv/www/.acme-challenges

Next, let's create a systemd service in /etc/systemd/system/dehydrated.service with the following contents:

[Unit]
Description=Run letsencrypt certificate refresh

[Service]
Type=oneshot
User=dehydrated
Group=dehydrated
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/srv/www/.acme-challenges /var/lib/dehydrated
PrivateTmp=yes
ExecStart=/usr/bin/dehydrated -c
ExecStartPost=+/bin/systemctl reload nginx.service

This will run dehydrated as the user/group we have created above, make the whole file system hierarchy read-only, hide home directories, and create a private /tmp, not shared with the host. Other than /tmp, only the ReadWritePaths will be writable.

When dehydrated has run sucessfully, we reload nginx. Note the + in the ExecStartPost line, which will cause this command to be run with full privileges.

You can now try if the service works:

systemctl daemon-reload
systemctl start dehydrated

Now, for the timer unit, create /etc/systemd/dehydrated.timer:

[Unit]
Description=Daily letsencrypt run

[Timer]
OnCalendar=*-*-* 00:00:00
Persistent=true

[Install]
WantedBy=timers.target

Let's enable and start it:

systemctl daemon-reload
systemctl enable dehydrated.timer
systemctl start dehydrated.timer

You can verify the service is now scheduled using systemctl list-timers.