Motivation

Running your own mail server goes far beyond simply sending and receiving messages. With the rise of TLS downgrade attacks, spoofing, and SMTP traffic interception, adopting modern standards like DANE, DNSSEC, and TLSA is no longer optional for anyone who takes security seriously.

This post documents the hardening process of a mail server running Postfix 3.11 on FreeBSD, all the way to achieving 100% on Internet.nl — with a Hall of Fame entry.


Prerequisites

  • Postfix 3.6+ (we use 3.11)
  • DNSSEC enabled on the domain (via Cloudflare, in this case)
  • EC-256 certificate issued via acme.sh
  • OpenSSL 3.x

1. EC-256 Certificate with acme.sh

ECDSA (EC-256) certificates are more efficient and secure than RSA 2048. To issue one via acme.sh:

acme.sh --issue --dns dns_cf -d mail.domain.tld --keylength ec-256
acme.sh --install-cert -d mail.domain.tld --ecc \
  --cert-file /usr/local/etc/postfix/tls/cert.pem \
  --key-file /usr/local/etc/postfix/tls/key.pem \
  --fullchain-file /usr/local/etc/postfix/tls/fullchain.pem \
  --reloadcmd "service postfix reload"

Note: acme.sh creates the certificate directory with an _ecc suffix for EC certificates. Check the correct path at ~/.acme.sh/mail.domain.tld_ecc/.


2. Postfix Configuration (main.cf)

Inbound TLS (smtpd)

# Certificate
smtpd_tls_cert_file = /usr/local/etc/postfix/tls/fullchain.pem
smtpd_tls_key_file = /usr/local/etc/postfix/tls/key.pem

# Protocols — TLS 1.3 only for mandatory connections
smtpd_tls_mandatory_protocols = >=TLSv1.3
smtpd_tls_protocols = >=TLSv1.2

# Ciphers
smtpd_tls_mandatory_ciphers = high
smtpd_tls_ciphers = high
smtpd_tls_exclude_ciphers = aNULL, CAMELLIA, SHA, ECDHE-ECDSA-AES256-CCM8, ECDHE-ECDSA-AES128-CCM8
smtpd_tls_mandatory_exclude_ciphers = aNULL, CAMELLIA, SHA, ECDHE-ECDSA-AES256-CCM8, ECDHE-ECDSA-AES128-CCM8

# Security
smtpd_tls_security_level = may
smtpd_tls_loglevel = 1

Note: smtpd_tls_prefer_server_ciphers does not exist in Postfix 3.6+. The smtpd_tls_fingerprint_digest parameter defaults to sha256 starting from version 3.6.

Outbound TLS (smtp) with DANE

# Protocols
smtp_tls_mandatory_protocols = >=TLSv1.2
smtp_tls_protocols = >=TLSv1.2

# Ciphers
smtp_tls_mandatory_ciphers = high
smtp_tls_ciphers = high
smtp_tls_exclude_ciphers = aNULL, CAMELLIA, SHA, DHE

# DANE — validates the remote server certificate via TLSA + DNSSEC
smtp_tls_security_level = dane
smtp_dns_support_level = dnssec
smtp_tls_loglevel = 1

3. DANE and TLSA

What is DANE?

DANE (DNS-Based Authentication of Named Entities) allows publishing TLS certificate information in DNS, protected by DNSSEC. This prevents downgrade attacks where an attacker strips STARTTLS from the SMTP negotiation.

Generating the TLSA Record

The 3 1 1 type (DANE-EE, SPKI, SHA-256) is the most recommended: it binds the hash of the certificate’s public key, and does not change as long as the private key remains the same — even across renewals.

openssl x509 -in /usr/local/etc/postfix/tls/fullchain.pem \
  -pubkey -noout | \
  openssl pkey -pubin -outform DER | \
  openssl dgst -sha256 -binary | \
  xxd -p -c 32

The output is the hash to be published in DNS:

_25._tcp.mail.domain.tld. IN TLSA 3 1 1 <hash_here>

Requirements

  • DNSSEC is mandatory on the mail server domain
  • The TLSA record must be on the MX domain (mail.domain.tld), not the email domain (domain.tld)

Verifying

drill -D TLSA _25._tcp.mail.domain.tld

4. Removing SHA-1 from Signature Algorithms

Internet.nl reports the signature algorithms advertised by the server, not just the ones negotiated. By default, OpenSSL advertises ECDSA+SHA1 in TLS 1.2, even if it is never actually used.

The solution is to configure /etc/ssl/openssl.cnf globally.

openssl.cnf Structure on FreeBSD

The file already has openssl_conf = openssl_init. Add ssl_conf to the [openssl_init] section and create the required sections:

[openssl_init]
providers = provider_sect
ssl_conf = ssl_sect          # <-- add this line

[ssl_sect]
system_default = system_default_sect

[system_default_sect]
CipherString = DEFAULT:@SECLEVEL=2
SignatureAlgorithms = ecdsa_secp256r1_sha256:ecdsa_secp384r1_sha384:ecdsa_secp521r1_sha512:rsa_pss_rsae_sha256:rsa_pss_rsae_sha384:rsa_pss_rsae_sha512:rsa_pkcs1_sha256:rsa_pkcs1_sha384:rsa_pkcs1_sha512

What each directive does:

DirectiveEffect
CipherString = DEFAULT:@SECLEVEL=2Removes SHA-1, MD5, requires RSA >= 2048, disables TLS compression
SignatureAlgorithmsRestricts the signature algorithms advertised in TLS 1.2/1.3

Important: CipherString alone is not enough — SignatureAlgorithms must be explicitly defined using IANA names (e.g. ecdsa_secp256r1_sha256), not the legacy ECDSA+SHA256 format.

After editing, restart all services:

service postfix restart
service nginx restart
service dovecot restart

Verifying

testssl.sh --starttls smtp mail.domain.tld:25 2>&1 | grep "TLS 1.2 sig_algs offered"

Expected output (no SHA1):

TLS 1.2 sig_algs offered: ECDSA+SHA256 ECDSA+SHA384 ECDSA+SHA512

5. Certificate Rollover with DANE

When replacing a certificate with a new private key, the TLSA hash changes. The correct procedure is:

  1. Generate the new certificate (without deploying it yet)
  2. Calculate the hash of the new key:
    openssl x509 -in /new/cert.pem -pubkey -noout | \
      openssl pkey -pubin -outform DER | \
      openssl dgst -sha256 -binary | \
      xxd -p -c 32
    
  3. Publish two TLSA records in DNS (old + new)
  4. Wait for propagation (check TTL with drill _25._tcp.mail.domain.tld TLSA)
  5. Deploy the new certificate and restart services
  6. Remove the old TLSA record

Golden rule: DNS first, certificate second. Doing it in reverse order will cause mail rejection by servers that validate DANE.


6. Result

After all configurations, your server will achieve 100% on Internet.nl, with a Hall of Fame entry.

✅ IPv6
✅ DNSSEC
✅ DMARC (p=reject) + DKIM + SPF (-all)
✅ STARTTLS + valid DANE
✅ TLS 1.3 + TLS 1.2 without SHA-1
✅ Secure cipher suites (ECDHE-ECDSA)
✅ RPKI

References