([!tag debian DNS]]

Introduction

Depuis quelques années déjà, j'utilise un serveur auto-hébergé qui fait à la fois des choses publiques (genre ce site web) mais également des choses privées, à disposition des machines qui se situent sur mon réseau local.

Parmi ces services privés, j'héberge un service de DNS à la fois local pour le réseau local mais également en tant que résolveur pour les autres machines. Chaque fois qu'une machine a besoin d'un nom public, elle passe par ce service DNS privé qui interroge toute la chaîne DNS avant de renvoyer la réponse aux clients.

Pourquoi faire comme ça me direz-vous ? Pourquoi ne pas utiliser simplement les serveurs DNS du fournisseur d'accès à Internet ? Ma réponse tient en plusieurs points:

  • D'abord parce que ces serveurs DNS sont parfois lents ou ont du mal à répondre. Effectivement, très souvent auparavant, j'avais de vrais de soucis avec les résolveurs DNS de Free. Tout le monde rallait à certain moments de la journée parce qu'il y avait un lag dans les réponses DNS ce qui donnait l'impression qu'Internet était lent !

  • Ensuite, pour des questions de vie privée. Avec toutes les requêtes DNS, mon FAI sait éxactement quels sont les sites que les gens de mon réseau local consultent. Ce n'est pas du tout son affaire de savoir ça. Notamment, ça ne regarde pas mon FAI de savoir que je me connecte à une plate-forme technique hyper-sécurisée du gouvernement. Le travail du FAI que je rémunère est de me fournir un accès à Internet, point. Et ne me dite pas que c'est pour prévenir des problèmes techniques: nous avons tout le loisir de faire remonter les problèmes quand ils surviennent, pas besoin de payer des gens pour faire de la surveillance préventive en scannant et analysant ce qui se passe au niveau DNS (en dehors de la supervision de base du service, bien entendu). Après, il se peut très bien que le FAI ne fasse rien de ces données mais, dans tous les cas, ce n'est pas contractuel, donc le doute est permis.

  • Enfin, c'est surtout parce que ces DNS sont souvent menteurs. Parfois c'est la justice française qui ordonne ça (une forme de censure légale) et dans ce cas, pourquoi pas. Mais c'est surtout que les DNS peuvent conduire à une forme d'abus de pouvoir privé, notamment si le FAI décide de lui même de ne pas répondre à certaines requêtes tout à fait légitimes.

  • Par ailleurs, ce n'est pas comme ça que fonctionne le DNS qui est un système complètement décentralisé: n'importe qui peut monter son propre service qui interroge les serveurs racines et rebondit ensuite sur les différents serveurs qui font autorité sur les domaines.

  • De plus, ça permet de soulager les ressources du FAI. Si tout le monde disposait de son serveur DNS dans sa machin box, il n'y aurait sans doute plus besoin de monter des serveurs monstres pour répondre aux requêtes DNS internes. Bon après, ça chargerait sans doute pas mal les serveurs racines.

  • Et pour terminer: le défi technique ! Se dire qu'on peut monter un service DNS (presque) celui d'un FAI donne l'impression qu'on sait faire des choses avec Internet. Ça peut toujours être un bon point à mettre sur un CV ou lors d'un entretien d'embauche.

Donc, depuis des années, j'utilise un service DNS interne. Pourtant le DNS évolue avec des choses comme DNSSEC, DNS over TLS et, plus récemment, DNS-over-HTTPS.

Je me suis dit qu'il fallait que je commence à m'intéresser à tout ce qui permet de chiffrer les accès clients-serveurs. Je l'ai déjà fait pour la messagerie électronique. A présent, ce sera pour le DNS avec DNS-over-TLS.

Pourquoi DNS-over-TLS et pas DNS-over-HTTPS ? Tout simplement parce qu'en 2019, DNS-over-TLS est la seule solution qui peut s'appliquer à tout un système d'exploitation et pas uniquement au navigateur web.

Cet article a pour objet de présenter l'installation d'un résolveur DNS exploitant DNS-over-TLS.

Quel serveur utiliser en 2019 ?

Traditionnellement, il existe bind dans sa version 9. Je l'ai utilisé pendant de nombreuses années. Il fait le job, reste simple à configurer et est packagé dans Debian. Néanmoins, depuis quelques années, de nombreuses distributions recommendent l'utilisation d'Unbound.

Comme Bind, il est codé en C (ce qui pour un gamin de 12 ans en 1990, signifie le graal de la puissance brute) ce qui permet de le déployer sur des petites configurations.

