El montaje con Postfix del post anterior son cuatro paquetes conectados entre sí, con un socket de milter, un socket SASL y un socketmap que los mantienen acompasados. Stalwart hace todo eso en un solo binario. Todo.
SMTP, IMAP y JMAP, el almacén de correo, SPF, DKIM, DMARC, ARC, la firma DKIM, DANE, MTA-STS, el informe de TLS, un filtro antispam y un motor Sieve, en un único proceso que arrancas una vez. Ningún milter que conectar, ningún demonio de políticas en un socket, ningún segundo paquete que tenga que coincidir con el primero. Para cada regla de esta serie que en Postfix requería una herramienta aparte, Stalwart ya tiene la respuesta dentro.
Es una ventaja real, y este post no finge lo contrario. Pero «integrado» y «seguro por defecto» son afirmaciones distintas, y la distancia entre ambas es todo este post. Sometí a Stalwart 0.16.27 exactamente a las reglas de la casa que cumplió el montaje con Postfix, en el mismo laboratorio, y capturé cada respuesta. Todas las reglas se activan. Activarlas es otra historia.
Los Ajustes Son Una Base De Datos, No Un Archivo
Empieza por lo que sorprende a todo el mundo, porque condiciona todo lo demás. Postfix se configura en main.cf, un archivo de texto que puedes leer, comparar y copiar al siguiente servidor. Stalwart tiene un único archivo de configuración, y esto es todo él:
{"@type":"RocksDb","path":"/var/lib/stalwart/"}
Eso nombra la base de datos. Todos los demás ajustes, los listeners, la política de TLS, las reglas de spam, los scripts Sieve, viven dentro de esa base de datos, y los cambias por la API JMAP o con stalwart-cli, seguido de una recarga:
stalwart-cli apply --file house-policy.json
stalwart-cli create Action/ReloadSettings
También hay una interfaz web.1 La configuración es estado en un servicio en marcha, no un archivo en disco, y eso corta por los dos lados. Ganas una API viva y ningún demonio que recargar a mano. Pierdes lo que un archivo de texto te da gratis: un diff, un comentario, un commit, una copia buena conocida para volver a desplegar. Quienes gestionan Stalwart de forma declarativa acaban guardando bajo control de versiones el archivo de plan que consume apply, que es el instinto correcto, y también exactamente la configuración basada en archivos con la que empezaron por no tener.
Lo Que Acepta Tal Como Viene
Antes de activar las reglas, aquí está Stalwart haciendo lo que hace recién instalado. Es la línea de partida justa, y no tranquiliza.
| Tal como viene, Stalwart… | |
|---|---|
| una sesión en texto plano, sin STARTTLS | aceptada |
| una factura firmada y manipulada | aceptada |
| un mensaje que falla SPF | aceptado |
| una falsificación de su propio dominio | aceptada |
| un dominio de remitente que no existe | aceptado |
| entrega a un servidor solo en texto claro | enviada en claro |
| un relay abierto en el puerto 25 | rechazado |
| un login con el remitente de sobre equivocado | rechazado |
un login que falsifica la cabecera From: | aceptado |
| salida hacia un certificado inválido, al reintentar | aceptada |
Esos dos rechazos son la única buena noticia de la columna, y un usuario autenticado todavía podría falsificar el From: visible.
Nada de esto es exclusivo de Stalwart. Es el mismo conjunto de valores por defecto permisivos que trae casi todo servidor de correo, por la misma razón de hace veinte años, y es todo el argumento del post de apertura de esta serie: /es/email/why-every-mail-setting-has-to-be-right/. Un servidor que lo hace todo no es un servidor que lo hace todo con seguridad. Eso sigue siendo cosa tuya de configurar.
Cuatro Sitios Donde Poner Una Regla
Aquí es donde el diseño de un solo binario se vuelve de verdad distinto de manejar. En Postfix, casi toda regla de entrada es una línea smtpd_*_restrictions o un milter. En Stalwart, el mismo conjunto de reglas cae en cuatro mecanismos separados, y necesitas los cuatro.
Un lenguaje de expresiones, un mapa de etiqueta a acción y un script Sieve son tres sintaxis distintas, y eso antes de los ajustes del listener. Un binario, cuatro cosas que aprender. Pero con la política aplicada, las capturas cuadran regla por regla con el montaje con Postfix:
| Regla | Respuesta de Stalwart |
|---|---|
| Sin texto plano de entrada | 550 5.7.1 Sender address not allowed. |
| Relay en el puerto 25 | 550 5.1.2 Relay not allowed. |
| Relay en 587 sin login | 503 5.5.1 You must authenticate first. |
| Login que envía como otro remitente de sobre | 501 5.5.4 You are not allowed to send from this address. |
Login que falsifica la cabecera From: | 503 5.5.3 You may only send as your own address |
| Un mensaje firmado, íntegro y alineado | aceptado |
| Un mensaje firmado y manipulado | 503 5.5.3 DKIM signature does not verify |
| Un fallo duro de SPF | 550 5.7.1 … excessive spam score. |
Cada respuesta viene del servidor de laboratorio en marcha, sobre IPv4 e IPv6. Las reglas se sostienen. Ahora, los tres sitios donde llegar hasta ahí me enseñó algo.
El Fallo De DKIM Necesita Un Script Sieve, No El Ajuste Obvio
Stalwart tiene un resultado DKIM integrado y una etiqueta de spam para una firma fallida, DKIM_REJECT, y lo obvio es rechazar con ella. No atrapa el caso que importa. Cuando un mensaje firmado ve cambiado su cuerpo en vuelo, que es el caso del fraude de facturas al que vuelve toda la serie, el verificador DKIM de Stalwart informa del resultado como neutral, no como fail. Leyendo el código de mail-auth, un desajuste del hash del cuerpo cae en el cubo neutral, así que la etiqueta de rechazo nunca se dispara y el mensaje manipulado se entrega.2
Así que la regla de DKIM es, en su lugar, un script Sieve, que mira directamente el resultado de la verificación y rechaza todo lo que no sea un pase limpio o una ausencia genuina de firma:
if anyof (string :is "${env.dkim.result}" "neutral",
string :is "${env.dkim.result}" "fail",
string :is "${env.dkim.result}" "permerror") {
ereject "DKIM signature does not verify: the message was changed or forged";
stop;
}
El correo sin firmar deja el resultado vacío y se deja en paz, porque sin firmar no es un fallo de DKIM; de eso se encarga DMARC con p=reject. Con el script en su sitio, la factura manipulada se rechaza en DATA:
B REFUSED (503, b'5.5.3 DKIM signature does not verify: the message was changed or forged')
El mismo script lleva la regla de la cabecera From:, ya que comparar la cabecera con el login también es tarea de Sieve. Dos reglas, un script, y ninguna es el ajuste al que acudirías primero.
La Comprobación De Remitente Desconocido Es Más Débil De Lo Que Parece
En Postfix, un dominio de remitente que no existe se rechaza en MAIL FROM, antes de enviar un byte del cuerpo, con un limpio 550 5.1.8 Domain not found. Stalwart llega al mismo rechazo, pero más tarde y de forma más frágil.
La comprobación es una etiqueta de spam, FROMHOST_NORES_A_OR_MX, que se dispara cuando el dominio del remitente no resuelve ni a un registro A ni a uno MX. El rechazo, por tanto, llega después de recibir el mensaje entero, como rechazo por puntuación de spam:
550 5.7.1 Message rejected due to excessive spam score.
Es la respuesta correcta por un camino más largo, y dos cosas de ella importan. Primero, leyendo el código del filtro antispam, la comprobación solo se ejecuta una vez que la dirección del remitente pasa una prueba de «es válida», donde válida significa que el dominio está bajo un sufijo público conocido, así que un dominio de primer nivel inventado se salta por completo la comprobación de A y MX. Segundo, y esta es la que muerde: la consulta trata un error de DNS de forma distinta a una ausencia de DNS. Un dominio que devuelve SERVFAIL, en lugar de un limpio «no existe el nombre», no recibe etiqueta, y el mensaje se acepta. Postfix lo aplaza. Stalwart lo deja entrar. Eso es un fallo-abierto en exactamente el caso ambiguo para el que existe un control de seguridad, y el único sitio de este montaje donde Stalwart es medible como menos seguro que el equivalente de Postfix.
IPv6 Es Un Recurso De Reserva Hasta Que Lo Cambias
IPv6 es obligatorio en esta serie, en ambas direcciones. Stalwart escucha en ambas familias sin problemas. La salida es la trampa: la ruta tal como viene resuelve IPv4 primero, así que un destino de doble pila se alcanza por IPv4 e IPv6 nunca es más que un recurso de reserva. En el mismo laboratorio, Postfix usó IPv6. Stalwart usó v4 hasta que la ruta se cambió para preferir v6:
"ipLookupStrategy": "v6ThenV4"
Tras eso, la entrega a un servidor de doble pila va por IPv6, confirmado en los registros al alcanzar la dirección v6. Es un solo ajuste. Tampoco es el valor por defecto, y en el mandato de IPv6 el valor por defecto está al revés.
MTA-STS Aplica, Y Rebota Demasiado Pronto
Stalwart publica su propia política MTA-STS, que es un detalle de verdad bueno, sin necesidad de un servidor web aparte como el montaje con Postfix necesita nginx. Pero viene en modo testing, que no aplica nada, y se sirve bajo el certificado del host de correo en lugar de uno que coincida con el nombre del host de la política, así que una descarga que verifica falla hasta que arreglas ambas cosas. Pon el modo en enforce y carga un certificado para el host mta-sts, y la política se sirve y se comprueba como es debido:
mta-sts.policy-fetch strict = true
mta-sts.authorized
El lado emisor también aplica, y rechaza un destino cuya política no lista su MX. Pero rechaza de la forma equivocada: un desajuste es un fallo permanente al primer intento, y el mensaje rebota. RFC 8461 es explícito en que no debería:
In all cases, MTAs SHOULD treat such failures as transient errors and retry delivery later.3
Stalwart sí vuelve a descargar la política antes de fallar, así que la regla dura del estándar, que compruebes primero si hay una política nueva, se cumple. El SHOULD más blando, que trates el fallo como temporal y reintentes, no. El coste es un mensaje rebotado donde un aplazamiento habría sido más amable, sobre un fallo que suele ser transitorio.
TLS De Salida: Obligatorio, Y Más Fuerte Donde Se Puede
Como en el montaje con Postfix, el lado de salida hace de encrypt el suelo y sube cada destino por encima de él cuando el destino se lo gana. El MtaTlsStrategy de Stalwart fija un nivel para cada mecanismo, y la política de la casa es:
| Mecanismo | Ajuste de la casa | Efecto |
|---|---|---|
| STARTTLS | require | nunca enviar en claro, a nadie |
| DANE | optional | autenticar donde se publica un TLSA firmado |
| MTA-STS | optional | respetar una política aplicada donde la hay |
| Certificados inválidos | desactivado | un certificado que no verifica aplaza el correo |
require en STARTTLS es el suelo que el valor por defecto de fábrica no tiene: recién instalada, la estrategia de salida acababa aceptando un certificado inválido al reintentar. Con el suelo puesto, un destino que no ofrece TLS utilizable se aplaza, no se degrada.
DANE Está, Pero El Laboratorio No Pudo Demostrarlo
El control de salida más fuerte, DANE, está integrado como el resto, y su TLS de salida falla-cerrado como debe: apuntado a un servidor cuyo certificado no pudo verificar, Stalwart rechazó y retuvo el mensaje en lugar de enviar en claro. Lo que el laboratorio sellado no pudo hacer es mostrar a DANE teniendo éxito, y esa es una limitación real que vale la pena anotar.
Stalwart solo activa DANE cuando puede validar él mismo la clave raíz del DNS, a través de una conexión de resolución que no sea UDP plano. Una raíz de laboratorio privada no puede ofrecer eso, así que desactiva DANE y registra un aviso en una ruta que no existe por defecto. Fácil de pasar por alto. Peor, para una zona firmada informa entonces de los registros TLSA como «not DNSSEC signed» cuando la causa real es su propio resolutor. El mensaje culpa a lo que no es.
Stalwart trae un diagnóstico de entrega de solo lectura para exactamente esto: un endpoint que recorre todo el camino de salida y nunca envía un mensaje. Transmite cada paso a medida que avanza:
GET /api/live/delivery/<domain>?timeout=20
| Paso | Lo que informa |
|---|---|
| Consulta MX | los servidores de correo, y si la consulta validó |
| MTA-STS | la política descargada, y si el MX está autorizado |
| TLS-RPT | la dirección de informes que el dominio publica |
| TLSA | los registros DANE, y si están firmados con DNSSEC |
| Connect, EHLO, STARTTLS | la sesión hasta TLS, con el cifrado |
| DANE verify, QUIT | la coincidencia del certificado, y luego un corte limpio |
Ningún MAIL FROM, ningún RCPT TO, nada entregado. Es la comprobación que hay que ejecutar contra un dominio DANE real para ver el caso positivo que este laboratorio no puede mostrar.
Demostrar el caso positivo de DANE significa ejecutar eso contra dominios reales que publican DANE desde una instancia conectada a internet. Es la única prueba de esta serie que un laboratorio sellado no puede hacer, y el límite honesto de lo que un laboratorio puede enseñarte.
¿Entonces, Lo Integrado Es Mejor?
Aquí está lo que todo el post ha estado rondando. Integrado es menos que instalar, menos que conectar, menos que mantener acompasado entre versiones, y hay valor real en un único proceso donde el resultado de SPF simplemente está ahí para la comprobación de DKIM y ambos están ahí para el script Sieve, sin ningún socket entre ellos. Para un despliegue pequeño que quiere una sola cosa que ejecutar y actualizar, es una respuesta de verdad buena, y nada de lo anterior es razón para no usarla.
Pero cada parte difícil de esta serie sobrevivió al traslado a un solo binario. Los valores por defecto siguen viniendo permisivos. Las reglas siguen teniendo que encontrarse y activarse, a través de cuatro mecanismos en vez de uno. El rechazo por fallo de DKIM sigue necesitando un script escrito a mano. La comprobación de remitente desconocido sigue fallando-abierto ante un error de DNS. MTA-STS sigue rebotando cuando debería aplazar. Nada de eso se volvió más fácil porque el código viva en un solo proceso. Son propiedades del problema, no del empaquetado.
Pon los dos montajes en fila y el patrón es evidente:
| Montaje con Postfix | Stalwart | |
|---|---|---|
| Partes que ejecutar | 4 paquetes, 3 sockets | 1 binario |
| Configuración | main.cf, un archivo de texto | una base de datos, por una API |
| SPF, DKIM, DMARC | milter de Rspamd | integrado |
| DANE, MTA-STS de salida | postfix-tlspol | integrado |
| Publicación de MTA-STS | nginx aparte | integrado |
| Rechazo por fallo DKIM | force action de Rspamd | un script Sieve (la etiqueta integrada no lo pilla) |
| Remitente desconocido | 550 en MAIL FROM | 550 tras DATA, falla-abierto ante SERVFAIL |
| Desajuste MTA-STS de salida | aplaza, reintenta | rebota al primer intento |
| IPv6 de salida | lo usó por defecto | reserva hasta cambiar a v6 primero |
| Valores por defecto de fábrica | permisivos | permisivos |
Que es la conclusión honesta, y la misma a la que llegó el post de Postfix por otro camino. Lo que hace seguro al correo no es el número de binarios. Es alguien decidiendo que cada regla importa, encontrando dónde vive, activándola, y enviando el mensaje que se supone que debe detener para comprobar que lo hizo. Stalwart te deja hacer todo eso, en un solo proceso, por una API ordenada. También, si lo dejas en paz, aceptará una sesión en texto plano, un remitente falsificado y una factura alterada, exactamente como todo lo demás. El binario es uno. El trabajo no.
Documentación de Stalwart — configuración por la API de gestión JMAP y
stalwart-cli, con los ajustes guardados en el almacén de datos en lugar de en un archivo. Aquí se usa la versión 0.16.27. ↩︎mail-auth, la biblioteca que Stalwart usa para DKIM — su verificador DKIM informa de un desajuste del hash del cuerpo como resultado neutral en lugar de un fallo, que es por lo que un rechazo solo sobre la etiqueta de fallo no atrapa un cuerpo manipulado. ↩︎
RFC 8461 §5 — SMTP MTA Strict Transport Security: ante un fallo de validación de política, «In all cases, MTAs SHOULD treat such failures as transient errors and retry delivery later», y un mensaje «MUST NOT be permanently failed» antes de que el emisor haya comprobado si hay una política actualizada. ↩︎