Postscreen, l'exterminateur de zombies

www.ed-diamond.com_client_cache_produit_250_lm147_504.jpg Par Julien Vehent

Cet article a été publié dans le numéro 147 de Gnu Linux Magazine France et mis a disposition sous Creative Common par les Editions Diamond.

Il y a plus d'un an de cela, je vous présentais dans ces pages un formidable répulsif à Sus scrofa domesticus, j'ai nommé DSPAM. Au même moment, notre rédacteur en chef préféré m'avouait sa passion déraisonnable et certainement bien personnelle pour ce formidable résidu de cuisine qu'est le pâté de jambon, communément appelé Spam de mon coté de l'atlantique. Fort de ma machine à tokens dopée aux amphétamines (toujours DSPAM), je m'empressais de pondérer les propos du malheureux finnois d'une statistique bien négative, et éventuellement repoussai lesdits échanges aux confins de mon dossier spam (sans pour autant bannir complètement l'intéressé, dont la conversation est toujours des plus constructive et bienvenue). Heureux de ma vie tranquille, lisant plaisamment mes courriels sans l'once d'une distraction viagriarienne “enlarge your peniche” pendant presque un an, je relâchais donc ma garde. Mais le spammer est malin, et sous prétexte de bricoler des bidules électroniques, le voilà qui libérait toute une armée de zombie spammer prêts à délivrer leurs contenus dégoûtants dans les entrailles de l'internet.

C'était sans compter sur la vigilance du Chevalier Wietse (Venema), qui décida alors que c'était assez, et qu'il était temps de faire quelque chose. C'est ainsi que naquit Postscreen, et fut intégré dans la version 2.8 de Postfix.

1 La vraie introduction

Postscreen est un processus de Postfix qui filtre le courrier entrant le plus tôt possible, l'objectif étant de rejeter le spam avant que cela ne consume des ressources sur le système. Postscreen a plusieurs niveau de filtrages, chaque niveau allant un peu plus dans le détail, et donc consommant un peu plus de ressources. Plus tôt un email est rejeté, meilleur le système se portera, et Postscreen essai donc d'être le plus efficace possible dès les premiers éléments de la connection SMTP.

Postscreen n'a pas vocation a protéger les utilisateurs interne du server mail. Typiquement, les emails venant de l'extérieur sont dirigés vers le port 25 de postfix, et les emails soumis par les utilisateurs internes vont sur le port 587. Postscreen va prendre en charge les emails reçus sur le port 25 uniquement, comme le montre le diagramme en figure 1.

Ce que Postscreen recherche, c'est un critère discriminant permettant de déterminer si un message provient d'un zombie ou non. S'il décide qu'un émetteur n'est pas un zombie, il place ce dernier en liste blanche pour éviter de consommer des ressources lors du prochain échange.

Certains critères discriminants sont plus simples à détecter que d'autres. Par exemple, il est courant qu'un zombie envoie l'intégralité de son message dès que la connexion est ouverte, cela pour accélérer la vitesse de délivrance avant que son IP ne se retrouve dans une Real-time Blackhole List (RBL) et soit donc bannie des principaux serveurs mails. Toutefois, le protocole SMTP déclare que c'est au serveur de parler le premier, typiquement en envoyant sa bannière. En surveillant les correspondants qui parlent avant leur tour, Postscreen peut détecter les zombies. Il ne va pas, en revanche, inspecter le corps d'un message. Dans la mesure où ce qu'il cherche à savoir, c'est si l'émetteur est un zombie ou non, le contenu du message n'est pas une métrique pertinente, et de toute façon cela consommerait trop de ressources.

D'une manière générale, Postscreen n'introduit pas de technique révolutionnaire dans la lutte contre le Spam. Mais il permet d'intégrer des contrôles efficaces de manière élégante dans un processus léger (comprendre: pas dans 40 scripts qui s'exécutent séquentiellement).

Ce que nous allons voir, c'est que les quelques 15+ lignes de configuration à ajouter à Postfix cachent en fait beaucoup d'intelligence. Et cette balade va également nous permettre de réviser les bases d'un protocole qu'on croit trop souvent être un dinosaure de l'Internet. Oui, SMTP est ancien, mais ce n'est pas pour autant qu'il est simpliste.

2 Architecture

Postscreen est intégré dans Postfix 2.8. Et quand je dis intégré, c'est parce que Postscreen n'est pas dissociable de Postfix. Il n'agit pas en tant que proxy SMTP, mais en tant que module interne directement sous le contrôle du Postfix Master. Au sein de l'architecture de Postfix, Postscreen se situe en première ligne, juste devant le processus “smtpd”. Le diagramme en figure 2, bien que non exhaustif, montre un peu plus en détail comment ces processus s'arrangent.