Par ailleurs, Unbound gère [un grand nombre de RFC sur le DNS]( https://nlnetlabs.nl/projects/unbound/rfc-compliance/).

Et pour terminer, Bind ne gère pas nativement les tunnels TLS. Pour activer DNS-Over-TLS, il faut s'appuyer sur stunnel alors qu'avec Unbound, il n'y a rien à faire.

Qu'est-ce-que nous voulons faire ?

  • Disposer d'un service de résolution DNS pour les clients du réseau local.
  • Le service ne doit pas être public (limité au réseau local).
  • Il doit être disponible en IPv6 et IPv4.
  • Il doit mettre en oeuvre DNS-over-TLS.
  • Il doit mettre en oeuvre le service DNS classique sur UDP et TCP.
  • Il doit faire en sorte de chiffrer au maximum ce qui sort du réseau local.
  • Il doit utiliser de préférence le protocole IPv6 pour les interrogations DNS.
  • Tout ce qui ne sert pas doit être éteint/fermé.

Configuration nftables

Avant de commencer, autant ouvrir les ports DNS (UDP/53 et TCP/53) et DNS-over-TLS (TCP/853) aux clients du réseau local. Mon réseau local est disponible en IPv4 et en IPv6 donc ça donne les éléments suivants:

Pour la chaîne en entrée

#!/usr/sbin/nft -h

# Configuration spécifique d'Unbound
# Ouverture des ports privés en entrée sur le réseau local
add rule inet public_server entree ip saddr 192.168.0.0/24 udp dport { 53 } accept
add rule inet public_server entree ip saddr 192.168.0.0/24 tcp dport { 53, 853 } accept
add rule inet public_server entree ip6 saddr 2a01:e0a:14:9be0::/64 udp dport { 53 } accept
add rule inet public_server entree ip6 saddr 2a01:e0a:14:9be0::/64 tcp dport { 53, 853 } accept

# Ouverture des ports privés en sortie sur le réseau local
add rule inet public_server sortie ip daddr 192.168.0.0/24 udp sport { 53 } accept
add rule inet public_server sortie ip daddr 192.168.0.0/24 tcp sport { 53, 853 } accept
add rule inet public_server sortie ip6 daddr 2a01:e0a:14:9be0::/64 udp sport { 53 } accept
add rule inet public_server sortie ip6 daddr 2a01:e0a:14:9be0::/64 tcp sport { 53, 853 } accept

N'oubliez pas non plus d'ouvrir les ports UDP/TCP/53 et TCP/853 pour que votre serveur puisse faire des requêtes DNS aux serveurs faisant autorité.

Installation de unbound

Sous Debian Buster, unbound est consituté d'au moins deux paquets:

  • unbound qui est le serveur en lui-même.
  • unbound-anchor qui est le programme permettant de récupérer les certificats DNSSEC de la racine DNS.

Ici, un simple apt-get install unbound suffit à faire le job.

Gestion des certificats Letsencrypt

Avant de commencer à configurer, n'oubliez pas la gestion des clefs de chiffrement. Si vous faîtes du DNS-Over-TLS, vous devez disposer d'un certificat en bonne et due forme. Il n'y a pas de miracle. Si vous utilisez Letsencrypt et que vous souhaitez réutiliser le certificat émis par cette AC, il vous faudra ruser un petit peu.

En effet, unbound est une application "apparmorée": apparmor gère finement quels fichiers l'application peut accéder et, bien évidemment, unbound n'a pas accès au répertoire /etc/letsencrypt.

Vous devrez donc amender la configuration apparmor d'unbound en ajoutant le contenu qui suit dans le fichier `/etc/apparmor.d/local/usr.sbin.unbound':

# Access to Let's Encrypt keys
  /etc/letsencrypt/live/votre_domaine/*.pem r,
  /etc/letsencrypt/archive/votre_domaine/*.pem r,

Ensuite, un simple systemctl restart apparmor devrait gérer les problèmes d'accès aux clefs letsencrypt.

Configuration globale d'unbound

Ce qui est pratique avec unbound c'est quon peut tout configurer dans un seul fichier ou choisir de séparer la configuration en plusieurs fichiers. C'est d'ailleurs le parti pris de Debian. Donc on va faire avec ce mode de configuration.

La configuration par défaut vous permet de bénéficier d'un résolveur DNS (un truc qui fait les requêtes DNS vers l'extérieur en passant par les serveurs racines) sur localhost.

Mais nous allons modifier cette configuration en ajoutant le fichier /etc/unbound/unbound.conf.d/resolver.conf avec le contenu suivant:

# Configuration du résolveur local
server:

  # Ports et adresses
  port: 53
  interface: 127.0.0.1
  interface: ::1
  interface: 192.168.0.4@53
  interface: 192.168.0.4@853
  interface: 2a01:e0a:14:9be0:bab:babe:bab:baba@53
  interface: 2a01:e0a:14:9be0:bab:babe:bab:baba@853
  interface-automatic: no
  outgoing-interface: 192.168.0.4
  outgoing-interface: 2a01:e0a:14:9be0:bab:babe:bab:baba
  prefer-ip6: yes
  do-ip4: yes
  do-ip6: yes
  do-udp: yes
  do-tcp: yes

  # DNS-over-TLS
  tls-service-key: "/etc/letsencrypt/live/votre_domaine/privkey.pem"
  tls-service-pem: "/etc/letsencrypt/live/votre_domaine/fullchain.pem"
  tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"
  tls-port: 853

  # Restriction de service
  access-control: 127.0.0.1 allow
  access-control: ::1 allow
  access-control: 192.168.0.0/24 allow
  access-control: 2a01:e0a:14:9be0::/64 allow

  # Configuration des logs
  verbosity: 1
  use-syslog: yes
  log-queries: yes
  log-servfail: yes

remote-control:
  control-enable: no

Un simple systemctl restart unbound devrait faire le job. Sinon journalctl est votre ami.

Quelques explications toutefois sur cette configuration. D'abord, la partie relative aux interfaces où le service unbound est disponible. Il y a en effet, de nombreuses déclarations d'interfaces. C'est essentiellement à cause de DNS-Over-TLS car Unbound a besoin qu'on lui indique sur quelles interfaces activer le port 853.

Pour le contrôle d'accès, normalement, la configuration du firewall fait que ça ne sert à rien mais j'ai préferé l'ajouter (ceinture et bretelle) au cas où la conf du firewall bouge. Par défaut tout ce qui n'est pas localhost est en mode "refused" (renvoie une réponse DNS REFUSED au client). Mettre en allow les sous-réseaux autorisés suffit à ouvrir le service aux clients des sous-réseaux.

Configuration des clients

Ok, vous êtes moderne et vous avez sytemd d'installé sur votre système. Vous vous dites donc que tout est correct et configuré correctement out-of-the-box. Mais ce ne sera pas forcément le cas et pour cette hypothèse, mieux vaut comprendre comment faire.

Systemd met à disposition un service de gestion de la résolution de noms DNS qui se nomme systemd-resolved. Ce dernier est configuré à un niveau global dans le fichier /etc/systemd/resolved.conf. Ensuite, si vous utilisez systemd-networkd, chaque carte réseau peut également embarquer sa propre configuration DNS. Néanmoins, tout ce qui sera déclaré dans resolved.conf s'appliquera à tout ce qui n'est pas déjà configuré.

systemd-resolved met à disposition 3 moyens de résolution de noms:

  • Par l'API systemd-resolved via une interface D-Bus.
  • Via l'API NSS.
  • Et enfin, via le traditionnel /etc/resolv.conf via un serveur DNS local (le stub), fourni par systemd-resolved sur le port 53 de 127.0.0.53 uniquement (pas en IPv6).

Pour résumer:

  • On déclare les serveurs DNS de base dans /etc/systemd/resolved.conf.
  • Si on a des interfaces réseaux qui utilisent des serveurs DNS spécifiques, on l'indique dans la conf networkd.
  • En utilisant nss-resolve, et un peu de conf, les applis compatibles NSS utiliseront systemd-resolved.
  • Pour tout le reste, on utilisera le stub.

Pour l'API par D-Bus, on n'a rien à faire !

Pour NSS, il faut installer le paquet libnss-systemd (mais il devrait déjà être installé de facto). Ensuite, il faut simplement modifier le fichier /etc/nsswitch.conf en ajoutant le module systemd dedans.

Pour info, NSS est une API qui vise à concentrer tout ce qui a trait aux noms qu'on peut trouver sur une machine, que ce soit des noms d'utilisateurs, de groupes, de machines, etc. Pour chaque type (on appelle ça une base de noms), on peut utiliser différents modules qui vont chercher dans des fichiers textes, sur un serveur DNS, sur un serveur LDAP les noms qu'on cherche. L'ordre des modules donne l'ordre de ce qui sera joué en termes de recherche. Par exemple, la ligne:

hosts:  files dns

indique qu'on commence par rechercher des noms de machine dans un fichier (généralement /etc/hosts) puis sur un serveur DNS.

Dans notre cas, il suffit d'ajouter le module systemd à cette ligne pour bénéficier de systemd-resolved:

hosts:  files resolve [!UNAVAIL=return] dns

On place resolve avant dns car il est possible d'utiliser un fichier resolv.conf différent de celui configuré par la méthode qui suit.

Pour le dernier cas où vos applications n'utilisent ni l'API resolved ni NSS, il vous reste à utiliser le serveur local DNS de systemd-resolved (le stub) en effectuant un lien entre le fichier de configuration dynamique généré par systemd-resolved et le traditionnel /etc/resolv.conf:

# ln -s /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

Un truc de base comme la commande host utilisera ce cas de figure.

Bien entendu, voici le contenu de votre fichier /etc/systemd/resolved.conf:

#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.
#
# Entries in this file show the compile time defaults.
# You can change settings by editing this file.
# Defaults can be restored by simply deleting this file.
#
# See resolved.conf(5) for details

[Resolve]
DNS=ipv6_de_votre_serveur_DNS ipv4_de_votre_serveur_DNS
#FallbackDNS=8.8.8.8 8.8.4.4 2001:4860:4860::8888 2001:4860:4860::8844
#Domains=
LLMNR=no
#MulticastDNS=yes
#DNSSEC=allow-downgrade
#Cache=yes
DNSStubListener=yes
DNSOverTLS=opportunistic
ReadEtcHosts=yes

A partir de cet instant, toutes vos requêtes doivent passer par votre serveur DNS via le protocole DNS-over-TLS.

Une tâche pour Ansible

Pour les gens qui utilisent Ansible pour gérer le déploiement de leurs machines, voici un fichier de tâche à insérer où vous voulez dans votre playbook:

---
# Installation et configuration de Unbound en mode résolveur
- name: Unbound installation
  apt:
    name: unbound
    install_recommends: no
    state: latest
    force_apt_get: yes
    autoclean: yes

- name: Unbound nftables configuration
  copy:
    src: nftables/unbound.nft
    dest: /etc/nftables/unbound.nft
    owner: root
    group: root
    mode: 0644

- name: Restart nftables
  systemd:
    name: nftables
    state: restarted

- name: Unbound resolver configuration
  copy:
    src: unbound/resolver.conf
    dest: /etc/unbound/unbound.conf.d/resolver.conf
    owner: root
    group: root
    mode: 0644

- name: Add unbound to ssl-cert group
  user:
    name: unbound
    append: yes
    groups: ssl-cert

- name: Improve apparmor configuration of unbound
  copy:
    src: apparmor.d/usr.sbin.unbound
    dest: /etc/apparmor.d/local/usr.sbin.unbound
    owner: root
    group: root
    mode: 0644

- name: Restart apparmor
  systemd:
    name: apparmor
    state: restarted

- name: Uninstall bind9
  apt:
    name: bind9
    install_recommends: no
    state: absent
    force_apt_get: yes
    autoclean: yes

- name: Restart unbound
  systemd:
    name: unbound
    state: restarted
...

Les fichiers mentionnés sont ceux présentés dans les paragraphes précédents.

Conclusions

Bon, voilà un sujet bien couvert. Après quelques enquêtes (apparmor est à prendre en compte très souvent) et un peu de configuration, vous devriez disposer d'un service DNS qui gère à la fois le DNS en clair et DNS-Over-TLS.

Au niveau des limites, vous comprendrez aisément qu'une machine hostile sur votre réseau local pourra quand même étudier les requêtes DNS en clair qui passent car DNS-over-TLS ne s'adresse qu'aux requêtes clients-résolveurs, pas vers des serveurs faisant autorité. Pour ce dernier point, il faudra sans doute que le protocole ADoT (Authoritative DNS over TLS) soit établi dans une RFC définitive.

Il reste d'autres choses à explorer comme DNS-over-HTTPS mais j'ai choisi de ne pas l'implémenter pour l'instant. D'abord parce que les implémentations au niveau des clients se limitent à Firefox dans mon cas, ce qui est bien mais pas suffisant. Ensuite, je pense que DNS-over-HTTPS n'apporte pas grand chose comparé à DNS-Over-TLS, c'est juste la même chose sauf qu'on tape sur le port 443. A moins d'être derrière un firewall nazi, ça ne fait pas plus que DNS-over-TLS. Enfin, les seules implémentations de serveurs DNS-over-HTTPS sont des résolveurs publics, pas les serveurs faisant autorité et ça ne viendra pas plus combler ce manque au niveau du chiffrement du DNS.

En attendant, vous avez quand même de quoi sécuriser plus globalement le flux entre vos clients et votre résolveur interne, ce qui est déjà pas si mal à votre niveau.

Posted mer. 28 août 2019 21:21:21

Introduction

Cela fait maintenant de nombreuses années que je gère ma propre plate-forme publique Internet et par la même occasion ma propre plate-forme de courrier électronique. D'une manière générale, je suis assez content de ce que j'ai mis en place.

C'est souvent le cas sur le courrier électronique, au moins sur la partie serveur. Les logiciels sont "vieux" matures, ils ont fait leur preuve. Les protocoles évoluent peu. Une fois que la chaîne est en place, à part suivre les correctifs de sécurité et les évolutions majeures des logiciels serveurs, il n'y a pas grand chose à faire et c'est tant mieux.

Ceci dit, le courrier électronique continue à évoluer dans le temps, notamment à l'initiative du renforcement de la sécurité et de la protection de la vie privée. C'est le cas avec la RFC8314 qui vient un peu bousculer la manière de gérer une plate-forme de courrier électronique. Cela faisait quelques temps que je l'avais vue passer et, après avoir trouvé un peu de temps et un prétexte pour revoir la configuration de mon serveur public, j'ai essayé de la mettre en oeuvre.

Cet article a pour but d'illustrer les principaux changements que j'ai intégré sur ma plate-forme de courrier électronique pour me conformer du mieux que possible à la RFC8314.

Qu'est-ce-qu'il y a dans cette RFC ?

Dans les faits, la RFC est plutôt courte en contenus spécifiques. Pour résumer sérieusement, elle indique qu'il faut faire en sorte de chiffrer au maximum tous les canaux d'accès clients à un serveur SMTP (un MTA).

Pour rappel un MTA peut faire grosso modo deux choses: - Récupérer des courriers électroniques envoyés par un autre MTA pour un des clients de notre plate-forme. - Envoyer, pour le compte des clients de la plate-forme, des courriers électroniques à d'autres MTA.

La RFC8314 s'attaque au deuxième point. Elle stipule qu'il est maintenant temps de passer au tout chiffré pour la communication entre tous les composants serveurs de la plate-forme (MTA/MDA comme Dovecot) et les clients. Pour ce faire, deux ports réseaux sont dédiés à ces communications:

  • le port 465 pour le protocole SMTPS.
  • le port 993 pour le protocole IMAPS.

Les accès non chiffrés sont à proscrire ainsi que les accès en STARTTLS. Cette dernière technique permettait d'initier une communication chiffrée à partir d'un port en clair. Le début de la conversation réseau était néamoins en clair. C'est ce que j'avais mis en place sur ma plate-forme de courrier électronique car c'était plus simple pour moi d'avoir un seul port pour chaque service (143 pour IMAP+STARTTLS et 25 pour SMTP+STARTTLS).

De plus, cette RFC lève une grosse ambiguité sur le port SMTP à utiliser pour les clients. Jusque là, on avait le choix entre le port 25 (qui sert également pour le traffic MTA à MTA), le port 587 (pour les clients en clair et en STARTTLS) et le port 465 pour l'accès complet TLS. Ce dernier n'était qu'un standard de fait, pas un truc obligatoire et, à une époque donnée, déconseillé. Le port IANA 465 a méme été affecté à un autre protocole même si de nombreux administrateurs de MTA continuaient à l'utiliser pour leurs clients.

Pour le coup, la RFC clarifie vraiment les choses. Néanmoins, elle permet de conserver certaines anciennes configurations, histoire de gagner du temps. Néanmoins, ce n'est pas l'approche que j'ai retenue. En effet, tant qu'à faire de faire une modification de configuration, autant y aller franco et se mettre dans les clous tout de suite. De plus, comme j'ai très peu de clients à gérer, la migration est assez simple.

La RFC8314 ne s'adresse qu'aux communications clients MTA et pas à celles entre MTA car, si en règle générale, un administrateur de MTA peut avoir un controle sur les clients de sa plate-forme, ce n'est pas vrai du tout pour les autres MTA. Certains peuvent très bien ne pas savoir utiliser de canal chiffré, ou ne pas vouloir (enfin en 2019, cette affirmation tient plus de la paresse que de l'écueil technique).

Le traffic de MTA continuera donc à se faire sur le port 25, que ce soit en clair ou en STARTTLS.

Résumé de ce que nous devons faire

  • Créer une conf dans Exim pour ouvrir le port 465 en TLS v1.2.
  • Interdire la soumission authentifiée des clients sur le port 25 (y compris pour STARTTLS) tout en maintenant l'accès pour les autres MTA.
  • Modifier la conf de Dovecot pour ouvrir le port imaps et fermer le port imap en clair (TLS sur demande).
  • Ouvrir le firewall sur les nouveaux ports.
  • Publier les annonces de service dans le DNS.

Ouverture des ports au niveau du pare-feu

Nous allons commencer avec le pare-feu. En effet, ça ne sert à rien de configurer un truc si on ne peut pas le tester. Pour ma part, j'ai juste ajouté les ports TCP 465 et 993 et retiré le port TCP 143 étant donné que je vais supprimer l'accès IMAP en clair.

Voici mon fichier de configuration de nftables (/etc/nftables.conf):

#!/usr/sbin/nft -f

# Configuration NFT de serveur

# On vide toutes les règles
flush ruleset

# Table pour un serveur public un peu fermé
table inet public_server {
  # Chaîne de gestion de tout ce qui rentre
  chain entree {
    # Chaîne de type filtre, en entrée
    type filter hook input priority 0;

    # Accepter tout ce qui vient de l'interface loopback
    meta iif lo accept

    # Accepte le trafic qui vient de la machine
    ct state established,related accept

    # Réponse au ping IPv4
    ip protocol icmp accept

    # OUverture de i2pd
    meta skuid i2pd accept

    # Ouverture des ports publics
    tcp dport { 25,53,80,443,465,993 } accept
    udp dport { 53 } accept

    # Compter et journaliser ce qui est rejeté
    counter log prefix "Connexion entrante rejetée: " level warn drop
  }

  # Chaîne de gestion de tout ce qui sort
  chain sortie {
    type filter hook output priority 0;

    # On accepte les sorties vers l'interface loopback
    meta oif lo accept

    # Réponse au ping IPv4
    ip protocol icmp accept

    # Ouverture pour i2pd
    meta skuid i2pd accept

    # Ouverture des ports publics
    tcp sport { 25,53,80,443,465,993  } accept
    udp sport { 53 } accept

    # Ouverture des ports externes classiques utilisés par le système
    tcp dport { 25,53,80,143,443,465,993,995 } accept
    udp dport { 53 } accept

    # count and drop any other traffic
    counter log flags skuid prefix "Connexion sortante rejetée: "
    level warn drop
    }
}

Je vous invite à le mettre en service au moment où vous terminez les configurations des différents logiciels serveurs histoire de minimiser l'arrêt de service (lancé en l'état, le port 143 peut encore être utilisé par des clients).

Configuration d'Exim4

Introduction

Pour comprendre ce qu'il faut faire, mieux vaut d'abord lire la documentation à ce sujet.

Autre point à noter au cas où: la version d'Exim4. Pour ma part, j'utilise le backport de Debian Stretch, soit la version 4.92-8 qui est la même que celle de Debian Buster (la nouvelle distribution stable de Debian au moment de la rédaction de cet article). Je peux donc utiliser la documentation du dessus.

Autre chose à savoir, Exim4 sous Debian utilise GNUTls et non openssl. Cela a une incidence sur la configuration.

Enfin, comme si cela n'était pas déjà assez compliqué, Debian a une manière particulière de configurer Exim4. Pour ma part, j'ai choisi la configuration éclatée. Ce qui suit concerne ce type de configuration. Néanmoins, vous pouvez vous en inspirer pour faire vos modifications.

Préparation d'un fichier de paramètres Diffie-Hellman digne de ce nom

Les bonnes pratiques de sécurité recommendent d'utiliser une meilleure taille de clef dans les échanges Diffie-Hellman. En général, la taille des échanges de clef est limitée à 2048. Pour ma part, je préfère la monter à 4096 octets grâce à la commande suivante:

$ openssl dhparam 4096 > dh.pem

Attention, la génération de ce fichier prend pratiquement 1h sur ma station de travail (de 2012).

Ensuite, vous pouvez utiliser ce fichier dans Exim4 (/etc/exim4/dh.pem) et également dans Dovecot (remplacer /etc/dovecot/dh.pem). Ce fichier peut être public.

Configuration TLS d'Exim4

La première des choses, c'est de modifier la configuration principale d'Exim (le main). Sous Debian, on trouve essentiellement conf.d/main/03_exim4-config_tlsoptions pour gérer ces éléments de configuration.

### main/03_exim4-config_tlsoptions
#################################

# TLS/SSL configuration for exim as an SMTP server.
# See /usr/share/doc/exim4-base/README.Debian.gz for explanations.

.ifdef MAIN_TLS_ENABLE
# On affiche qu'on souhaite avertir tout le monde qu'on fait du TLS.
.ifndef MAIN_TLS_ADVERTISE_HOSTS
MAIN_TLS_ADVERTISE_HOSTS = *
.endif
tls_advertise_hosts = MAIN_TLS_ADVERTISE_HOSTS

# Configuration des clefs/certificats TLS
.ifdef MAIN_TLS_CERTKEY
tls_certificate = MAIN_TLS_CERTKEY
.else
.ifndef MAIN_TLS_CERTIFICATE
MAIN_TLS_CERTIFICATE = CONFDIR/exim.crt
.endif
tls_certificate = MAIN_TLS_CERTIFICATE

.ifndef MAIN_TLS_PRIVATEKEY
MAIN_TLS_PRIVATEKEY = CONFDIR/exim.key
.endif
tls_privatekey = MAIN_TLS_PRIVATEKEY
.endif

# On indique le fichier de paramétrage de Diffie-Hellman
.ifdef _HAVE_GNUTLS
tls_dhparam = /etc/exim4/dh.pem
tls_dh_max_bits = 4096
.endif

# On ajoute le port submissions
tls_on_connect_ports = 465

# On active le port submissions en plus du smtp simple
daemon_smtp_ports = 25 : 465

.else
# Si l'option MAIN_TLS_ENABLE n'est pas activée, on n'affiche rien.
tls_advertise_hosts =
.endif

Une fois rédigé, il reste à renseigner les valeurs des macros sus-citées (ce qui est en majuscule). Pour ma part, je place tout dans conf.d/main/000_localmacros:

# on active TLS:
MAIN_TLS_ENABLE = true

# Emplacements des fichiers de clef pour TLS:
MAIN_TLS_CERTIFICATE = /etc/exim4/keys/medspx.fr.cert.pem
MAIN_TLS_PRIVATEKEY = /etc/exim4/keys/medspx.fr.pkey.pem

Les fichiers de clefs mentionnés ci-dessus sont des symlinks vers une configuration Let's Encrypt.

Gestion de l'authentification

Pour ma part, je n'ai pas eu grand chose à faire si ce n'est relire ma conf existante et considérer qu'elle était suffisante, à un petit détail près. En effet, pour l'authentification, je n'ai activé que le pilote d'authentification PLAIN. Depuis le début, je fais du STARTTLS et j'ai bien sûr indiqué que la commande SMTP AUTH ne serait présentée (et activée) uniquement lorsque la communication est chiffrée (par STARTTLS ou par TLS en direct).

Néanmoins, je souhaite aussi désactiver l'authentification en dehors du port dédié à ça (le port 465). Tout ce que j'ai trouvé de bon est d'utiliser la directive server_advertise_condition et de la faire tester que le port utilisé sur le serveur est le port 465. En pratique, ça donne ces quelques lignes dans le fichier conf.d/auth/20_exim4-config_virtual:

### auth/20_exim4-config_virtual
#################################

# Configuration de l'authentification avec un compte virtuel

plain_server:
  driver = plaintext
  public_name = PLAIN
  server_condition = "${if crypteq{$auth3}{\\\{md5\\\}${extract{1}{:}{${lookup{$auth2}lsearch{CONFDIR/passwd}{$value}{fail}}}}}{1}{0}}"
  server_set_id = $auth2
  server_prompts = :
  server_advertise_condition = ${if eq{$tls_in_cipher}{} {}{${if eq{$received_port}{465} {*}{}}} }

L'astuce consiste à indiquer dans la partie de vérification de la présentation de l'option AUTH une double condition: si la connexion est chiffrée (par TLS ou STARTTLS), alors on regarde si le port utilisé est 465, dans ce cas, on renvoie '*', dans les 2 autres cas, on ne renvoie rien, ce qui a pour effet de ne pas présenter l'option AUTH et donc d'invalider toute tentative d'authentification.

On aurait pu désactiver complètement STARTTLS sur le serveur mais c'est un mécanisme de chiffrement utilisé aussi par les serveurs MTA qui vous relaient un message (et sur lesquels, vous avez peu d'impact).

Si vous lisez la variable server_condition, vous vous rendrez compte que ma méthode de gestion des comptes authentifiés est pourrie (mais j'assume). En effet, le fichier d'authentification se nomme /etc/exim4/passwd et il contient un truc du genre:

nom_du_compte:hash_md5_du_mot_de_passe

Pour générer le hash MD5 du mot de passe, il suffit de faire:

$ echo -n the_harsh_password | md5sum

C'est la méthode de stockage de mots de passe que j'utilise depuis longtemps et elle a fait ses preuves, notamment parce qu'elle se passe de tout lien entre le compte utilisé dans Exim et le compte de l'utilisateur sur le système. Ok, MD5 en 2019... Oui, j'avais la flemme de changer tous les comptes !

Remise en service

Un simple systemctl restart exim4 devrait suffire. Pour vérifier que le service est fonctionnel sur le bon port, vous pouvez utiliser ss:

# ss -tlp

Configuration de Dovecot

Planification

Maintenant, passons à Dovecot. Nous voulons supprimer l'écoute sur le port 143, activer l'écoute sur le port 993 (imaps) et également indiquer d'utiliser TLSv1.2 ainsi que l'utilisation de Diffie-Hellman un peu partout.

Pour ma part, j'utilise la version 2.3 de Dovecot (il y a eu des évolutions entre la 2.2 et la 2.3 sur TLS notamment).

Gestion des ports

Pour cette partie, il suffit d'éditer le fichier /etc/dovecot/conf.d/10-master.conf:

# On désactive le protocole IMAP sur le port réservé
service imap-login {
  inet_listener imap {
    port = 0
  }

# On active le service IMAPS
  inet_listener imaps {
    port = 993
    ssl = yes
  }

  # Nombre de connexions clientes simultanées max avant kill du processus.
  service_count = 10
}

# On désactive complètement POP3
service pop3-login {
  inet_listener pop3 {
    port = 0
  }
  inet_listener pop3s {
    port = 0
    #ssl = yes
  }
}

Implémentation de TLSv1.2

Tout se passe dans le fichier /etc/dovecot/conf.d/10-ssl.conf:

##
## SSL settings
##

# On force l'utilisation de TLS par défaut
ssl = required

# Le chemin vers les clefs/certificats pour le chiffrement
ssl_cert = </etc/letsencrypt/live/medspx.fr/fullchain.pem
ssl_key = </etc/letsencrypt/live/medspx.fr/privkey.pem

# Paramètres Diffie-Hellman
ssl_dh = </etc/dovecot/dh.pem

# Version minimum de TLS: v1.2
ssl_min_protocol = TLSv1.2

# Liste des ciphers autorisés
ssl_cipher_list = ALL:!kRSA:!SRP:!kDHd:!DSS:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK:!RC4:!ADH:!LOW@STRENGTH

# On indique qu'il faut   suivre l'ordre des ciphers du serveur
ssl_prefer_server_ciphers = yes

# On supprime la compression TLS (qui peut poser problème)
ssl_options = no_compression

Remise en service

Un simple systemctl restart dovecot devrait suffire. Pour vérifier que le service est fonctionnel sur le bon port, vous pouvez utiliser ss:

# ss -tlp

Annonces DNS

Il reste à faire les annonces de services sur le DNS. Pour ma part, je n'ai eu à ajouter que peu de choses: les deux lignes qui suivent...

_imaps._tcp 84600 IN SRV 0 5 993 medspx.fr.
_submissions._tcp 84600 IN SRV 0 5 465 medspx.fr.

La première concerne imaps sur le port 993, la seconde le service de soumission client mail sur le port 465. Pour ce dernier, le nom du service est submissions avec un s pour indiquer que ce service est en TLS direct.

Pensez également à supprimer les anciennes déclarations des ports qui ne sont maintenant plus accessibles.

Configuration des clients

msmtp

J'utilise mstmp pour faire remonter les messages de courrier électroniques des serveurs vers mon compte (humain) afin d'avoir des informations en cas de problème.

Voici un fichier de configuration global pour msmtp (/etc/msmtprc):

# Configuration par défaut
defaults
aliases /etc/aliases
auth on
tls on
tls_starttls off

# Définition du compte
account medspx.fr
domain medspx.fr
host medspx.fr
port 465
from jerk@medspx.fr
user jerk
password password

# Activation du compte par défaut
account default : medspx.fr

La phrase importante ici c'est tls_starttls. En effet, par défaut, lorsqu'on active TLS dans msmtp, celui-ci essaye de faire du STARTTLS. Or, nous avons du TLS implicite (par défaut). Il faut donc désactiver STARTTLS et c'est ce que fait la variable tls_starttls à off.

mutt

Pour mutt, il suffit de remplacer les url du type imap: par imaps: et smtp par smtps, pardi !

Voici un exemple de configuration (~/.muttrc):

set smtp_url = "smtps://mederic.ribreux@medspx.fr/"
set spoolfile = "imaps://medspx.fr/INBOX"
set folder = "imaps://medspx.fr/"

Attention, mutt utilise la libsasl2 pour gérer l'authentification SMTP (et bizarrement, pas pour IMAP). Or, par défaut cette dernière a besoin de modules pour gérer l'authentification en mode PLAIN. Vous devez donc installer le paquet libsasl2-modules.

Thunderbird

De ce côté, rien à signaler, il suffit de modifier les protocoles STARTTLS en SSL/TLS purs. Ça marche, y compris pour la version ESR de thunderbird !

Roundcube

J'utilise Roundcube et j'ai dû modifier l'URL de connexion pour la partie IMAP. Néanmoins, ce n'est pas suffisant. En effet, je souhaite que le traffic entre Roundcube et le serveur IMAP se fasse sur localhost et non en sortant sur le réseau. Le problème de passer par localhost, c'est que le certificat qui est utilisé par Dovecot prend en compte un CN qui est le FQDN du serveur et non localhost.

Pour contourner le problème, on peut utiliser la directive de configuration 'imap_conn_options':

$config['default_host'] = 'imaps://localhost:993';
$config['imap_conn_options'] = array(
  'ssl' => array(
      'peer_name'  => 'FQDN of server'
  ),
);

Pareil pour la partie SMTP:

$config['smtp_server'] = 'ssl://localhost';
$config['smtp_conn_options'] = array(
  'ssl' => array(
        'peer_name'  => 'FQDN of server'
  ),
);

$config['smtp_port'] = 465;

Conclusions

Ouf, c'est fini et ça semble fonctionner. Encore une fois, Exim4 est riche de fonctionnalités mais reste complexe à configurer. J'ai dû passer environ 2ou 3h de recherches pour vraiment comprendre et trouver comment faire. Et encore, je n'ai pas touché à la gestion des algos de chiffrement pour TLSv1.2. En même temps, je ne fais pas ça tous les jours...

Du côté de Dovecot, ça se passe plus facilement. Seule la génération de la conf TLS prend un peu de temps pour comprendre.

Du côté de la configuration des clients, j'ai eu aussi quelques suprises, notamment pour mutt (à cause du SASL) et pour Roundcube (ce qui m'a valu quelques bannissement par fail2ban).

Ce RFC fait quand même du bien au sysadmins de la chaîne de courrier électronique. En effet, il clarifie avec précision ce qu'il faut désormais faire. Auparavant, il subsistait un problème avec le standard de fait (donc pas un standard) du port 465. J'ai toujours refusé de le mettre en place sur ma plate-forme de courrier électronique à cause de ce manque de clarification.

Maintenant, tout est (à peu près) correct et je suis reparti pour 2 ou 3 ans de paix avec mon courrier électronique qui arrive à destination et mes clients qui communiquent maintenant uniquement sur canal chiffré.

Références

Posted mar. 16 juil. 2019 23:34:57 Tags:

Il y a quelques semaines, j'ai commencé à remarquer un phénomène étrange sur ma station de travail personnelle. A chaque démarrage à froid, le BIOS interrompait la séquence de boot en m'indiquant que des paramètres internes avaient été modifiés. J'étais alors obligé d'aller dans le BIOS, de choisir manuellement le périphérique de boot et de lancer la procédure de démarrage avant de retomber sur mon écran de Grub préferré.

Au début, je n'y ai pas trop prêté attention mais, à la longue, ça commençait à devenir pénible car j'étais sollicité à chaque démarrage. Avec le temps, j'ai essayé de corriger le problème en refaisant le tour de la configuration du BIOS. Il devait démarrer par le disque SSD qui est le périphérique de démarrage attitré.

Mais à chaque fois que je débranchait le PC (chaque fois que je l'éteinds en fait), aucun de mes paramètres n'étaient conservés. J'ai commencé à me poser d'autres questions. Encore un coup de UEFI ou de SecureBoot ? Ou alors, quelqu'un a essayé de m'implanter un module pirate ? Peut-être les chinois du FBI ?

Et puis, un détail m'a frappé, comme une évidence: l'ordinateur remettait à zéro la date du jour. A chaque démarrage de la machine, la date du jour était le 01/01/2009. Au début, je ne voyais rien car, après démarrage, cette machine utilise le protocole NTP pour synchroniser son horloge. Alors, j'ai pris mon courage à deux mains pour déterrer cette tour, enlever la poussière qui s'y était accumulée depuis plusieurs années, démonter la carte graphique pour pouvoir avoir accès à ce que je pensais être le coupable idéal: la pile de l'horloge RTC.

Voici le voltage que j'ai pu mesurer à ses bornes:

Batterie RTC HS

Verdict: à 39.6 mV, la pile est déchargée. Par bonheur, j'en avais une autre qui traînait dans mon stock et qui affichait la valeur correcte d'un peu plus de 3V. J'ai donc fait l'échange et mon problème a été résolu (attention à remettre la pile dans le bon sens, sinon ça ne marche pas).

Que retirer de cette histoire ? C'est la première fois que je vois une batterie CR2032 HS depuis le temps que je possède des ordinateurs. Ça ne m'était jamais arrivé auparavant. Ça signifie que je possède cette tour depuis longtemps. Effectivement, je l'ai achetée en 2012, ce qui fait près de 7 ans au moment de la rédaction de cet article.

Pour autant, je crois que je n'ai jamais gardé une machine aussi longtemps. C'est la machine la plus puissante de mon parc et je sais que je n'ai pas encore atteint ses limites. Je m'en sert très régulièrement pour tout un tas de travaux qui vont de compilations lourdes à de l'encodage vidéo. Et je trouve qu'elle a toujours du répondant. Serait-ce le signe que l'amélioration des performances des ordinateurs commence à stagner ? Qu'on a enfin atteint les limites ? Que les logiciels sont enfin moins consommateurs de ressources ? Que mon utilisation personnelle de cette machine est en phase avec ses capacités ? Je n'en sais rien...

... mais, sans nul doute, c'est un signe !

Posted lun. 15 juil. 2019 18:39:45 Tags:

Introduction

Si vous avez lu ma page dédiée à mon infrastructure de sauvegarde, vous savez que j'utilise Borg et que mon installation est particulière.

En effet, le serveur de sauvegarde n'est pas relié directement au réseau local (qui est en Wifi chez moi) mais relié en ethernet à une station de travail principale. Cette dernière héberge le contenu le plus volumineux et relier le serveur de sauvegarde directement par un câble réseau me permet de disposer d'un débit de l'ordre du Gigabits/seconde soit près de 125Mo/seconde. Le Wifi ne monterait qu'à 54MBits/s soit 6,5Mo/seconde, près de 20 fois plus lent.

Donc, ma station de travail qui est également reliée au Wifi sert de passerelle Wifi au serveur de sauvegarde. Dans un sens j'ai le meilleur des deux mondes car les clients à sauvegarder du réseau local peuvent tous attaquer le serveur local via la station de travail.

Néanmoins depuis près d'un an, je loue un serveur dans le "cloud" et j'ai besoin de récupérer une partie de son contenu pour faire une sauvegarde. Or, il est difficile pour ce serveur de franchir les 2 NAT.

J'ai testé l'utilisation de SSHFS et je dois dire que, même si ça marchait, je n'étais pas du tout satisfait des performances.

Au final, après avoir déployé Wireguard sur toutes mes machines, je me suis demandé s'il était simple de passer par le VPN pour faire la sauvegarde du client "cloud". Cet article montre un élément d'explication de la méthode que j'ai employée avec succès.

Un rappel du schéma d'infrastructure de backup

Voici un petit schéma de comment le tout est organisé:

                                                        __   _
                                                      _(  )_( )_
                                     Wi-Fi           (_   _    _)
        +--------+                   .....             (_) (__)
        |        |                  .......            Internet
        |        |                 .  ...  .            ^   ^
        +--------+                     .                |   |
        +--------+                  +------+            |   |
          Laptop ^ - - - - - - - - >|      |<-----------+   |
                                    +------+                |
                                     Router                 | 
                                       ^                    |
                                       |                    |
                                                            |
                                       |                    |
 +-------+        +-------+                            +----+--+
 |       |  Eth   |       |            |               |       |
 |       +<------>+       +< - - - - - +               |       |
 |       |        |       |                            |       |
 |       |        |       |                            |       |
 +-------+        +-------+                            +-------+
BackupServer      WorkStation                         CloudServer
  • Workstation fait du NAT pour BackupServer.
  • Router fait du NAT pour le réseau local vers Internet.

Configuration du VPN wireguard

Vous allez me dire que Wireguard pour écrire derrière un double NAT c'est un peu overkilling. Vous n'aurez sans doute pas tort mais, à bien y réfléchir, ce n'est pas si compliqué à faire et, dans tous les cas, cela demande bien moins de compétences réseau de bas niveau que d'autres solutions.

Ce qui est bien dans Wireguard, c'est qu'il suffit de peu pour faire en sorte que les clients puissent se parler les uns les autres et ce, peu importe le réseau qui est en dessous. A partir du moment où un client arrive à sortir sur Internet, il fait partie du VPN.

Le principe que j'ai suivi est donné dans l'exemple de passerelle Wireguard que j'ai trouvé sur la mailing list du logiciel.

Globalement il consiste à disposer d'une machine qui servira de passerelle à l'ensemble du VPN. Cette dernière connaît tous les clients potentiels et elle dispose de toutes les clefs publiques des clients du VPN. Chaque client ne connaît que la passerelle et accepte tout ce qui vient du sous-réseau du VPN (la variable AllowedIPs de la passerelle est le sous-réseau Wireguard). De cette manière, la passerelle peut transmettre n'importe quel paquet venu d'un client A vers un client B.

Bien entendu, la passerelle sait faire de l'IP Forwarding.

Dans mon cas, la passerelle est le serveur "Cloud". De cette manière, elle peut facilement atteindre le serveur de sauvegarde via le VPN car Wireguard traverse tous les NAT. L'intérêt de passer par l'interface réseau du VPN (wg0) est que borg ou ssh peuvent l'utiliser directement pour atteindre la machine cible sans avoir à se préocupper de la gestion réseau derrière.

Configuration nftables fonctionnelle

Mais avant de pouvoir faire passer Wireguard, il faut encore configurer comme il se doit la station de travail et notamment activer le NAT de manière sécurisée. C'est sur ce point que j'ai dû bataillé ferme et que j'ai pris le temps de comprendre comment ça fonctionnait.

Globalement, il faut:

  • Activer l'ip forwarding au niveau de la station de travail.
  • Mettre en place du NAT entre le serveur de sauvegarde et la station de travail vers le LAN et Internet.
  • Faire en sorte que la station de travail puisse faire partie du VPN.

L'IP forwarding s'active en indiquant la ligne suivante dans /etc/systctl.conf:

net.ipv4.ip_forward=1

Pour le reste, j'ai utilisé nftables qui est l'outil de configuration de netfilter recommendé dans Debian. Voici mon fichier de configuration que j'ai détaillé:

#!/usr/sbin/nft -f

# On vide la table
flush ruleset

# Gestion de la table ip (v4)
table ip filter {
  # On scanne ce qui passe en entrée
  chain input {
    type filter hook input priority 0; policy drop;

    # On autorise les connexions est déjà acceptées
    ct state {established, related} accept

    # On jete ce qui est invalide
    ct state invalid drop

    # On accepte ce qui vient de loopback
    iifname lo accept

    # On accepte ce qui vient de Wireguard
    iifname wg0 accept

    # On accepte le trafic qui vient de la carte Ethernet
    # C'est tout ce qui vient du serveur de sauvegarde.
    iifname eno1 accept

    # On permet le ping
    ip protocol icmp accept comment "Accept all icmp types"

    # On accepte tout ce qui vient du réseau local
    ip saddr 192.168.0.0/24 accept

    # On rejete le reste    
    counter log prefix "Rejecting input " level warn reject
  }

  # Chaîne qui sert à faire le forwarding
  chain forward {
    type filter hook forward priority 0; policy accept;
    # Règles d'acceptation du forward
    ## ça vient du réseau ethernet et ça veut sortir sur le wifi
    iifname "eno1" oifname "wlp6s0" accept

    ## ça vient du réseau Wifi et ça veut rentrer sur ethernet
    ## ON accepte ce qui est en relation ou établi et ce qui vient
    ## du LAN Wifi.
    iifname "wlp6s0" oifname "eno1" ct state established accept
    iifname "wlp6s0" oifname "eno1" ct state related accept
    iifname "wlp6s0" oifname "eno1" ip saddr 192.168.0.0/24 accept

    ## Sinon, on jette
    iifname "wlp6s0" oifname "eno1" drop
    counter log prefix "rejecting forward shit " level warn
  }

  # Chaîne de sortie (non filtrée)
  chain output {
    type filter hook output priority 0; policy accept;
  }

}

# Gestion du NAT via nftables
table ip nat {
  # Globalement, on accepte tout
  chain prerouting {
    type nat hook prerouting priority 0; policy accept;
  }

  chain input {
    type nat hook input priority 0; policy accept;
  }

  chain output {
    type nat hook output priority 0; policy accept;
  }
  # Mais au moment du postrouting, on active le masquerade
  # sur l'interface Wi-fi de la station de travail
  chain postrouting {
    type nat hook postrouting priority 0; policy accept;
    oifname "wlp6s0" masquerade
    # Et on compte les paquets pour voir ce qui passe.
    counter log prefix "nat postrouting " level warn
  }
}

Configuration du client à sauvegarder dans le cloud

Enfin, pour le client final, il reste à configurer certains points:

  • borgmatic.
  • la clef SSH vers le dépôt Borg.

Mais tout est déjà décrit dans la documentation générale.

Conclusions

Voilà, grâce à ce dispositif, je peux utiliser borgbackup sur un client dans le "cloud" qui peut écrire directement sur mon serveur de sauvegarde, situé derrière 2 réseaux IP nattés. Les performances sont bonnes et grâce à la déduplication et au fait que le client conserve les blocs de fichiers en cache, je suis passé d'une sauvegarde de près d'une heure via SSHFS à quelques secondes via Wireguard. C'est vraiment impressionnant !

En conclusion, Wireguard c'est bien, mangez-en...

Posted mar. 26 févr. 2019 10:20:45 Tags:

Introduction

Cela fait quelques temps que j'ai vu passer le logiciel Wireguard. Étant donné qu'il est créé et maintenu par zx2c4, dont j'utilise l'excellent cgit, je me suis dit que ça valait le coup d'y passer un temps certain.

Qu'est-ce-que Wireguard ? Pour faire simple, c'est un VPN. Il est écrit en C avec quelques lignes (5000) de code et semble prometteur pour l'avenir, tant du point de vue de la sécurité que des performances.

Dans cet article, nous allons simplement installer et configurer Wireguard sur une série de machines qui formeront notre VPN.

Installer Wireguard sur une machine Debian Buster

Wireguard est composé de deux éléments:

  • un module pour le noyau Linux.
  • un ensemble d'outils en espace utilisateur.

Sous une machine Debian classique, on peut utiliser la dernière version de Wireguard en installant:

# apt install wireguard-dkms wireguard-tools

Normalement, ces actions devraient vous conduire à installer de quoi compiler le module noyau en live (DKMS), c'est à dire le compilateur gcc en version 7 ainsi que les fichiers d'en-tête du noyau Linux. Sur une station de travail, ce n'est pas si lourd.

Installer WireGuard sur un Scaleway C1

J'ai un serveur Scaleway C1. Comme pour n'importe quel fournisseur de serveur dédié, vous ne pouvez pas forcément faire ce que vous voulez. Hé oui, ce n'est pas votre matériel. Forcément, ça s'en ressent aussi dans la partie logicielle.

Je me suis inspiré de ce document. Mais j'ai du fortement améliorer mes compétences en gestion de noyau pour faire fonctionner réellement Wireguard sur cette machine. Car, il faut bien l'avouer, ma dernière compilation de kernel Linux doit dater d'il y a au moins 10 ans.

Si vous suivez la documentation sus-hyperliée, vous n'obtiendrez rien de satisfaisant. En effet, il existe des petites subtilitées qui font que ça ne fonctionnera pas directement.

Le premier problème auquel j'ai dû me confronter est le fait que ma compilation de noyau ne donnait rien d'utilisable en termes de modules. J'ai même réussi à bloater une partie du C1 pendant quelques jours avec des modules qui ne se chargeaient plus. J'ai finalement pris le parti d'essayer de comprendre quelques options, notamment celle de CONFIG_MODULE_SRCVERSION

Ensuite, après avoir réglé ce premier problème et réussi à charger mes modules compilés avec modprobe, j'ai eu beaucoup de mal pour faire fonctionner le module Wireguard qui ne se chargeait tout simplement pas. Après quelques jours de repos (quand un truc ne marche pas, il faut le laisser se reposer, l'éviter pendant un temps. Quand on y revient, c'est souvent la voie du succès alors), je me suis rendu compte que le CPU/SOC du Scaleway C1 est un Marvell Armada 370 et que ce dernier ne gère pas les extensions NEON.

Malheureusement, Wireguard impose d'utiliser les extensions NEON dans le noyau si vous avez un CPU sous ARMv7, ce qui est le cas du CPU du Scaleway C1. J'ai donc dû modifier le fichier Kconfig de Wireguard pour modifier la ligne:

select KERNEL_MODE_NEON if CPU_V7

en

select KERNEL_MODE_NEON if (CPU_V7 && !MACH_ARMADA_370)

et le tour était joué. Je ne pensais pas devoir aller si loin mais j'avoue que quand j'ai réussi à faire un modprobe wireguard sans aucun message d'erreur, j'ai jubilé !

J'en remet le contenu à ma sauce:

# On récupère les éléments de version du Kernel de la machine
arch="$(uname -m)"
release="$(uname -r)"
upstream="${release%%-*}"
local="${release#*-}"
 
# On récupère les sources
mkdir -p /usr/src
wget -O "/usr/src/linux-${upstream}.tar.xz" "https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-${upstream}.tar.xz"
tar xf "/usr/src/linux-${upstream}.tar.xz" -C /usr/src/
ln -fns "/usr/src/linux-${upstream}" /usr/src/linux
ln -fns "/usr/src/linux-${upstream}" "/lib/modules/${release}/build"
 
# On récupère la configuration du kernel utilisé
zcat /proc/config.gz > /usr/src/linux/.config
printf 'CONFIG_LOCALVERSION="%s"\nCONFIG_CROSS_COMPILE=""\n' "${local:+-$local}" >> /usr/src/linux/.config
echo "CONFIG_MODULE_SRCVERSION=y" >> /usr/src/linux/.config
wget -O /usr/src/linux/Module.symvers "http://mirror.scaleway.com/kernel/${arch}/${release}/Module.symvers"

# On prépare la configuration
make oldconfig

# On patche Wireguard in-tree 
/home/medspx/WireGuard/contrib/kernel-tree/create-patch.sh | patch -p1
sed 
make menuconfig

make prepare modules_prepare
make CFLAGS_MODULE=-fno-pic -j9
make CFLAGS_MODULE=-fno-pic -j9 modules
make modules_install

Configurer le VPN

Ok, nous avons maintenant deux bécanes avec Wireguard. Construisons donc un VPN avec. Je pars du principe que vous n'avez pas de firewall entre les deux, pour simplifier les commandes.

Avec Wireguard, ce qui est bien, c'est qu'on peut construire le VPN en live puis balancer la configuration finale dans un fichier de configuration avec une seule commande. Tout se passe avec les commandes classiques ip et wg.

-- Génération des clefs privées/publiques
# wg genkey > /etc/wireguard/private

-- Ajout de l'interface wireguard (wg0)
# ip link add dev wg0 type wireguard

-- Affectation d'une IP virtuelle sur l'interface wireguard
# ip addr add 192.168.3.4/24 dev wg0

-- On ajoute notre clef privée
# wg set wg0 private-key /etc/wireguard/private

-- On peut maintenant voir la configuration en dynamique
# wg showconf wg0

-- On va maintenant ajouter la définition d'un autre client
-- Vous avez besoin de sa clef publique que vous pouvez
-- obtenir avec wg show wg0
# wg set wg0 peer LHqF7DAsJoRftEWmj+b2HnBKonZqLwJwdO9eLQYx+lI=
-- allowed-ips 192.168.3.1/32 endpoint medspx.fr:54003

-- Ensuite, il suffit d'activer l'interface réseau
ip link set wg0 up

-- Sur l'autre machine, il faut également ajouter ce nouveau peer
# wg set wg0 peer clef_publique allowed-ips 192.168.3.4/32

-- et maintenant, la connexion devrait être effective
# wg show wg0

-- On peut maintenant stocker la configuration en dur
# wg showconf wg0 > /etc/wireguard/wg0.conf

-- Un peu plus tard, vous pouvez recharger cette configuration
# wg setconf wg0 /etc/wireguard/wg0.conf

Configuration avec systemd-networkd

Une fois la configuration dynamique passée, il peut être intéressant d'utiliser systemd pour garder cette configuration. Pour ma part, je n'utilise que systemd pour la configuration de mes interfaces réseaux. Wireguard est géré par systemd-networkd depuis systemd 237. Cela signifie que c'est présent dans Debian Buster mais également dans Debian Stretch si vous avez activé les dépôts backports.

Vous devez créer un fichier /etc/systemd/networkd/30-wg0.netdev:

[NetDev]
Name = wg0
Kind = wireguard
Description = Wireguard

[WireGuard]
ListenPort = auto
PrivateKey = [CLIENT PRIVATE KEY]

[WireGuardPeer]
PublicKey = [SERVER PUBLIC KEY]
PresharedKey = [PRE SHARED KEY]
AllowedIPs = 192.168.3.1/32
Endpoint = medspx.fr:54003
PersistentKeepalive = 60

Le ListenPort à auto indique que le port change chaque fois que vous lancez l'interface. C'est une configuration plutôt intéressante pour des stations en mode client sans configuration spécifique de parefeu.

Si vous avez un serveur publique avec une IP fixe, je vous conseille de déclarer un port spécifique et de l'ouvrir dans votre firewall.

Le PersistentKeepalive est également intéressant si vous êtes derrière un NAT (donc plutôt réservé aux stations de type client). Avec cette configuration, Wireguard envoi un petit paquet réseau de temps en temps (selon l'intervalle indiqué) pour réouvrir l'interface vers l'extérieur depuis le réseau interne. Cela permet de laisser la connexion ouverte au niveau du routeur NAT et donc de laisser votre client disponible pour le reste du VPN.

Si vous configurez une sorte de serveur VPN, vous devez indiquer tous les clients enregistrés en ajoutant autant de directives [WireGuardPeer] que de clients, afin de partager les clefs publiques.

Puis, vous aurez besoin d'un fichier pour configurer votre interface dans /etc/systemd/networkd/30-wg0.network:

[Match]
Name = wg0

[Link]
RequiredForOnline = no

[Network]
Address = 192.168.3.4/24

[Route]
Gateway = 192.168.3.1
Destination = 192.168.3.0/24

J'ai indiqué la directive RequiredForOnline à no car, pour certaines machines (typiquement un portable avec une connexion erratique), cela permet de démarrer le système sans attendre que Wireguard soit activé.

Aller un peu plus loin que le test du ping

Nous allons mettre en place ssh derrière une machine du VPN. L'astuce consiste simplement à mettre la ligne suivante dans /etc/ssh/sshd_config:

# L'adresse publique
ListenAddress 10.0.0.1:2222

# On écoute sur l'adresse Wireguard
ListenAddress 192.168.3.1:22

A partir de cet instant, vous pouvez utiliser un client ssh depuis une des machines du VPN pour attaquer directement le serveur via Wireguard.

On le voit, la grande force de Wireguard est d'utiliser une interface réseau virtuelle dédiée. De fait, n'importe quelle configuration classique d'un logiciel serveur peut être utilisée sans commandes spéciales.

Si vous avez un firewall

Pour ma part, mon C1 a un firewall configuré avec nftables. J'ai choisi de laisser passer tout ce qui vient ou va vers wg0.

Globalement ma configuration est la suivante:

#!/usr/sbin/nft -f

# Configuration NFT de debianplug

# On vide toutes les règles
flush ruleset

# Table pour un serveur public un peu fermé
table inet public_server {
  # Chaîne de gestion de tout ce qui rentre
  chain entree {
    # Chaîne de type filtre, en entrée
    type filter hook input priority 0;

    # Accepter tout ce qui vient de l'interface loopback
    meta iif lo accept

    # Accepter tout ce qui vient de Wireguard
    meta iif wg0 accept

    # Accepte le trafic qui vient de la machine
    ct state established,related accept

    # Réponse au ping IPv4
    ip protocol icmp accept

    # Ouverture des ports publics
    # Le port 54003 est celui de Wireguard
    tcp dport { 25,53,80,143,443,1376,4416,4444,5222,5269,9714,54003 } accept
    udp dport { 53, 9714, 54003 } accept

    # Ouverture du port nbd
    tcp sport { 4416 } accept

    # Compter et journaliser ce qui est rejeté
    counter log prefix "Connexion entrante rejetée: " level warn drop
  }

  # Chaîne de gestion de tout ce qui sort
  chain sortie {
    type filter hook output priority 0;

    # On accepte les sorties vers l'interface loopback
    meta oif lo accept

    # On accepte les sorties vers Wireguard
    meta oif wg0 accept

    # Réponse au ping IPv4
    ip protocol icmp accept

    # Ouverture des ports publics
    tcp sport { 25,53,80,143,443,1376,4444,5222,5269,9714,54003  } accept
    udp sport { 53,9714,54003 } accept

    # Ouverture des ports externes classiques utilisés par le système
    tcp dport { 25,53,80,143,443,995,4416,8443 } accept
    udp dport { 53,67,68,123 } accept

    # count and drop any other traffic
    counter log flags skuid prefix "Connexion sortante rejetée: " level warn drop
    }
}

Dans cette configuration, j'ouvre le port 54003 de Wireguard sur cette machine et j'autorise tout ce qui passe par l'interface wg0. De cette manière, tout marche !

Conclusions

Ce truc est vraiment extra ! D'abord, ça marche et ça passe pas mal de caca. Le code est super light. J'ai eu l'occasion de le parser rapidement pour trouver mes erreurs de symboles de Kernel et j'ai été étonné du nombre ultra limité de fichiers et de lignes (5000 seulement, sachant que mon programme de base pgMitm en fait près de 1000 à lui tout seul pour pas grand chose). L'implémentation est vraiment élégante et fine. Je ne parle pas de la configuration en live qui témoigne d'un vrai réalisme dans la mise en place de réseaux qui se solde généralement par un nombre impressionnant de systemctl restart systemd-networkd.

Comme d'habitude, Jason Donenfield et zx2c4 sortent un truc de qualité et je suis fier de leur rendre un hommage appuyé.

Posted dim. 23 déc. 2018 19:06:30 Tags:

Introduction

Depuis la rédaction de mon article sur Enigma, je suis toujours tenté d'aller plus loin avec GnuPG. En effet, une fois qu'on a pris le temps de créer un trousseau de clefs, on a toujours envie de le garnir d'un maximum de clefs différentes, histoire de pouvoir ouvrir le plus de portes possibles sans se prendre la tête.

Dans le monde de l'informatique, c'est pareil. En ce qui me concerne, je tape le plus souvent une phrase de passe lorsque je lance des commandes via ssh. Mais, plutôt que d'avoir un trousseau GPG d'un côté et des clefs privées/publiques pour SSH, ne peut-on pas tout faire avec un seul outil ?

Après quelques recherches, il semble que c'est possible depuis peu. En effet, on peut gérer l'authentification SSH par clef en utilisant GnuPG. Voici comment j'y suis parvenu en utilisant GnuPG en version 2.1, sous Debian Stretch.

Comprendre ce qu'on fait, c'est mieux

Avant de se lancer, il faut toujours prendre le temps de comprendre ce qu'on fait plutôt que de suivre bêtement une suite de commandes. Ça prend plus de temps mais vous deviendrez un meilleur administrateur système en suivant cette voie.

Je ne vais pas rentrer dans la théorie du chiffrement asymétrique mais juste faire quelques rappels sur le fonctionnement de GPG et de SSH.

Commençons par SSH... SSh permet de se connecter de manière sécurisée à une machine distante qui dispose d'un serveur SSH en écoute sur un port précis (le 22 par défaut). Si SSH permet d'ouvrir une session en utilisant un login/mot de passe, il encourage surtout l'authentification par clefs privée/publique. En règle générale, la clef privée est protégée par une phrase de passe (un long mot de passe).

SSH dispose d'ailleurs d'un agent qui gère ces clefs privées/publiques. En effet, si on utilise souvent SSH, on doit taper sans cesse la phrase de passe de la clef privée. L'agent SSH permet de disposer d'une espèce de cache sécurisé de phrase de passe. Cela évite de taper tout le temps la phrase de passe, le tout, avec une méthode sécurisée sur le poste de travail.

En ce qui concerne GPG, il existe également un agent (gpg-agent). Son rôle est de gérer une espèce de cache de clefs (privées et publiques) pour éviter de vous demander de taper votre mot de passe de clef privée sans arrêt. Il permet aussi de transmettre les clefs publiques à d'autres programmes (comme ssh par exemple). Finalement l'agent GPG fait presque la même chose que l'agent SSH sauf que chacun des programmes dispose de son jeu de clefs à lui, avec ses formats spécifiques.

Enfin, comme l'agent GPG a besoin que vous tapiez votre mot de passe de clef privée de temps en temps, il existe un programme qui permet de saisir ce mot de passe (ou cette phrase de passe bien souvent). Ce programme se nomme pinentry et peut être servi par plusieurs binaires selon l'environnement. En effet, si vous avez une session une session Gnome, vous aurez droit à une belle fenêtre adaptée au style de ce gestionnaire de bureau. En revanche, si vous êtes sur un terminal pur, il vous faudra un pinentry capable de s'afficher sur un terminal (pinentry-curses ou pinentry-tty). Pour ma part, comme j'utilise i3-wm et beaucoup de Qt (grâce à QGIS), j'utilise pinentry-qt4.

Création d'une sous-clef pour l'authentification SSH

Bon, le premier truc à faire, c'est de générer une sous-clef (subkey) dédiée à la fonction d'autorisation. En effet, SSH n'accepte pas n'importe quel type de clef et l'agent GPG non plus.

Voici quelles sont les étapes à suivre. Ce n'est pas très compliqué, il suffit d'utiliser la fonction --edit-key de gpg avec l'option --expert qui seule donne accès à la création de clef d'authentification:

gpg --edit-key --expert mon_compte
gpg (GnuPG) 2.1.18; Copyright (C) 2017 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.


La clef secrète est disponible.

sec  rsa4096/1234567890123456
     créé : 2017-07-22  expire : 2018-07-22  utilisation : SC
     confiance : ultime        validité : ultime
ssb  rsa4096/1234567890123456
     créé : 2017-07-22  expire : 2018-07-22  utilisation : E
ssb  rsa2048/1234567890123456
     créé : 2017-07-22  expire : 2018-07-22  utilisation : S
ssb  rsa2048/1234567890123456
     créé : 2017-07-22  expire : 2018-07-22  utilisation : E
[  ultime ] (1). mon_compte

gpg> addkey
Sélectionnez le type de clef désiré :
   (3) DSA (signature seule)
   (4) RSA (signature seule)
   (5) Elgamal (chiffrement seul)
   (6) RSA (chiffrement seul)
   (7) DSA (indiquez vous-même les capacités)
   (8) RSA (indiquez vous-même les capacités)
  (10) ECC (signature seule)
  (11) ECC (indiquez vous-même les capacités)
  (12) ECC (chiffrement seul)
  (13) Clef existante
Quel est votre choix ? 8

Actions possibles pour une clef RSA : Signer Chiffrer Authentifier 
Actions actuellement permises : Signer Chiffrer 

   (S) Inverser la capacité de signature
   (C) Inverser la capacité de chiffrement
   (A) Inverser la capacité d'authentification
   (Q) Terminé

Quel est votre choix ? A
...
Quel est votre choix ? C
...
Quel est votre choix ? S

Actions possibles pour une clef RSA : Signer Chiffrer Authentifier 
Actions actuellement permises : Authentifier 

   (S) Inverser la capacité de signature
   (C) Inverser la capacité de chiffrement
   (A) Inverser la capacité d'authentification
   (Q) Terminé

Quel est votre choix ? Q
les clefs RSA peuvent faire une taille comprise entre 1024 et
4096 bits.
Quelle taille de clef désirez-vous ? (2048) 4096
La taille demandée est 4096 bits
Veuillez indiquer le temps pendant lequel cette clef devrait être
valable.
         0 = la clef n'expire pas
      <n>  = la clef expire dans n jours
      <n>w = la clef expire dans n semaines
      <n>m = la clef expire dans n mois
      <n>y = la clef expire dans n ans
Pendant combien de temps la clef est-elle valable ? (0)
La clef n'expire pas du tout
Est-ce correct ? (o/N) o
Faut-il vraiment la créer ? (o/N) o
...

Vous disposez maintenant d'une clef GnuPG pour l'authentification. Reste à pouvoir l'utiliser correctement dans SSH...

Substitution de ssh-agent par gpg-agent

Depuis quelques versions maintenant, l'agent de gestion des passphrases de GnuPG (v2 pour rappel) peut servir d'agent SSH en lieu et place de celui fourni avec OpenSSH.

Sur un système moderne comme Debian Stretch, il faut juste le configurer dans le fichier de configuration de l'agent GPG pour lui indiquer l'option enable-ssh-support dans son fichier de configuration. Ce dernier se nomme ~/.gnupg/gpg-agent.conf. S'il n'existe pas, vous devrez le créer et lui ajouter une ligne avec enable-ssh-support.

Ensuite, il faut juste recharger l'agent GPG. Néanmoins, il faut également désactiver l'agent SSH. Ce n'est pas une mince affaire parce qu'il est géré par systemd. Le moyen le plus simple et le plus efficace consiste à fermer et rouvrir la session graphique. En effet, l'unité systemd qui gère l'agent SSH se lance uniquement lors de l'ouverture d'une session graphique.

Bien entendu, si vous avez du temps et que vous souhaitez personnaliser un peu plus le comportement de l'agent GPG, je vous conseille de lire la documentation de ce dernier via man gpg-agent.

Modification du fichier sshcontrol

Pour que l'agent GPG transmette bien des clefs d'authentification à ssh, il faut encore lui indiquer quelle clef utiliser. En effet, si vous avez un trousseau GnuPG étoffé, vous ne voudrez sans doute pas autoriser toutes les clefs à être utilisées par ssh. L'agent GPG dispose d'un fichier dédié dans ~/.gnupg/sshcontrol.

Nous allons donc simplement indiquer notre clef d'authentification que nous avons créé auparavant dans ce fichier. Mais attention, ce dernier exige non pas un uid de clef mais un keygrip. Vous pouvez l'obtenir à l'aide de l'option --with-keygrip:

gpg --list-public-keys --with-keygrip mon_compte
/home/mon_compte/.gnupg/pubring.gpg
-------------------------------
pub   rsa4096/879357560768796E 2017-07-22 [SC] [expire : 2018-07-22]
      1C848291661BD7E20B873A27879357560768796E
      Keygrip = 1801833AA0573C2372AD196729C6B004A4BF891B
uid                [  ultime ] mon_compte <mon_compte@example.com>
sub   rsa4096/792D62F3336393FA 2017-07-22 [E] [expire : 2018-07-22]
      Keygrip = F8894EEFCF321067DA881E2AC471C9BACF0B8067
sub   rsa2048/A44D23DA9C451C04 2017-07-22 [S] [expire : 2018-07-22]
      Keygrip = 8582CAA7B033E8B775809BA05ED37B3463CB0F30
sub   rsa2048/B040BDF79DB1058C 2017-07-22 [E] [expire : 2018-07-22]
      Keygrip = 60B08365121D1B34E911C9EA8138946DB1FE8FCA
sub   rsa4096/519BE81D08A417C7 2017-11-11 [A] [expire : 2018-07-22]
      Keygrip = EB994608FCAB196BF0D79602D3A04F043F5DEE9F

Dans mon cas, la sous-clef utilisée est la dernière (elle a l'option [A] pour authentification). Son keygrip est EB994608FCAB196BF0D79602D3A04F043F5DEE9F. Vous avez juste à renseigner cette valeur dans le fichier ~/.gnupg/sshcontrol avec votre éditeur de texte préféré ou une redirection shell.

Pour vérifier que ça fonctionne, la commande ssh-add -l doit maintenant indiquer la clef d'authentification rentrée en amont.

Export de la sous-clef dans les clefs autorisées de SSH

Maintenant que tout est configuré sur la machine qui va se connecter à distance, il faut quand même indiquer à la machine d'en face quelle clef publique doit être acceptée. Dans le temps, il fallait utiliser un programme tiers de la suite monkeysphere mais, depuis GnuPG 2.1, il existe une commande d'export qui permet de le faire assez simplement. Il s'agit de l'option --export-ssh-key. Elle exporte une clef publique au format accepté par SSH. Il n'y a qu'à l'utiliser de la manière suivante:

gpg --export-ssh-key mon_compte >> ~/.ssh/authorized_keys

Par défaut, cette commande exporte la première clef d'authentification au format SSH, directement prêt à être intégrée dans un fichier d'autorisation SSH.

Pour exporter la clef à distance, vous pouvez simplement utiliser la commande ssh dédiée: ssh-copy-id mon_compte@machine_distante. En effet, cette commande utilise le résultat de ssh-add pour envoyer les clefs à distance. Comme ssh-add utilise maintenant GnuPG, il n'y a pas de problème pour l'utiliser.

Que faire si ça plante ?

Déjà, vous devez voir si l'agent SSH est toujours actif. Si c'est le cas, c'est mauvais signe, vous devez trouver le moyen de le gicler. Le plus simple est de lancer une commande du type set | grep SSH. Elle doit vous retourner uniquement la variable SSH_AUTH_SOCK qui contient la socket de l'agent SSH. Ce denier doit être l'agent GPG. C'est facilement reconnaissable dans le contenu de la variable qui doit afficher un truc avec gpg-agent.ssh.

Autre point qui peut poser problème: il manque une sous-clef privée d'authentification ou une clef d'authentification est inutilisable. Ça m'est arrivé assez facilement en exportant une sous-clef d'authentification en oubliant d'incorporer la sous-clef privée. Après l'import dans un autre trousseau sur une autre machine, la commande gpg --list-secret-keys m'indiquait une sous-clefs inutilisable (ssb#). J'ai du procéder à une exportation complète en utilisant l'option --export-secret-subkeys de GnuPG.

Autre symptôme lié au point précédent: si ssh-add -l n'affiche rien ou un message d'erreur, je vous conseille de jeter un coup d'oeil à votre journal système utilisateur (via journalctl --user -xe) pour voir si vous avez des erreurs concernant une clef manquante dans le fichier sshcontrol. En effet, pour le keygrip d'une clef situé dans le fichier sshcontrol soit pris en compte, il faut absolument qu'il y ait le fichier correspondant dans ~/.gnupg/private-keys-v1.d/. Si ce n'est pas le cas, vous avez sans doute un problème avec une sous-clef privée.

Conclusions

Avec cette recette, vous pouvez enfin vous passer des clefs SSH classiques et de son agent. Ce mode de gestion unifié qui passe par GnuPG vous permettra de renforcer le rôle de cette solution de chiffrement qui se révèle finalement assez complète.

De plus, cela vous encouragera à avoir GnuPG un peu partout sur vos machines et à vous organiser pour gérer votre trousseau au mieux.

Prochaine étape ? Peut-être utiliser une clef de sécurité ou une carte OpenPGP pour gérer le trousseau. Ou encore mettre en place Pass qui utilise nativement GnuPG pour sécuriser les autres mots de passe...

Posted lun. 20 nov. 2017 22:05:30 Tags:

Introduction

Bon, vous le savez, nous vivons dans un monde où la surveillance des communications devient quasiment totale. Différents acteurs comme la Stasi (la sécurité de l'État) ou encore Google ou Facebook veulent absolument savoir ce que vous vous racontez entre-vous sous couvert de différents motifs.

Alors que nombre de pays ont instauré depuis très longtemps le secret de la correspondance, il vous faudra recourrir à des mesures techniques plus complexes pour assurer que moins de personnes ne lisent vos précieux courriers électroniques.

L'informatique a depuis longtemps réglé ce problème en utilisant le chiffrement asymétrique. Néanmoins ce dernier a l'inconvénient de ne pas être si trivial à employer.

Et si finalement, ce n'était plus si complexe que ça ? Comment faire pour chiffrer et signer facilement ses courriers électroniques avec un minimum de sécurité ?

La réponse (en fait une des nombreuses réponses) se trouve dans l'extension Enigma de Roundcube. Je présente dans cet article, la manière dont j'ai pu le déployer sur mon serveur Debian Stretch.

Principes d'Enigma

Je ne vais pas revenir sur le fonctionnement du chiffrement asymétrique; c'est un pré-requis à la lecture de cet article.

Ce qui est plus important à retenir, c'est qu'Enigma vous propose de stocker vos clefs GPG privées et publiques directement sur le serveur qui héberge Roundcube. De cette manière, vous disposez d'un moyen simple pour toujours avoir votre clef sous la main, lorsque vous souhaitez l'utiliser pour votre courrier électronique.

Enigma n'est qu'une encapsulation de GnuPg. Son fonctionnement permet de transférer tout ce qui est sensible à l'environnment de GnuPg (en version 2.1 sous Debian Stretch) qui est fait pour ça.

A mon sens, cela permet de faire de ces fonctions de chiffrement/signature quelquechose de simple à utiliser au quotidien plutôt qu'un truc de crypto-anarchiste.

Sachez que si vous importez votre clef privée avec un mot de passe de protection, Enigma vous demandera de renseigner ce mot de passe dès qu'il en aura besoin. Votre clef privée est donc potentiellement à l'abri d'un vol immédiat (mais d'un crackage par force brute ou dictionnaire).

Installation des paquets

Pour faire simple, il vous faut installer au moins le paquet roundcube-plugins. Ce paquet suggère d'utiliser php-crypt-gpg. Néanmoins, ce paquet n'est pas diponible dans Debian Stretch, la version stable de Debian au moment de la rédaction de cet article. Ce paquet est indispensable pour faire fonctionner le plugin Enigma.

Vous pourriez tout simplement attendre que la prochaine version stable de Debian entre en production mais voilà, vous avez lu mon introduction et vous avez décidé de protéger un peu plus vos communications. Comment faire ?

Bon, par bonheur, le paquet php-crypt-gpg est disponible dans Debian Buster, la version testing de Debian. De plus, dans sa version actuellement disponible, il peut parfaitement s'installer dans Debian stable. Donc, un simple téléchargment suivi d'une installation manuelle suffit à rendre le plugin opérationnel:

# apt install --no-install-recommends gnupg2 php-console-commandline
# wget http://ftp.fr.debian.org/debian/pool/main/p/php-crypt-gpg/php-crypt-gpg_1.6.0-1_all.deb
# dpkg -i php-crypt-gpg_1.6.0-1_all.deb  

Oui, vous allez me dire que j'aurais pu utiliser l'apt-pinning. Mais, vous le savez, j'administre mes machines personnelles sur mon temps libre qui est ultra compté. Je vais au plus court et j'assume totalement...

Configuration

Voilà, vous avez tout ce qu'il faut pour activer Enigma. Reste encore à le configurer correctement. Voyons comment faire en détails.

Vous devez d'abord ajouter le plugin Enigma dans la liste des plugins autorisés. La liste est disponible dans le fichier /etc/roundcube/config.inc.php:

// List of active plugins (in plugins/ directory)                                                                                                                       
$config['plugins'] = array(
'archive',
'calendar',
'carddav',
'emoticons',
'managesieve',
'zipdownload',
'enigma'
);

Le principe d'Enigma est de stocker votre clef privée (et publique) sur le serveur qui héberge Roundcube, de manière à pouvoir signer et déchiffrer à volonté. Enigma vous impose de déclarer un répertoire pour stocker les clefs. Nous allons utiliser le répertoire /var/local/enigma. Attention, ce répertoire doit être accessible en écriture à l'utilisateur de Roundcube, c'est à dire www-data:

# mkdir -p /var/local/enigma
# chown www-data:www-data /var/local/enigma

Enfin, vous devez simplement configurer le plugin en rédigeant son fichier de configuration dédié, situé dans /etc/roundcube/plugins/enigma/config.inc.php. Ce fichier permet d'imposer certains paramètres à tous les utilisateurs de votre instance Roundcube (oui, un webmail, c'est fait généralement pour plusieurs comptes). En voici un exemple commenté.

<?php
// Enigma Plugin options
// --------------------

// Le pilote utilisé pour PGP
$config['enigma_pgp_driver'] = 'gnupg';

// Le pilote utilisé pour le contenu S/MIME
$config['enigma_smime_driver'] = 'phpssl';

// Activer le debug dans les journaux
$config['enigma_debug'] = false;

// Répertoire de stockage des clefs (privées et publiques) 
$config['enigma_pgp_homedir'] = '/var/local/enigma';

// Emplacement du binaire gpg (auto-détecté par défaut)
$config['enigma_pgp_binary'] = '';

// Emplacement de gpg-agent (auto-détecté par défaut)
$config['enigma_pgp_agent'] = '';

// Activer la vérification des signatures par défaut
$config['enigma_signatures'] = true;

// Activer le déchiffrement des messages
$config['enigma_decryption'] = true;

// Autoriser le chiffrement et la signature
$config['enigma_encryption'] = true;

// Activer la signature par défaut
$config['enigma_sign_all'] = false;

// Activer le chiffrement des messages par défaut
$config['enigma_encrypt_all'] = false;

// Activer l'envoi de la clef publique dans chaque courriel
$config['enigma_attach_pubkey'] = true;

// Durée par défaut de stockage des mots de passe de clefs privées (en
// minutes). 0 indique que le mot de passe est stocké pendant toute la
// session. 
$config['enigma_password_time'] = 5;

// Interdire la génération de clef privée sur le serveur
$config['enigma_keygen_server'] = false;

// Forcer la configuration de certains paramètres
// (Rien dans notre cas)
$config['enigma_options_lock'] = array();

Avec ces éléments, Enigma est prêt à fonctionner.

Utilisation

Ok, Enigma est prêt, il ne reste qu'à le remplir avec vos clefs. Pour cela, vous devez les extraires au format asc par exemple à l'aide de la commande gpg. N'oubliez pas qu'il faut la clef privée et la clef publique:

$ gpg --armor --export-secret-keys my_account@example.com > privkey.asc
$ gpg --armor --export my_account@example.com > pubkey.asc

Ensuite, ouvrez Roundcube et allez dans la page des paramètres, dans l'onglet Préférences: la section "Chiffrement" doit apparaître:

Préférences Enigma

Vous devriez retrouver tous vos paramètres du fichier de configuration. Néanmoins, en fonction des options de verrou, certains paramètres ne peuvent pas forcément être modifiés ici.

Ensuite, vous devez importer au moins une clef privée et une clef publique (vous pouvez faire les deux en même temps). Pour cela, rendez-vous toujours dans l'onglet "Clés GPG" et cliquez sur le bouton "Importer" situé en haut à gauche:

Import des clefs

Choisissez les fichiers .asc précédemment générés et le tour est joué. Vous devriez voir une référence dans la liste des clefs. Cliquer sur cette référence affichera une page complète d'information sur la dite clef.

Passons maintenant aux opérations de chiffrement et de signature. Composez un nouveau courriel et vous devriez voir apparaître une nouvelle option de sécurité avec un cadenas:

Options de chiffrement/signature des messages

Lorsque vous cochez l'option, joindre ma clef publique, elle sera systématiquement envoyée en pièce-jointe de votre message. Chez moi, elle est cochée par défaut, de manière à ce que les gens puissent la récupérer à moindre frais.

Conclusions

Finalement, l'installation du plugin est assez simple. La méthode que je propose pose quand même un certain problème, principalement au niveau de la sécurité sur la maintenance du paquet php-crypt-gpg. Ne l'oubliez pas !

Néanmoins, le plugin est fonctionnel et il vous permettra de signer vos courriels et de déchiffrer ce que vos correspondant équipés d'une solution de chiffrement asymétrique auront envoyé et ce, de manière assez simple, dès que vous aurez accès à votre instance Roundcube.

Cela vous donnera enfin l'impression de rejoindre une communauté de gens sérieux avec la sécurité de leurs communications. L'étape d'après consiste à convertir tous vos correspondants à utiliser le chiffrement asymétrique pour leur correspondance. Ce n'est pas gagné mais pas impossible !

Posted sam. 18 nov. 2017 08:34:23 Tags:

Introduction

Lors de mon dernier travail sur Cockpit, je me suis rendu compte que ce dernier reposait beaucoup sur systemd. En effet, en essayant l'interface Web de Cockpit, je me suis rendu compte qu'on pouvait ajouter pas mal de renseignements sur la machine via systemd. Vu la quantité d'informations disponibles, un article s'imposait pour présenter l'ensemble des modifications de mon système.

De même, ayant adopté systemd, il m'a semblé plus intéressant d'essayer de gérer la configuration de mes machines en utilisant les mécanismes systemd qui semblent maintenant assez bien éprouvés.

Bon, je sais que systemd est un sujet encore épineux en 2017 mais, je crois qu'en utilisant ses mécanismes de configuration le plus possible, on arrive à une méthode de configuration un peu plus universelle. En effet, sous réserve que d'autres distributions reposent sur les services systemd présentés dans cet article, un administrateur système qui changerait d'environnement serait moins perdu (il y a de grandes différences entre un système RedHat et un système Debian) et, de fait, serait beaucoup plus efficace.

Mais voyons plutôt ce que nous pouvons faire avec systemd.

L'ensemble de ce qui suit a été réalisé sur plusieurs machines sous Debian Stable (Stretch au moment de la rédaction de cet article).

Informations sur la machine via /etc/machine-info

hostnamectl est une commande systemd qui permet de customiser plusieurs éléments:

  • l'emplacement physique de la machine
  • l'environnement de la machine (production/dev/test/etc).
  • le type de machine (chassis).
  • Un nom élégant.
  • l'icône de la machine (dont le nom est au format XDG).

Ces informations sont stockées dans le fichier /etc/machine-info. On peut également les modifier avec hostnamectl.

Gestion du temps via systemd-timesyncd

J'utilisais depuis des temps immémoriaux l'antique ntpdate pour synchroniser l'heure de mon serveur public. Attention, cette option est ultra importante car mon serveur public n'a pas d'horloge temps réel indépendante. Donc, dès qu'il boote, il lui faut récupérer l'heure via le réseau, histoire d'avoir les bonnes dates dans les logs.

Mais avec systemd, il y a un outil pour faire ça: timedatectl. Si je peux virer un paquet de mon système, autant le faire non ? Par ailleurs, la documentation de timedatectl indique que ce dernier stocke la date et l'heure sur disque ce qui permet de reprendre à une date moins vieille que epoch lors du reboot de la machine.

La configuration de timedatectl est assez simple, vous devez simplement renseigner le fichier /etc/systemd/timesyncd.conf avec la bonne liste de serveurs, dans le cadre d'une machine à configuration statique.

#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.
#
# Entries in this file show the compile time defaults.
# You can change settings by editing this file.
# Defaults can be restored by simply deleting this file.
#
# See timesyncd.conf(5) for details.

[Time]
NTP=0.debian.pool.ntp.org 1.debian.pool.ntp.org 2.debian.pool.ntp.org 3.debian.pool.ntp.org

Dans le cas, d'une machine servie par DHCPv4, je vous conseille de ne pas configurer ce fichier mais d'utiliser la directive UseNTP dans la configuration du service networkd et de servir la liste des serveurs NTP via votre serveur DHCP: votre configuration sera alors centralisée correctement.

Ensuite, n'oubliez pas de supprimer le paquet ntpdate, ce qui supprime aussi l'unité systemd associée. Enfin, vous devrez activer le service systemd-timesyncd.service tout en ayant pris soin de recharger le démon systemd.

Gestion des interfaces réseaux via systemd-networkd

Bon ça fait des années que j'utilise le système ifup de Debian qui marche assez bien finalement. Donc pour moi, NetworkManager est un peu "overkill". Néanmoins, systemd présente un mécanisme intéressant pour la gestion des interfaces réseau: il s'agit de systemd-networkd.

Globalement la logique est un peu la même que celle de ifup:

  • On définit un fichier .network de définition d'un périphérique réseau.
  • On y indique les différentes options disponibles dedans.
  • Au démarrage de la bécane, systemd se charge "d'allumer" les différents périphériques réseau.

Voici quelques fichiers que j'utilise au boulot et à la maison, pour vous montrer différentes utilisations possibles et que networkd sait être versatile...

Connexion de l'interface filaire ethernet de base via DHCP

Dans ce qui suit, l'interface réseau ethernet se nomme eno1. Pour savoir ce que vous avez, un simple ip link vous donnera la liste des interfaces repérées par le noyau.

Vous devez créer un fichier se terminant par .network dans le répertoire /etc/systemd/network/ (ex: /etc/systemd/network/ethernet.network) et contenant les lignes à suivre:

[Match]
Name=eno1

[Network]
Description="Primary Ethernet card"
DHCP=yes

Comme vous le voyez, c'est très simple ! Le principe est d'inscrire une directive de match qui permettra de repérerer la carte réseau concernée par le fichier .network. Puis, on indique qu'on souhaite utiliser le DHCP.

Connexion de l'interface filaire ethernet de base via DHCP avec IPv6

Dans le cas qui suit, on souhaite récupérer une adresse IPv4 via DHCP mais utiliser les mécanismes IPv6 d'allocation automatique d'adresses.

[Match]
Name=eno1

[Network]
Description="Primary Ethernet card"
DHCP=ipv4
LinkLocalAddressing=ipv6
IPv6AcceptRA=true

[DHCP]
UseDNS=true
UseDomains=true
UseNTP=true
UseRoutes=true
UseTimezone=true
ClientIdentifier=mac

Pour la directive DHCP, on souhaite:

  • Configurer le DNS via DHCP.
  • Configurer le domaine par défaut via DHCP.
  • Configurer les serveurs NTP utilisés par DHCP.
  • L'authentification DHCP se fera par adresse mac.

Connexion de l'interface filaire ethernet de base via DHCP filtrant

Au bureau, j'ai un serveur DHCP filtrant sur les adresses MAC et qui s'attend à avoir des machines Windows. Pour cela, on va indiquer qu'on souhaite identifier le client DHCP via l'adresse MAC de la carte réseau et on va ajouter un identifiant de vendeur qui aura pour contenu MSFT 5.0 (un truc qui dit que c'est un MS-Windows):

[Match]
Name=eno1

[Network]
Description="Primary Ethernet card"
DHCP=ipv4
LinkLocalAddressing=no

[DHCP]
UseDNS=true
ClientIdentifier=mac
VendorClassIdentifier="MSFT 5.0"

Connexion Wifi avec WPA

C'est la connexion classique sur une borne wifi. Ici, il y a une petite astuce: nous allons utiliser wpa_supplicant (le classique) et créer une "fausse" unité systemd pour indiquer à systemd-networkd qu'il faut utiliser wpa_supplicant.

D'abord, la configuration de la carte réseau Wifi (wlp1s0 chez moi) dans /etc/systemd/network/wlp1s0.network:

[Match]
Name=wlp1s0

[Network]
Description="Primary WiFi card"
DHCP=ipv4
LinkLocalAddressing=ipv6
IPv6PrivacyExtensions=true
IPv6AcceptRA=true

[DHCP]
UseDNS=true
UseNTP=true
UseDomains=true
ClientIdentifier=mac

Dans cette configuration, on configure IPv4 par DHCP, IPv6 par les mécanismes d'annonce de routeur et, bien sûr, on active les extensions de vie privée sur IPv6. Rien que du classique.

Ensuite, il vous faut un fichier de configuration pour wpa_supplicant, nommé de la bonne manière, c'est-à-dire en tenant compte du nom de l'interface. Dans notre cas, ce sera /etc/wpa_supplicant/wpa_supplicant-wlp1s0.conf:

ctrl_interface=/var/run/wpa_supplicant
eapol_version=1
ap_scan=1
fast_reauth=1

network={
    ssid="Foobar1"
    psk="password1"
    priority=1
}
network={
    ssid="Foobar2"
    psk="password2"
    priority=2
}

Enfin, il reste à créer une "fausse" unité systemd et à l'activer:

# systemctl enable wpa_supplicant@wlp1s0.service
# systemctl start wpa_supplicant@wlp1s0.service

Normalement, ça devrait bien fonctionner...

Connexion filaire statique avec une route supplémentaire

Pour la maison, j'ai une bécane qui sert de pont "Wifi" pour le réseau 192.168.1.0. Voici la configuration spécifique de sa carte ethernet:

[Match]
Name=eno1

[Network]
Description="Primary Ethernet card"
DHCP=no
LinkLocalAddressing=no
IPForward=ipv4

[Address]
Address=192.168.1.7/24
Broadcast=192.168.1.255

Bien entendu, vous devez utiliser un mécanisme de forwarding IPv4 via nftables pour que ça fonctionne complètement (mais ça, c'est hors-sujet pour aujourd'hui).

Création d'un pont pour des machines virtuelles sur DHCP filtrant

Au boulot, j'ai besoin d'avoir un pont ethernet pour mes machines virtuelles sous KVM. Pour faire simple, j'utilise un pont via networkd. Mais il y a un peu de magie dans l'histoire: n'oublions pas que mon serveur DHCP est filtrant sur l'adresse MAC. Du coup, je suis obligé d'affecter l'adresse MAC de la carte réseau ethernet au pont et de mettre une fausse adresse MAC pour la carte réseau. C'est chiant mais sinon, ça ne fonctionne pas du tout !

Vous devez créer 3 fichiers:

  • Un fichier de périphérique réseau virtuel pour le pont (br0.netdev).
  • Un fichier de configuration réseau pour le pont (br0.network).
  • Un fichier qui va inscrire la carte ethernet physique dans le pont.

Contenu de /etc/systemd/network/br0.netdev:

[NetDev]
Description="Network Bridge for KVM"
Name=br0
Kind=bridge

Contenu de /etc/systemd/network/br0.network:

[Match]
Name=br0

[Link]
MACAddress=88:51:fc:4e:8a:91

[Network]
Description="Network configuration for Bridge"
DHCP=ipv4
LinkLocalAddressing=no

[DHCP]
UseDNS=true
UseMTU=true
UseDomains=true
ClientIdentifier=mac
VendorClassIdentifier="MSFT 5.0"

Contenu de /etc/systemd/network/ethernet-slave.network:

[Match]
Name=eno1

[Link]
MACAddress=88:51:fc:4e:8a:92

[Network]
Description="Primary Ethernet card (bridged)"
Bridge=br0

Ensuite, relancez le service:

# systemctl restart systemd-networkd.service

Un peu de ménage

Une fois que votre configuration est effective, vous pouvez supprimer les anciens paquets (oui, je sais, ça fait bizarre de supprimer un paquet aussi essentiel que ifupdown mais, vous n'en avez plus besoin):

# apt purge isc-dhcp-client isc-dhcp-common bridge-utils ifupdown
# apt autoremove

Gestion du résolveur DNS via systemd-resolved

Bon, c'est une opération assez simple qui consiste simplement à activer le service systemd-resolved et à remplacer le fichier /etc/resolv.conf par un lien symbolique vers le fichier maintenu par systemd:

# systemctl enable systemd-resolved.service
# systemctl start systemd-resolved.service
# ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf

Une fois opérationnel, le service se charge de gérer la résolution de noms. Plus besoin de gérer le resolv.conf à la main ou via ifupdown.

Gestion des logs

Après quelques mois de fonctionnement correct, je me suis résolu à passer à journald en totalité. En effet, je trouve que ce dernier, qui est installé si vous utilisez systemd remplit bien son rôle. En maitrisant les commandes de consultation, on arrive finalement à s'en sortir assez facilement.

J'en ai donc profité pour configurer son stockage permanent et virer les résidus de syslog. Voici le contenu de mon fichier /etc/systemd/journald.conf:

[Journal]
Storage=persistent
SystemMaxUse=200M
MaxFileSec=1month
ForwardToSyslog=no

Suivi de systemctl restart systemd-journald. Pour virer rsyslog:

# apt purge rsyslog

Ça fait toujours un paquet en moins à gérer.

Abandonner le traditionnel cron

Oui, systemd gère un peu l'équivalent de cron via les "timers". C'est une approche qui me séduit depuis le début même si, à l'usage, une simple crontab est plus simple à gérer. Car dans le cas de systemd, vous aurez deux choses à faire:

  • créer un service à lancer.
  • créer un timer pour le service.

Néanmoins, d'un point de vue de sysadmin, cela fait sens car de toute manière, le script que vous lancez par le démon cron doit bien se situer quelquepart. Avec systemd, vous avez un peu de travail supplémentaire mais vous pouvez lire assez facilement l'emplacement du service/script. Ce référencement est quasiment obligatoire (il est indiqué dans la définition du service), c'est donc plus beaucoup plus propre, aucun risque de se demander où est situé le script à lancer.

Néanmoins, se débarrasser de cron n'est pas chose aisée. En effet, certains paquets appellent explicitement cron ou au moins le paquet cron-daemon. Mais, bonne nouvelle, il reste un subterfuge: il existe un "cron like" basé sur systemd: systemd-cron. Ce paquet installe le paquet virtuel cron-daemon.

Donc, un simple:

# apt install --no-install-recommends systemd-cron

fera l'affaire. Bon, vous me direz: quel est l'intérêt de remplacer cron par un autre paquet ? Moi, je vous répondrais que c'est pour la beauté du geste !

Conclusions

En fin de l'année 2017, systemd a bien progressé. Il fait beaucoup de choses de facto. Ce n'est pas parfait, notamment pour cron mais c'est surtout lié à la manière d'empaqueter les logiciels sous Debian.

Dans mon objectif de réduire le nombre de paquets à administrer sur une machine aux capacités limitées, systemd remplit parfaitement son rôle. Je ne peux que vous recommander de l'utiliser. Certes parfois, vous allez rencontrer des bugs car tout n'est pas parfait. Mais je trouve que le concept d'un système de configuration déclaratif est quand même beaucoup plus clair qu'un système mélangeant code Shell et fichier de conf.

Par ailleurs, et pour relancer une énième polémique, je trouve que systemd est finalement assez KISS et correspond bien à la philosophie Unix qui implique d'utiliser des outils dédiés pour chaque action. Car, c'est bien ce qu'on retrouve dans systemd: les informations sur la machine se gèrent avec hostnamectl, le réseau avec networkd, la gestion du temps avec timesynd, la gestion de la résolution DNS avec resolved. Systemd propose donc bien pleins d'outils dédiés pour chaque action d'administration système.

Dans tous les cas, pour moi, il n'y a plus de débats possibles: mon serveur public démarre bien plus vite et sans accroc depuis que j'utilise systemd. Auparavant, j'avais toujours mon service dovecot qui ne fonctionnait pas (simplement parcequ'il s'activait au mauvais moment par rapport à l'état de configuration de la carte réseau). J'ai passé des journées entières pour débugguer cette situation sans jamais y parvenir. La migration systemd a tout simplement réglé mon problème après quelques heures de recherche dans les logs.

Posted ven. 17 nov. 2017 20:21:35 Tags:

Introduction

Tout bon administrateur système qui s'auto-héberge est souvent confronté à un problème d'intervention distante. En effet, il arrive parfois que votre machine hébergée tombe en panne ou qu'une configuration soit mal balancée et que vous vous en rendiez compte alors que vous n'avez pas accès physiquement à la machine.

Il est donc indispensable de disposer d'une solution technique qui permette cette intervention à distance. Le moyen le plus simple que j'ai pu trouver est de mettre en place un accès à un shell via un navigateur web. L'intérêt est de ne pas avoir à utiliser de machine ou de logiciel spécifique pour accéder à ce terminal spécifique. Ce n'est sans doute pas la méthode la plus sécurisée mais c'est le meilleur compromis que j'ai pu trouver entre facilité et rapidité d'accès et sécurité.

Jusqu'à présent, j'utilisais une solution hyper-légère nommée shellinabox. Elle faisait bien son job mais elle a un problème majeur: elle ne semble plus vraiment maintenue (disons, à ultra-minima). Et comme il s'agit ici de donner un accès au coeur d'un système à partir d'un simple accès Internet... autant ne pas rigoler sur la sécurité.

Je me suis donc tourné vers d'autres solutions d'administration à distance par navigateur web et je suis tombé sur un projet assez prometteur: Cockpit.

J'ai décidé de le tester pour voir s'il pouvait se substituer astucieusement à ShellInABox et voici mes conclusions...

A propos de Cockpit

Cockpit est un projet assez récent d'administration distante. Il est fortement lié au projet Fedora et donc en utilise les technologies les plus emblèmatiques, notamment en ce qui concerne systemd. Néanmoins, il est présent dans Debian, depuis la version stable Stretch, dans les dépôts backports.

La philosophie de Cockpit est de viser à un outil d'administration distante le plus léger possible, accessible par le web et qui repose le plus possible sur les outils déjà existants. C'est une philosophie qui me va très bien.

Par ailleurs, Cockpit étant assez jeune, il ne dispose pas encore de trop de modules et il ne nécessite pas encore trop de dépendances de paquets.

En conséquence, à la lecture de ce manifeste, il m'a semblé possible de le déployer sur mon SheevaPlug qui est un matériel assez limité.

Un cahier des charges réduit

Sur le papier, Cockpit épouse parfaitement ce que je cherche depuis des années comme outil d'administration:

  • Il est léger et modulaire.
  • Il est maintenu dans la distribution que j'utilise.
  • Il dispose d'un nombre limité de dépendances.
  • Il s'appuie sur les programmes existants.
  • Il épouse complètement le système sur lequel il repose.
  • Il offre une solution complète avec un shell web interne qui est indispensable.
  • Il semble correctement maintenu (par rapport au nombre de contributions mensuelles) et sans doute plus sécurisé que ShellInABox.
  • Il semble possible de l'héberger derrière un serveur mandataire inverse (reverse proxy).
  • Le système d'authentification semble robuste (car basé sur PAM).

Installation et configuration aux petits oignons

Bon, disons-le rapidement, j'ai un peu galéré pour faire ce déploiement à ma sauce. C'est surtout la partie serveur mandataire inverse qui m'a posé problème. Néanmoins, j'y suis parvenu et je vous livre ma méthode.

Installation

Vous devez activer les dépôts backports de Debian Stretch pour installer Cockpit.

Ensuite, vous devez savoir ce que vous voulez installer comme modules. Je vous laisse le soin d'étudier la liste dans les paquets Debian (tout ce qui commence par cockpit-).

Pour ma part, je n'utilise jamais NetworkManager et cette machine n'est pas un serveur de virtualisation. Ainsi, je n'ai vraiment besoin que du module "storaged" qui gère les espaces disques. Voici ce que j'ai utilisé pour l'installation à proprement parler:

apt-get install --no-install-recommends cockpit cockpit-storaged

L'ensemble, dépendances incluses, pèse moins de 15Mo, ce qui est très léger mais certes, plus gros que ShellInABox).

A partir de ce moment, Cockpit est disponible sur le port 9090 de votre machine. Mais, comme vous avez de bonnes règles de pare-feux, vous ne devriez pas pouvoir vous y connecter comme ça...

Principes de configuration de Cockpit

Sans rentrer dans les détails, voici comment est structuré la configuration de Cockpit:

  • D'abord, il existe un fichier de type INI nommé /etc/cockpit/cockpit.conf. Il possède peu de directives mais reste très important pour modifier le comportement web par défaut.
  • Si vous utilisez l'accès par HTTPS, le service utilise des certificats stockés dans /etc/cockpit/ws-certs.d/.
  • Cockpit est activé par systemd via une socket. On trouve donc la définition du service dans /usr/lib/systemd/system/cockpit.service et la définition de la socket dans /usr/lib/systemd/system/cockpit.socket. C'est la définition de la socket qui permet de définir les ports et adresses réseaux sur lesquels le service Cockpit est disponible.
  • Enfin, il y a la configuration Apache.

Accès depuis l'extérieur

Je souhaite pouvoir accéder à l'instance Cockpit depuis l'extérieur. Pour cela, je vais juste utiliser un reverse-proxy (ce sera Apache, comme à mon habitude). C'est une installation assez complexe à mettre en oeuvre et pas forcément bien documentée (car sans doute atypique). Néanmoins, je fais plus confiance à Apache qu'à Cockpit pour la sécurité de l'exposition à Internet.

J'ai pas mal galéré pour obtenir quelquechose de correct, aussi voici un résumé des opérations à mettre en oeuvre:

  • On va d'abord modifier l'adresse d'écoute par défaut de cockpit pour la faire pointer vers 127.0.0.1:9090 et non vers toutes les interfaces réseau. Cela permet de réduire l'exposition extérieure et de limiter l'accès par le serveur mandataire inverse.
  • Ensuite, nous allons indiquer à Cockpit d'utiliser un niveau d'arborescence web supplémentaire. En effet, je ne souhaite pas utiliser de VirtualHost dédié car c'est finalement assez lourd (oui, il faut rajouter une entrée DNS et surtout mettre à jour le certificat du site web). Ainsi le service cockpir sera disponible à l'emplacement /webadmin/ du domaine.
  • Cockpit est un service disponible en HTTP. En 2017 qui dit HTTP dit forcément HTTPS et donc certificats. Malheureusement de ce côté-ci, Cockpit impose d'utiliser un fichier regroupant clef privée/clef publique. Ceci n'est pas compatible avec un système basé sur l'AC LetsEncrypt qui met à jour très fréquemment et de manière automatique les certificats. Nous allons donc utiliser le flux non chiffré, ce qui ne pose pas de problème car l'accès ne sera pas direct.
  • Enfin, nous allons configurer une directive de reverse-proxy pour servir cockpit depuis l'URL /webadmin/, comme évoqué plus haut. En matière d'authentification, nous allons utiliser celle de Cockpit et non celle d'Apache car les deux ne sont pas compatibles.
  • Attention, Cockpit utilise fortement des connexions en mode WebSocket, il faudra le prendre en compte dans la configuration d'Apache.

Gestion des adresses et des ports

Cockpit utilise massivement les mécanismes systemd (ce qui est bien en 2017). Par défaut, il écoute sur toutes les IPv6 sur le port 9090. Dans notre cas, nous ne souhaitons uniquement le faire tourner sur l'IPv4 locale: 127.0.0.1. Ainsi, il ne sera, de fait, pas disponibles directement depuis l'extérieur de cette machine.

Pour ce faire vous devez créer un fichier /etc/systemd/system/cockpit.socket.d/listen.conf qui contiendra les lignes suivantes:

[Socket]
ListenStream=
ListenStream=127.0.0.1:9090

Un petit coup de systemctl daemon-reload suivi d'un systemctl restart cockpit.socket devrait mettre à jour cette configuration.

Arborescence et non chiffrement

Comme évoqué plus haut, nous devons indiquer à Cockpit qu'il doit utiliser un niveau d'arborescence supplémentaire et qu'il doit accepter des connexions non chiffrées. Pour cela, il faudra créer un fichier /etc/cockpit/cockpit.conf avec le contenu suivant:

[WebService]
Origins = https://example.com http://127.0.0.1:9090
ProtocolHeader = X-Forwarded-Proto
AllowUnencrypted = true
LoginTitle = "Remote Administration Service"
UrlRoot = /webadmin/


[LOG]
Fatal = criticals warnings

La directive Origins permet d'indiquer les domaines de requête accepté. Mettez-y le nom de votre domaine (et n'oubliez pas les URL en HTTP et en HTTPS). J'ai également ajouté l'URL localhost, au cas où.

La directive AllowUnencrypted permet d'autoriser le traffic en HTTP. Cela ne posera pas de problème car ce traffic sera uniquement entre le service Apache interne et Cockpit.

Enfin, la directive UrlRoot permet d'indiquer à Cockpit qu'il est disponible au niveau de l'emplacement https://example.com/webadmin/. Cette directive lui permet d'adapter les URL internes de Cockpit à cet emplacement.

Gestion des WebSockets

Cockpit utilise des websocket, notamment pour tout ce qui est "temps réel", vous devez donc autoriser votre serveur Apache à mettre en tunnel ces requêtes. Cela se fait de manière assez simple en activant le module proxy_wstunnel:

# a2enmod proxy_wstunnel

Serveur mandataire inverse

Voici le coeur du sujet ! Pour mémoire, l'application Cockpit ne sera disponible que sur un flux chiffré (via HTTPS) à l'emplacement /webadmin/.

  # Configuration pour Cockpit
  ## Reverse proxy pour Cockpit
  <Location "/webadmin">
    ProxyPass http://127.0.0.1:9090/sysadmin
    ProxyPassReverse http://127.0.0.1:9090/sysadmin
    RequestHeader set Front-End-Https "On"
    ProxyPreserveHost On
  </Location>
  ## Reverse proxy Websocket pour Cockpit
  <Location "/webadmin/cockpit/socket">
    ProxyPass "ws://127.0.0.1:9090/sysadmin/cockpit/socket"
  </Location>

Vous pouvez noter que je n'utilise pas, contrairement à mon habitude, le module d'authentification d'Apache. C'est une condition nécessaire car l'authentification de Cockpit réutilise celle d'Apache qui pose problème dans mon cas.

Par ailleurs, nous avons besoin de deux directives Location:

  • une pour l'emplacement de base de Cockpit.
  • l'autre pour la partie WebSocket.

Après cette étape et une relance de votre service Apache, Cockpit devrait être disponible correctement à l'URL indiquée.

Une revue rapide de Cockpit

Comme je n'ai pas installé beaucoup de modules, on ne voit pas grand chose et il faut dire que, pour l'instant, Cockpit ne dispose pas de beaucoup de choses.

L'écran d'authentification est assez basique mais vous pouvez noter qu'il permet également de rebondir via SSH sur d'autres machines disponibles par la première machine. Je peux donc accéder à mon parc de bécanes à distance ce qui est un vrai plus.

L'écran d'accueil affiche quelques stats en flux continu:

Il est possible de changer la langue de l'interface (tout n'est pas traduit).

Voici le module des journaux qui est assez bien fait tout en restant simple:

Le module des services est vraiment calqué sur Systemd et c'est tant mieux: on peut voir les services et également les timers, c'est plutôt bien foutu tout en restant léger.

Enfin, le module dédié au stockage permet d'avoir des informations sur les disques des machines. Si vous avez un compte administratif, vous pouvez même créer des partitions à distance.

Et pour terminer: l'arme absolue: le Terminal:

Ce dernier est pleinement fonctionnel. Il gère la couleur et semble plus rapide que celui de ShellInABox. Par ailleurs, je note moins de problème avec les touches spéciales que dans ShellInABox. Le copier-coller passe directement sous le contrôle du navigateur web ce qui permet de faire des copier-coller plus simple, que ce soit sur le terminal web ou du terminal web vers le poste local.

Conclusions

En dehors de la galère de serveur mandataire inverse (reverse proxy), Cockpit fonctionne plutôt bien et reste relativement simple à configurer.

Comparé à ShellInABox, c'est franchement plus graphique. Les différents modules que j'ai installé répondent plutôt au besoin même si, éducation oblige, je me tournerai forcément plutôt vers le terminal. Ce dernier est d'ailleurs très intéressant et plus complet, notamment au niveau des touches, comparativement à ShellInABox. On peut à peu près utiliser Emacs dessus sans trop de problème, sauf pour la sélection.

Dans tous les cas, j'ai rapidement adopté Cockpit et je l'ai déployé sur toutes mes machines internes: ça ne coûte pas grand chose, ça ne mange pas trop de performances et puis, on ne sait jamais !

Posted lun. 09 oct. 2017 21:34:05 Tags:

Introduction

Ça devait arriver tôt ou tard mais, c'est arrivé. Depuis quelques jours, mon adresse email 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 antispam 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 antispam 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 (antispam, 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éé 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 es 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 leurs 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 floppé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 rejetté 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 oeuvre 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, grace 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 à moindre frais.

Toutefois, ce n'est pas une solution ultime et il y a fort à parier que d'ici quelques 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...

Posted mar. 24 nov. 2015 19:54:00