PSA: Is your Hestia mail serving an incomplete TLS chain? How to check (and fix) in 2 minutes

Hi all,

while debugging IMAP connection failures from a PHP client against one of our older HestiaCP servers, we went down a TLS rabbit hole and learned a few things that may save others some time. Sharing the findings, a quick self-check, and a fix.

TL;DR: current HestiaCP (tested on a fresh 1.9.6 install) handles TLS chains correctly on every automated path — Let’s Encrypt web, mail and host certificates are all installed as full chains. But there are two ways a Hestia server can still end up serving a leaf-only certificate (no intermediate), which makes strict TLS clients fail with unable to get local issuer certificate while browsers often still work:

  1. Manually uploaded certificates without the CA bundle.
    If you paste only the certificate + key (no chain) when adding SSL to a mail domain or the control panel, Hestia accepts it silently and Exim/Dovecot will serve the bare leaf. No warning is shown. Mail clients like Outlook/Thunderbird and any language runtime with strict verification (PHP streams, Go, Python requests) will refuse the connection.

  2. Legacy configurations.
    Servers that started life years ago (early Hestia, VestaCP migrations, or hand-rolled mail SSL) can carry stale regular files in /usr/local/hestia/ssl/mail/ and custom Dovecot local_name entries pointing at <domain>.crt (leaf) instead of <domain>.pem (fullchain). Modern Hestia uses symlinks to the user’s .pem — but nothing detects or heals the old layout, and LE renewals keep renewing while your services keep serving the certificate without its chain. Bonus trap: certificate.crt (Dovecot default + panel) is only refreshed on renewal when `hostname -f` matches the certificate’s domain — if they differ, it stays frozen forever.

Self-check (run on your server, 30 seconds):


# One or more mail domains? Check the chain Dovecot serves with SNI:
echo | openssl s_client -connect 127.0.0.1:993 -servername mail.YOURDOMAIN.TLD 2>/dev/null | grep "Verify return"

# Same for SMTP submission:
echo | openssl s_client -connect 127.0.0.1:587 -starttls smtp -servername mail.YOURDOMAIN.TLD 2>/dev/null | grep "Verify return"

# Default context (no SNI) + panel:
echo | openssl s_client -connect 127.0.0.1:993 2>/dev/null | grep "Verify return"

Verify return code: 0 (ok) → you’re fine. 21 (unable to verify the first certificate) → you’re serving a leaf without its intermediate.

Fix for the legacy case — align the stale files to the fullchain .pem that Hestia already renews for you:


# Example for a mail cert under /usr/local/hestia/ssl/mail (adjust user/domain):
SRC=/home/USER/conf/web/DOMAIN/ssl/DOMAIN.pem
install -m 0644 "$SRC" /usr/**local**/hestia/ssl/mail/mail.DOMAIN.crt
systemctl reload exim4 dovecot

If you have custom Dovecot local_name blocks, point their ssl_cert at the .pem, not the .crt. We also keep a small idempotent script in cron that re-syncs these files after every LE renewal and reloads the services only when something changed — happy to share it if useful.

Fix for the manual-upload case:
re-upload the certificate including the full chain (certificate + intermediate in the CA field), or switch the domain to Let’s Encrypt which always installs the chain.

We’ve also opened a small feature request suggesting Hestia warn when a chainless certificate is uploaded (link below) — on current versions the acceptance is completely silent, and the resulting failures show up days later in the least obvious place (a customer’s mail client).

Hope this saves someone the afternoon it cost us. Happy to answer questions.

Silvano

(Bluix Group)

1 Like