L'un des principe fondateur de Postfix est son architecte modulaire, pas surprenant donc de retrouver autant de composants sur le chemin d'un email. Mais dans le cas qui nous intéresse, on se concentrera sur postscreen et dnsblog (ce dernier permettant, comme on va le voir, d'interroger les RBL).

3 Installation du serveur

Nous allons travailler sous Debian. Comme Postfix 2.8 n'est pas présent dans l'actuelle version stable, c'est wheezy que nous allons utiliser. Partons d'une installation de base, presque sans configuration si ce n'est pour quelques commandes SASL afin d'authentifier les utilisateurs. Le fichier main.cf ressemble à cela:

myorigin = /etc/mailname
smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU)
biff = no
append_dot_mydomain = no
# SASL configuration
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_local_domain = $mydomain
smtpd_sasl_security_options = noanonymous
smtpd_sasl_authenticated_header = yes

Tous les autres paramètres prenant les valeurs par défaut, on obtient un système qui accepte les emails pour le domaine local du serveur.

3.1 Pour le lecteur pressé

La configuration de Postscreen se trouve dans /etc/postfix/main.cf, avec le reste des paramètres de configuration de Postfix. On va tout d'abord créer un fichier /etc/postfix/postscreen_access.cidr contenant les IP et réseaux autorisés ou rejectés.

# cat /etc/postfix/postscreen_access.cidr 

# Rules are evaluated in the order as specified.
# Blacklist 192.168.* except 192.168.0.1.
127.0.0.1       permit
192.168.1.1     permit
192.168.1.0/24  reject

Le reste de la configuration dans main.cf est ci-dessous:

# Postcreen configuration
postscreen_access_list = cidr:/etc/postfix/postscreen_access.cidr
postscreen_blacklist_action = enforce

postscreen_dnsbl_sites =
    zen.spamhaus.org*3
    dnsbl.njabl.org*2
    bl.spameatingmonkey.net*2
    dnsbl.ahbl.org
    bl.spamcop.net
    dnsbl.sorbs.net
postscreen_dnsbl_threshold = 3
postscreen_dnsbl_action = enforce

postscreen_greet_banner = Bienvenue et merci d'attendre qu'on vous assigne une place
postscreen_greet_action = enforce

postscreen_pipelining_enable = yes
postscreen_pipelining_action = enforce

postscreen_non_smtp_command_enable = yes
postscreen_non_smtp_command_action = enforce

postscreen_bare_newline_enable = yes
postscreen_bare_newline_action = enforce

Et finalement, il faut activer le processus postscreen dans le fichier /etc/postfix/master.cf. Par défaut, les connections sur le port 25 sont prises en charge par le processus smtpd:

# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (yes)   (never) (100)
# ==========================================================================
smtp      inet  n       -       -       -       -       smtpd

Il faut remplacer cette ligne par les lignes suivantes, qui vont diriger les connections entrantes vers postscreen, puis vers smtpd et également activer dnsblog.

# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (yes)   (never) (100)
# ==========================================================================
smtp      inet  n       -       -       -       1       postscreen
smtpd     pass  -       -       -       -       -       smtpd
dnsblog   unix  -       -       -       -       0       dnsblog

4 Tests de pré-acceptance de connexion

En SMTP, le serveur parle le premier. C'est évidemment le client qui initie la connexion TCP, mais une fois cette dernière établie, il est de coutume d'attendre que le serveur envoi sa bannière avec le fameux code 220. Postscreen utilise ce délai pour réaliser 3 tests différents.

Le délai, tout d'abord, peut être configuré dans le paramètre postscreen_greet_wait. Celui-ci définit combien de temps postscreen doit attendre avant d'envoyer la seconde partie de la bannière (greet banner, voir plus loin). Ce temps n'est pas gaché, car postscreen l'utilise pour interroger les RBLs et vérifier son access list. Par défault, postscreen fera attendre un client inconnu pendant 6 secondes (ou 2 secondes en période de forte charge).

Voyons maintenant en détail comment fonctionnent les 3 tests pre-220: Access list, RBLs et PreGreet.

4.1 Access List

postscreen_access_list = cidr:/etc/postfix/postscreen_access.cidr
postscreen_blacklist_action = enforce

Le fichier CIDR est une “lookup table” au format ASCII, qui n'a donc pas besoin d'être convertie au format DB via la commande postmap. Toutefois, la table est chargée en mémoire, donc un reload de Postfix (signal SIGHUP sur tous les modules) est nécessaire après chaque changement.

Le fichier postscreen_access.cidr est évalué séquentiellement, comme les règles d'un firewall. Donc la règle associée à la première entrée qui correspond est utilisée.

# Rules are evaluated in the order as specified.
# Blacklist 192.168.* except 192.168.0.1.
127.0.0.1       permit
192.168.1.1     permit
192.168.1.0/24  reject

Dans notre exemple précédent, la machine 192.168.1.1 est explicitement autorisée à émettre. On peut tester cela avec la commande postmap.

# postmap -q "192.168.1.1" cidr:/etc/postfix/postscreen_access.cidr 
permit

En revanche, une autre machine du réseau sera explicitement rejetée:

# postmap -q "192.168.1.55" cidr:/etc/postfix/postscreen_access.cidr 
reject

N'importe quelle autre adresse qui n'est pas listée ne retournera rien, et postmap sortira avec la valeur de retour “1”:

# postmap -q "10.11.12.13" cidr:/etc/postfix/postscreen_access.cidr 
# echo $?
1

Si un hôte doit être rejeté, postscreen appliquera l'action définie dans postscreen_blacklist_action. Le défaut est configuré à “ignore”, ainsi postscreen prendra note du résultat du test dans les logs, mais ne bloquera pas la connexion. On peut choisir, en plus d'ignore l'une des deux options suivante:

  • drop coupe la connexion TCP avant même de renvoyer une bannière
  • enforced laisse le client continuer jusqu'à l'étape RCPT TO, et coupe la connexion à ce moment là.

Ces trois actions seront ré-utilisables pour chaque point d'analyse que postscreen réalise. Nous allons nous y intéresser dès maintenant, et ne plus y revenir après.

4.1.1 ignore

En mode ignore, le défaut, l'envoi fonctionne correctement:

$ nc 192.168.1.220 25
220-Bienvenue et merci d'attendre qu'on vous assigne une place
220 samchiel.localdomain ESMTP Postfix (Debian/GNU)
ehlo mail
250-samchiel.localdomain
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-AUTH PLAIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
mail from:<jean-kevin@example.net>
250 2.1.0 Ok
rcpt to:<julien>
250 2.1.5 Ok
data
354 End data with <CR><LF>.<CR><LF>
subject: hello world.
test de postscreen.
.
250 2.0.0 Ok: queued as 4D638D40069
quit
221 2.0.0 Bye

4.1.2 enforced

En mode enforced, c'est déjà différent. Tout d'abord, on voit que postscreen renvoie la pre-bannière et quelques secondes se passent avant de recevoir la seconde partie de la bannière. Ensuite, après la commande MAIL FROM, la connection est refusée avec un message SMTP 550:

$ nc 192.168.1.220 25
220-Bienvenue et merci d'attendre qu'on vous assigne une place
220 samchiel.localdomain ESMTP Postfix (Debian/GNU)
ehlo mail
250-samchiel.localdomain
250-SIZE 10240000
250-VRFY
250-ETRN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
mail from:<jean-kevin@example.net>
250 2.1.0 Ok
rcpt to:<julien>
550 5.3.2 Service currently unavailable
quit
221 2.0.0 Bye

Au même moment, dans les logs on voit la chose suivante:

Oct 16 16:14:24 samchiel postfix/postscreen[9324]: CONNECT from [192.168.1.165]:49618
Oct 16 16:14:24 samchiel postfix/postscreen[9324]: BLACKLISTED [192.168.1.165]:49618
Oct 16 16:14:35 samchiel postfix/postscreen[9324]: BARE NEWLINE from [192.168.1.165]:49618
Oct 16 16:14:40 samchiel postfix/postscreen[9324]: NOQUEUE: reject: RCPT from [192.168.1.165]:49618: 550 5.3.2 Service currently unavailable; from=<jean-kevin@example.net>, to=<julien>, proto=ESMTP, helo=<mail>
Oct 16 16:14:46 samchiel postfix/postscreen[9324]: DISCONNECT [192.168.1.165]:49618

Bien que la connection ait été marquée comme BLACKLISTED dès sa réception, postscreen a laissé le client poursuivre ses commandes, pour ne le rejeter qu'après avoir reçu le destinataire.

4.1.3 drop

En mode drop, on fait moins dans la finesse, et plus dans l'efficacité :

$ nc 192.168.1.220 25
521 5.3.2 Service currently unavailable

Et dans les logs :

CONNECT from [192.168.1.165]:56567
BLACKLISTED [192.168.1.165]:56567
DISCONNECT [192.168.1.165]:56567

Clair, net et précis. On ne s'embarrasse pas d'une discussion avec le client, ce dernier est rejeté immédiatement.

4.1.4 Pour les fanatiques

