Recursos sobre Sendmail

La versión de Sendmail con la que trabajo es la 8.12.x 8.13.x (Dic 2004).

Contenidos

Preparando Sendmail para enviar y recibir correo

En primer lugar, como recomienda man afterboot(8), hay que revisar /etc/mail/aliases y preparar los alias para, al menos, los usuarios root, manager y dumper, y actualizar la base de datos con:

# newaliases
/etc/mail/aliases: 30 aliases, longest 10 bytes, 330 bytes total

Además tenemos que asegurarnos de que tenemos un hostname válido y con entrada DNS. A veces una entrada en /etc/hosts puede facilitar las cosas. Si el nombre de la máquina no es correcto o no resuelve a una IP, tendremos problemas.

Por último pondremos un alias para las pseudo-cuentas que se usan en los servicios y que nunca recibirán correo, apuntando a /dev/null. Por defecto el aliases de OpenBSD lleva varios ejemplos de este tipo.

mysendmail.cf

OpenBSD viene preparado con una configuración de Sendmail para entrega de correo local y no podremos recibir correo con ella.

Podemos observar ese fichero en /etc/mail/localhost.cf, pero no entenderíamos gran cosa :) Vamos a intentar evitar en lo posible manipular los ficheros cf, porque no están para eso.

Vamos a llamar a nuestra configuración mysendmail.cf y al fichero m4 que lo genera mysendmail.mc.

Para crear nuestra configuración nos situamos en el directorio /usr/share/sendmail/cf. Aquí podemos encontrar multitud de ejemplos de ficheros mc, incluyendo el openbsd-localhost.mc que genera nuestra configuración actual.

Hay muchos ejemplos interesantes, pero vamos a fijarnos en uno: openbsd-proto.mc. Crearemos nuestra configuración a partir de él:

# cp openbsd-proto.mc mysendmail.mc
Y usando $EDITOR, le echamos un vistazo:
divert(-1)
#
# Copyright (c) 1998 Sendmail, Inc.  All rights reserved.
# Copyright (c) 1983 Eric P. Allman.  All rights reserved.
# Copyright (c) 1988, 1993
#       The Regents of the University of California.  All rights reserved.
#
# By using this file, you agree to the terms and conditions set
# forth in the LICENSE file which can be found at the top level of
# the sendmail distribution.
#
#

#
#  This is the prototype file for a configuration that supports nothing
#  but basic SMTP connections via TCP.
#

