Server — Mail — TLS Certificate
The Dovecot and Postfix TLS articles used a certificate signed by February’s internal CA. Mail clients connecting to Dovecot over IMAPS will see a certificate warning, because the internal CA is not in their trust store. Remote mail servers delivering to Postfix may log warnings or behave inconsistently, depending on how strictly they verify certificates. MTA-STS enforcement requires a publicly trusted certificate for the mta-sts subdomain.
A Let’s Encrypt certificate solves all of these problems. It is publicly trusted, free, and renews automatically. The 90-day certificate lifetime is short enough to limit exposure if a key is compromised, and certbot handles renewal without intervention. The only requirement is that the mail server’s hostname is publicly resolvable and that port 80 or 443 is accessible for the ACME challenge.
What the certificate covers
A single certificate should cover the hostnames that clients and other servers use to connect to February’s mail services:
mail.yourdomain.com— the SMTP/IMAPS hostnamemta-sts.yourdomain.com— required for MTA-STS policy hosting
Both can be covered by one certificate with multiple Subject Alternative Names, which certbot handles in a single command.
Prerequisites
The ACME HTTP-01 challenge requires port 80 to be accessible from the internet. Nginx is already installed from the MTA-STS article. Port 80 needs to be open in UFW:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Confirm both hostnames resolve to February’s public IP from outside the network. The DNS records for mail.yourdomain.com and mta-sts.yourdomain.com must be live and propagated before attempting to obtain a certificate. Test from an external resolver:
dig mail.yourdomain.com @1.1.1.1
dig mta-sts.yourdomain.com @1.1.1.1
Both should return February’s public IP.
Installing certbot
sudo apt install certbot python3-certbot-nginx
Certbot’s Nginx plugin handles both the ACME challenge and the Nginx configuration automatically.
Obtaining the certificate
Request a certificate covering both hostnames:
sudo certbot certonly --nginx -d mail.yourdomain.com -d mta-sts.yourdomain.com --email admin@yourdomain.com --agree-tos --no-eff-email
The --nginx flag uses the Nginx plugin, which temporarily modifies Nginx’s configuration to handle the ACME challenge and restores it afterward. certonly obtains the certificate without modifying Nginx’s SSL configuration, since Postfix and Dovecot will reference the certificate files directly.
On success, the certificate files are at:
/etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem — certificate + chain
/etc/letsencrypt/live/mail.yourdomain.com/privkey.pem — private key
/etc/letsencrypt/live/mail.yourdomain.com/cert.pem — certificate only
/etc/letsencrypt/live/mail.yourdomain.com/chain.pem — chain only
These are symlinks into /etc/letsencrypt/archive/ where the actual files live. The symlinks always point to the current version, so Postfix and Dovecot will pick up renewed certificates automatically after a service reload — without any path changes.
Updating Postfix
Edit /etc/postfix/main.cf and update the certificate paths:
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.yourdomain.com/privkey.pem
Use fullchain.pem rather than cert.pem for the certificate. fullchain.pem includes the intermediate CA certificate, which some mail servers require to verify the chain. A certificate presented without its chain causes verification failures at remote servers that do not cache intermediate certificates.
Reload Postfix:
sudo postfix check
sudo systemctl reload postfix
Updating Dovecot
Edit /etc/dovecot/conf.d/10-ssl.conf and update the paths:
ssl_cert = </etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.yourdomain.com/privkey.pem
Restart Dovecot:
sudo systemctl restart dovecot
Updating Nginx for MTA-STS
The MTA-STS article configured Nginx with a placeholder certificate path. Update /etc/nginx/sites-available/mta-sts to use the Let’s Encrypt certificate:
server {
listen 443 ssl;
server_name mta-sts.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mail.yourdomain.com/privkey.pem;
root /var/www/mta-sts;
location /.well-known/mta-sts.txt {
default_type text/plain;
add_header Cache-Control "max-age=86400";
}
location / {
return 404;
}
}
Reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
Certificate permissions
Let’s Encrypt private keys in /etc/letsencrypt/ are readable only by root by default. Postfix and Dovecot need to read the private key. The correct approach is to add the service users to a group that has read access.
Check the current permissions:
sudo ls -la /etc/letsencrypt/live/mail.yourdomain.com/
sudo ls -la /etc/letsencrypt/archive/mail.yourdomain.com/
The archive directory is where the actual files live. The live directory contains symlinks. Both need to be accessible.
Create a group for certificate access and add the relevant users:
sudo groupadd certreaders
sudo usermod -aG certreaders postfix
sudo usermod -aG certreaders dovecot
sudo usermod -aG certreaders www-data
Grant the group read access to the Let’s Encrypt directories:
sudo chgrp -R certreaders /etc/letsencrypt/live /etc/letsencrypt/archive
sudo chmod -R g+rx /etc/letsencrypt/live /etc/letsencrypt/archive
Restart both services to pick up the new group membership:
sudo systemctl restart postfix dovecot
Confirm Postfix can read the certificate:
sudo -u postfix openssl x509 -in /etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem -noout -subject -dates
If this returns the certificate details, Postfix has the necessary access. If it returns a permission denied error, the group permissions did not apply correctly.
Automatic renewal hooks
Let’s Encrypt certificates expire after 90 days. Certbot’s systemd timer handles renewal automatically, but Postfix and Dovecot do not pick up the new certificate until they are reloaded. The correct hook for this is the deploy hook, which runs only when a certificate is successfully renewed.
Create the deploy hook:
sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-mail-services.sh
#!/bin/bash
systemctl reload postfix
systemctl restart dovecot
systemctl reload nginx
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-mail-services.sh
Postfix can be reloaded (graceful, no dropped connections) rather than restarted. Dovecot should be restarted to ensure all processes pick up the new certificate; a reload is not always sufficient for Dovecot’s SSL context.
The deploy hook is in renewal-hooks/deploy/ rather than renewal-hooks/post/. The post hook runs after every renewal attempt regardless of success. The deploy hook runs only after a certificate is actually renewed. On a server with multiple certificates, the deploy hook also runs once per certificate, which is important if February ever has more than one.
Verify the hook will run correctly by doing a dry run:
sudo certbot renew --dry-run
A successful dry run confirms certbot can connect to Let’s Encrypt, complete the challenge, and would renew the certificate if it were due. The deploy hook runs in dry run mode too, so the services get reloaded. Watch the output for any errors.
Verifying the new certificate
Test Postfix:
openssl s_client -connect mail.yourdomain.com:25 -starttls smtp 2>/dev/null | openssl x509 -noout -issuer -subject -dates
The issuer should show Let’s Encrypt (specifically C=US, O=Let's Encrypt) rather than February’s internal CA. The subject should show mail.yourdomain.com.
Test Dovecot:
openssl s_client -connect mail.yourdomain.com:993 2>/dev/null | openssl x509 -noout -issuer -subject -dates
Same result expected: Let’s Encrypt issuer, correct subject, valid dates.
Test the MTA-STS policy file:
curl -sI https://mta-sts.yourdomain.com/.well-known/mta-sts.txt | grep -E "HTTP|content-type"
curl -s https://mta-sts.yourdomain.com/.well-known/mta-sts.txt
The first command should show HTTP/2 200. The second should return the policy file content.
What changes with a public certificate
Once the Let’s Encrypt certificate is in place, several things that previously required manual trust configuration work automatically:
Mail clients connecting to Dovecot over IMAPS no longer show certificate warnings. Thunderbird, Apple Mail, and mobile clients all trust Let’s Encrypt certificates natively.
Remote mail servers delivering to Postfix can verify the certificate without any special configuration. This is particularly important for MTA-STS enforcement: a domain that publishes an enforce policy will refuse to deliver to a server with an unverifiable certificate.
The TLSA records from the DANE article remain valid. Let’s Encrypt certificates use a stable key by default across renewals, so the TLSA record (which hashes the public key) does not need updating when the certificate renews. If certbot ever generates a new key, the TLSA record must be updated before the new certificate is deployed.
Monitoring expiry
Despite automatic renewal, monitoring certificate expiry independently is worth doing. A certbot misconfiguration, a DNS problem, or a rate limit hit can cause renewal to fail silently.
Check the current expiry:
sudo certbot certificates
The output shows all managed certificates and their expiry dates. Add this to a periodic check or configure an alert via the monitoring setup when that article arrives in the series.