ssl certificate

About

Every service that terminates TLS needs the cert.crt / cert.key pair. There are two ways to get it:

Command

Certificate

When to use

make openssl

self-signed, 365 days

first start and experiments. Browsers show a certificate warning

make cert

free certificate from Let’s Encrypt (ACME)

production. Valid for 3 months, renew it automatically, see section 4

1. Generate a self-signed certificate

cd bind-docker/office

make openssl
The script writes nginx/certs/cert.crt and nginx/certs/cert.key, with CN=$DOMAIN and a validity of 365 days
It does nothing when both files already exist, delete them to generate a new pair
make runs this target as well, so the first make creates the pair when it is missing

Note that only nginx gets a certificate this way. bindsmtp and bindimap read their pair from smtp/certs and imap/certs, and without a certificate bindimap does not start. Those directories are filled by section 2, so use the self-signed certificate for HTTPS only, or install a free certificate as described next.

2. Get a free Let’s Encrypt certificate

make cert
The office suite has a built-in ACME (Let’s Encrypt) client, make cert is make generate copy
make generate calls the /acme/create interface over HTTP, and the issued pair is written into the BIND_CERT_DIR of bindinbox, which is bind-mounted from bindinbox/certs
make copy then installs the pair into every TLS service, see section 3
The certificate is valid for 3 months, so keep the automatic renewal of section 4 enabled

Requirements:

  • DOMAIN resolves to the public IP address of the server and port 80 is reachable from the internet, which is how the HTTP-01 challenge is answered

  • the containers are running, because the command goes through nginx on port 80 to the application

3. Which service uses which certificate

make copy (sh bin/cert-distribute.sh) copies the pair from bindinbox/certs (bindmeet/certs for the standalone meeting deployment) into the services that terminate TLS:

Directory

Service

nginx/certs

nginx: HTTPS on 443 and the MQTT TLS port

smtp/certs

bindsmtp: SMTP submission with STARTTLS

imap/certs

bindimap: IMAPS

Run it on its own after replacing the certificate by hand:

make copy

4. Automatic renewal

make cron      # install the renewal job, replaces an existing one

make uncron    # remove it

sh bin/cert-cron.sh show   # print the installed job
The job runs make renew once a day and appends the output to logs/cert-renew.log
make renew (sh bin/cert-renew.sh) is idempotent: it inspects the certificate nginx currently serves with openssl x509 -checkend and exits immediately when more than CERT_RENEW_DAYS days are left
Only when the certificate is close to expiry does it call the /acme/renew interface, install the new pair and reload the services
Renewing only when it is needed is what keeps the deployment clear of the Let’s Encrypt duplicate certificate limit, which is 5 certificates per week for the same set of domains

After a renewal the services pick up the new pair like this:

Service

What happens

nginx

nginx -s reload: the certificate is loaded without dropping connections, nginx is reloaded and not restarted. When nginx is not running, the script prints nginx is not running; it will use the new certificate on next start and the new pair is used on the next start

bindsmtp, bindimap

docker compose restart: these services read the pair at startup, so they are restarted

To renew even when the certificate is still valid, for example after changing DOMAIN:

CERT_FORCE_RENEW=1 sh bin/cert-renew.sh
The crontab entry belongs to the user that runs make cron, and that user needs passwordless access to the Docker daemon, be in the docker group or run make cron as root, otherwise the job cannot reach the containers
Without crontab, run cd <deployment directory> && make renew from a systemd timer or another scheduler

Variable

Default

Description

CERT_RENEW_DAYS

30

renew when fewer days than this are left

CERT_CRON_SCHEDULE

0 3 * * *

crontab schedule that make cron installs

CERT_FORCE_RENEW

unset

when set, renew without checking the remaining days; pass it on the command line, it is not read from .env