Mettre en place un mécanisme de liste grise sur Exim4 sous Debian 🔗
Introduction¶
Ça devait arriver tôt ou tard mais, c'est arrivé. Depuis quelques jours, mon adresse e-mail personnelle est spammée. Depuis que j'ai mis en place mon serveur de courrier électronique (vers 2010 quand même) je n'avais jamais eu de spam. Quand je dis jamais, c'était vraiment jamais. Je n'avais d'ailleurs pas du tout mis en place de système anti-spam sur mon serveur. Il est donc temps de remédier à tout ça, car je n'ai pas l'intention de faire du ménage manuellement tous les jours.
Sur une machine aussi légère qu'un Sheevaplug, plus on ajoute de services, plus la machine a du mal. Comme jusqu'à présent, je n'avais aucun problème de spam, je ne voyais pas trop l'intérêt d'investir du temps de sysadmin à me former sur ces techniques. Mais, depuis quelques jours, je vois bien que ce que j'ai retardé le plus possible est inéluctable.
Il me faut donc travailler sur le sujet. Mais avant d'aller plus loin, sachez qu'il existe plusieurs techniques de lutte contre le spam. La plus connue est celle qui consiste à faire passer le contenu du message réceptionné à un service d'analyse de spam (le plus connu étant SpamAssassin. Il en existe une deuxième qui mérite toute mon attention, c'est celle du greylisting ou mise sur liste grise.
Cette technique est beaucoup plus légère à mettre en place que celle d'un système anti-spam complet. Son principe est simple à retenir: lorsqu'un autre MTA veut vous envoyer un courriel, s'il n'est pas déjà connu dans une base de données locale, votre MTA lui demande de renvoyer le message plus tard (avec un code 451). Dans la majorité des cas, cette simple manipulation permet de réduire fortement le spam, car les spammeurs ne renvoient que très rarement le message alors que les MTA "normaux" le font. L'inconvénient de cette technique est qu'elle diffère l'acheminement du courriel à l'utilisateur final lorsque le MTA distant n'est pas connu.
Dans la pratique, un très grand nombre de services de courrier électronique mettent en œuvre le greylisting. Et, de toute manière, sachez que le courriel n'a pas vocation à être distribué tout de suite (c'est de l'asynchrone), car il peut être trituré dans tous les sens (anti-spam, antivirus, analyses de contenu chez Google, archivage automatique, comparaison à des filtres Sieve, etc.) avant d'arriver dans votre boîte.
Cet article a pour vocation de vous montrer comment mettre en place un mécanisme de liste grise pour le MTA par défaut de Debian: Exim4.
Le choix des armes¶
Dans notre cas, le choix des armes est assez limité, car nous allons nous tourner vers un petit programme qui fait très bien son travail: greylistd. Ce dernier est un programme en Python qui écoute sur une socket Unix. Lorsqu'une requête arrive sur la socket, greylistd compare les arguments avec sa base de données interne. Cette dernière est constituée de triplets qui regroupent les informations suivantes:
- Adresse IP de l'hôte qui souhaite nous envoyer du courrier électronique.
- Contenu du champ FROM de l'expéditeur.
- Adresse de courrier électronique à qui est destiné le courriel.
Chaque ligne de "la base de données" (c'est plus un fichier qu'autre chose) contient un triplet ainsi que l'heure de mise en liste grise. Si le triplet n'est pas connu, il est mis dans la base de données de liste grise et tant qu'il n'y est pas resté le temps attendu, le serveur renverra true (pour indiquer que le triplet est bien dans la liste grise).
Pour résumer, greylistd
stocke des triplets dans une base et les
garde dedans pendant un certain temps. Assez simple en matière de
fonctionnement. Mais rassurez-vous, on peut le customiser assez
largement notamment en ajoutant des exceptions comme des listes
blanches ou en faisant varier certaines durées de conservation des
listes.
Pour terminer, Exim4 interroge greylistd via une socket Unix. Il faudra donc configurer greylistd pour qu'il fasse ce qu'on attend de lui, mais il faudra également configurer Exim4 pour que ce dernier sache interroger greylistd. Nous allons voir en détail de quoi il s'agit dans ce qui suit.
Installation et configuration de greylistd¶
L'installation est ultra-basique et le paquet a peu de dépendances en dehors de Python:
# aptitude install greylist
L'installation crée un groupe greylist
auquel est ajouté
automatiquement l'utilisateur qui fait tourner le démon Exim4. De
plus, l'installation du paquet créé une unité systemd pour la gestion
du démarrage du démon. Donc, de ce côté, rien à faire.
La configuration est également assez simple, car il suffit de
renseigner le fichier /etc/greylistd/config
dont le contenu est le
suivant:
########################################################################
### FILE: /etc/greylistd/config
### PURPOSE: Configuration settings for the "greylistd(8)" daemon
########################################################################
[timeouts]
# Délai initial avant d'autoriser les triplets inconnus à sortir de la liste.
# La valeur par défaut est de 10 minutes soit 600 secondes
retryMin = 600
# Vie maximale des triplets où il n'y a eu aucune tentative de renvoi
# 8 heures soit 28800 secondes
retryMax = 28800
# Vie maximale des triplets qui sont sortis de la liste grise.
# C'est la durée de conservation de l'information du triplet qui a été correctement renvoyé.
# Ici, la valeur vaut 60 jours. Donc pendant deux mois, les MTA "approuvés" ne seront pas
# mis en attente.
expire = 5184000
[socket]
# Chemin de la socket Unix sur laquelle greylistd écoute.
# Le répertoire parent doit être accessible en écriture à l'utilisateur `greylistd`.
# Le chemin sous Debian est le suivant:
path = /var/run/greylistd/socket
# Le mode d'écriture de la socket.
# Ici, c'est 660
mode = 0660
[data]
# Intervalle de sauvegarde de données sur le système de fichier.
# On écrit toutes les 10 minutes.
update = 600
# Chemin du fichier qui contient l'état des triplets.
# Par défault: "/var/lib/greylistd/states".
statefile = /var/lib/greylistd/states
# Chemin du fichier qui contient les triplets
tripletfile = /var/lib/greylistd/triplets
# Conserver les triplets sans hachage.
savetriplets = true
# Vérifier uniquement le premier mot du triplet (l'adresse IP).
# Permet d'autoriser les adresses de réseaux dans les listes blanches.
singlecheck = false
# Mettre à jour uniquement les adresses IP dans les triplets.
# (conséquence de la précédente option)
singleupdate = false
Et c'est tout ! La configuration par défaut semble être un bon compromis, notamment sur la durée d'exclusion du MTA non connu (10 minutes). De toute façon, on pourrait mettre moins ou plus, normalement, les spammeurs ne prennent pas la peine de faire de renvoi.
Il reste néanmoins un dernier fichier installé par défaut:
/etc/greylistd/whitelist-hosts
. Nous reviendrons plus tard sur ce
fichier et son contenu. Passons maintenant à la suite: la
configuration du MTA pour utiliser greylistd.
Configuration d'Exim4¶
Maintenant que greylistd fonctionne sur sa petite socket, voyons comment faire en sorte qu'Exim4 lui parle. Et là, comme dans tout ce qui concerne Exim, il va vous falloir une bonne dose de concentration !
D'abord, tentons de voir comment intégrer greylistd et le MTA. Ce que
nous voulons, c'est que le MTA renvoie un code 421 lorsqu'un triplet
n'est pas connu de greylistd. Cette vérification est effectuée au
moment de la soumission du mail à notre MTA. Dans Exim4, le mécanisme
qui permet de filtrer un mail est une ACL (Access Control List). Dans
notre cas, nous souhaitons que tout ce que greylistd indiquera comme
en liste grise renvoie un code SMTP 421. L'ACL concernée est
acl_rcpt_check
qui gère la réception des mails. Elle est définie par
défaut dans la configuration d'Exim4.
Pour éviter de faire des erreurs, greylistd dispose d'un programme qui
modifie automatiquement la configuration d'Exim4 pour l'utiliser. Mais
comme j'aime bien savoir ce qui est fait sur les machines que
j'administre et dont j'ai la responsabilité, je préfère vous présenter
ce qui sera inséré dans votre fichier
/etc/exim4/conf.d/acl/30_exim4-config_check_rcpt
:
Voici le contenu et on en discute après:
acl_check_rcpt:
.ifdef GREYLISTD
# greylistd(8) configuration follows.
# This statement has been added by "greylistd-setup-exim4",
# and can be removed by running "greylistd-setup-exim4 remove".
# Any changes you make here will then be lost.
#
# Perform greylisting on incoming messages from remote hosts.
# We do NOT greylist messages with no envelope sender, because that
# would conflict with remote hosts doing callback verifications, and we
# might not be able to send mail to such hosts for a while (until the
# callback attempt is no longer greylisted, and then some).
#
# We also check the local whitelist to avoid greylisting mail from
# hosts that are expected to forward mail here (such as backup MX hosts,
# list servers, etc).
#
# Because the recipient address has not yet been verified, we do so
# now and skip this statement for non-existing recipients. This is
# in order to allow for a 550 (reject) response below. If the delivery
# happens over a remote transport (such as "smtp"), recipient callout
# verification is performed, with the original sender intact.
#
defer
message = $sender_host_address is not yet authorized to deliver \
mail from <$sender_address> to <$local_part@$domain>. \
Please try later.
log_message = greylisted.
!senders = :
!hosts = : +relay_from_hosts : \
${if exists {/etc/greylistd/whitelist-hosts}\
{/etc/greylistd/whitelist-hosts}{}} : \
${if exists {/var/lib/greylistd/whitelist-hosts}\
{/var/lib/greylistd/whitelist-hosts}{}}
!authenticated = *
!acl = acl_local_deny_exceptions
!dnslists = ${if exists {/etc/greylistd/dnswl-known-good-sender}\
{/etc/greylistd/dnswl-known-good-sender}{}} :
domains = +local_domains : +relay_to_domains
verify = recipient
condition = ${readsocket{/var/run/greylistd/socket}\
{--grey \
${mask:$sender_host_address/24} \
$sender_address \
$local_part@$domain}\
{5s}{}{false}}
# Deny if blacklisted by greylist
deny
message = $sender_host_address is blacklisted from delivering \
mail from <$sender_address> to <$local_part@$domain>.
log_message = blacklisted.
!senders = :
!authenticated = *
domains = +local_domains : +relay_to_domains
verify = recipient
condition = ${readsocket{/var/run/greylistd/socket}\
{--black \
$sender_host_address \
$sender_address \
$local_part@$domain}\
{5s}{}{false}}
.endif
…
Explications de la configuration d'Exim4¶
Oui, je le dis, Exim4 est certes très versatile et très bien documenté mais sa configuration implique d'avoir lu une bonne partie du manuel pour pouvoir faire des choses simples. Aussi, comme j'aime bien comprendre ce que je fais, voici les éléments d'explication qui vont vous permettre de comprendre dans les détails le comportement de la configuration que nous venons d'ajouter.
Prenons les choses au début. defer
indique qu'on va demander de
dérouter temporairement le courrier entrant si ce dernier répond aux
conditions énumérées dans l'ACL. Concrètement, dans cette condition,
Exim va renvoyer un code d'erreur 421 qui indique qu'il faut réessayer
plus tard. Pour le savoir, il suffit de lire la doc exim !
message
est le contenu qu'on va renvoyer au serveur de messagerie
qui vient d'envoyer le message à notre serveur. Concrètement, le
message en anglais indique que l'adresse utilisée pour se connecter
n'est pas encore validée et qu'il faut revenir plus
tard. log_message
est le message qui va nous servir à indiquer qu'il
y a eu une mise sur liste grise dans les logs (ici le mainlog
d'Exim4). Concrètement, greylisted sera ajouté à la fin de la ligne de
log rejeté temporairement.
Vient ensuite, une série de conditions qui vont nous permettre d'écarter les messages qu'il ne faut surtout pas mettre en liste grise (comme les messages locaux ou les messages des utilisateurs authentifiés).
La condition !senders
est une condition inverse: on va dérouter
temporairement tous les messages dont l'expéditeur n'est pas dans la
liste. Ici, la liste est vide ce qui signifie que lorsqu'il n'y a pas
d'expéditeur (ce qui est le cas lors d'un envoi local), on ne procède
pas à la mise sur liste grise. C'est un peu complexe à intégrer mais
c'est la forme la plus élégante sous Exim4.
!hosts
est également une condition inverse: on rejette
temporairement tout ce qui n'est pas dans la liste (car hosts
est
une condition de liste d'hôtes). Le contenu de la liste est assez
simple à comprendre. Le premier ':' indique que les valeurs vides sont
dans la liste. Vient ensuite la variable de liste nommée
relay_from_hosts
(c'est une variable, car il y a le caractère '+'
devant). Par défaut sous Debian, cette liste contient le contenu de la
macro MAIN_RELAY_TO_DOMAINS qui contient elle-même le contenu de la
variable dc_relay_nets
du fichier de configuration d'Exim à la sauce
Debian (/etc/exim4/update-exim4.conf.conf
). Dans mon cas, cette
liste est vide (je ne fais pas de relais) mais rien n'empêche de la
laisser par défaut. Ensuite, vient une directive conditionnelle
substituée: ${if
exists{fichier}{valeur_si_vrai}{valeur_si_faux}}
. La commande
exists
du test if
vérifie que le fichier donné en argument
(/etc/greylistd/whitelist-hosts
) existe. Si c'est le cas, le nom
complet du fichier est donné, sinon, on ne renvoie rien (le
'{}'
). Il faut savoir que sous Exim4, si vous mettez le chemin
complet d'un fichier dans une variable de liste, le fichier est lu
directement par Exim et son contenu alimente la liste. Donc, pour
résumer, on rejette temporairement toutes les machines dont le nom
d'hôte ou l'adresse IP n'est pas vide ou identifiée dans la liste des
relais connus ou présent dans les fichiers
/etc/greylistd/whitelist-hosts
et
/var/lib/greylistd/whitelist-hosts
. Voilà donc l'explication de la
présence de ces fichiers lors de l'installation de greylistd. Je vous
invite à insérer les noms de domaines (avec un wildcard) que vous ne
souhaitez pas mettre en liste grise dans ces fichiers.
!authenticated
est encore une condition négative. Elle indique que
toute connexion authentifiée (en gros les utilisateurs locaux qui
envoient des courriels) ne sera pas rejetée automatiquement (le
caractère *
indique toute connexion authentifiée).
!acl
est une condition négative qui indique que les connexions qui
respectent l'acl nommée acl_local_deny_exceptions
ne seront pas
mises sur liste grise. Sous Debian, cette acl permet d'accepter des
noms d'hôtes et des expéditeurs situés dans des fichiers de référence
(des whitelists). Dans mon cas, ces fichiers sont vides mais ça peut
toujours être intéressant de faire avec cette ACL qui ne mange pas de
pain.
!dnslists
est une condition négative. dnslists
est un moyen pour
Exim4 d'exploiter les listes noires DNS. Pour faire simple, ces listes
sont gérées par des services sur Internet qui permettent de dire:
"cette adresse IP est sur liste noire pour le spam de courrier
électronique". Le mécanisme est assez simple: Exim4 fait une requête
DNS sur ces serveurs. Par exemple si le contenu de dnslists
contient
blackholes.mail-abuse.org
, Exim4 fait une requête DNS sur
adresse_ip_inversée.blackholes.mail-abuse.org et si le serveur en face
renvoie une réponse DNS valide, Exim4 rejette la connexion de l'IP en
question. Plutôt malin comme fonctionnement, non ? Dans notre cas de
figure, on place dans cette liste le contenu de
/etc/greylistd/dnswl-known-good-sender
qui est censé contenir des
noms de serveurs DNS permettant de mettre sur liste blanche les IP qui
sont requêtées (c'est bon, vous suivez encore). Dans mon cas, je
ne souhaite pas utiliser ces services, car je me suis déjà retrouvé sur
une liste noire de spammeurs simplement parce que mon adresse IP
faisait partie d'un bloc lié à des comptes ADSL alors que, bien
évidemment, je n'ai jamais envoyé de spam. Mes tentatives de
désinscription s'étant soldées par un renvoi vers le FAI (ce dernier
devant apporter la preuve au service de liste noire), ce qui est
mission impossible. De plus, je n'aime pas dépendre d'un énième
service externe juste pour envoyer du courrier électronique. Donc,
pour ma part, je laisse la liste des serveurs vides.
domains
est la liste des domaines servis qui seront rejetés
temporairement. C'est la condition qui va nous permettre de retarder
la réception des courriels du/des domaine(s) géré(s) par le
serveur. On a ici deux variables de liste: local_domains
et
relay_to_domains
. Elles sont remplies par les macros:
MAIN_LOCAL_DOMAINS et MAIN_RELAY_TO_DOMAINS, elles-mêmes définies par
des variables dc_
du fichier de configuration d'Exim4 à la sauce
Debian.
verify = recipient
est une condition de vérification: on cherche à
vérifier que l'adresse de réception existe vraiment (sinon, ce n'est
pas la peine de retarder la livraison).
Enfin, on trouve la condition qui permet de vérifier que notre triplet
est valide. Elle est indiquée par la condition condition
qui est une
condition générique. Si le contenu de la condition renvoie true alors
la condition est vérifiée et s'applique. Dans notre cas, nous avons
une variable ${readsocket{la_socket}{liste des
arguments}{5s}{}{false}}. readsocket
indique qu'on souhaite faire
une requête sur une socket (Unix ou TCP). Dans notre cas, c'est une
requête Unix sur /var/run/greylistd/socket
qui est la socket du
démon greylistd. Les arguments sont les suivants:
--grey
indique qu'on souhaite interroger la liste grise de greylistd.${mask:$sender_host_address/24}
indique un masque réseau formé de l'adresse IP de l'hôte qui souhaite nous envoyer du courrier électronique et d'un préfixe réseau (ici 24 et mis en arbitraire). On aurait pu utiliser uniquement l'adresse IP ($sender_host_address
} mais cela pose un problème pour les gros fournisseurs de services de courrier électronique. En effet, chez eux, il est courant que le MTA qui a fait une première demande soit différent du MTA qui effectue le renvoi du courrier électronique initial. Si on ne trace que l'adresse IP, on va se retrouver dans un cas où la seconde présentation du courrier électronique initial aboutisse à l'ajout d'un nouveau triplet avec une adresse IP source différente, le tout pour l'envoi du même courriel. Dans le meilleur des cas, le courriel est remis beaucoup plus tard (après avoir épuisé les adresses IP des MTA du fournisseur); au pire, le courriel n'est jamais remis. C'est pourquoi, en ajoutant une adresse de réseau, on autorise les machines situées sur le même réseau à être considérées comme un seul cas de présentation de courriel. C'est particulièrement flagrant quand on reçoit du courrier de chez google.com qui dispose d'une flopée de MTA avec des IP différentes. Pourquoi un /24 ? C'est juste pour répondre à une plage assez large (255 hôtes) sans être trop grande.$sender_address
: le contenu du champ FROM de l'expéditeur.$local_part@$domain
: l'adresse de courrier électronique à qui est destiné le courriel.
On ajoute un timeout de la requête de 5s (le fameux {5s}). Ensuite, on
trouve l'option de gestion des lignes dans la réponse à la
requête. Dans notre cas, c'est vide ('{}'
) donc on ne fait rien
(cette option permet de transformer les caractères retour à la ligne
par ce que l'on veut; dans notre cas, nous n'en avons pas
besoin). Enfin la dernière option {false}
indique le comportement en
cas d'échec de la requête sur la socket. Dans notre cas, si le serveur
n'est pas disponible ou qu'il y a eu une erreur, la réponse sera
false
et notre courrier ne sera pas rejeté temporairement.
Pour l'autre directive deny
, on applique les mêmes recettes mais
cette fois-ci on se permet de renvoyer au MTA que son message a été
rejeté définitivement (et que ce n'est pas la peine de recommencer)
car il est dans la liste noire (le --black
).
Voilà pour les explications détaillées. Finalement, c'est un cas de configuration pas si complexe que ça si l'on met de côté l'aspect "Eximiesque" des choses…
Sachez que vous devez également ajouter ces règles à l'ACL
acl_check_data
(située dans le fichier
/etc/exim4/conf.d/acl/40_exim4-config_check_data
) avec les
modifications qui suivent:
acl_check_data:
.ifdef GREYLISTD
# greylistd(8) configuration follows.
# This statement has been added by "greylistd-setup-exim4",
# and can be removed by running "greylistd-setup-exim4 remove".
# Any changes you make here will then be lost.
#
# Perform greylisting on incoming messages with no envelope sender here.
# We did not subject these to greylisting after RCPT TO:, because that
# would interfere with remote hosts doing sender callout verifications.
#
# Because there is no sender address, we supply only two data items:
# - The remote host address
# - The recipient address (normally, bounces have only one recipient)
#
# We also check the local whitelist to avoid greylisting mail from
# hosts that are expected to forward mail here (such as backup MX hosts,
# list servers, etc).
#
defer
message = $sender_host_address is not yet authorized to deliver \
mail from <$sender_address> to <$recipients>. \
Please try later.
log_message = greylisted.
senders = :
!hosts = : +relay_from_hosts : \
${if exists {/etc/greylistd/whitelist-hosts}\
{/etc/greylistd/whitelist-hosts}{}} : \
${if exists {/var/lib/greylistd/whitelist-hosts}\
{/var/lib/greylistd/whitelist-hosts}{}}
!authenticated = *
!acl = acl_local_deny_exceptions
condition = ${readsocket{/var/run/greylistd/socket}\
{--grey \
${mask:$sender_host_address/24} \
$recipients}\
{5s}{}{false}}
# Deny if blacklisted by greylist
deny
message = $sender_host_address is blacklisted from delivering \
mail from <$sender_address> to <$recipients>.
log_message = blacklisted.
!senders = :
!authenticated = *
condition = ${readsocket{/var/run/greylistd/socket}\
{--black \
$sender_host_address \
$recipients}\
{5s}{}{false}}
.endif
…
Le .ifdef
est de ma patte. Il me permet de désactiver le greylisting
en commentant simplement une MACRO dans le fichier
/etc/exim4/conf.d/main/000_localmacros
.
Pour mettre en œuvre cette configuration il faut lancer la traditionnelle séquence:
# update-exim4.conf && systemctl restart exim4
Pour aller plus vite¶
En fait, vous n'êtes pas obligé de faire les modifications de
configuration d'Exim4 à la main comme je l'ai décrit juste
avant. greylistd fourni un exécutable pour le faire:
greylistd-setup-exim4
. Un simple:
greylistd-setup-exim4 add -netmask=24
suffira pour appliquer la configuration visualisée plus haut dans Exim4. Mais vous conviendrez que maintenant, grâce aux explications précédentes, vous maîtrisez un peu plus votre MTA. Et c'est une bonne chose !
Conclusion¶
Une fois la configuration en place, je vous invite à lire vos logs
(dans /var/log/exim4
) pour vérifier si certains messages sont bien
rejetés temporairement. Bien entendu, si vous avez des MTA que vous
souhaitez accepter de facto, vous pouvez renseigner le fichier
/etc/greylistd/whitelist-hosts
.
Bien entendu, il faudra laisser un peu de temps à cette configuration pour se mettre en place. Dès l'activation, la base de greylistd va mettre un peu de temps à se remplir, ce qui signifie que pendant au moins 10 minutes, plus personne ne va recevoir de courriel.
Je n'en ai pas parlé, mais il existe une commande permettant de
travailler avec le démon en espace utilisateur. Il s'agit de la
commande greylist
qui permet, par exemple, d'avoir la liste des
triplets dans les différentes listes avec la commande list
:
greylist list
. Avec cette commande, on peut avoir un état global des
différentes bases de données et des triplets qui y résident. Cela
permet de voir si le spam est vraiment stoppé ou non: la majorité des
adresses de spam sont dans la liste grise avec une seule tentative
d'envoi.
Finalement, le système de liste grise reste assez simple à mettre en place, notamment en ce qui concerne le binaire chargé de gérer la liste. On voit qu'il s'agit d'un simple script Python qui fait juste son travail. On peut le déployer simplement sur des configurations légères et il y a fort à parier que la charge de gestion de la liste grise restera négligeable par rapport à la charge engendrée par Exim4. Voilà donc un moyen simple de lutter contre le spam à moindres frais.
Toutefois, ce n'est pas une solution ultime et il y a fort à parier que d'ici quelque temps je constaterai peut-être que des courriels indésirables passeront ce filtre. Il sera alors temps d'étudier la mise en place d'un vrai système anti-spam. Toutefois, je souhaite mesurer la durée d'efficacité de la technique du greylisting. Cet article me servira de point zéro…