Mettre en place un mécanisme de liste grise sur Exim4 sous Debian🔗

Posted by Médéric Ribreux 🗓 In blog/ Sysadmin/

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:

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_TODOMAINS, 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:

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…