divert(0)dnl
VERSIONID(`@(#)openbsd-proto.mc $Revision: 1.6 $')
OSTYPE(openbsd)
FEATURE(nouucp, `reject')
FEATURE(`no_default_msa')
MAILER(local)
MAILER(smtp)
DAEMON_OPTIONS(`Family=inet, address=0.0.0.0, Name=MTA')dnl
DAEMON_OPTIONS(`Family=inet6, address=::, Name=MTA6, M=O')dnl
DAEMON_OPTIONS(`Family=inet, address=0.0.0.0, Port=587, Name=MSA, M=E')
DAEMON_OPTIONS(`Family=inet6, address=::, Port=587, Name=MSA6, M=O, M=E')
CLIENT_OPTIONS(`Family=inet6, Address=::')dnl
CLIENT_OPTIONS(`Family=inet, Address=0.0.0.0')dnl
dnl
dnl Some broken nameservers will return SERVFAIL (a temporary failure)
dnl on T_AAAA (IPv6) lookups.
define(`confBIND_OPTS', `WorkAroundBrokenAAAA')dnl
dnl
dnl Enforce valid Message-Id to help stop spammers
dnl
LOCAL_RULESETS
HMessage-Id: $>CheckMessageId

SCheckMessageId
R< $+ @ $+ >            $@ OK
R$*                     $#error $: 553 Header Error

Tal cual es perfectamente usable, solo tenemos que hacer:

# make mysendmail.cf && cp mysendmail.cf /etc/mail/
rm -f mysendmail.cf
( cd /usr/share/sendmail/cf && /usr/bin/m4 /usr/share/sendmail/cf/../m4/cf.m4
mysendmail.mc > /usr/share/sendmail/cf/mysendmail.cf )
chmod 444 mysendmail.cf

Hay preparar al sistema para que use nuestra configuración (recordemos que estamos usando /etc/mail/localhost.cf), añadiendo a /etc/rc.conf.local:

sendmail_flags="-L sm-mta -C/etc/mail/mysendmail.cf -bd -q30m"

Reiniciamos sendmail:

# cat /var/run/sendmail.pid
27997
/usr/sbin/sendmail -L sm-mta -C/etc/mail/localhost.cf -bd -q30m
# kill -TERM 27997
# /usr/sbin/sendmail -L sm-mta -C/etc/mail/mysendmail.cf -bd -q30m

En este punto nuestro Sendmail debe estar funcionando y recogiendo correos. Podemos probar:

# echo quit | telnet 127.0.0.1 25
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
220 mydomain.com ESMTP Sendmail 8.12.9/8.12.9; Sat, 4 Dec 2003 22:50:13 +0100 (CET)
Connection closed by foreign host.

Nota: Puede que el pipe con echo no funcione como esperamos (comprobado en OpenBSD 3.8), así que podemos hacer el telnet al puerto 25 y luego introducir el quit 'a mano'. El efecto es el mismo, por si el comando anterior falla.

Aceptando correo

En este momento puede ser buena idea que indiquemos a Sendmail qué correo tiene que aceptar como local. Echamos un vistazo a nuestra configuración:

# cat mysendmail.cf | grep Fw
Fw/etc/mail/local-host-names

Probablemente ese no sea el resultado de la búsqueda :P

Para que Sendmail busque en el fichero /etc/mail/local-host-names tendremos que indicarlo en el fichero mc, añadiendo a los FEATURE existentes:

FEATURE(use_cw_file)

Ahora tendremos que regenerar el fichero cf como lo hemos hecho antes y copiarlo a su lugar en /etc/mail.

Este proceso puede ser tedioso en un principio, pero es porque estamos empezando ;)

En /etc/mail/local-host-names pondremos los nombres con los que recibiremos correo (un dominio por linea):

mydomain.com

El correo recibido en mydomain.com (como bar@mydomain.com) se entregará localmente en nuestra máquina.

Hacemos que Sendmail recargue la configuración:

# kill -HUP `cat /var/run/sendmail.pid | head -n 1`

Ahora podemos hacer otras pruebas, como emplear mail(1) para enviar un correo a un usuario local.

# mail user
Subject: probando
uh!
.
EOT

Y enviar un correo a un usuario de nuestro dominio:

# mail user@mydomain.com
Subject: probando otra vez
uh!
.
EOT

Relay

Si queremos emplear este SMTP para entregar correo desde otras máquinas, ya sea en nuestra red local o en internet, tarde o temprano nos daremos cuenta de que no podemos enviar correo al exterior :P. Esto es porque no hemos definido desde que máquinas estamos dispuestos a hacer relay (desde localhost sí se hace).

Si ejecutamos:

# cat mysendmail.cf | grep FR-o
FR-o /etc/mail/relay-domains

Respira tranquilo que esta vez sí funciona :) No es necesario añadir nada al fichero mc (por ahora).

Vemos como en /etc/mail/relay-domains es donde se espera que introduzcamos los nombres de las máquinas/redes desde/hacia las que podemos hacer RELAY. Por ejemplo:

192.168.0.
mydomain.com
10.1.1.12

Permitiremos RELAY a cualquier correo que 'venga de' o 'vaya hacia' la red 192.168.0.0/32, el dominio mydomain.com (y sus sub-dominios como foo.mysdomain.com) y la máquina con IP 10.1.1.12.

Hay que ser cuidadoso con los valores que ponemos aquí si no queremos dejar la máquina abierta a los spammers.

No es recomendable permitir RELAY a ninguna máquina en internet, a no ser que introduzcamos mecanismos que permitan comprobar que el usuario que pretende mandar el correo es quien dice que es.

En entornos "seguros", como nuestra red local, no hay problema. Podemos permitir RELAY a nuestra red local (por ejemplo con 192.168.0., si nuestra red local es 192.168.0.0/24), siempre y cuando ésta esté configurada correctamente.

Hacemos que Sendmail recargue la configuración:

# kill -HUP `cat /var/run/sendmail.pid | head -n 1`

Y ya tenemos nuestro servidor perfectamente configurado para recibir y enviar correo.

Asegurando Sendmail

No me termina de convencer tal como hemos dejado funcionando nuestro Sendmail, así que vamos a afinar un poco más la configuración.

En primer lugar yo prefiero borrar las lineas de nuestro mc:

DAEMON_OPTIONS(`Family=inet, address=0.0.0.0, Port=587, Name=MSA, M=E')
DAEMON_OPTIONS(`Family=inet6, address=::, Port=587, Name=MSA6, M=O, M=E')

Y solo usar el puerto 25 (SMTP). Digamos que es una preferencia personal, o digamos que la entrega por el puerto 587 no es necesaria y que con un puerto abierto ya es suficiente.

Además es buena idea eliminar la configuración referente a inet6 si no vamos a usar IPv6 (lo más probable es que no).

Antispam

Otra cosa que deberíamos cambiar es que Sendmail no permita el uso de VRFY y EXPN. VRFY permite verificar si el usuario foo existe en nuestro servidor y EXPN nos indica el nombre del usuario real al que corresponde un determinado alias.

Parece claro que VRFY y EXPN no son una buena idea por el uso que pueden hacer de ellos los spammers, por lo que los vamos a desactivar añadiendo a nuestro mysendmail.mc la linea:

define(`confPRIVACY_FLAGS',``authwarnings,noexpn,novrfy'')

authwarnings es el valor que tiene la opción por defecto y lo mantenemos, y con noexpn y novrfy deshabilitamos EXPN y VRFY respectivamente.

"Ocultando" Sendmail

Ahora vamos a intentar 'ocultar' nuestro Sendmail. Es cierto que esto no aporta nada a la seguridad, pero si no dejamos ver que usamos Sendmail probablemente nos ahorraremos algún intento de explotar un fallo del que seguramente ya estaremos protegidos.

Para ello añadimos la siguiente linea:

define(`confSMTP_LOGIN_MSG',``$j $b'')

Que mostrará el siguiente mensaje al entrar al servidor:

220 myhostname.com ESMTP Fri, 5 Dec 2003 16:38:46 +0100 (CET)

Además hay que verificar:

# cat mysendmail.cf | grep HelpFile
O HelpFile=/etc/mail/helpfile

Y que en /etc/mail/helpfile no haya ninguna referencia ni a Sendmail ni a su versión. Una buena idea puede ser substituir el fichero completo por algo como:

smtp    ESMTP $j $b
smtp    This server supports the following commands:
smtp    HELO EHLO MAIL RCPT DATA RSET NOOP QUIT HELP VERB ETRN DSN AUTH STARTTLS

Donde se muestran los comandos soportados por el servidor. Como se puede observar no aparece ni EXPN ni VRFY.

Con esto nuestro Sendmail está listo para un entorno hostil :)

Administrando la cola

Sendmail define dos colas que se pueden encontrar en:

Aunque realmente desde Sendmail se considera a ese par de directorios como una única cola de trabajos, por eso no veremos órdenes orientadas a una u otra cola, sino a la cola (no vamos a tocar por ahora la cola del MSP, aunque con -Ac -bp podemos consultar su estado).

La principal orden que emplearemos para consultar el estado de la cola es:

# sendmail -bp
/var/spool/mqueue is empty
		Total requests: 0

En este ejemplo la cola está vacía.

Vamos a mandar un mensaje y forzar que se vea algo en la cola, por ejemplo enviando a una máquina que no recibe correo:

# mail nouser@domain.without-mail.com
Subject: Probando, ignorar
gracias XD
.
EOT

Si consultamos la cola rápidamente, veremos:

# sendmail -bp
		/var/spool/mqueue (1 request)
-----Q-ID----- --Size-- -----Q-Time----- ------------Sender/Recipient-----------
hB8HQQhK013863*       9 Mon Dec  8 18:26 <reidrac@mydomain.com>
					 <nouser@domain.without-mail.com>
		Total requests: 1

La salida del comando nos porporciona esta vez mucha información. En primer lugar, según el manual, vemos el identificador en la cola para ese mensaje (Q-ID), el tamaño del mensaje en bytes (aunque aquí no aparece, tendré que investigarlo :?), la fecha en la que el mensaje entró en la cola, quién envió el mensaje y quién debería recibirlo (una linea por recipiente en caso de ser más de uno), así como información post-proceso del mensaje (nada en este caso porque está siendo procesado el mensaje en este momento).

Además el Q-ID puede acabar en un caracter de estado:

Si esperamos un poco, Sendmail acabará de procesar el mensaje. Volvemos a consultar el estado de la cola:

# sendmail -bp
		/var/spool/mqueue (1 request)
-----Q-ID----- --Size-- -----Q-Time----- ------------Sender/Recipient-----------
hB8HQQhK013863        9 Mon Dec  8 18:26 <reidrac@mydomain.com>
                 (Deferred: Connection timed out with domain.without-mail.com)
					 <nouser@domain.without-mail.com>
		Total requests: 1

Ahora podemos observar que el '*' ha desaparecido, por lo que sabemos que Sendmail ha terminado de procesar el mensaje. Además vemos que el mensaje ha sido aplazado (deferred) porque no se pudo establecer conexión con domain.without-mail.com (recordemos que no tiene servidor de correo).

El tiempo que va a estar ese mensaje en la cola, hasta que se descarte por considerar que la entrega es imposible, depende de los valores que pongamos en confTO_QUEUE* en nuestro mc. Los valores por defecto los podemos ver en el README que viene con nuestra distribución de Sendmail.

Procesando la cola

Si recordamos la linea de ejecución de Sendmail veremos que se indica mediante -q30m que se procese los mensajes almacenados en la cola cada 30 minutos. Se puede forzar el proceso mediante:

# sendmail -q

Si deseáramos procesar un mensaje concreto emplearíamos -qI Q-ID, por ejemplo:

# sendmail -qI hB8HQQhK013863

O indicando el remitente con -qS remitente:

# sendmail -qS '<reidrac@mydomain.com>'

O indicando uno de los destinatarios con -qR destinatario:

# sendmail -qR '<nouser@domain.without-mail.com>'

Manipulando la cola

En un momento dado puede que deseemos manipular la cola porque sabemos que un mensaje concreto nunca va a llegar a su destino. Esto debe hacerse con sumo cuidado para no cometer errores y eliminar solo lo que queremos.

Nos situamos en /var/spool/mqueue/, o donde se encuentre la cola de Sendmail y listamos el contenido del directorio:

# ls
dfhB8HQQhK013863   qfhB8HQQhK013863

Podemos observar que hay dos ficheros: df[Q-ID] y qf[Q-ID]. Los ficheros df contienen los datos del mensaje (sin cabeceras) y los fichero qf poseen las cabeceras y datos de control que necesita Sendmail para manejar los mensajes de la cola.

Si queremos eliminar un mensaje concreto, nada más sencillo que:

# rm +(df|qf)hB8HQQhK013863

Es decir, eliminar cualquier fichero que comienze por df o qf y termine en el Q-ID del mesaje que deseamos eliminar.

Si ahora consultamos la cola:

# sendmail -bp
/var/spool/mqueue is empty
                Total requests: 0

Comprobamos que vuelve a estar vacía.

Control de acceso

En un momento dado podemos decidir actuar de forma diferente a la habitual (aceptar el mensaje) para ciertas direcciones de origen, destino o incluso según desde que IP se conecten al servidor. Sendmail permite definir unas reglas se consultarán cada vez que el servidor reciba un nuevo correo.

Ojo que estas reglas se aplican a las direcciones de destino/origen, así como a la información de conexión, pero NO a las cabeceras. Para eso hay otros mecanismos.

Si deseamos activar este control de acceso tendremos que añadir a nuestro mc:

FEATURE(`access_db', `hash -o -T<TMPF> /etc/mail/access')
FEATURE(`blacklist_recipients')

Y regenerar e instalar el cf como ya hemos visto.

El blacklist_recipients es solo necesario si queremos aplicar reglas a usuarios locales y máquinas o direcciones en nuestro dominio. Resulta muy útil cuando queremos deshabilitar el buzón de un usuario sin borrar su cuenta.

Con esto ya podremos usar /etc/mail/access con el siguiente formato:

Connect:ip			REJECT
From:host.dom			REJECT
From:user@host.dom		OK
To:user@myhost.dom		RELAY
To:baduser@myhost.dom		ERROR:550 Cuenta bloqueada por abuso.
To:userdisabled@		ERROR:550 Esta cuenta esta deshabilitada.

Podemos observar distintas combinaciones, aunque siempre será elemento / acción.

Destacar en el ejemplo el caso de To:userdisabled@, que gracias a blacklist_recipients nos permite mostrar un mensaje de error siempre que vaya dirigido el correo al usuario local userdisabled. La @ indica que es un usuario local, y evita que se vean afectados correos que salgan del servidor hacia un usuario userdisabled en otro dominio.

El elemento puede ser una dirección de correo, nombres de dominio y direcciones IP, siempre precedido de una etiqueta:

La acción puede ser, principalmente, una de estas:

Para que estas reglas tengan efecto hay que tener en cuenta que hemos definido access como un hash, así que tendremos que prepara la base de datos para que sendmail la pueda utilizar:

# makemap hash /etc/mail/access < /etc/mail/access

En OpenBSD solo tendremos que ejecutar make dentro de /etc/mail.

Filtrando cabeceras

Sendmail permite filtrar las cabeceras de los correos (entre otras cosas, pero vayamos por partes), permitíendonos rechazar el mensaje completo si se satisfacen unas condiciones que indicamos.

Es importante tener en cuenta que las reglas son tres partes separadas por tabuladores: la primera parte especifica un patrón de búsqueda, la segunda indica la acción a llevar a cabo (cuando el patrón de búsqueda encuentra una conicidencia) y la tercera, opcional, es un comentario.

Además las reglas se agrupan bajo un identificador (o ruleset) que indica, además, donde acaba la agrupación anterior.

Esto tiene diferentes aplicaciones, como las que se describen a continuación.

Antivirus

Existen antivirus que pueden funcionar en equipo con los diferentes servidores de correo, incluyendo Sendmail.

No obstante podríamos plantearnos incluir algunas comprobaciones de cabeceras para filtrar a los virus más molestos del momento.

Modificando el final de nuestro fichero mc, añadimos:

LOCAL_RULESETS
HMessage-Id: $>CheckMessageId
HContent-Type:: $>CheckVirus

SCheckMessageId
R< $+ @ $+ >		$@ OK
R$*			$#error $: 553 Header Error

SCheckVirus
R$*;$*;boundary="====_ABC1234567890DEF_===="	$#error $@ 5.7.1 $: "Mail 
rejected because virus (Nimda)"

He mantenido la regla que verificaba que el correo tuviera un Message-Id correcto y he comprobado que el boudary dentro del MIME del mensaje (de haberlo) no sea el que emplea el virus Nimda.

Filtrando spam

Quizás queramos evitar que un spammer, que nos está bombardeando con correos, nos continue molestando:

LOCAL_CONFIG
C{Spammer}		foo.bar-spam.com

LOCAL_RULESETS
HMessage-Id: $>CheckMessageId
HFrom: $>CheckFrom

SCheckMessageId
R< $+ @ $+ >		$@ OK
R$*			$#error $: 553 Header Error

SCheckFrom
R$*			$: $>3 $1
R$*<@$={Spammer}.>	$#error $@ 5.7.1 $: "550 We don't accept junk mail"
R$*<@$={Spammer}>	$#error $@ 5.7.1 $: "550 We don't accept junk mail"

Se rechazará cualquier correo que venga de foo.bar-spam.com. Si además queremos que se rechace el correo que provenga de sus subdominios, habría que modificar la última regla:

R$*<@$*$={Spammer}.>	$#error $@ 5.7.1 $: "550 We don't accept junk mail"
R$*<@$*$={Spammer}>	$#error $@ 5.7.1 $: "550 We don't accept junk mail"

El $* que hemos añadido (en negrita) actua como comodín para cualquier subdominio (por ejemplo dudu.foo.bar-spam.com).

Probando las reglas

Nunca, repito: nunca hay que incluir ninguna regla sin probarla antes. Es cierto que algunas, como las que filtran la llegada de mensajes que contienen un virus, son complicadas de probar. En ese caso intentaremos forzar el mensaje en la medida de lo posible. Confiar en una regla que luego resulta ser imperfecta puede poner en peligro la actividad normal del servidor.

Para probar las reglas empleamos el cf generado en /usr/share/sendmail/cf/ antes de moverlo a su lugar definitivo.

Supongamos que vamos a probar nuestras reglas para filtrar a ese spammer que tanto nos molesta.

Para ello arrancamos Sendmail con:

# sendmail -bt -C/usr/share/sendmail/cf/mysendmail.cf
ADDRESS TEST MODE (ruleset 3 NOT automatically invoked)
Enter <ruleset> <address>
> =S CheckFrom
R$*			$: $> 3 $1
R$* < @ $={Spammer} . >	$# error $@ 5 . 7 . 1 $: "550 We don't accept junk mail"
R$* < @ $={Spammer} >	$# error $@ 5 . 7 . 1 $: "550 We don't accept junk mail"

Nos muestra la regra CheckFrom como la ha leido Sendmail. Ahora la probamos con una dirección:

> CheckFrom "Johnny Melavo" <johnny@foo.bar-spam.com>
CheckFrom          input: "Johnny Melavo" < johnny @ foo . bar-spam . com >
canonify           input: "Johnny Melavo" < johnny @ foo . bar-spam . com >
Canonify2          input: johnny < @ foo . bar-spam . com >
Canonify2        returns: johnny < @ foo . bar-spam . com >
canonify         returns: johnny < @ foo . bar-spam . com >
CheckFrom        returns: $# error $@ 5 . 7 . 1 $: "550 We don't accept junk mail"

Vemos como la dirección de Johnny se ajusta a la regla y es rechazada. Vamos a probar con una dirección similar, pero que en este caso no es de nuestro spammer:

> CheckFrom "Lee M. Pio" <lee@bar-spam.com>
CheckFrom          input: "Lee M. Pio" < lee @ bar-spam . com >
canonify           input: "Lee M. Pio" < lee @ bar-spam . com >
Canonify2          input: lee < @ bar-spam . com >
Canonify2        returns: lee < @ bar-spam . com >
canonify         returns: lee < @ bar-spam . com >
CheckFrom        returns: lee < @ bar-spam . com >

Nuestro amigo Lee no encuentra problemas, porque no envía desde la máquina identificada como fuente de spam (foo.bar-spam.com).

Salimos de las pruebas con un simple CTRL + d.

Ya se podría pasar a producción esa regla y estaríamos atentos a los logs buscando la aparición de rechazos a los correos de Johnny para estar seguros de que la regla es eficiente.

Definiendo clases

En la regla antispam ya hemos definido una clase:

C{Spammer}		foo.bar-spam.com

Las clases son definiciones que asocian a una letra o palabra (si empleamos {} en la definición, como en este caso) a una expresión o expresiones, que no pueden contener espacios.

Las clases de emplean en las reglas en la búsqueda de ocurrencias. Algunos ejemplos:

Cx		hostname.dom
C{MiClase}	abc cba

Aquí definimos la clase x con la cadena hostname.dom y la clase MiClase con las cadenas abc y cba.

Esto resulta interesante porque podemos encapsular varias cadenas bajo la misma clase y así aumentamos la eficiencia de la reglas, aunque en determinados casos no se tiene en cuenta todo el contenido de las clases y solo la primera cadena.

En nuestro ruleset contra Johnny podríamos añadir más máquinas, creando una lista de cadenas separadas por espacios.

Esa utilidad puede no ser tan interesante si nos enfrentamos a grandes listas de cadenas, porque tarde o temprano el fichero de configuración pasaría a ser inutilizable.

Afortunadamente existe la posibilidad de definir una clase leyendo los elementos desde un fichero:

F{Spammer}	/etc/mail/spammer-CheckFrom

Pondremos un elemento por linea, por ejemplo:

foo.bar-spam.com
ads.cyberspam.com
scum.net
hotmail.com

Todos los correos que vengan de esas máquinas serán rechazados.

Aunque no sería acosejable emplear la lista del ejemplo (aun suponiendo que las direcciones ficticias fueran reales). Puede ser tentador filtrar el correo que venga de hotmail.com ;), pero eso seguramente haría que correos legítimos no llegaran a su destino.

Copyright © 2003-2006 Juan J. Martinez <reidrac *at* usebox.net>
Se permite la copia textual y distribución de este documento en su totalidad,
por cualquier medio, siempre y cuando se mantenga esta nota de copyright.