TLS encrypts a mail session, but only per hop and only when both ends cooperate. Postfix's own TLS_README is worth reading in full. This guide distills what actually needs deciding.
Two independent directions¶
Postfix has separate settings for TLS as a server (receiving, smtpd_*) and as a client (sending, smtp_*). It's easy to configure one and forget the other.
# /etc/postfix/main.cf
# ---- Server side: mail coming IN (port 25, and the defaults for 587/465) ----
smtpd_tls_chain_files = /etc/letsencrypt/live/mail.domain.tld/privkey.pem,
/etc/letsencrypt/live/mail.domain.tld/fullchain.pem
smtpd_tls_security_level = may # offer STARTTLS; don't require it on port 25
smtpd_tls_auth_only = yes # never offer AUTH before STARTTLS
smtpd_tls_loglevel = 1
smtpd_tls_received_header = yes # record protocol/cipher in Received:
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache # lmdb: if BDB is unavailable
smtpd_tls_mandatory_protocols = >=TLSv1.2
smtpd_tls_protocols = >=TLSv1.2
# ---- Client side: mail going OUT ----
smtp_tls_security_level = may # use TLS when the peer offers it
smtp_tls_loglevel = 1
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt # RHEL: /etc/pki/tls/certs/ca-bundle.crt
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtp_tls_policy_maps = hash:/etc/postfix/tls_policy # optional, see below
Parameters you can delete from old configs¶
| Old setting | Status / replacement |
|---|---|
smtp_use_tls, smtpd_use_tls |
Deprecated (warnings since Postfix 3.9). Use *_tls_security_level = may |
smtp_enforce_tls, smtpd_enforce_tls |
Deprecated. Use *_tls_security_level = encrypt |
smtp_tls_enforce_peername |
Deprecated. Use smtp_tls_security_level = verify or secure |
smtp_tls_per_site |
Deprecated. Use smtp_tls_policy_maps |
smtpd_tls_cert_file + smtpd_tls_key_file |
Still work, but smtpd_tls_chain_files (Postfix 3.4+) loads key and chain together atomically and supports RSA + ECDSA side by side |
smtpd_tls_CAfile pointing at your own certificate |
A misunderstanding in many old configs. This file lists CAs for verifying client certificates and isn't needed for normal STARTTLS |
tls_random_source = dev:/dev/urandom |
Unnecessary on any current system |
smtpd_tls_dh1024_param_file, smtpd_tls_eecdh_grade |
Leave at defaults (deprecated) |
smtpd_tls_session_cache_database under /var/run |
Must live under Postfix's data_directory (see Troubleshooting below) |
On Postfix 3.11+ with compatibility_level = 3.11, smtp_tls_security_level already defaults to may, but setting it explicitly costs nothing and documents intent.
Security levels¶
| Level | Meaning | Use for |
|---|---|---|
none |
No TLS | Almost never |
may |
Opportunistic: use TLS if offered, otherwise plaintext. Certificates aren't verified | Port 25 in both directions (the internet default) |
encrypt |
TLS mandatory, certificate not verified | Submission ports (server side); smart hosts (client side), to protect credentials |
dane |
Opportunistic DANE: verify against DNSSEC-signed TLSA records when present, else may |
Outbound to the internet, if you run a validating resolver |
verify |
Mandatory TLS + certificate must match the expected name via MX lookup | Specific partner domains |
secure |
Like verify, but names come from your policy, not DNS |
High-assurance partners |
On the server side, port 25 stays at may. Requiring TLS there would reject the minority of legitimate servers that don't support it. The submission services (587/465) set smtpd_tls_security_level=encrypt in master.cf (see Virtual Domains and Mailboxes). If a client connects to a port with encrypt and skips STARTTLS, it gets 530 5.7.0 Must issue a STARTTLS command first.
Per-destination policy¶
Opportunistic by default, but mandatory (and optionally verified) for specific domains:
smtp_tls_policy_maps = hash:/etc/postfix/tls_policy
# /etc/postfix/tls_policy
partner.tld encrypt
.partner.tld encrypt # leading dot = all subdomains
bank.tld secure match=.bank.tld
[smtp.provider.tld]:587 encrypt # a relayhost: key must match relayhost exactly
sudo postmap /etc/postfix/tls_policy && sudo postfix reload
If the map is referenced in main.cf but was never compiled, Postfix logs fatal: open database /etc/postfix/tls_policy.db: No such file or directory. Create the file (even empty) and postmap it.
Certificates¶
For a server that receives mail from the internet or from users' clients, use a certificate from a publicly trusted CA. Let's Encrypt is free and automated. The name on the certificate must be what clients connect to (your MX hostname and/or mail.domain.tld).
sudo certbot certonly --standalone -d mail.domain.tld # or --webroot / DNS challenge
smtpd_tls_chain_files =
/etc/letsencrypt/live/mail.domain.tld/privkey.pem,
/etc/letsencrypt/live/mail.domain.tld/fullchain.pem
Reload Postfix (and Dovecot) after each renewal with a certbot deploy hook:
# /etc/letsencrypt/renewal-hooks/deploy/mail.sh
#!/bin/sh
systemctl reload postfix dovecot
Postfix reads its keys before dropping privileges and chrooting, so root-only permissions on /etc/letsencrypt/live are fine.
Private CAs. For a closed set of internal hosts that only exchange mail with each other, a private CA works, provided every client trusts it (smtp_tls_CAfile on the senders, plus secure or verify in their policy if you want verification). The certificate belongs on the host presenting it: the receiving side of each hop, or both sides if both directions are enforced. Issuing it on the wrong host relative to which side needs to present a certificate is a classic self-inflicted failure, and the resulting errors look generic.
Distribute private CA certificates (public half only) with your configuration management, or scp for a one-off. Private keys never leave the host they belong to.
DANE and MTA-STS (outbound)¶
- DANE: if the host has a local DNSSEC-validating resolver (e.g. unbound), enable it outbound. Postfix then verifies certificates for every destination that publishes TLSA records, and stays opportunistic elsewhere:
ini
smtp_dns_support_level = dnssec
smtp_tls_security_level = dane
- MTA-STS: Postfix enforces MTA-STS via a policy service. The common choice is
postfix-mta-sts-resolver, queried throughsmtp_tls_policy_maps = socketmap:inet:127.0.0.1:8461:postfix. Postfix 3.11 addedsmtp_tls_enforce_sts_mx_patternsto make that matching correct.
To publish these for your own domain so others must use TLS to you, see Email Authentication and Anti-Spam.
Verifying that encryption actually happened¶
Don't just trust the config.
In a delivered message's headers, Postfix records the TLS details when smtpd_tls_received_header = yes:
Received: from mail.example.org (mail.example.org [192.0.2.40])
(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256)
(No client certificate requested)
by mx.domain.tld (Postfix) with ESMTPS id D9B5220469
for <[email protected]>; Fri, 9 Oct 2026 20:37:19 -0400 (EDT)
ESMTPS (rather than ESMTP) and the explicit protocol and cipher confirm the hop was encrypted. You'll still see old entries like TLSv1 with cipher ADH-AES256-SHA. Anonymous Diffie-Hellman (ADH) encrypts against passive eavesdropping but authenticates nobody, and TLS 1.0/1.1 should be disabled today (>=TLSv1.2 above).
In the log (*_tls_loglevel = 1):
postfix/smtp[1234]: Verified TLS connection established to mx.example.org[192.0.2.40]:25: TLSv1.3 ...
postfix/smtp[1234]: Untrusted TLS connection established to ... # encrypted, cert not verified (normal at level may)
postfix/smtpd[5678]: Anonymous TLS connection established from ...
With the tools:
posttls-finger -c -l secure mx.example.org # what Postfix's client would see, including DANE/policy
posttls-finger -c -l may "[smtp.provider.tld]:587"
openssl s_client -connect mail.domain.tld:25 -starttls smtp -servername mail.domain.tld </dev/null \
| openssl x509 -noout -subject -issuer -dates
Troubleshooting TLS¶
SELinux is preventing … smtpd from read access on the file … .pem. The certificate or key is somewhere without a context Postfix may read, typically a home directory or a custom path. The fix is to keep TLS material in standard locations (/etc/pki/tls/, /etc/letsencrypt/), or label your custom directory correctly:
sudo semanage fcontext -a -t cert_t '/srv/certs(/.*)?'
sudo restorecon -Rv /srv/certs
Generating a one-off policy module with audit2allow works, but it hides the real problem and is easy to forget. Don't disable SELinux to "test". Use ausearch -m avc -ts recent to see exactly what's denied instead.
STARTTLS "just doesn't work", with no clear error. Before suspecting the config, rule out the environment:
- Another MTA owns the port. A Sendmail or Exim that was never disabled, or a leftover from an earlier mail suite install (a Zimbra install left enabled is a known example), means you're talking to something other than the Postfix you configured:
bash
sudo ss -tlnp | grep -E ':(25|587|465) '
systemctl list-units --type=service | grep -Ei 'sendmail|exim|zimbra'
sudo systemctl disable --now <service>
- Something in between intercepts SMTP. Some firewalls and antivirus gateways "inspect" SMTP and strip the
STARTTLSadvertisement. CompareEHLOresponses from inside and outside your network.
warning: TLS library problem: … SSL_CTX_use_PrivateKey_file … or key values mismatch. The key path is stale (left over after a certificate was renamed or renewed), or the key doesn't match the certificate. Use smtpd_tls_chain_files with both from the same issuance, and confirm the paths in postconf -n exist.
warning: TLS library problem … enabling PIX workarounds. Not your problem. The peer is behind a Cisco PIX/ASA with its SMTP "fixup" feature enabled, and Postfix detected it and switched on compatibility workarounds. It's informational.
warning: request to update table btree:/var/run/smtpd_tls_session_cache in non-postfix directory /var/run. Since Postfix 2.5 the TLS session cache must live under data_directory. Postfix redirects and warns. Fix the setting to silence it:
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
no shared cipher / wrong version number. The peer only supports protocols or ciphers you've disabled, or you're speaking STARTTLS to an implicit-TLS port (or the reverse). Check the port against smtp_tls_wrappermode and smtpd_tls_wrappermode.