How do I use Let's Encrypt certs in qpsmtpd

I am successfully using the Let's Encrypt certs with stunnel4 on my Debian stable system. I'm now trying to use it with the starttls support in qpsmtpd, but I find the documentation for it opaque. The documentation for the configuration command itself says:

tls [ cert_path priv_key_path ca_path dhparam_path ]

cert_path

Path to the server certificate file. Default: ssl/qpsmtpd-server.crt

priv_key_path

Path to the private key file. Default: ssl/qpsmtpd-server.key

ca_path

Path to the certificate authority file. Default: ssl/qpsmtpd-ca.crt

The instructions for the "tls" option on plugins [qpsmtpd Wiki] gives an example:

server.key (private key) server.crt (signed public key) CA.crt (certificate authority key)

For a self-signed certificate, enter the following line in config/plugins:

tls /full/path/to/server.crt /full/path/to/server.key /full/path/to/CA.crt

For a certificate signed by a CA such as DigiCert, which is signed by Entrust (ie chained):

cat server.crt DigiCertCA.crt TrustedRoot.crt > cert-bundle.pem

tls /full/path/to/cert-bundle.pem /full/path/to/server.key /full/path/to/DigiCertCA.crt

I've tried using Let's Encrypt's fullchain.pem as the first argument to TLS, privkey.pem as the second, and IdenTrust_Public_Sector_Root_CA_1.crt, as the third, and a few other permutations, but I always get the following error in the qpsmtpd log:

Cannot locate cert/key! Run plugins/tls_cert to generate

plugins/tls_cert only generates a self-signed certificate, so that's no help.

Does anyone have any suggestions as to what options I should use to get qpsmtpd to use my certificate?

Thanks,
Bill

1 Like

qpsmtpd seems to be a little bit "picky" when it comes to the files it requires. I've tested it here locally and it can work, unfortunately just not "out of the box". For example, the following doesn't work:

tls /etc/letsencrypt/live/example.com/fullchain.pem /etc/letsencrypt/live/example.com/privkey.pem /etc/letsencrypt/live/example.com/fullchain.pem

However, when I simply copy the fullchain.pem and privkey.pem files to /etc/qpsmtpd/ssl/, it does work:

tls /etc/qpsmtpd/ssl/fullchain.pem /etc/qpsmtpd/ssl/privkey.pem /etc/qpsmtpd/ssl/fullchain.pem

What I noticed is that the permissions of privkey.pem are 644 in the letsencrypt directory. That's because certbot puts restrictions on the directory where the private keys are kept. By copying the private key out of this restricted directory to the unrestricted /etc/qpsmtpd/ssl directory, I've exposed my private key to every non-root service on my server, including qpsmtp. That's the reason why it works after I've copied it.

It seems qpsmtp tries to load the files as the user smtpd on my Gentoo system. It probably doesn't run as root on your server too.

You can check the user qpsmtp is running on by running:

lsof -i -P -n | grep LISTEN | grep :25

This identifies the service listening on port 25 (SMTP) and in the third column is the username.

Then, I suggest you use a script called by certbot after a renewal to copy the files to the correct location:

First, run this once:

mkdir -p /etc/qpsmtpd/ssl

(the directory might already exist)

Then, put the following script somewhere, for example, /usr/local/bin/deploy-qpsmtp-cert-files

#!/bin/bash

# Change the username to the user you've found earlier if applicable:
USER="smtpd"

# Directory where all qpsmtpd configuration files reside:
QPSMTPD_CONF_DIR="/etc/qpsmtpd/"

cp "$RENEWED_LINEAGE"{fullchain,privkey}".pem" "$QPSMTPD_CONF_DIR/ssl/"
chmod 600 "$QPSMTPD_CONF_DIR/ssl/privkey.pem"
chown $USER: "$QPSMTPD_CONF_DIR/ssl/"{fullchain,privkey}".pem"

Make the script executable by running chmod +x /usr/local/bin/deploy-qpsmtp-cert-files

Now, you can use that script in certbot as the deploy hook for your qpsmtp certificate like:

certbot ...other_options.. --deploy-hook /usr/local/bin/deploy-qpsmtp-cert-files

Note that all of this is necessary due to, in my opinion, improper design by qpsmtp, as I believe private keys should always be only readable by the root user! There might be security implications when the private key is owned by a user such as smtpd. If there is a design flaw or exploitable bug in qpsmtpd, the private key might get exposed! This is obviously very bad.

If I've got the time, I might file an issue with qpsmtpd regarding this design flaw.

4 Likes

Oh, one more thing I didn't have time for: the deploy script also needs to reload or restart qpsmtpd after the files have been copied!

3 Likes

Thank you so much, Osiris. That was a wonderfully detailed response.

I had tried using the live paths and had still gotten the error, and then tried (temporarily!) changing permissions on that directory, but was still getting the error. Turns out there's a bug in the way the tls plugin verifies its arguments:

unless (-f $cert && -f $key && -f $ca) {
      $self->log(LOGERROR,
                   "Cannot locate cert/key!  Run plugins/tls_cert to generate");

Since the contents of the live directory are symlinks, this test fails. It should really be using "-r" instead. (It should also test each one individually and tell you which file is the problem...)

So, given what you wrote, I put together a small Makefile (below) based on your script. One minor improvement is that I made $QPSMTPD_CONF_DIR/ssl readable only by the qpsmtpd group while still having everything owned by root. Thus, only qpsmtpd can read the key and certificates, and doesn't have permissions to modify them.

While I share your concerns about qpsmtpd being able to read the file, there's no way that the daemon can perform a TLS negotiation without at least having the key in memory, so a security flaw is going to make that key accessible regardless. The only way I see around that is to have a separate process responsible for the negotiations, which gets pretty complicated.

The other idea I am considering is to use a separate key/cert pair for email, which should at least limit the scope of the damage if there is a compromise. That's a project for another day, though.

Thanks again,
Bill

QPSMTPD_CONF_DIR = /etc/qpsmtpd
QPSMTPD_GROUP = qpsmtpd

QPSMTPD_CERTS = $(QPSMTPD_CONF_DIR)/ssl

all: $(QPSMTPD_CERTS)/fullchain.pem $(QPSMTPD_CERTS)/privkey.pem

$(QPSMTPD_CERTS)/%: $(RENEWED_LINEAGE)/% | $(QPSMTPD_CERTS)
        cp $< $@
        chmod go-w,a+r $@

$(QPSMTPD_CERTS):
        mkdir $@
        chgrp $(QPSMTPD_GROUP) $@
        chmod 750 $@
3 Likes

Don't forget to reload qpsmtpd after renewal :wink:

Also, how does one implement a Makefile as certbot --deploy-hook? Or are you planning on running the Makefile in a different way after each renewal?

1 Like

I expect I'll simply run "make" as the deploy hook -- so far I have just been testing it by running "make" from the command line. I haven't decided how (or if!) I want to automatically restart qpsmtpd.

1 Like

You'd have to check if you can run cd $dir && make in the deploy hook. I'm not sure which directory is used when you'd only run make without a cd.

Manually restarting/reloading is obviously possible too. Just remember to do it, as it won't use the newly renewed certificates until you do.

1 Like

It seems sending the SIGHUP signal will restart the qpsmtpd service. You could implement that in your deploy script. (But only for prefork and forkserver.)

1 Like

You probably can, based on the documentation for --deploy-hook, but make -C does the job just as well.

2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.