# Postfix and Dovecot TLS with Let's Encrypt certificates

> Secure SMTP, submission and IMAP with Let's Encrypt: Postfix smtpd_tls_chain_files, Dovecot 2.3 and 2.4 settings, reload on renewal, STARTTLS tests.

Source: https://ssl.sh/guides/postfix-dovecot-tls/
Last updated: 2026-11-10

Postfix and Dovecot can share one Let's Encrypt certificate for `mail.example.com`. Postfix offers STARTTLS on port 25 and TLS on submission ports 587 and 465, Dovecot serves IMAP over TLS on port 993, and a Certbot deploy hook reloads both after renewal.

The steps apply to Postfix 3.4 or later and Dovecot 2.3 or 2.4 on Debian, Ubuntu or RHEL-compatible systems with Certbot 4.1 or later.

## Prerequisites

The mail host needs a certificate for the name clients and other servers connect to.

- Postfix and Dovecot installed and working without TLS, with SASL authentication via Dovecot for submission.
- DNS: an `A`/`AAAA` record for `mail.example.com` (for example `203.0.113.10`), the domain's `MX` pointing to it, and a matching reverse DNS (PTR) record.
- Inbound TCP 25, 465, 587 and 993 open; TCP 80 open for the HTTP-01 challenge (or use DNS-01).
- Certbot from snap: `sudo snap install --classic certbot && sudo ln -s /snap/bin/certbot /usr/local/bin/certbot`.

## Step 1: Obtain a certificate for the mail host name

Certbot's standalone mode obtains the certificate when nothing else listens on port 80:

```bash
sudo certbot certonly --standalone -d mail.example.com
```

If a web server on the same host already serves port 80, use `--webroot -w /var/www/html` or `--nginx` instead. The files are in `/etc/letsencrypt/live/mail.example.com/`.

## Step 2: Configure Postfix

Postfix needs three settings: the certificate chain, opportunistic TLS on port 25, and TLS logging.

```bash
sudo postconf -e 'smtpd_tls_chain_files = /etc/letsencrypt/live/mail.example.com/privkey.pem, /etc/letsencrypt/live/mail.example.com/fullchain.pem'
sudo postconf -e 'smtpd_tls_security_level = may'
sudo postconf -e 'smtpd_tls_mandatory_protocols = >=TLSv1.2'
sudo postconf -e 'smtp_tls_security_level = may'
sudo postconf -e 'smtpd_tls_loglevel = 1'
sudo postconf -e 'smtp_tls_loglevel = 1'
```

Enable submission (587, STARTTLS) and submissions (465, implicit TLS) in `/etc/postfix/master.cf`:

```text
submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
submissions inet n      -       y       -       -       smtpd
  -o syslog_name=postfix/submissions
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
```

```bash
sudo postfix check && sudo systemctl reload postfix
```

## Step 3: Configure Dovecot

Dovecot 2.3 and 2.4 use different setting names. On Dovecot 2.3 (Ubuntu 24.04, Debian 12), edit `/etc/dovecot/conf.d/10-ssl.conf`:

```text
ssl = required
ssl_cert = </etc/letsencrypt/live/mail.example.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.example.com/privkey.pem
ssl_min_protocol = TLSv1.2
```

On Dovecot 2.4 (for example Debian 13), the settings were renamed and take plain paths:

```text
ssl = required
ssl_server_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
ssl_server_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem
```

Check your version with `dovecot --version`, then validate and reload:

```bash
sudo doveconf -n | grep -E '^ssl'
sudo systemctl reload dovecot
```

## Step 4: Reload both services on renewal

A Certbot deploy hook reloads Postfix and Dovecot after each renewal of the mail certificate:

```bash
sudo tee /etc/letsencrypt/renewal-hooks/deploy/mail.sh > /dev/null <<'EOF'
#!/bin/sh
case "$RENEWED_DOMAINS" in
  *mail.example.com*) systemctl reload postfix dovecot ;;
esac
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/mail.sh
```

If you publish DANE TLSA records, reissue once with `--reuse-key` so future renewals keep the same key: `sudo certbot certonly --standalone -d mail.example.com --reuse-key --force-renewal`.

## Verify

Every port should present the `mail.example.com` certificate, and OpenSSL should end with `Verify return code: 0 (ok)`. Test each port from another machine. The [STARTTLS checker](https://ssl.sh/tools/starttls-checker/) covers SMTP; with OpenSSL:

```bash
openssl s_client -connect mail.example.com:25 -starttls smtp -servername mail.example.com </dev/null | head -n 20
openssl s_client -connect mail.example.com:587 -starttls smtp -servername mail.example.com </dev/null | grep -E 'subject|issuer|Verify'
openssl s_client -connect mail.example.com:465 -servername mail.example.com </dev/null | grep -E 'subject|issuer|Verify'
openssl s_client -connect mail.example.com:993 -servername mail.example.com </dev/null | grep -E 'subject|issuer|Verify'
```

If the host also serves HTTPS, the [SSL checker](https://ssl.sh/tools/ssl-checker/) or `curl ssl.sh/mail.example.com` shows the same certificate's chain and expiry.

## Automatic renewal

The Certbot snap timer runs `certbot renew` twice a day and follows Let's Encrypt's ARI window on Certbot 4.1 and later; the deploy hook then reloads Postfix and Dovecot. Standalone renewals need port 80 free at that moment, so if you later add a web server, switch the certificate to webroot with `sudo certbot reconfigure --cert-name mail.example.com --webroot -w /var/www/html`.

```bash
sudo certbot renew --dry-run --run-deploy-hooks
```

Mail clients cache nothing about your certificate, so shorter Let's Encrypt lifetimes (64 days from February 10, 2027) need no change. Add `mail.example.com` to [ssl.sh monitoring](https://ssl.sh/monitor/) (in development).

## Troubleshooting

- **warning: cannot get RSA private key from file**: the order in `smtpd_tls_chain_files` is wrong; list the key first, then the chain.
- **Dovecot: ssl_cert: Can't open file**: on 2.3 the `<` prefix is missing; on 2.4 you used the old setting names. Run `doveconf -n` to see which settings are active.
- **Clients report an untrusted or incomplete chain**: you pointed at `cert.pem`; use `fullchain.pem`, see [missing intermediate certificate](https://ssl.sh/errors/missing-intermediate-certificate/).
- **Certificate name mismatch in mail clients**: clients connect to a name not in the certificate (for example the bare domain); see [NET::ERR_CERT_COMMON_NAME_INVALID](https://ssl.sh/errors/net-err-cert-common-name-invalid/).
- **A partner's server rejects your MTA's client certificate**: Let's Encrypt certificates no longer carry the client authentication EKU; see [client auth EKU removed](https://ssl.sh/errors/client-auth-eku-removed/).
