Skip to content
Linux Maintenance · 7 min read

Website Cert Fine, Mail Cert Expired: Plesk's Stale Postfix SNI Map

Let's Encrypt renewed, web and IMAP fine, but SMTP still sent the expired cert. Plesk's Postfix SNI map was stale. How I found it and fixed it.

A small client called me today. "Certificate expired?!" Three question marks, three exclamation marks. My first thought was the website. My second thought was the posts I already wrote about Plesk and Let's Encrypt dying. Both thoughts were wrong.

The website was fine. The certificate had been renewed. Only one part of the server had not noticed.

TL;DR: Plesk renewed the Let's Encrypt cert. nginx and Dovecot (IMAP) used the new one. Postfix (SMTP) did not. Postfix on this box reads its per-domain certs from a Plesk-managed SNI map, /var/spool/postfix/plesk/certs.db, and that file had not been touched for weeks. So SMTP on 465 and 587 still handed out the old cert, which had expired five days earlier. The client's mail program complained when sending. Fix: replace the two entries in the map with the current cert, postfix reload, check every port from outside. And since postmap -i does not overwrite, you have to delete the entry first.

Step 1: check every port, not just 443

When someone says "certificate", don't guess which one. A domain with mail has at least four TLS endpoints. Check them all, from outside, with SNI:

for h in example.com www.example.com mail.example.com; do
  echo "== $h :443"
  echo | timeout 10 openssl s_client -connect $h:443 -servername $h 2>/dev/null \
    | openssl x509 -noout -subject -enddate
done

for p in 993 465 587; do
  echo "== mail :$p"
  extra=""; [ $p = 587 ] && extra="-starttls smtp"
  echo | timeout 10 openssl s_client -connect mail.example.com:$p \
    -servername mail.example.com $extra 2>/dev/null \
    | openssl x509 -noout -subject -enddate
done

What came back, shortened:

== example.com :443      notAfter=Jan  5 2027
== www.example.com :443  notAfter=Jan  5 2027
== mail :993             notAfter=Jan  5 2027
== mail :465             notAfter=Oct 22 2026
== mail :587             notAfter=Oct 22 2026

Web good. IMAP good. SMTP dead since October 22. That explains the call: reading mail worked, but every time they sent a mail, Outlook or whatever they use threw the "certificate expired" warning.

So this was not a renewal failure. Renewal worked. The new cert was on disk and two of three services used it. This is a different problem from the one in Plesk stopped renewing the cert, where nothing renewed at all.

Step 2: where does Postfix get its cert?

postconf -n | grep -iE "tls_.*(cert|key)_file|sni"
smtpd_tls_cert_file = /etc/postfix/postfix.pem
smtpd_tls_key_file = $smtpd_tls_cert_file
tls_server_sni_maps = hash:/var/spool/postfix/plesk/certs

Two sources. postfix.pem is the default cert for clients that send no SNI. tls_server_sni_maps is the per-hostname list. Plesk owns that one.

Dovecot, for comparison, has one small config file per hostname, and those point straight at Plesk's cert file:

grep -A2 local_name /etc/dovecot/conf.d/14-plesk-sni-example.com.conf
local_name example.com {
  ssl_server_cert_file = /opt/psa/var/certificates/<random-name>
  ssl_server_key_file = /opt/psa/var/certificates/<random-name>

Same file for cert and key. That file is a bundle: private key first, then the certificate chain. When Plesk renews, it updates that file, Dovecot reads it, done. Postfix does not read that file. Postfix reads the map.

Then the timestamp:

ls -la /var/spool/postfix/plesk/certs*
-rw-r--r-- 1 root root 94208 Oct  7 07:13 /var/spool/postfix/plesk/certs.db

Only a .db file, no text source next to it. Last written October 7. Whatever Plesk did on renewal, the new cert never ended up in this map. I don't know why it stopped. This box has had its Plesk troubles before (see what still works on an expired Plesk license), but I did not prove a link, so I won't claim one.

Gotcha: postmap -q prints the private key

My first move was postmap -q mail.example.com hash:/var/spool/postfix/plesk/certs. Bad idea. In this map the value is not a file path. It is the whole PEM bundle, base64-encoded. That is the format Postfix uses when a SNI table is built with postmap -F: the key and cert contents go into the database, not the file names. So postmap -q dumped the client's private key, base64, straight into my terminal.

Nothing left the session. Still, a private key in scrollback is not where it belongs. If that happens to you, the clean move is to reissue the cert with a new key when you get the chance.

Here is the safe way to look at the map. It decodes each value and only prints the expiry:

postmap -s hash:/var/spool/postfix/plesk/certs | while read k v; do
  end=$(echo "$v" | base64 -d | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
  echo "$k | $end"
done
mail.example.com   | Oct 22 19:02:37 2026 GMT
example.com        | Oct 22 19:02:37 2026 GMT
mail.example.org   | Oct 23 02:39:46 2026 GMT
example.org        | Oct 23 02:39:46 2026 GMT
...

openssl x509 reads the first certificate in the stream and ignores the key, so you get the date and nothing secret. Eight entries on this box, and the client's two were not the only ones past their date. Another domain on the same server was expired in the map too. Today I only fixed the client's two.

Step 3: the fix

Back up the map, build the new value from the current Plesk cert file, swap the two entries, reload:

M=/var/spool/postfix/plesk/certs
F=/opt/psa/var/certificates/<random-name>   # path from the Dovecot SNI conf

cp -a $M.db $M.db.bak-$(date +%Y%m%d%H%M)

V=$(printf "\n%s\n" "$(cat $F)" | base64 -w0)

for h in example.com mail.example.com; do
  postmap -d $h hash:$M
  echo "$h $V" | postmap -i hash:$M
done

postfix reload

Then check what the map holds now, again without printing the key:

for h in example.com mail.example.com; do
  postmap -q $h hash:$M | base64 -d | openssl x509 -noout -enddate
done
notAfter=Jan  5 06:14:37 2027 GMT
notAfter=Jan  5 06:14:37 2027 GMT

Gotcha: postmap -i does not overwrite

My first try skipped the postmap -d line. Postfix said this:

postmap: warning: /var/spool/postfix/plesk/certs.db: duplicate entry: "example.com"
postmap: warning: /var/spool/postfix/plesk/certs.db: duplicate entry: "mail.example.com"

And the check still showed October 22. postmap -i adds entries. If the key already exists, it warns and keeps the old value. It does not fail, so in a script it looks like success. Delete first with postmap -d, then insert. That second run worked.

Step 4: test from outside, every SMTP port

The map looking right is not proof. What the client's mail program sees is proof:

for h in example.com mail.example.com; do
  for p in 465 587 25; do
    printf "%s:%s -> " $h $p
    o=""; [ $p != 465 ] && o="-starttls smtp"
    echo | timeout 10 openssl s_client -connect $h:$p -servername $h $o 2>/dev/null \
      | openssl x509 -noout -enddate
  done
done

All six lines said January 5, 2027. Port 25 matters too. That is where other mail servers deliver to you. They are usually less strict about certs than Outlook, but there is no reason to serve them garbage.

What I told the client: restart the mail program once, then the warning is gone.

Gotcha: always send SNI when you test

The default cert, /etc/postfix/postfix.pem, is valid until mid-2027. If I had tested with openssl s_client without -servername, Postfix would have handed me that default cert. It looks perfectly healthy, and I would have told the client "all fine, must be your side". The broken cert only shows up when you ask for the client's hostname. Same lesson as in my Postfix SNI map post, different box, different panel.

Side finding: webmail had the wrong cert

While checking port 443 on mail.example.com, I noticed it served a cert for a completely different site on the same server. Not expired, just the wrong name. Browsers warn on that too. Mail programs don't care, because they only talk to 993, 465 and 587. If the client uses webmail in the browser, that needs fixing as well. It's on my list, it was not the cause of today's call.

What stops this from happening again

Honest status: nothing yet. The fix today was manual. Plesk still does not update this map. The current cert runs until January 5, 2027. Let's Encrypt certs usually get renewed about 30 days before they expire, so somewhere around early December Plesk will renew again, Dovecot and nginx will pick it up, and Postfix will keep serving the cert I put in today until it expires. Same call, a few weeks later.

The options I see:

  • A small daily sync job. For every hostname in the Dovecot SNI confs, read the Plesk cert file it points to, compare the expiry with what the Postfix map holds, and if they differ, do the delete, insert, reload from above. I offered to write that. If I do, it will be a systemd timer and not a crontab line.
  • At least a check. The postmap -s loop above, run daily, alerting when any entry is under 14 days. That alone would have caught this before the client did.
  • Get mail off this Plesk box. It is on its way out anyway. On a panel like KeyHelp the SNI map is a text file that the panel rewrites, which removes this whole class of problem, as long as you watch the map type.

Whichever one it is, the real lesson is boring: "the cert renewed" and "every service uses the new cert" are two different facts. Check the second one, on every port, with SNI.


Related posts