SSL/TLS en Postfix y en Dovecot: por qué y cómo

Mascote do LinuxPro fechando um grande cadeado dourado no servidor de e-mail, com envelopes trancados passando por um túnel protegido até outro servidor e o cão caramelo ao lado

Un servidor de correo sin TLS entrega dos cosas gratis a quien esté en medio: el contenido de los mensajes y la contraseña de cada usuario, que viaja cada vez que el móvil consulta el buzón. Y no es solo privacidad: desde febrero de 2024 Gmail exige conexión TLS al todo remitente. A continuación, SSL/TLS completo en un servidor Postfix + Dovecot: certificado Let's Encrypt, los puertos correctos, versiones mínimas, autenticación solo con cifrado y la protección contra el ataque que elimina STARTTLS. Todo probado en laboratorio en Ubuntu 24.04 y en 26.04, que incluyen Dovecots de versiones distintas, con sintaxis distintas.

Por qué el servidor de correo necesita TLS

  • Contraseñas. IMAP, POP3 y SMTP autenticado envían usuario y contraseña al inicio de cada sesión. Sin TLS, cualquiera en el Wi-Fi del café, en el proveedor o en un router comprometido lee en texto plano. Un único inicio de sesión capturado da acceso a toda la bandeja — y al restablecimiento de contraseña de todo lo que esté vinculado a ese correo.
  • Contenido. Contrato, recibo, factura, informe médico: el mensaje completo pasa legible entre el cliente y el servidor y entre servidores.
  • Entregabilidad. Las reglas de Gmail para cualquier remitente incluyen “Use a TLS connection for transmitting email”, junto a SPF/DKIM y DNS inverso — el mismo paquete del artículo sobre DKIM y DMARC en Postfix.
  • Un protocolo antiguo es un protocolo roto. A RFC 8996 (marzo de 2021) dice que TLS 1.0 y 1.1 “MUST NOT be used”. El mínimo hoy es TLS 1.2, y el TLS 1.3 es el que negocian los clientes modernos.

Los puertos y el tipo de TLS de cada uno

Diagrama das portas: o usuário envia pela 465 com TLS implícito ou pela 587 com STARTTLS obrigatório, lê a caixa pela 993 ou 995 com TLS implícito; o Postfix troca mensagens com outros servidores pela 25 com STARTTLS oportunista; o Dovecot autentica o SMTP do Postfix por socket local

  • 25 (SMTP) — servidor hablando con servidor. Usa STARTTLS oportunista (RFC 3207): la conversación empieza en texto plano y sube a TLS si ambos lados lo ofrecen. No se puede exigir TLS aquí a todo el mundo — buena parte de internet todavía lo entrega sin él —, así que el estándar es may.
  • 465 (submissions) — el usuario enviando, con TLS implícito: la conexión ya nace cifrada, como en HTTPS.
  • 587 (submission) — el usuario enviando con STARTTLS, pero obligatorio: el servidor rechaza cualquier comando antes del TLS.
  • 993 (IMAPS) y 995 (POP3S) — lectura del buzón con TLS implícito.
  • 143 y 110 — IMAP y POP3 sin TLS implícito. No exponer a internet.

A RFC 8314 (enero de 2018) recomienda TLS implícito para envío y lectura — 465, 993 y 995 — y acepta la 587 con STARTTLS durante la transición. En la práctica, ofrezca 465 y 587: clientes antiguos y algunos teléfonos móviles siguen viniendo configurados en la 587.

Versiones en el laboratorio

Paquete Ubuntu 24.04 Ubuntu 26.04 Debian 13
postfix 3.8.6 3.10.6 3.10.13
dovecot-core 2.3.21 2.4.2 2.4.1
certbot 2.9.0 4.0.0 4.0.0
openssl 3.0.13 3.5.5

La línea que importa es la de Dovecot: el 2.4 cambió los nombres de las opciones de TLS y no acepta la configuración del 2.3 sin ajustes. Voy a mostrar las dos.

