Horoa – La voie est libreLa voie est libre

Exim/LDAP/TLS: histoire d'une chasse au bug

Exim est un MTA (Mail Transfer Agent) utilisé sur de nombreux systèmes de type UNIX. La première version a été écrite en 1995 par Philip Hazel pour le service informatique de l’Université de Cambridge. Basé au départ sur smail, il a largement évolué pour devenir un MTA flexible et robuste. J’ai eu l’occasion travailler par le passé sur une intégration de ce très bon MTA avec un annuaire LDAP (entre autres). Afin de résume ce travail, j’avais publié ce « howto » sur howtoforge: Setting Up A Mail Server Using Exim4, Clamav, Dovecot, SpamAssassin And Many More On Debian Lenny

Ce document étant désormais obsolète, j’ai entrepris de le réécrire en utilisant les versions actualisées des logiciels, et en apportant quelques (nécessaires) améliorations. Parmi ces améliorations: l’encryption des connections LDAP en utilisant StartTLS.
Et c’est la que les ennuis commencent!

Donc en gros, j’étais parti pour actualiser la conf, et tout se passait plutôt bien. Mais dès lors que TLS entre dans la boucle, la config (qui marchait sans problème avant ce changement) fait planter le serveur Exim dès lors que celui ci cherche à faire une requête LDAP.

L’erreur:

Au premier abord, l’erreur reportée par exim est loin d’être claire:

gave DEFER: failed to initiate TLS processing on an LDAP session to server ldap.middle.earth:389 - ldap_start_tls_s() returned -11: Connect error

Et coté serveur (openldap en debug mode) ce n’est guère mieux avec un message du type « l’erreur rencontrée est: succès »…

tls_read: want=5, got=5
0000: 30 05 02 01 02 0....
ldap_read: want=8 error=Success
5235dcba ber_get_next on fd 17 failed errno=0 (Success)
5235dcba connection_read(17): input error=-2 id=1002, closing.
5235dcba connection_closing: readying conn=1002 sd=17 for close
5235dcba connection_close: conn=1002 sd=17
5235dcba daemon: removing 17

A la rigueur, le dump réseau est encore ce qu’il y a de plus utile, pourvu que l’on dispose de la clé privée du serveur:

TLS client terminate tpc donnexion unexpectedly at the end of the TLShandshake

Il semble, aux vues du dump, que le client TLS (exim), mette fin à la connexion alors même que les deux partis semblent avoir négocié la poignée de main TLS. Le tout sans même reporter une erreur TLS, à peine un warning (pas suffisant pour faire échouer la session TLS).

Le bazar autour de GnuTLS:

Il est à noter que j’utilise Debian et (dans la mesure du possible) les paquets fournis par la distribution. Ceci est relativement important lorsque l’on parle d’ encryptions, car Debian compile la plupart de ses paquets contre GnuTLS et non contre openSSL. Ce choix est dû à une interprétation de la GPL, si j’ai bien compris. Nombre de personnes semblent s’en plaindre mais il n’y a pas de changement prévus de ce coté là.
Ainsi, à la lumière de ces nombreux bugs, je décide de recompiler ma librairie LDAP contre openSSL (et Exim dans la mesure ou ce dernier link les librairies LDAP).

Un regard plus affuté:

Malgré un stack recompilé avec OpenSSL, les lookups LDAP ne fonctionne toujours pas. Bonne nouvelle cependant la poignée de main TLS échoue (si si c’est une bonne nouvelle) de façon compréhensible.
On peut le constater en regardant un nouveau network dump.

TLS wireshark

L’erreur reportée maintenant est donc le fait d’Exim, qui rejette le certificat du serveur, car il ne peut le valider, retournant une alerte TLS: « unknow certificate ».
Etant dans un environement de test, j’utilise server-side, un certificat auto-signé et j’attends des client qu’ils ne s’en offusque pas. Cela permet aussi de ne pas avoir a deployer des certificats CA sur tous les clients, plutot precieux lors d’une phase detets. Exim est donc configuré comme suit:

ldap_default_servers = ldap.middle.earth::389
ldap_start_tls = true
ldap_require_cert = allow

Selon la documentation d’Exim et le man de ldap.conf(5), la dernière option devrait permettre aux sessions TLS d’être établies avec le serveur même si le client ne peut authentifier le serveur.
En y regardant de plus près, le debug d’Exim révèle que le MTA utilise la valeur « try » au lieu de « allow » comme spécifié dans la conf:

30430 after ldap_url_parse: host=ldap.middle.earth port=389
30430 ldap_initialize with URL ldap://ldap.middle.earth:389/
30430 initialized for LDAP (v3) server ldap.middle.earth:389
30430 LDAP_OPT_X_TLS_TRY set
30430 binding with user=uid=exim,dc=middle,dc=earth password=oops
30430 failed to initiate TLS processing on an LDAP session to server ldap.middle.earth:389 - ldap_start_tls_s() returned -11: Connect error
30430 lookup deferred: failed to initiate TLS processing on an LDAP session to server ldap.middle.earth:389 - ldap_start_tls_s() returned -11: Connect error