Une des façons d'appliquer le filtrage par IP et d'utiliser la liste des réseaux bannis par spamhaus: le DROP (Don't Route Or Peer). Avec un script simple, on peut récupérer cette liste à intervalle régulier et l'intégrer dans postscreen. L'objectif est de couper toute connexion venant d'un de ces réseaux bannis, et donc d'économiser des ressources.

#! /bin/sh

# ce script est sur github à l'adresse http://1nw.eu/!Wg

URL=http://www.spamhaus.org/drop/drop.lasso
DROPLASSO=$(mktemp)
ACLFILE=$(mktemp)
POSTSCREEN_ACCESS_FILE=/etc/postfix/postscreen_access.cidr
DATE=$(date +%Y%m%d)

wget --quiet $URL -O $DROPLASSO

if [ -e $DROPLASSO ]
then
  # decoupe le fichier source sur le point virgule
  # et prepare le fichier de destination
  for network in $(cat $DROPLASSO | awk '{print $1}' |\
    grep -E "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\/[0-9]{1,2}$")
  do
    echo -e "$network\treject" >> $ACLFILE
  done
fi

cp $POSTSCREEN_ACCESS_FILE{,.bkp$DATE}
cp $ACLFILE $POSTSCREEN_ACCESS_FILE
postfix reload

Ce qui va générer le fichier suivant :

[...]
95.64.43.0/24   reject
95.64.44.0/24   reject
95.64.45.0/24   reject
95.64.46.0/24   reject
95.64.59.0/24   reject
95.64.9.0/24    reject
95.64.98.0/23   reject

On pourrait aussi réaliser cela dans le firewall du serveur directement. Ou bien se baser entièrement sur la RBL que fournit spamhaus. À vous de voir.

4.2 PreGreet Banner

postscreen_greet_banner = Bienvenue et merci d'attendre qu'on vous assigne une place
postscreen_greet_action = enforce 

Lorsqu'un client se connecte à postscreen, ce dernier renvoie une première bannière définit dans le paramètre postscreen_greet_banner :

$ nc 192.168.1.220 25
220-Bienvenue et merci d'attendre qu'on vous assigne une place
220 samchiel.localdomain ESMTP Postfix (Debian/GNU)

Ce que l'on ne peut pas voir ici, c'est que la première ligne de la bannière est envoyée immédiatement, mais que la seconde ligne n'est envoyée que 6 secondes plus tard. L'objectif est de piéger les zombies ne respectant pas le protocole, en leur faisant croire que c'est à eux de parler.

Pour bien comprendre l'astuce, il faut jeter un oeil à la RFC 5321 sur SMTP, qui prime la chose suivante:

The format for multiline replies requires that every line, except the
last, begin with the reply code, followed immediately by a hyphen,
"-" (also known as minus), followed by text.  The last line will
begin with the reply code, followed immediately by <SP>, optionally
some text, and <CRLF>.  As noted above, servers SHOULD send the <SP>
if subsequent text is not sent, but clients MUST be prepared for it
to be omitted.

For example:
  250-First line
  250-Second line
  250-234 Text beginning with numbers
  250 The last line

Traduction: si la réponse renvoyée par le serveur est de la forme <code><tiret><texte> (ex: 220-Bienvenue blablabla), alors c'est une réponse sur plusieurs lignes. La dernière ligne de la réponse sera de la forme <code><espace><texte>.

Postscreen se contente donc de respecter le protocole. Il envoi la première ligne d'une réponse multiligne, puis attend et envoi le reste après quelques secondes. Un client respectant la norme va attendre d'avoir reçu l'ensemble de la bannière avant de parler. Un zombie ne fera certainement pas cela, et se verra donc rejeté avec la ligne suivante dans les logs:

PREGREET 10 after 1.8 from [192.168.1.165]:37925: ehlo zombie.example.net\n

4.3 Real-Time Blackhole List (RBL)

Les RBLs ont fait partie du paysage des serveurs mails depuis bien longtemps maintenant. Rien de nouveau ici, si ce n'est la manière dont postscreen les utilise. Outre la possibilité d'interroger une RBL avant même d'allouer des ressources au message, c'est-à-dire sans avoir a charger 40 tonnes de modules Perl et autres joyeusetés, postscreen permet d'allouer un poids à chaque RBL qu'il va interroger. On peut ainsi avoir une relativement longue liste de RBL, certaines de bonnes qualités, d'autres moins, et les associer sans risquer de bloquer tout le traffic.

Mais avant cela, un petit rappel sur le fonctionnement des RBL.

4.3.1 RBL et DNS

Une RBL est typiquement un répondeur qui va renvoyer le statut associé à une adresse IP. La particularité des RBL est que la discussion entre le client et le serveur se fait via le protocole DNS, en UDP.

Pas besoin ici d'établir une coûteuse connexion TCP, de faire de l'authentification HTTP en REST, etc etc…. Un simple paquet UDP contenant une requête DNS retourne le statut d'une IP. L'astuce est de fournir l'IP sous la forme inversée (comme pour les enregistrements PTR du reverse DNS) et d'interroger le serveur RBL comme suit:

$ dig +short 12.13.95.143.zen.spamhaus.org @localhost
127.0.0.2

On voit ici la réponse courte (+short). La requête demande le statut de l'IP 143.95.13.12 à zen.spamhaus.org, et il faut pour cela inverser l'ordre des champs et ajouter zen.spamhaus.org à la fin. On peut ensuite lancer cette requête sur n'importe quel resolver DNS (en l'occurrence, l'instance de unbound installée sur mon portable), le processus de résolution va chercher la réponse sur les serveurs de Spamhaus.

Si vous voulez le détail, essayez la commande “dig +trace 12.13.95.143.zen.spamhaus.org @8.8.8.8” qui va lancer la même commande au resolver de google.

Ensuite, spamhaus nous renvoie l'IP 127.0.0.2, comme pour avertir que la machine 12.13.95.143.zen.spamhaus.org peut être trouvée à cette adresse, ce qui n'a évidemment pas de sens. En fait, spamhaus retourne un code d'état qui correspond au niveau de dangerosité de l'IP.

Return Code Zone Description
127.0.0.2 SBL Spamhaus SBL Data
127.0.0.3 SBL Spamhaus SBL CSS Data
127.0.0.4 XBL CBL Data
127.0.0.5 XBL Customized NJABL Data
127.0.0.10 PBL ISP Maintained
127.0.0.11 PBL Spamhaus Maintained

Comme notre requête précédente a retournée 127.0.0.2, cela signifie que spamhaus a listé cette adresse dans sa liste “Spamhaus SBL Data”. La description exacte est disponible sur spamhaus.org, mais pour faire court : accepter des emails provenant d'IP dans cette liste est une mauvaise idée.

4.3.2 Mise en place

En mettant en jeu plusieurs RBLs en parallèle, et en jouant avec les poids de chacune, on obtient ainsi un meilleur contrôle sur le niveau de rejet. Ici, nous allons choisir de placer la barre à un score de 3.

D'une manière générale, zen.spamhaus.org est une source fiable. On peut donc attribuer un poids de valeur 3 à cette RBL. Les suivantes sont un peu moins fiables, et suivant leur réputation, on descend un peu le score. La liste ci-dessous provient d'une étude faite par rob0 sur la mailing-list de postfix début 2011 [http://1nw.eu/!De].

postscreen_dnsbl_sites =
    zen.spamhaus.org*3
    dnsbl.njabl.org*2
    bl.spameatingmonkey.net*2
    dnsbl.ahbl.org
    bl.spamcop.net
    dnsbl.sorbs.net

postscreen_dnsbl_threshold = 3

postscreen_dnsbl_action = enforce

Il est également possible de filtrer la réponse d'une RBL sur un code retour particulier, en précisant ce dernier à côté de l'adresse de la source.

zen.spamhaus.org=127.0.0.2*2

Pondération, filtrage des codes retour et multiples RBLs le tout dans un processus léger géré directement par Postfix. C'est probablement l'une des innovations les plus élégantes que Wietse Venema a mis en place avec cette version.

4.3.3 Vérifier si votre serveur est dans une RBL

Cela m'arrive régulièrement : un des fournisseur de RBL décide de blacklister l'IP d'un serveur que je maintiens. Soit parce que j'ai oublié de configurer le reverse DNS (ou autre), soit parce que quelqu'un s'en est plaint (aller comprendre).

Du coup, plutôt que de découvrir dans les logs que les emails ne sont pas reçus, j'utilise un script bash tout simple, installé dans cron.daily, qui vérifie la présence d'une liste de serveur dans les principales RBLs. Si vous voulez dormir tranquille, je vous conseille d'en faire de même :

#!/usr/bin/env bash

# ce script est sur github à l'adresse http://1nw.eu/!u2

DEBUG="$1"
SRV="smtp.example.net smtp2.example.net smtp.example.com"
RBL="zen.spamhaus.org dnsbl.njabl.org bl.spameatingmonkey.net dnsbl.ahbl.org bl.spamcop.net dnsbl.sorbs.net b.barracudacentral.org spamtrap.trblspam.com swl.spamhaus.org list.dnswl.org"
for server in $SRV
do
    ip=$(dig +short $server)
    r_ip=$(echo $ip|awk -F"." '{for(i=NF;i>0;i--) printf i!=1?$i".":"%s",$i}')
    for rbl in $RBL
    do
        if [ ! -z "$DEBUG" ]
        then
            echo "testing $server ($ip) against $rbl"
        fi
        result=$(dig +short $r_ip.$rbl)
        if [ ! -z "$result" ]
        then
            echo "$server ($ip) is in $rbl with code $result"
        fi
        if [[ ! -z "$DEBUG" && -z "$result" ]]
        then
            echo "\`->negative"
        fi
    done
done

Spécifiez vos machines dans la variable $SRV, et lancez le script sans argument (ou avec un argument pour voir le détail).

$ /etc/cron.daily/rbl-check -d

testing smtp.example.net (10.20.30.40) against zen.spamhaus.org
`->negative
testing smtp.example.net (10.20.30.40) against dnsbl.njabl.org
`->negative
testing smtp.example.net (10.20.30.40) against bl.spameatingmonkey.net
`->negative
testing smtp.example.net (10.20.30.40) against dnsbl.ahbl.org
`->negative

Si une IP est présente dans une blacklist, alors le script retournera le code de retour renvoyé par la RBL.

$ /etc/cron.daily/rbl-check
smtp.example.net (10.20.30.40) is in zen.spamhaus.org with code 127.0.0.11

Chaque RBL ayant sa propre procédure d'enlèvement des faux positifs, je vous laisse le plaisir de découvrir cela par vous même.

5 Tests effectués après le SMTP 220

Une fois la batterie de tests pré-220 passée, postscreen peut réaliser des tests en profondeur sur la communication entre le client et le serveur. Ces tests sont essentiellement basés sur un respect de la norme, que beaucoup de zombie ignorent.

5.1 Command pipelining test

postscreen_pipelining_enable = yes
postscreen_pipelining_action = enforce

Dans les premiers temps de SMTP, avant que ESMTP (Extended SMTP) ne soit publié en 1995, le protocole était half-duplex. Le client et le serveur ne pouvaient envoyer qu'une commande à la fois, et chacun devait attendre la réponse de l'autre avant de continuer.

Postscreen ne suppose pas ESMTP et son jeu d'extension. C'est pourquoi, lorsqu'un client se connecte à postscreen, ce dernier ne retournera pas l'extension PIPELINING. Un client respectant la norme prendra cela en compte et enverra ses commandes une par une, mais un zombie ignorera certainement cela et cherchera à émettre le plus vite possible.

 nc 192.168.1.220 25
220-Bienvenue et merci d'attendre qu'on vous assigne une place
220 samchiel.localdomain ESMTP Postfix (Debian/GNU)
ehlo mail
250-samchiel.localdomain
250-SIZE 10240000
250-VRFY
250-ETRN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
mail from:<jean-kevin@example.net>
rcpt to:<julien>
data
subject: test
test
.250 2.1.0 Ok
550 5.5.1 Protocol error
554 5.5.1 Error: no valid recipients
502 5.5.2 Error: command not recognized
502 5.5.2 Error: command not recognized

Dans l'exemple ci-dessus, nous avons essayé d'envoyer au même moment un jeu complet de commandes:

mail from:<jean-kevin@example.net>
rcpt to:<julien>
data
subject: test
test
.

Postscreen a répondu OK a la première (le “mail from”) et renvoyé un code 550 pour la suivante. Simultanément, on trouve la ligne suivante dans les logs:

COMMAND PIPELINING from [192.168.1.165]:39158 after rcpt

5.2 Non-SMTP command test

postscreen_non_smtp_command_enable = yes
postscreen_non_smtp_command_action = enforce

Ce test est un simple filtre qui bloque les commandes CONNECT, GET et POST utilisées par les zombies lors de connections via des proxy. Ce filtre était déjà présent dans Postfix depuis la version 2.2, donc il s'agit la uniquement de positionner le test en amont du daemon smtpd.

5.3 Bare newline test

postscreen_bare_newline_enable = yes
postscreen_bare_newline_action = enforce

Encore un test simple mais que beaucoup de client semblent traiter incorrectement. Dans la norme SMTP, il est imposé que chaque fin de ligne soit terminée par les caractères <CR><LF> “Carriage Return & Line Feed”. Beaucoup de zombies n'utilise que le <LF> en fin de ligne, comme c'est le cas lorsque nous testons avec netcat dans un terminal.

$ nc 192.168.1.220 25
220-Bienvenue et merci d'attendre qu'on vous assigne une place
220 samchiel.localdomain ESMTP Postfix (Debian/GNU)
mail from:<jean-kevin@example.net>
250 2.1.0 Ok
rcpt to:<julien>
550 5.5.1 Protocol error

La commande ci-dessus génère l'entrée de log suivante: BARE NEWLINE from [192.168.1.165]:38749. En ajoutant l'option -C à netcat, ce qui modifie la façon dont ce dernier traite les fins de lignes, le problème disparaît.

$ nc -C 192.168.1.220 25
220-Bienvenue et merci d'attendre qu'on vous assigne une place
220 samchiel.localdomain ESMTP Postfix (Debian/GNU)
mail from:<jean-kevin@example.net>
250 2.1.0 Ok
rcpt to:<julien>
450 4.3.2 Service currently unavailable
quit
221 2.0.0 Bye

Et la trace suivante apparaît dans les logs:

CONNECT from [192.168.1.165]:36721
NOQUEUE: reject: RCPT from [192.168.1.165]:36721: 450 4.3.2 Service currently unavailable; from=<jean-kevin@example.net>, to=<julien>, proto=SMTP, helo=<>
PASS NEW [192.168.1.165]:36721
DISCONNECT [192.168.1.165]:36721

“Comment ça le problème disparaît ? Pas vrai ça, postscreen a répondu avec un code 450 !”

Oui, et merci à toi Jean-Kevin d'avoir suivi jusqu'au bout, et de nous offrir une transition impeccable. Parlons donc un peu de Greylist…

5.4 Greylist

Comme on vient de le voir, un client qui passe tous les tests en profondeur se retrouve déconnecté sans raison apparente. Rassurez-vous, postscreen n'est pas fou, et c'est là le travail de sa dernière fonction.

Postscreen n'est pas un serveur SMTP; et il n'a qu'une connaissance très limitées des commandes SMTP. Donc ces tests en profondeur posent un problème : ils obligent postscreen à prendre en charge la communication jusqu'au point où il pourra décider si un client est légitime ou non. Mais à ce moment-là, postscreen n'a aucun moyen de donner la connexion au daemon smtpd.

La solution que Venema a trouvé est toute simple: reproduire le mécanisme de liste grise. Quand un nouveau client arrive, il est pris en charge par postscreen. Si le client passe tous les tests correctement, postscreen le met en liste blanche et coupe la connexion avec un code “450 Service indisponible”.

Le client doit donc établir une nouvelle connexion, qui sera cette fois adressée directement au daemon smtpd, sans passer par postscreen.

La première connexion du client produira le comportement suivant:

$ nc 192.168.1.220 25

220-Bienvenue et merci d'attendre qu'on vous assigne une place
220 samchiel.localdomain ESMTP Postfix (Debian/GNU)
ehlo mail
250-samchiel.localdomain
250-SIZE 10240000
250-VRFY
250-ETRN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
mail from:<jean-kevin@example.net>
250 2.1.0 Ok
rcpt to:<julien>
450 4.3.2 Service currently unavailable
quit
221 2.0.0 Bye

Le client a réussi tous les tests et postscreen le déconnecte. Dans les logs, on voit la ligne PASS NEW [192.168.1.165] qui signifie que le client a été ajouté à la WHITELIST.

Lors de la seconde connexion, point de double bannière, le client est envoyé directement au daemon smtpd et la ligne suivante est loguée PASS OLD [192.168.1.165].

$ nc 192.168.1.220 25

220 samchiel.localdomain ESMTP Postfix (Debian/GNU)
ehlo mail
250-samchiel.localdomain
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-AUTH PLAIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
mail from:<jean-kevin@example.net>
250 2.1.0 Ok
rcpt to:<julien>
250 2.1.5 Ok

Et voilà comment, presque sans le vouloir, postscreen vient de réimplémenter les listes grises. On notera toutefois que mécanisme de liste grise induit toujours un délai, et que ce dernier peut parfois aller jusqu'à plusieurs heures lorsque le client ne se représente pas. Malheureusement, postscreen ne peut rien faire pour pallier à ce problème. Mais si cela vous gêne, réfléchissez à deux fois avant d'activer les deep protocol tests.

6 Quelques statistiques

Si Postscreen écrit des logs très détaillé, pour obtenir une vision plus globale, il faut y aller à grands coups de scripts. Au cours de mes expérimentations avec Postscreen, j'ai écrit Postscreen-Stats: un script python qui produit des statistiques depuis le syslog de postfix (généralement situé dans /var/log/maillog ou mail.info).

Le script est sur github à l'adresse https://github.com/jvehent/Postscreen-Stats.

Voici les résultats pour 24h d'activité de mail.python.org, qui gère le trafic des mailing lists de python.org (statistiques cordialement fournies par Patrick Ben Koetter via la liste postfix-users)

=== Postscreen statistics ===
7 BARE NEWLINE
15 COMMAND COUNT LIMIT
165 COMMAND PIPELINING
736 COMMAND TIME LIMIT
38678 CONNECT
24189 DNSBL
25812 HANGUP
543 NOQUEUE 450 deep protocol test reconnection
4 NOQUEUE too many connections
486 PASS NEW
3455 PASS OLD
9373 PREGREET
7206 WHITELISTED

=== Clients statistics ===
3 avg. dnsbl rank
12301 clients
309 reconnections
4050 seconds avg. reco. delay

=== First reconnection delay (graylist) ===
delay| <10s   |>10to30s| >30to1m| >1to5m | >5to30m|
count|4       |18      |4       |83      |110     |
   % |1.3     |5.8     |1.3     |27      |36      |

delay|>30mto2h| >2hto5h|>5hto12h|>12to24h| >24h   |
count|46      |18      |17      |6       |3       |
   % |15      |5.8     |5.5     |1.9     |0.97    |

Ces résultats montrent que 12301 clients se sont connectés sur ce serveur, en moyenne 3 fois car le total de CONNECT est de 38678. Sur ces 38678 connections, un peu moins des deux tiers (24189) ont été bloqués par une DNSBL, environ 24% par la règle PREGREET, etc.

Une version plus récente de Postscreen-Stats permet de voir le détail par client unique. Voici les statistiques pour une installation de postscreen que je maintiens. Cela représente une semaine d'activité d'un serveur situé sur la côte Est des USA.

=== unique clients/total postscreen actions ===
2131/11010 CONNECT
1/1 BARE NEWLINE
30/33 COMMAND COUNT LIMIT
13/16 COMMAND PIPELINING
6/6 COMMAND TIME LIMIT
463/536 DNSBL
305/503 HANGUP
12/15 NON-SMTP COMMAND
1884/2258 NOQUEUE 450 deep protocol test reconnection
1/42 NOQUEUE too many connections
1577/1600 PASS NEW
866/8391 PASS OLD
181/239 PREGREET
5/84 WHITELISTED

=== clients statistics ===
4 avg. dnsbl rank
505 blocked clients
2131 clients
840 reconnections
32245.4285714 seconds avg. reco. delay

=== First reconnection delay (graylist) ===
delay| <10s   |>10to30s| >30to1m| >1to5m | >5to30m|
count|12      |21      |21      |196     |261     |
   % |1.4     |2.5     |2.5     |23      |31      |

delay|>30mto2h| >2hto5h|>5hto12h|>12to24h| >24h   |
count|88      |40      |29      |53      |119     |
   % |10      |4.8     |3.5     |6.3     |14      |
   
=== Top 20 Countries of Blocked Clients ===
 167 (33.00%) United States
  59 (12.00%) India
  33 ( 6.50%) Russian Federation
  26 ( 5.10%) Indonesia
  23 ( 4.60%) Pakistan
  21 ( 4.20%) Vietnam
  20 ( 4.00%) China
  13 ( 2.60%) Brazil
  11 ( 2.20%) Korea, Republic of
   9 ( 1.80%) Belarus
   8 ( 1.60%) Turkey
   7 ( 1.40%) Iran, Islamic Republic of
   7 ( 1.40%) Ukraine
   6 ( 1.20%) Kazakstan
   6 ( 1.20%) Chile
   5 ( 0.99%) Italy
   5 ( 0.99%) Romania
   4 ( 0.79%) Poland
   4 ( 0.79%) Spain
   3 ( 0.59%) Afghanistan

Ici, moins de clients (seulement 2131). On voit que 463 d'entre eux ont été bloqués par la DNSBL, certains même deux fois (le total est de 536). Autre information intéressante : 33% des zombies sont venus des USA, 12% d'Inde et seulement 4% de Chine. Contrairement à ce que l'on pourrait penser de prime abord, la Chine n'est pas le leader dans ce domaine (en tout cas, pas sur mon serveur…).

On voit également toute l'utilité du mécanisme de pondération des RBL fourni par postscreen. En moyenne, un zombie se fait rejeter par DNSBL avec un poids de 4, ce qui signifie qu'un zombie est connu d'au moins deux RBL différentes. Cela confirme l'idée qu'une IPs ayant un poids de 2 ou moins ne devrait pas être rejetée, car c'est probablement un faux positif.

Il est également possible d'appliquer des filtres afin d'affiner les résultats. Par exemple, si vous ne voulez voir que les PREGREET, utilisez:

$ ./postscreen_stats.py -f maillog.3 -r short -y 2011 --geofile=../geoip/GeoIPCity.dat -G -a "PREGREET"
=== unique clients/total postscreen actions ===
181/246 CONNECT
1/1 BARE NEWLINE
11/14 COMMAND COUNT LIMIT
13/16 COMMAND PIPELINING
1/1 COMMAND TIME LIMIT
161/170 DNSBL
40/42 HANGUP
12/15 NON-SMTP COMMAND
37/62 NOQUEUE 450 deep protocol test reconnection
1/1 PASS NEW
1/5 PASS OLD
181/239 PREGREET

=== clients statistics ===
4 avg. dnsbl rank
181 blocked clients
181 clients
0 reconnections

Ce que l'on voit ci-dessus, en filtrant pour n'avoir que les PREGREET, c'est qu'un zombie qui se fait prendre dans la règle de PREGREET va souvent être également marqué par le DNSBL, et parfois par quelques autres règles.

Autre exemple: si vous voulez tous les HANGUP et NON-SMTP COMMAND, utilisez le filtre -a “HANGUP|NON-SMTP COMMAND”.

Un mot sur les paramètres :

  • -f est le fichier syslog
  • -r le type de rapport
  • -y est l'année. syslog ne stocke pas l'année dans le format de la date, donc si, en 2012, vous travaillez sur des logs de 2011, il faut en informer le script.
  • –geofile est le chemin vers une base de donnée de geoip. Maxmind fournit une version d'évaluation gratuite sur leur site.
  • -G permet d'utiliser le module pygeoip plutot que GeoIP. C'est vous qui voyez, ils font la même chose.

Et cerise sur le gâteau, l'option –mapdest permet de créer un fichier HTML contenant une Google Map des zombies. C'est tout simple à utiliser:

$ ./postscreen_stats.py -f maillog.3 -r none -y 2011 --geofile=../geoip/GeoIPCity.dat -G --mapdest=postscreen_report_2012-01-15.html

Google map will be generated at postscreen_report_2012-01-15.html
using MaxMind GeoIP database from ../geoip/GeoIPCity.dat
Creating HTML map at postscreen_report_2012-01-15.html

Le fichier HTML généré peut être ouvert dans tous navigateur connecté au Net (requis par l'API de Google Maps) et affichera une carte similaire à celle ci-dessous. Cliquez sur chaque marqueur pour obtenir les détails. Vous pouvez aussi ajouter un filtre à la génération pour réduire le nombre de points.

7 En conclusion

Postscreen est élégant. Il est léger, s'intègre parfaitement devant Postfix, et permet de nettoyer le trafic de manière efficace sans consommer 15 CPU et des giga de mémoire.

En fin de compte, Wietse Venema, c'est un peu le Shaun of the Dead du mail. Et les amateurs de pâté de jambon n'ont qu'à bien se tenir car à partir de maintenant, mon serveur mail, il tire d'abord et il pose les questions après. C'est ça la puissance de Postscreen !

Une fois de plus, merci à Xavier Skapin d'avoir eu la patience de relire cet article.

fr/ressources/dossiers/postfix/postscreen.txt · Last modified: 2012/07/05 18:25 (external edit)
CC Attribution-Noncommercial-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0