El certificado: Let's Encrypt en el nombre del servidor

El certificado tiene que tener el nombre que los clientes y los otros servidores usan para llegar hasta usted — el del registro MX y el que está configurado en Thunderbird y en el móvil. En esta entrada, mail.example.com. Si usas alias como smtp. e imap., inclúyelos todos en el mismo certificado con varios -d.

El servidor de correo generalmente no tiene sitio web; el modo standalone de certbot levanta un servidor web temporal en el puerto 80 solo para la validación:

sudo apt install -y certbot
sudo ufw allow 80/tcp
sudo certbot certonly --standalone -d mail.example.com \
  --agree-tos -m postmaster@example.com --no-eff-email

Si ya hay un nginx o Apache en la máquina, usa --webroot -w /var/www/html en lugar de --standalone. El resultado queda en /etc/letsencrypt/live/mail.example.com/: fullchain.pem (certificado + intermedios) y privkey.pem (la clave). Los directorios live e archive son legibles solo por root — y está bien: Postfix y Dovecot abren el certificado como root, antes de soltar privilegios. No cambies los permisos.

Renovación y los plazos que se están acortando

El paquete instala el certbot.timer, que renueva solo. Lo que no hace es avisar a Postfix y a Dovecot: siguen sirviendo el certificado antiguo desde memoria hasta recargarlo. Un deploy hook resolve — certbot ejecuta los scripts de ese directorio solo cuando una renovación sale bien:

sudo tee /etc/letsencrypt/renewal-hooks/deploy/recarrega-email.sh >/dev/null <<'EOF'
#!/bin/sh
systemctl reload postfix dovecot
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/recarrega-email.sh
sudo certbot renew --dry-run

En Ubuntu 24.04 (certbot 2.9), los hooks de ese directorio solo se ejecutan con el subcomando renew — que es lo que utiliza el timer, así que la renovación automática funciona; en la primera emisión, recargue manualmente.

Esto dejó de ser un detalle. Let’s Encrypt anunció que el certificado por defecto de 90 días baja a 64 días el 10 de febrero de 2027 y a 45 días el 16 de febrero de 2028. Y el CA/Browser Forum aprobó en 2025 un límite máximo para todas las CA: 200 días desde el 15 de marzo de 2026, 100 días a partir de marzo de 2027 y 47 días a partir de marzo de 2029. La renovación manual se acabó; renovar sin recarga es certificado caducado en producción.

Postfix: TLS en main.cf

Debian y Ubuntu ya instalan Postfix con TLS activado, pero con el certificado autofirmado ssl-cert-snakeoil y sin versión mínima en los puertos de los usuarios. Cambia el certificado y ajusta lo demás:

sudo postconf -X smtpd_tls_cert_file smtpd_tls_key_file
sudo postconf -e \
  'smtpd_tls_chain_files = /etc/letsencrypt/live/mail.example.com/privkey.pem, /etc/letsencrypt/live/mail.example.com/fullchain.pem' \
  'smtpd_tls_security_level = may' \
  'smtpd_tls_auth_only = yes' \
  'smtpd_tls_mandatory_protocols = >=TLSv1.2' \
  'smtp_tls_security_level = may' \
  'smtp_tls_mandatory_protocols = >=TLSv1.2' \
  'smtpd_tls_loglevel = 1' \
  'smtp_tls_loglevel = 1' \
  'smtpd_tls_received_header = yes'
  • smtpd_tls_chain_files (Postfix 3.4+): primero la clave, después la cadena. Sustituye el par smtpd_tls_cert_file/smtpd_tls_key_file, por eso el postconf -X antes.
  • smtpd_* es Postfix recibiendo; smtp_* es Postfix entregando a otros servidores. Ambos necesitan configuración.
  • security_level = may en el puerto 25, en ambos sentidos: TLS cuando el otro lado lo ofrece. Los puertos de los usuarios quedan con encrypt no master.cf, a continuación.
  • smtpd_tls_auth_only = yes: el servidor ni anuncia AUTH antes del TLS. Contraseña en texto plano resulta imposible, no solo desaconsejada.
  • *_mandatory_protocols = >=TLSv1.2 (sintaxis de Postfix 3.6+): sirve donde el TLS es obligatorio — los puertos 465 y 587 de los usuarios, a continuación. En el laboratorio, con esta línea, ambos rechazaron TLS 1.1 (Cipher is (NONE)) en ambos Ubuntu.
  • ¿Por qué no exigir TLS 1.2 también en el puerto 25? Porque allí el TLS es oportunista y, si el handshake falla, Postfix entrega en texto plano. Rechazar TLS 1.0 de un servidor antiguo no bloquea el mensaje — solo hace que viaje sin ningún cifrado, lo que es peor. Por eso el puerto 25 se queda con el valor por defecto de Postfix (smtpd_tls_protocols e smtp_tls_protocols en >=TLSv1, sin SSLv2/SSLv3). En el laboratorio, el Postfix por defecto de 24.04 y de 26.04 negoció TLS 1.0 y 1.1 de verdad en la 25 — es lo esperado. Quien quiera TLS fuerte de verdad entre servidores usa MTA-STS o DANE, al final del post.
  • loglevel = 1 registra una línea por conexión TLS, y received_header graba la versión y el cifrado en la cabecera Received: de cada mensaje — útil para demostrar, después, que llegó cifrado.

Postfix: los puertos 465 y 587 en master.cf

O /etc/postfix/master.cf del paquete trae ambas entradas comentadas. Descomente o añada, usando en la 5ª columna el mismo valor que la línea smtp inet de su archivo (y en 24.04, que ejecuta smtpd en chroot; n en 26.04):

submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
submissions inet n      -       y       -       -       smtpd
  -o syslog_name=postfix/submissions
  -o smtpd_tls_wrappermode=yes
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject

smtpd_tls_security_level=encrypt hace TLS obligatorio — es lo que activa el smtpd_tls_mandatory_protocols — y smtpd_tls_wrappermode=yes es el TLS implícito de 465. En ambas, solo pasa quien se ha autenticado. Quien comprueba la contraseña es Dovecot, mediante SASL:

sudo postconf -e 'smtpd_sasl_type = dovecot' 'smtpd_sasl_path = private/auth'
sudo postfix check && sudo systemctl restart postfix

La ruta private/auth es relativa a /var/spool/postfix y funciona con y sin chroot.

Dovecot 2.3 (Ubuntu 24.04)

Cree /etc/dovecot/conf.d/99-tls.conf — el 99 garantiza que se lea en último lugar y prevalezca sobre el 10-ssl.conf del paquete:

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
ssl_prefer_server_ciphers = yes
disable_plaintext_auth = yes

En 2.3, el < antes de la ruta es obligatorio: indica “lee el contenido de este archivo”. Sin él, Dovecot intenta usar la propia ruta como certificado.

Dovecot 2.4 (Ubuntu 26.04 y Debian 13)

El mismo archivo, con los nombres nuevos:

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
ssl_min_protocol = TLSv1.2
auth_allow_cleartext = no
  • ssl_cert/ssl_key se han convertido en ssl_server_cert_file/ssl_server_key_file, con ruta pura, sin <.
  • disable_plaintext_auth = yes se convirtió en auth_allow_cleartext = no (que ya es el valor por defecto).
  • ssl_prefer_server_ciphers se convirtió en ssl_server_prefer_ciphers, y ssl_dh se convirtió en ssl_server_dh_file.
  • O dovecot.conf debe comenzar con dovecot_config_version y tener dovecot_storage_version — el paquete ya lo trae. Configuración copiada de un servidor 2.3 no arranca: consulta la guía de migración 2.3 → 2.4.

El socket de autenticación para Postfix (2.3 y 2.4)

En /etc/dovecot/conf.d/99-postfix-auth.conf, igual en ambas versiones:

service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }
}
sudo doveconf -n >/dev/null && echo config OK
sudo systemctl restart dovecot

ssl = required con la autenticación sin texto plano hace que Dovecot rechace el inicio de sesión fuera de TLS. En el laboratorio, llegando desde otra máquina por el puerto 143:

# Dovecot 2.4
a1 NO [PRIVACYREQUIRED] Cleartext authentication disallowed on non-secure (SSL/TLS) connections.
# Dovecot 2.3 (POP3 na 110)
-ERR [AUTH] Plaintext authentication disallowed on non-secure (SSL/TLS) connections.

Atención al probar: Dovecot considera seguras las conexiones del propio 127.0.0.1. El inicio de sesión sin TLS desde el servidor funciona — la prueba que vale es desde otra máquina.

Firewall

sudo ufw allow 25,465,587,993,995/tcp

Deja 143 y 110 cerradas hacia fuera. Quien use webmail en la misma máquina accede por localhost; los clientes externos van por 993 y por 995.

Probando cada puerto

O openssl s_client muestra versión, cifrado y si la cadena del certificado fue verificada:

openssl s_client -brief -starttls smtp -connect mail.example.com:25  </dev/null
openssl s_client -brief -connect mail.example.com:465 </dev/null
openssl s_client -brief -starttls smtp -connect mail.example.com:587 </dev/null
openssl s_client -brief -connect mail.example.com:993 </dev/null
openssl s_client -brief -connect mail.example.com:995 </dev/null
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Verification: OK

Esa fue la salida en los cinco puertos, en ambos Ubuntu. Para confirmar que TLS antiguo se rechaza en los puertos de los usuarios, fuerza la versión — y mira el cifrado, no la línea de protocolo, que el openssl imprime incluso cuando el handshake falla:

openssl s_client -tls1_1 -cipher 'DEFAULT:@SECLEVEL=0' \
  -connect mail.example.com:465 </dev/null 2>&1 | grep 'Cipher is'
# Cipher is (NONE)   ← recusado, como deve ser

Envío autenticado, verificando el certificado, con el swaks:

sudo apt install -y swaks
swaks --server mail.example.com:465 --tlsc --tls-verify \
  --auth PLAIN --auth-user ana --from ana@example.com --to voce@gmail.com
=== TLS started with cipher TLSv1.3:TLS_AES_256_GCM_SHA384:256
<~  235 2.7.0 Authentication successful
<~  250 2.0.0 Ok: queued as 4D83AB47D87

Sustituya :465 --tlsc por :587 --tls para probar el otro puerto. En 587, sin STARTTLS, Postfix responde 530 5.7.0 Must issue a STARTTLS command first a cualquier comando — y el EHLO ni lista AUTH.

Para el lado servidor-servidor, Postfix trae el posttls-finger, que se conecta como Postfix se conectaría a un MX y dice el nivel de confianza obtenido:

posttls-finger -c -P /etc/ssl/certs "[mail.example.com]:25"

El resultado termina en Verified TLS connection established cuando el nombre coincide con el certificado y la cadena es válida. Untrusted indica cadena desconocida o nombre diferente — en el laboratorio, apareció al conectar por la IP en lugar del nombre. Para un análisis completo de cifrados y vulnerabilidades, el testssl.sh (GPLv2) acepta --starttls smtp; y el internet.nl prueba STARTTLS, DANE y DNSSEC de su dominio desde internet.

El ataque que elimina STARTTLS

El punto débil de la puerta 25 es el propio “oportunista”. STARTTLS se anuncia en texto plano; quien esté en medio puede borrar la línea 250-STARTTLS de la respuesta, y el remitente, configurado con may, entrega en texto plano pensando que el otro lado no soporta TLS. En las puertas 587 y 993 eso no ocurre, porque TLS es obligatorio y la conexión simplemente se cae.