A ce stade, il est donc clair que quelque chose ne tourne pas rond dans la façon qu’Exim a de gérer l’établissement de sessions TLS/LDAP. Jettons un coup d’oeil au code.

The code:

A partie du debug obtenu plus haut, on identifie la partie du code qui retourne qui, semble t’il, nous intéresse. Et d’entrée il y a des choses qui me dérangent dans le fichier src/lookups/ldap.c ; on dirait que le parsing des URL LDAP surclasse les options de connexions en utilisant « LDAP_OPT_X_TLS »:

/* If not using ldapi and TLS is available, set appropriate TLS options: hard
for "ldaps" and soft otherwise. */

#ifdef LDAP_OPT_X_TLS
if (!ldapi)
  {
  int tls_option;
  if (strncmp(ludp->lud_scheme, "ldaps", 5) == 0)
    {
    tls_option = LDAP_OPT_X_TLS_HARD;
    DEBUG(D_lookup) debug_printf("LDAP_OPT_X_TLS_HARD set\n");
    }
  else
    {
    tls_option = LDAP_OPT_X_TLS_TRY;
    DEBUG(D_lookup) debug_printf("LDAP_OPT_X_TLS_TRY set\n");
    }
  ldap_set_option(ld, LDAP_OPT_X_TLS, (void *)&tls_option);
  }
#endif /* LDAP_OPT_X_TLS */

Ça ressemble fortement à une erreur d’implémentation, étant donné que cela empêche l’utilisation de certificats auto-signés pour les connexions ldaps:///, ou ldap:///+TLS. La seule alternative permise dans cette logique est d’utiliser une connexion LDAP non-chiffrée (en supposant que le serveur ne renvoie pas de certificat ou ne supporte pas TLS). Mais aucune de ces alternatives n’est satisfaisante dans mon contexte.

Plus loin dans le fichier, se trouve une section qui correspond très exactement à l’option d’Exim qui ne semble pas fonctionner: ldap_require_cert:

#ifdef LDAP_OPT_X_TLS_REQUIRE_CERT
if (eldap_require_cert != NULL)
  {
  int cert_option = LDAP_OPT_X_TLS_NEVER;
  if (Ustrcmp(eldap_require_cert, "hard") == 0)
    {
    cert_option = LDAP_OPT_X_TLS_HARD;
    }
  else if (Ustrcmp(eldap_require_cert, "demand") == 0)
    {
    cert_option = LDAP_OPT_X_TLS_DEMAND;
    }
  else if (Ustrcmp(eldap_require_cert, "allow") == 0)
    {
    cert_option = LDAP_OPT_X_TLS_ALLOW;
    }
  else if (Ustrcmp(eldap_require_cert, "try") == 0)
    {
    cert_option = LDAP_OPT_X_TLS_TRY;
    }
  ldap_set_option(ld, LDAP_OPT_X_TLS_REQUIRE_CERT, &cert_option);
  }
#endif

Dans cette dernière section, tout semble cohérent, et du coup je ne sais plus trop quelle section pause problème.
En résumé on a deux partie de code distinctes, qui semblent faire la même chose, qlors que manifestement, aucune ne fait réellement le boulot! I am lost… Lost, but not stranded yet! (Si vous reconnaissez la référence, n’hésitez à la mettre en commentaire, vous pouvez gagner… ma considération).

The patch:

Au terme de quelque heures de recherche autour des posts de la mailing list d’openldap, je finis par trouver des posts intéressants.
Pour commencer, le premier et le second post expliquent que l’option LDAP_OPT_X_TLS est utilisée en LDAPv2 pour établir des sessions ldaps:/// uniquement et a été dépréciée avec l’arrivée de LDAPv3 et StartTLS. En ce qui concerne openldap, cette option n’a donc plus d’effet et n’existe que pour assurer une retro-« compatibilité ».
Mais la vrai réponse se trouve en fait dans ce post: http://www.openldap.org/lists/openldap-software/200706/msg00173.html, l’auteur y explique que les options de sessions LDAP ne peuvent être appliquées au niveau des handle LDAP, mais doivent ere appliqués globalement en utilisant NULL comme handle LDAP (premier argument de la fonction ldap_set_option()).
A la lumière de ces informations, (et de quelques autres remarques que j’ai pu me faire), J’ai écris un petit patch que j’ai appliqué au package debian (Exim 4.80-7).
Ce patch implémente aussi les changements du bug Exim 1375, il n’est pas possible d’utiliser LDP+TLS correctement. Mon patch est dsponible ici

Une fois le patch appliqué, les lookups LDAP fonctionent enfin avec TLS!
Donc pour résumer:

  • Recompiler les librairies LDAP contre openSSL au lieu de GnuTLS.
  • Patcher Exim avec le patch fournie ci dessous
  • Recompiler Exim contre openSSL,la librairie LDAP fraichement recompilée et le patch

Un grand merci au channel IRC #Exim, et plus particulièrement à Todd Lyons (alias cannonball), qui a été d’une grande d’aide pour débuger ce problème!

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *