Auto ACME with Traefik for non-Traefik services

Auto ACME with Traefik for non-Traefik services
Photo by Mauro Sbicego / Unsplash

I've started using Traefik as a reverse proxy for many of my services since it can handle auto-renewing SSL certificates via ACME/Let's Encrypt for you. But what if you have a service like Postfix which aren't a great fit for Traefik? Can you still automate certs?

Turns out you definitely can and it's fairly simple once you have Traefik renewing certificates for FQDN's it controls. The following directions are for Postfix, but can easily be adapted to any other service.

Configure Traefik to renew your mail server certificate

Ensure you have a certificateResolver configured in your static config file (typically traefik.yaml):

certificatesResolvers:
  myresolver:
    acme:
      httpChallenge:
        entryPoint: web
      email: 'postmaster@synfin.net'
      storage: '/letsencrypt/acme.json'
      keyType: 'RSA4096'

You can use any ACME challenge you want, but you need to specify a storage location that is a mounted directory so we can read the acme.json file with the certificate data.

Then in your Traefik dynamic config (not to be confused with the static config used for the previous block!) you need to configure the SSL certificate renewal:

    mail:
      service: "nginx-traefik@docker"
      rule: "Host(`mail.synfin.net`)"
      entrypoints:
        - websecure
        - web
      tls:
        certresolver: myresolver
        domains:
        - main: mail.synfin.net

The above tells Traefik to use my above certificatesResolvers.myresolver to renew the certificate for mail.synfin.net and attach an entry point to my dynamic Nginx service running via docker. Ngnix doesn't actually need to serve any content for this domain- Traefik will do everything necessary. Since I'm using HTTP-01 challenges, I need to specify both my http (web) and https (websecure) entrypoints.

Generate the PEM files

Add a new shell script named /etc/cron.daily/renew-mail-cert :

#!/bin/bash
set -e

# set directory to where your Traefik acme.json file lives
ACME_FILE=/home/docker/traefik/acme.json

# the FQDN of the domain to renew
DOMAIN=mail.synfin.net

# Base directory for where to write the SSL certificate files
BASE_DIR=/etc/postfix

# Path/name of GNU Sed binary, jq and others
GSED=/usr/bin/sed
JQ=/usr/bin/jq
BASE64=/usr/bin/base64
SHA1SUM=/usr/bin/sha1sum
POSTFIX=/usr/sbin/postfix

##### END CONFIGURATION #####
command -v $GSED >/dev/null || (echo "Missing sed binary" && exit 1)
command -v $JQ >/dev/null || (echo "Missing jq binary" && exit 1)
command -v $BASE64 >/dev/null || (echo "Missing base64 binary" && exit 1)
command -v $SHA1SUM >/dev/null || (echo "Missing sha1sum binary" && exit 1)
command -v $POSTFIX >/dev/null || (echo "Missing postfix binary" && exit 1)

# GNU sed search replace for the certificate (1st cert)
CERT_MATCH='s|^-----BEGIN CERTIFICATE-----([^-]*)-----END CERTIFICATE-----(.*)|-----BEGIN CERTIFICATE-----\1-----END CERTIFICATE-----\n|'

# GNU sed search replace for the CA (2nd cert)
CA_MATCH='s|^-----BEGIN CERTIFICATE-----([^-]*)-----END CERTIFICATE-----\n(.*)|\2|'

# JQ queries to extract the cert & key base64 data
JQ_QUERY_CERT=".myresolver.Certificates[] | select(.domain.main==\"${DOMAIN}\") | .certificate"
JQ_QUERY_KEY=".myresolver.Certificates[] | select(.domain.main==\"${DOMAIN}\") | .key"

CERT_CHAIN=$(cat $ACME_FILE | $JQ -r "$JQ_QUERY_CERT" | $BASE64 -d)
CERT=$(echo -n "${CERT_CHAIN}" | $GSED -zEe "$CERT_MATCH")
CA=$(echo -n "${CERT_CHAIN}" | $GSED -zEe "$CA_MATCH")


NEW_CERT_HASH=$(echo "$CERT" | $SHA1SUM)
OLD_CERT_HASH=$(cat ${BASE_DIR}/cert.pem | $SHA1SUM)
NEW_CA_HASH=$(echo "$CA" | $SHA1SUM)
OLD_CA_HASH=$(cat ${BASE_DIR}/ca.pem | $SHA1SUM)

if [ "${NEW_CERT_HASH}" != "${OLD_CERT_HASH}" -o "${NEW_CA_HASH}" != "${OLD_CA_HASH}" ]; then
    echo "Updating SSL certificate and CA for postfix"
    echo "$CERT" >${BASE_DIR}/cert.pem
    echo "$CA" >${BASE_DIR}/ca.pem
    umask 077
    cat $ACME_FILE | $JQ -r "$JQ_QUERY_KEY" | $BASE64 -d > ${BASE_DIR}/privkey.pem
    $POSTFIX reload
fi

Validate SSL cert generation

  1. Restart Traefik if necessary and verify the new cert is in acme.json
  2. Run /etc/cron.daily/renew-mail-cert
  3. Verify the *.pem files are correct in $BASE_DIR. I'd recommend inspecting the generated ca.pem and cert.pem using openssl x509 -text -in <filename> .

Configure and restart Postfix

From my /etc/postfix/main.cf:

# smtp SSL config...
smtp_tls_key_file = /etc/postfix/privkey.pem
smtp_tls_cert_file = /etc/postfix/cert.pem
smtp_tls_CAfile = /etc/postfix/ca.pem

# as well as smtpd...
smtpd_tls_key_file = /etc/postfix/privkey.pem
smtpd_tls_cert_file = /etc/postfix/cert.pem
smtpd_tls_CAfile = /etc/postfix/ca.pem

And then run postfix check && postfix reload.