Diagrama comparando dois cenários: sem proteção, um intermediário apaga o STARTTLS da resposta e a mensagem segue em texto puro; com MTA-STS ou DANE, o remetente já sabe que o destino exige TLS, recusa a conexão rebaixada e mantém a mensagem na fila

Dos soluciones, que protegen los mensajes que llegan a tu dominio:

  • MTA-STS (RFC 8461): publicas, vía HTTPS, una política diciendo “mis MX exigen TLS con un certificado válido”. Los remitentes que implementan el estándar pasan a rechazar entregas sin esto.
  • DANE (RFC 7672): el certificado del MX se fija en un registro TLSA firmado con DNSSEC. Requiere DNSSEC en el dominio.

MTA-STS es el más sencillo de empezar. Dos registros y un archivo:

_mta-sts.example.com.   IN TXT  "v=STSv1; id=20260923T120000;"
_smtp._tls.example.com. IN TXT  "v=TLSRPTv1; rua=mailto:tls-reports@example.com"

Y en https://mta-sts.example.com/.well-known/mta-sts.txt, servido con un certificado válido:

version: STSv1
mode: testing
mx: mail.example.com
max_age: 604800

Empieza con mode: testing: los remitentes entregan normalmente, pero envían informes de fallo a la dirección del registro _smtp._tls (TLS-RPT, RFC 8460). Sin sorpresas durante algunas semanas, cambia a mode: enforce y sustituye el id del registro TXT — es el cambio del id que avisa a los remitentes para que vuelvan a buscar la política. Cada MX del dominio debe estar en una línea mx:.

Para proteger los mensajes que salen, Postfix sabe usar DANE del lado del remitente: smtp_dns_support_level = dnssec e smtp_tls_security_level = dane. Los destinos con registro TLSA pasan a exigir TLS verificado; los que no lo tienen siguen con may. Esto depende de un resolver local que valide DNSSEC (unbound, por ejemplo) — sin él, no lo actives.

Lo que suele romperse

  • El cliente se queja de certificado inválido — está configurado con un nombre que no está en el certificado (imap.example.com, la IP, el nombre antiguo del servidor). Emítelo con todos los nombres usados o estandarice los clientes.
  • Funciona en Thunderbird y falla en otro servidor — usó cert.pem en lugar de fullchain.pem y faltó el intermedio. Los navegadores y algunos clientes completan la cadena por sí solos; los servidores de e-mail no.
  • El certificado caducó, pero la renovación se ejecutó — faltó el deploy hook y el servicio siguió con el certificado antiguo en memoria. openssl s_client ... | openssl x509 -noout -enddate muestra lo que realmente se está sirviendo.
  • Dovecot no arranca después de actualizar a 26.04 — configuración 2.3 en 2.4. doveconf -n apunta a la línea.
  • certbot --standalone fallo — el puerto 80 está ocupado por un servidor web o cerrado en el firewall. Use --webroot.
  • Clientes antiguos dejan de conectar tras exigir TLS 1.2 — sistemas y clientes de correo muy antiguos, sin soporte para TLS 1.2. Es el precio justo; TLS 1.0 está prohibido por la RFC 8996.
  • No sale nada por el puerto 25 — muchos proveedores de nube bloquean la 25 de salida por defecto. No es TLS: es solicitar la liberación en el panel del proveedor.

Resumen

Certificado válido en el nombre del MX, renovación con recarga, TLS 1.2 como mínimo en todo lo que lleva contraseña (465, 587, 993, 995), el puerto 25 oportunista como debe ser, contraseña solo dentro del TLS, 465/587/993/995 abiertas y 143/110 cerradas. Con esto el servidor está al día; con MTA-STS, también pasa a proteger lo que llega. Los próximos pasos del mismo pílón: DKIM y DMARC en Postfix, el panel go-postfixadmin para los buzones virtuales y la historia de las piezas en La historia de Postfix e La historia de Dovecot.