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

Introduction

Le DNS, c'est la base de tout Internet aujourd'hui. On ne s'en rend même plus compte mais sans DNS, pas d'Internet facile à utiliser, pas de noms sur le réseau, juste des adresses IP à taper à la main. Si écrire une adresse IPv4 reste possible, il n'en est pas de même pour une adresse IPv6 ! Le DNS est donc obligatoire. Pour ceux qui l'ignorent, le Domain Name System est une méthode permettant de faire le lien entre un nom de machine sous forme quasi-humaine en adresse IP, utilisable par un système d'exploitation. La forme quasi-humaine est une syntaxe de parcours d'arbre. Chaque branche possède un nom et le délimiteur est le caractère point ('.').

Ainsi, medspx.homenet.org indique qu'on veut accéder à la machine nommée medspx, située dans le domaine homenet, lui-même situé dans le domaine général org. Je vous invite à lire la page d'introduction de Wikipedia sur le sujet pour une introduction plus complète.

Cet article tentera de montrer comment monter son propre serveur DNS en auto-hébergement pour permettre de nommer des machines sur le réseau local ainsi que pour servir de résolveur DNS global. Au cours de ces étapes, il nous faudra faire une petite introduction sur le fonctionnement du DNS. Pour la mise en oeuvre, nous allons utiliser le vénérable serveur Bind dans la configuration de Debian Wheezy. Il existe d'autres serveurs avec des fonctionnalités différentes mais Bind reste l'implémentation de référence (pas forcément la plus performante) et vous trouverez pléthore de documentation sur son utilisation.

Avant de commencer, sachez que cet article n'a pas vocation à servir de référence. Il illustre juste la mise en place d'une configuration adaptée à un réseau local avec des règles de sécurité bien définies.

Les différents modes du DNS

Pour notre cas d'illustration, nous allons étudier deux modes de fonctionnement du DNS:

  • le serveur faisant autorité sur un domaine qui nous appartient.
  • le serveur faisant office de résolveur centralisé.

Ces deux modes sont bien différents. Commençons par le plus simple à expliquer: le serveur faisant autorité. Comme son nom l'indique, un serveur qui fait autorité est un serveur capable de faire la correspondance entre des noms complets (FQDN) et des adresses IP. Le terme d'autorité indique que le serveur est la source de référence de la correspondance. En tant que référence, il peut donc être requêté par d'autres machines pour lui demander de faire la correspondance et de renvoyer l'adresse IP. Prenons le domaine example.com. Le serveur faisant autorité est la machine capable de donner l'adresse IP de la machine a.example.com.

Le système DNS est donc composé d'un tas de serveurs qui font autorité. La "base de données" des noms/adresses IP est donc distribuée sur ces serveurs faisant autorité. Mais, en plus d'être distribué, le DNS est également hiérarchique. En effet, il faut un moyen de contacter les serveurs faisant autorité. Ce moyen se situe au niveau des domaines supérieurs. Dans example.com, com est un domaine supérieur. Le serveur qui gère le domaine com (qui fait autorité sur le domaine) connaît donc l'adresse IP du serveur DNS faisant autorité pour le domaine example.com. C'est assez simple à gérer et on voit bien l'importance de la filiation pour la résolution de noms.

Lorsque vous avez un nom de domaine (example.com) et que vous avez un serveur faisant autorité dessus, cela signifie que vous pourrez définir autant de noms de machines situées sous ce domaine. Votre serveur DNS stockera ces informations de référence et les mettra à disposition des machines sur le réseau. Si vous êtes connectés à Internet, cela signifie que potentiellement, d'autres machines pourront vous poser des questions.

A l'inverse, un résolveur est un mode de fonctionnement bien particulier qui permet de regrouper les demandes de résolution de nom en un point central. Concrètement, chaque client du réseau va demander au résolveur de faire la requête DNS pour son compte.

Pour mieux comprendre le fonctionnement d'un résolveur, étudions rapidement ce qui se passe quand on fait une requête DNS complète. A la base, la machine doit disposer d'un minimum d'informations. Ces informations de base sont les adresses IP des serveurs d'autorité de la racine. La racine, c'est le point culminant du système mondial distribué du DNS. Les serveurs (car ils sont plusieurs) de la racine connaissent les adresses IP des domaines TLD (Top Level Domain) comme .com, .org, .net, etc. Ces serveur de TLD connaissent ensuite les correspondances pour les domaines du type linuxfr.org, homenet.org, duckduckgo.com, etc. La racine n'a pas de nom, c'est juste le caractère '.'. Une adresse DNS complète est du type 'medspx.homenet.org.'. Mais l'usage veut qu'on supprime le point terminal.

Donc le résolveur doit connaître les adresses IP de la racine. Ensuite, une résolution complète prend la forme qui suit:

  • le résolveur décompose le domaine requêté (ici www.medspx.homenet.org.) Il extrait le dernier domaine (.org).
  • Il interroge la racine pour connaître l'IP du serveur DNS qui fait autorité sur le domaine .org.
  • La racine lui donne une IP.
  • Le résolveur récupère l'IP et interroge le serveur DNS qui fait autorité sur .org. Il lui demande l'adresse du DNS de homenet.
  • le DNS .org répond et donne une IP.
  • Le résolveur récupère l'IP et interroge le serveur DNS qui fait autorité sur homenet. Il lui demande l'adresse du DNS de medspx.
  • Le DNS homenet répond et donne une IP.
  • Le résolveur demande l'adresse IP de la machine www au serveur DNS faisant autorité sur medspx.
  • medspx répond et donne l'IP.
  • La résolution est terminée, le résolveur renvoie la réponse au client qui a fait la requête.

Voilà comment fonctionne la résolution DNS. On le voit, à chaque fois, on interroge la source faisant autorité. Aucune machine ne fait autorité pour tout le DNS. La distribution de l'autorité permet de distribuer les données sur de nombreuses branches d'un arbre. L'intérêt est que lorsqu'une branche est coupée, l'arbre reste vivant et on peut continuer à interroger les autres branches (d'un niveau égal ou supérieur au domaine coupé). C'est donc un modèle relativement résilient.

L'exemple typique d'un résolveur DNS est le serveur DNS de votre FAI: il fait la requête DNS complète pour vous. L'intérêt du résolveur c'est qu'il peut mettre en cache les données. On peut imaginer qu'au sein du réseau d'un FAI, ou même à l'échelle d'un réseau local, de nombreux clients fassent la même requête. Genre, quelle est l'adresse IP de debian.org ou de google.fr ? Au lieu de faire la requête complète qui interroge la racine, le domaine TLD et le domaine final, on ne fait qu'un seul passage. C'est un peu plus performant.

Toutefois, sachez que les résolveurs DNS des FAI ne sont pas parfaits. D'abord, ils peuvent tomber en panne. Ça été le cas pour le FAI Free sur le réseau duquel j'ai pu mesurer plus d'une dizaine de fois en quelques années des coupures de service DNS pendant plus de 10 minutes. Ce qui se passe est assez sidérant: plus rien de ce qui peut aller sur le réseau ne fonctionne. Mais si vous connaissez une adresse IP, vous pouvez y accéder sans problème. Autre point plus inquiétant et plus en lien avec l'actualité, les résolveurs DNS de votre FAI peuvent mentir ! Ce comportement est loin d'être anodin. En effet, comme je l'avais dit en introduction, sans DNS, pas grand chose de ce qui doit aller sur le réseau ne fonctionne ! Si votre résolveur DNS dit qu'un nom de machine n'existe pas alors qu'il existe quand même, vous n'y avez tout simplement pas accès. Pire, le résolveur DNS peut indiquer une autre adresse IP et vous diriger vers une mauvaise machine.

Le mensonge DNS est généralement la technique utilisée pour censurer des sites web: le gouvernement décide qu'un site ne doit pas être consulté et indique aux FAI l'adresse (le FQDN) à filtrer. Les résolveurs DNS des FAI renvoient une fausse adresse IP à leurs clients. Le site web disparaît. Si vous avez votre propre résolveur ce n'est pas possible (sauf en faisant du DPI mais c'est un autre sujet).

N'importe qui peut néanmoins mettre en place un résolveur complet sur sa propre machine. Il suffit de connaître les adresses IP des serveurs de la racine (la liste est publique) et le tour est joué, avec le bon logiciel. Si vous avez un serveur chez vous, vous pouvez mettre en place un service de résolution centralisée. Ce dernier fonctionnera techniquement (à peu près) comme je l'ai indiqué et ne sera pas sujet à d'éventuels mensonges.

A propos des fichiers de zone

Les fichiers de zone sont les fichiers de données du serveur. En effet, comme tout programme démon sous Unix, un serveur DNS dispose de fichiers de configuration permettant de déterminer son comportement. Mais comme nous l'avons déjà vu auparavant, le DNS sert essentiellement à faire la correspondance entre des noms et des adresses IP et il faut bien que ces données soient stockées quelque part.

Ces données sont stockées dans des fichiers de zone qu'on nomme également "resource records" (RR). Ces fichiers ont un format bien particulier qu'il convient de présenter.

  • Dans ces fichiers, les commentaires sont précédés du caractère ';'
  • Les fichiers de zone peuvent contenir des directives et des enregistrements.
  • Les directives sont peu nombreuses ($TTL, $INCLUDE, $ORIGIN).
  • Chaque enregistrement du fichier contient 5 champs séparés par un espace ou un TAB.
    • Le premier champ indique le nom du domaine ou le nom d'une machine. Il répond à trois règles:
    • si le nom est absent, on prend le nom de l'enregistrement précédent
    • le nom doit commencer au premier caractère de la ligne.
    • si le nom n'est pas un nom complet FQDN, on utilise le nom de la zone pour faire un FQDN
    • un nom FQDN complet comprend également la racine. Voilà pourquoi on ajoute toujours un point à la fin des adresses.
    • le second champ correspond à la validité de l'enregistrement plus communément dénommée Time To Live (TTL).
    • le troisième champ indique le protocole utilisé. Sur ce point, seul le protocole IN (Internet) subsiste.
    • le quatrième champ indique le type d'information de l'enregistrement. Vous pouvez trouver une liste de ces types sur cette page.
    • Enfin le dernier champ contient les données qui peuvent être réparties sur plusieurs lignes avec les délimiteurs '(' et ')'.

Avec ces 5 champs, on peut définir de nombreuses choses.

Prenons un premier exemple qui nous permettra de présenter différents types d'information (le quatrième champ):

server1        IN      A   192.168.0.2

Dans cet exemple:

  • le premier champ vaut server1, il s'agit du nom de machine
  • pour le deuxième champ, il aurait fallu trouver un TTL mais il est souvent défini en amont via la directive $TTL. En règle générale, on ne met rien.
  • on trouve ensuite la valeur IN qui indique le protocole utilisé. C'est tout le temps le même et on peut omettre ce champ. Pour des raisons historiques (et pour ne pas oublier qu'il y a 5 champs), je le laisse.
  • le quatrième champ indique A pour "Address" et permet de dire que ce qui suit est l'adresse IP qui correspond au nom de la machine.
  • le cinquième champ contient 192.168.0.2 qui est la donnée de l'enregistrement qui correspond à l'IP de la machine server1.

Étudions maintenant le type CNAME:

server2                IN        CNAME        server1

Ici, nous définissons que la machine server2 sera un alias de server1. Toute résolution de nom de server2 renverra comme réponse que server2 est un alias de server1 (si on veut l'ip de server2, il faudra faire une requête DNS pour l'IP de server1).

Enfin, il existe un type bien particulier: le type SOA. Ce dernier permet d'indiquer qu'on définit un serveur faisant autorité (master). Étudions un exemple de déclaration SOA:

@   IN  SOA dns.example.org. admin.example.org. (
          201401071     ; Numéro de série du fichier de zone
             604800     ; Refresh
          86400     ; Retry
        2419200     ; Expire
         604800 )   ; Negative Cache TTL
  • Le premier champ qui vaut détermine le nom de la zone utilisée. le symbole @ indique que le nom de la zone est issu des fichiers de configuration du serveur bind (directive zone "nom_de_zone").
  • Le TTL n'est pas indiqué
  • On retrouve notre bon protocole IN.
  • Suivi du type d'enregistrement: SOA pour "Start of Authority"
  • Enfin, on trouve les données réparties de la manière suivante:
    • (
    • numéro de série du fichier de zone (on met souvent la date du jour+l'heure)
    • le temps de rafraichissement. c'est utilisé s'il existe un serveur esclave qui doit interroger régulièrement le serveur maître.
    • le temps entre deux essais de rafraichissement.
    • le temps d'expiration: si le serveur maître n'est pas disponible pour les serveurs DNS esclaves pendant une durée supérieure à ce temps, alors le serveur esclave cesse d'être serveur d'autorité.
    • TTL minimum: c'est le temps pendant lequel vous souhaitez que les données DNS restente en cache des résolveurs externes.
    • Les temps sont exprimés en secondes.

Notre cahier des charges

Comme d'habitude, avant de mettre en place un service, il faut étudier nos besoins.

  • Il nous faut un résolveur central pour les machines de notre réseau local.
  • Ce résolveur doit pouvoir mettre en place un système de cache pour éviter les requêtes incessantes vers l'extérieur.
  • Le service réseau du résolveur ne doit être accessible qu'au réseau local.
  • En plus du résolveur nous voulons pouvoir nommer des machines sur notre réseau local.
  • Le nom de domaine ne sera disponible que sur le réseau local.
  • Le nom de domaine n'aura de signification que pour le réseau local.
  • Le service réseau du serveur DNS faisant autorité au niveau local sera accessible uniquement aux machines du réseau local.
  • Les machines du réseau local configurées par DHCP devraient obtenir une configuration automatique vers le serveur DNS.
  • Le seul moyen de configurer le serveur sera via des fichiers configuration.
  • Le serveur sera unique.

Configuration du résolveur Bind sous Debian Wheezy

Installation du paquet

Le service Bind s'installe avec peu de paquets:

# aptitude install bind9

Répartition de la configuration

Sous Debian, la configuration de Bind se retrouve dans le répertoire /etc/bind/. Elle se répartit entre les fichiers de zones et les fichiers de configuration du serveur. Ces derniers sont distribués. Pour ma part, j'ai choisi de concentrer la configuration du serveur en un seul fichier nommé named.conf. Les fichiers de zone seront créés selon le code: un fichier de zone par zone !

Une fois que la configuration a été modifiée, il faut relancer le service pour la prendre en compte via:

# service bind9 restart

Configuration du serveur

Voici la configuration du serveur que j'héberge sur mon réseau local. Nous allons la commenter ensemble pour mieux la comprendre. Pour des raisons de simplification, j'ai concentré ma configuration dans un seul fichier qui reste modeste. Je procède toujours ainsi pour les configurations qui doivent rester simple car cela permet de ne pas chercher pendant des heures quel fichier modifier.

Vous pouvez bien sûr lire la documentation de référence de Bind pour connaître la signification et la syntaxe de chaque directive.

// This is the primary configuration file for the BIND DNS server named.
//
// Please read /usr/share/doc/bind9/README.Debian.gz for information on the 
// structure of BIND configuration files in Debian, *BEFORE* you customize 
// this configuration file.
//
// If you are just adding zones, please do that in /etc/bind/named.conf.local

// On commence pas définir une acl qui permet de définir notre réseau local
// cette acl est nommée reseau_local et elle concerne le réseau 192.168.0.0/24, un réseau IPv4.
acl reseau_local { 192.168.0.0/24; };

// la directive controls permet d'indiquer comment configurer le démon
// de contrôle de Bind. Pour ma part, je ne souhaite pas que ce démon soit présent
// pour des questions de simplicité et de sécurité
controls { };


// Ici, on inclue les clefs de DNSSEC. Je ne vais pas revenir dessus car je pourrais en parler pendant des heures.
// Sachez juste que DNSSEC est une tentative de sécurisation du système DNS.
include "/etc/bind/bind.keys";

// Voici les options générales du service Bind.
options {
   // On lui indique d'écouter sur localhost (pour la résolution interne de la machine qui héberge le résolveur, eat your own dog food)
    // ainsi que sur l'ip de la machine qui écoute sur le réseau local.
   // Bien entendu, on écoute sur le port 53 car c'est la norme de base du DNS.
        listen-on { 127.0.0.1; 192.168.0.x; };
        port 53;
    // répertoire de travail du serveur
    directory "/var/cache/bind";

    // un minimum de configuration de DNSSEC
    dnssec-validation auto;

    // Le serveur 
    auth-nxdomain no;    # conform to RFC1035

    // on écoute sur le port localhost IPv6. Conclusion, le service DNS ne sera pas disponible par IPv6.
    listen-on-v6 { ::1; };

    // cette simple directive permet de dire qu'on souhaite mettre en place un résolveur.
   // Pas très compliqué !
   recursion yes;
};

// Configuration des journaux systèmes (les logs)
// Attention, cette configuration est en mode paranoïaque: tout est loggué, y compris les requêtes DNS des clients
// Les logs peuvent donc être très volumineux.
logging {
    // on commence à définir des "canaux" de logs: ce sont des définitions d'emplacement de fichiers
         channel security_warning {
         // Les avertissements de sécurité iront dans /var/log/bind/security.log
         // on garde 3 versions du fichier et on créé un nouveau fichier tous les 100ko
                 file "/var/log/bind/security.log" versions 3 size 100k;
                 severity warning;
                 print-severity  yes;
                 print-time      yes;
         };

         channel client_info {
                 file "/var/log/bind/requests.log" versions 2 size 10m;
                 severity info;
                 print-severity  yes;
                 print-time      yes;
         };

         channel bind_log {
                 file "/var/log/bind/bind.log" versions 3 size 1m;
                 severity info;
                 print-category  yes;
                 print-severity  yes;
                 print-time      yes;
         };

     // ensuite, on fait la répartition vers les fichiers en fonction de la catégorie
         category default { bind_log; };
     category client { client_info; };
     category resolver { client_info; };
     category queries { client_info; };
         category lame-servers { null; };
         category update { null; };
         category update-security { null; };
         category security { security_warning; }; 
};

// Définition des zones
// c'est notre domaine local
zone "ici" {
          // le serveur fait autorité, il est de type master.
          type master;
      // le fichier de zone est stocké dans /etc/bind/db.ici
      file "/etc/bind/db.ici";
      // localhost et les machines définies dans l'acl "reseau_local" (voir plus haut) peuvent faire des requêtes DNS sur ce serveur
       allow-query { localhost; reseau_local; };
       // On ne peut pas mettre à jour les données de ce serveur autrement que par la configuration.
       allow-update { none; };
};

// Cette zone permet de configurer la résolution DNS inverse (on donne une IP et on obtient le nom en échange).
zone "0.168.192.in-addr.arpa" {
        type master;
    file "/etc/bind/db.192.168.0";
    allow-query { localhost; reseau_local; };
    allow-update { none; };
};

// C'est ici qu'on indique le fichier contenant les IP des serveur racine.
// consultez ce fichier, vous verrez les adresses !
zone "." {
     type hint;
     file "/etc/bind/db.root";
};

// Cette "zone" permet aux clients de merde (MS-Windows) qui font des
// requêtes sur localhost, d'avoir une réponse censée (127.0.0.1 ou ::1)
// Dans le cas normal, un client normal ne fait PAS de demande de résolution de localhost
// Il sait par défaut que ça correspond à 127.0.0.1 ou ::1.
zone "localhost" {
     type master;
     file "/etc/bind/db.local";
};

// C'est la même chose que pour localhost mais en résolution inverse
zone "127.in-addr.arpa" {
     type master;
     file "/etc/bind/db.127";
};


zone "0.in-addr.arpa" {
     type master;
     file "/etc/bind/db.0";
};

zone "255.in-addr.arpa" {
     type master;
     file "/etc/bind/db.255";
};

// Include empty zones for RFC 1918 DNS queries (from fucked clients)
include "/etc/bind/zones.rfc1918";
};

On le voit, cette configuration est assez simple. Tout tient dans un seul fichier et nous avons peu de zones à servir. Intéressons-nous maintenant aux données des fichiers de zone.

Fichiers de zones

Voici quelques fichiers de zone précédemment référencés dans la configuration du serveur DNS. Je ne vais pas indiquer les fichiers de zone de base (racine, localhost, reverse localhost) mais bien les fichiers de notre configuration

/etc/bind/db.ici:

;
; Fichier de zone du réseau local ici.
;
; Le TTL global sera de 1 journée
$TTL    1d
; Notre premier enregistrement est un type SOA pour indiquer
; que le fichier gère un serveur faisant autorité.
; le nom du serveur de nom qui fait autorité est debianplug.ici.
; l'adresse email est du type email.nom_de_domaine (on remplace @ par un .).
@  IN  SOA serveur.ici. admin.ici. (
             201401071     ; Numéro de série du fichier de zone
                604800     ; Refresh
             86400     ; Retry
           2419200     ; Expire
            604800 )   ; TTL
; Indique que le nom du serveur faisant autorité pour la zone est serveur.ici.
; Si on a une configuration avec des esclaves, on peut mettre plusieurs enregistrements NS
@  IN  NS  serveur.ici.
; un enregistrement A (adresse IPv4) pour le nom "serveur"
serveur        IN  A   192.168.0.1
; un enregistrement AAAA (adresse IPv6) pour le nom "serveur"
serveur        IN  AAAA    2a21:2e35:8c57:24ef:bad:cafe:bad:caca
machinea   IN  A   192.168.0.2
machineb   IN  A   192.168.0.3
machinec   IN  A   192.168.0.4
machinec   IN  AAAA    2a21:2e35:8a57:24ef:babe:babe:babe:babe
; un alias de machined vers machinec
machined   IN  CNAME   machinec

Voici la zone inverse, stockée dans /etc/bind/db.192.168.0:

;
; Fichier de configuration de la zone DNS inverse locale
;
; Le TTL sera de 3 jours
$TTL 3d

; la zone est un peu spéciale. le "réseau inversé".in-addr.arpa.
; est une zone dédiée à la résolution inverse.
; le serveur d'autorité sera toujours notre serveur DNS.
0.168.192.in-addr.arpa.       IN      SOA     serveur.ici. admin.ici.     (
                  2014013101 ;Serial Number
                          8H ;refresh
                          2H ;retry
                          4W ;expire
                          1d)
; Le nom du serveur désservant la zone sera bien serveur.ici
@    IN    NS      serveur.ici.
; ensuite on trouve les enregistrements de type PTR (pointeur)
1         IN      PTR     serveur.ici.
2         IN      PTR     machinea.ici.
3         IN      PTR     machineb.ici.
4         IN      PTR     machinec.ici.

Sécurité

Une partie de la sécurité est gérée au niveau de fichier de configuration de Bind. En effet, on a indiqué qu'on souhaitait que le serveur soit uniquement accédé par les adresses IPv4 du réseau local 192.168.0.0/24. Néanmoins, il faut bien avoir à l'esprit que si Bind est une implémentation de référence, elle a un lourd historique de failles de sécurité.

Il serait donc vraiment indispensable de fermer le port 53 à toute machine non autorisée. Voici quelques règles pour iptables au format iptables-save:

-A INPUT -s 192.168.0.0/24 -i eth0 -p tcp -m tcp --dport 53 -j ACCEPT
-A INPUT -s 192.168.0.0/24 -i eth0 -p udp -m udp --dport 53 -j ACCEPT
-A OUTPUT -o eth0 -p udp -m udp --dport 53 -j ACCEPT
-A OUTPUT -o eth0 -p tcp -m tcp --dport 53 -j ACCEPT

Et voici celles pour ip6tables:

-A INPUT -s 2a21:2e35:8c57:24ef::/64 -i eth0 -p tcp -m tcp --dport 53 -j ACCEPT
-A INPUT -s 2a21:2e35:8c57:24ef::/64 -i eth0 -p udp -m udp --dport 53 -j ACCEPT
-A OUTPUT -o eth0 -p udp -m udp --dport 53 -j ACCEPT
-A OUTPUT -o eth0 -p tcp -m tcp --dport 53 -j ACCEPT

Globalement, on ouvre le port 53 en entrée uniquement pour les machines du réseau local. Par contre on permet à toute requête DNS vers le port 53 de sortir. En effet, notre machine est également un résolveur DNS qui doit pouvoir interroger les autres serveurs DNS de référence.

Tests de fonctionnement

Maintenant que notre configuration est prête, il faut réaliser quelques tests:

  • Voir si le résolveur local fonctionne correctement.
  • Voir si les machines de notre réseau local sont correctement identifiées.

Vous pouvez utiliser la commande dig issue du paquet dnsutils:

$ dig @ip_du_serveur_dns_à_tester nom_de_domaine_complet

Dans notre cas, voici quelques exemples:

$ dig @192.168.0.1 machinea.ici
; <<>> DiG 9.9.5-9-Debian <<>> @192.168.0.1 machinea.ici
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 43547
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 3

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;machinea.ici.     IN  A

;; ANSWER SECTION:
machinea.ici.      86400   IN  A   192.168.0.2

;; AUTHORITY SECTION:
ici.           86400   IN  NS  serveur.ici.

;; ADDITIONAL SECTION:
serveur.ici.       86400   IN  A   192.168.0.1
serveur.ici.       86400   IN  AAAA    2a21:2e35:8c57:24ef:bad:cafe:bad:caca

;; Query time: 2 msec
;; SERVER: 192.168.0.1#53(192.168.0.1)
;; WHEN: Thu Mar 26 20:32:47 CET 2015
;; MSG SIZE  rcvd: 123

On voit que la réponse est correcte: on obtient une section ANSWER avec l'adresse IP de la machine. On voit également que les renseignements sur la partie du serveur faisant autorité sont corrects. Vous pouvez également utiliser la commande host qui affiche moins de détails pour vérifier votre configuration effective de serveur DNS.

Configuration des machines clientes du réseau local

Une fois que le service DNS fonctionne correctement, il faut faire en sorte que les clients du réseau local l'utilisent. Deux méthodes sont disponibles:

  • la méthode manuelle qui consiste à configurer le poste de travail en lui donnant l'adresse IP du serveur DNS.
  • on peut demander au service DHCP d'indiquer l'adresse IP du serveur DNS.

Pour la méthode manuelle, je ne parlerai que de la distribution Debian. Vous pouvez utiliser le fichier /etc/resolv.conf pour paramétrer manuellement le service DNS. En voici un exemple:

domain ici
search ici
nameserver 192.168.0.1

Ici, le domaine auquel appartient la machine se nomme ici. Par défaut, la recherche DNS ajoutera l'extension .ici lors de la recherche des noms courts. Enfin, le serveur de nom aura pour adresse IP 192.168.0.1

Cette configuration manuelle peut également être répercutée dans le fichier /etc/network/interfaces à l'aide des quelques lignes à ajouter à la configuration d'une interface:

iface eth0 inet static
        address 192.168.0.10
        netmask 255.255.255.0
        network 192.168.0.0
        broadcast 192.168.0.255
    dns-nameservers 192.168.0.1
    dns-domain ici
    dns-search ici

Mais on peut aller plus loin et indiquer au serveur DHCP de fournir cette information aux clients qui demandent une adresse IP. Il suffit d'ajouter les lignes suivantes dans la configuration du serveur DHCP (/etc/dhcp/dhcpd.conf):

# Options communes à tous les sous-réseaux gérés par le serveur DHCP.
## Définition de la résolution de nom
option domain-name "ici";
option domain-name-servers 192.168.0.4;

Conclusion

Avec relativement peu d'effort de configuration, on peut mettre en place un service DNS efficace pour un réseau chez soi. Même si le serveur DNS Bind peut (presque) tout faire, on voit bien qu'on peut limiter son périmètre pour gérer un seul serveur DNS centralisé qui fait tout. Cette configuration est adaptée pour les petits réseaux locaux, typiquement les réseaux qu'on retrouve dans la majorité des foyers de ce pays (et d'autres).

Utiliser un vrai service de cache de résolution DNS présente l'intérêt de limiter les requêtes DNS qui partent sur Internet.

Autre intérêt, le DNS étant un élément de base, conserver les logs du DNS permet de savoir à peu près ce que font les clients du réseau local. Quand on regarde les statistiques, on voit que ça n'est pas brillant: les clients font des requêtes DNS toutes les 5 secondes pour le même domaine: il n'y a pas de cache local. Un résolveur est donc quasi-indispensable sur un réseau local.

De plus, notre configuration permet d'avoir de vraies réponses DNS (genuine): le mensonge DNS n'est pas possible (sauf avec des moyens assez complexes à mettre en oeuvre).

L'inconvénient de notre installation est qu'elle concentre en un seul point la gestion du DNS ce qui peut poser de sérieux problèmes lorsque le service devient inopérant (le serveur est cassé ou la configuration est perdue). En règle générale, pour les réseaux un peu plus sérieux, on délègue le service DNS à plusieurs machines. C'est expressément prévu dans Bind avec la notion de serveurs esclaves qui viennent régulièrement interroger le serveur maître.

Si vous souhaitez installer un serveur qui fait autorité sur Internet et non plus uniquement sur votre petit réseau local, sachez que vous DEVEZ disposer d'au moins un serveur de secours toujours disponible.

En attendant, vous pouvez vous faire la main avec la configuration de Bind en local à peu de frais...

Posted jeu. 26 mars 2015 19:46:00 Tags:

Introduction

Parfois, on veut balancer des gros fichiers (des vidéos, des archives PDF, de la musique, tout une collection de photos) d'un ordinateur à un autre. Le faire avec une clef USB ou même un disque USB prend du temps: il faut d'abord réaliser un transfert sur le périphérique intermédiaire et seulement après, on peut faire la deuxième copie. De plus, il faut avoir à disposition un câble USB et peut-être aussi une alimentation pour le disque USB intermédiaire.

Voici la méthode que j'utilise qui est finalement assez simple à mettre en oeuvre dans le cadre de ma configuration. D'abord, la majorité des machines que j'administre à la maison sont connectées au réseau local par du Wifi. De facto, elles disposent toutes d'un port Ethernet qui est le plus souvent inutilisé !

Information essentielle: les cartes réseaux de maintenant savent très bien se passer des fameux câbles Ethernet croisés qu'il fallait auparavant avoir à disposition. Donc, de facto, vous avez un connecteur Gigabit à votre disposition. Gigabit signifie que vous pouvez transférer des fichiers aux alentours de 85-90 Mo/s. Un film 720p encodé en H264 ou Webm d'environ 1,2 Go mettra donc 15 secondes pour être transféré !

Voyons comment faire sous Debian...

Configurations réseau ethernet

Le principe est simple: nous allons créer un sous-réseau manuel (192.168.1.0/24) et configurer chaque carte pour utiliser ce sous-réseau.

Voici ce que donne une partie du fichier /etc/network/interfaces pour la carte eth0 d'une machine:

# Configuration Ethernet pour la copie rapide
allow-hotplug eth0

iface eth0 inet static
      address 192.168.1.x
      netmask 255.255.255.0
      network 192.168.1.0
      broadcast 192.168.1.255

Vous pouvez également créer un fichier dédié à cette interface avec le contenu sus-cité et le placer dans /etc/network/interfaces.d/.

Il faut remplacer x par un numéro unique pour chaque machine. Pour ne pas me tromper, je prends le numéro terminal de l'adresse IP attribuée par DHCP, ce qui me permet de ne pas avoir de doublon, quoiqu'il arrive.

Configuration automatique de l'interface réseau

Le terme allow-hotplug indique que la carte réseau sera configurée automatiquement lorsqu'un câble réseau sera branché. Mais ce n'est pas le cas si vous n'avez pas installé et configuré le paquet ifplugd. Ce dernier assure le travail du "hotplug". Vous devez donc l'installer et également le configurer à minima.

Tout ce que vous avez à faire, c'est de modifier le fichier /etc/default/ifplugd pour indiquer quelles interfaces surveiller. Voici un exemple de configuration:

# This file may be changed either manually or by running dpkg-reconfigure.
#
# N.B.: dpkg-reconfigure deletes everything from this file except for
# the assignments to variables INTERFACES, HOTPLUG_INTERFACES, ARGS and
# SUSPEND_ACTION.  When run it uses the current values of those variables
# as their default values, thus preserving the administrator's changes.
#
# This file is sourced by both the init script /etc/init.d/ifplugd and
# the udev script /lib/udev/ifplugd.agent to give default values.
# The init script starts ifplugd for all interfaces listed in
# INTERFACES, and the udev script starts ifplugd for all interfaces
# listed in HOTPLUG_INTERFACES. The special value all starts one
# ifplugd for all interfaces being present.
INTERFACES="eth0"
HOTPLUG_INTERFACES="eth0"
ARGS="-q -f -u0 -d10 -w -I"
SUSPEND_ACTION="stop"

J'ai juste renseigné eth0 dans les variables INTERFACES et HOTPLUG_INTERFACES. Après avoir relancé le service, cette configuration devrait fonctionner. A noter que lorsque vous débranchez la prise, l'interface réseau reste active (sans IP forcément).

Faire la copie

Une fois la configuration réalisée, il suffit de brancher le câble réseau entre les deux machines. L'autoplug permet d'activer les cartes toutes seules.

Ensuite, scp fera le reste:

$ scp ip_machine_distante:/emplacement/des/fichiers/a/copier /repertoire/vers_où/copier

Même si SSH chiffre la communication, on a des vitesses de transfert de l'ordre de 90Mo/s...

Conclusion

Avec juste un câble ethernet, on a de quoi copier rapidement de grandes quantités de fichiers avec une configuration minimaliste...

Je ne sais pas si des câbles USB de machine à machine existent mais ça serait une bonne idée !

Posted mar. 24 mars 2015 15:10:12

Introduction

Pour gérer un agenda, il existe un standard depuis maintenant une dizaine d'années. Il s'agit du protocole CalDAV qui est normalisé par la RFC 4791. Techniquement, ce protocole n'est qu'une surcouche au protocole WebDAV qui permet "l'écriture" depuis un client HTTP vers un serveur HTTP (implémentation de la méthode PUT). Les fichiers qu'on peut écrire peuvent prendre différents formats et pour CalDAV, on stocke et on renvoie uniquement des fichiers ICS.

Il existe de nombreuses implémentations de serveur CalDAV. La plus connue et la plus aboutie étant sans doute Davical qui est l'implémentation de référence. C'est plutôt un logiciel fait pour héberger plusieurs milliers de comptes CalDAV et son utilisation implique d'employer PostgreSQL pour tout ce qui concerne le stockage des données. Mais mettre en place Davical requière pas mal de compétences d'administration système, en plus d'avoir un niveau correct en matière d'administration de base de données. Pourtant, CalDAV se prète assez aux utilisations légères. En effet, tout passe par HTTP et un évènement est généralement une donnée de quelques milliers d'octets au plus.

Fort heureusement, il existe des implémentations, certes moins complètes, mais qui ont l'intérêt de s'installer relativement facilement. Je peux citer ici les serveurs Radicale et Baikal

Dans cet article, nous allons étudier comment mettre en place un service CalDAV basé sur Baikal 0.2.7 au sein de l'environnement Debian Wheezy (je sais, Jessie va bientôt sortir mais pour la production, on repose encore pour quelques semaines/mois sur Wheezy, la distribution stable de Debian). Cette mise en place se déroulera selon quelques règles de sécurité et quelques paramétrages personnels.

Enfin, sachez que cet article n'a pas vocation à être une documentation de référence. Je n'ai pas la prétention d'être un expert du sujet. Néanmoins, les éléments présentés permettront de donner une approche plus concrète d'une installation de ce service.

A propos de Baikal et de notre cahier des charges

Commençons par les choses qui fachent: Baikal est codé en PHP ! Je sais, vous voudriez un truc plus hype (JS/Rails/Python) mais il va falloir faire avec. Il repose sur SabreDAV qui est un framework WebDAV en PHP. C'est le modèle de référence en PHP ce qui est donc plutôt une bonne chose pour Baikal.

En plus de toute l'architecture liée à la gestion de CalDAV/CardDAV, baikal propose une interface d'administration web (basée sur PHP) qui permet de le configurer assez simplement. Néanmoins, cette interface ne permet pas de consulter son agenda avec un navigateur web. Ce sera le cas pour la version 2.0 de baikal qui est actuellement en version release-candidate mais qui présente encore de nombreuses failles qui le rende non compatible avec de la production informatique.

Nous allons donc utiliser baikal dans une version 0.2.7 et voici donc notre cahier des charges:

  • Nous voulons un service CalDAV accessible depuis Internet.
  • Nous voulons un service CardDAV accessible depuis Internet.
  • Le service CalDAV ne sera accessible que via un canal de communication chiffré.
  • L'accès au service CalDAV devra reposer sur un mécanisme d'authentification éprouvé.
  • J'ai pour habitude de déléguer assez de confiance dans le module d'authentification d'Apache. C'est ce dernier qui sera utilisé comme pour toutes les applications PHP hébergées sur mon serveur auto-hébergé.
  • Le mécanisme d'authentification HTTP utilise des comptes dédiés définis dans un fichier texte et lisible par Apache.
  • L'installation devra pouvoir être réalisée sous Debian Wheezy.
  • L'interface administrateur ne sera pas disponible après l'installation.
  • Apache sera le serveur Web en entrée et le service sera accessible via un port 443 ouvert (et non un port dédié).
  • Le service sera accessible via une URL dédiée sur le serveur HTTP (via un alias).
  • Nous allons utiliser un stockage des données au format SQLite.
  • Nous n'allons pas utiliser les mécanismes de stockage de Debian pour le stockage de la base de données.
  • Le service doit être relativement modeste et ne demander que peu de ressources serveur.
  • La cible d'utilisation est d'environ 2 à 5 utilisateurs simultanés.
  • Le service CalDAV devra pouvoir être accessible aux clients CalDAV de Firefox OS. Ces derniers ne gèrent qu'une authentification de type Basic (et non Digest).
  • Comme le service sera accessible sur Internet, il faudra prendre des mesures pour empêcher les attaques d'authentification par la force brute.

Nos besoins sont donc assez limités mais ils recommandent quand même un certain niveau de sécurité.

Il faut bien noter, avant de commencer l'installation que les méthodes d'authentification de Baikal 0.2.7 sont assez légères: par défaut, elles reposent sur une authentification indépendante où le mot de passe est stocké (le hash uniquement) dans la base de données de Baikal (qui peut être au format SQLite ou MySQL). Or, ce n'est pas ce que nous voulons !

En effet, PHP est connu pour être une porte d'entrée assez ouverte en cas d'attaque publique, il n'y a qu'à voir toutes les attaques portées sur les logiciels PHP un peu connus (comme Wordpress par exemple) pour voir que tôt ou tard, le système sera cracké. Un bon frein à toutes ces attaques et ces failles, c'est de placer l'accès à ces services PHP derrière une authentification HTTP. Ce mécanisme d'authentification est géré par le serveur Web lui-même et comme il ne fait que ça, il y a assez peu d'attaques qui fonctionnent dessus. De plus, les attaques sont facilement repérables dans les logs du serveur Web ce qui n'est pas du tout le cas pour Baikal qui ne remonte même pas les erreurs d'authentification (SabreDAV non plus d'ailleurs, sauf dans des versions plus modernes).

Mais utiliser un mécanisme d'authentification HTTP va poser des problèmes à Baikal: en effet, rien n'est prévu à ce niveau pour gérer autre chose que le mécanisme d'authentification sur la base de données. Il faudra donc modifier le code de Baikal. Je vous rassure, c'est très facile, il suffit de modifier deux lignes !

Configuration du service CalDAV de baikal sous Debian Wheezy

Installation du paquet

Baikal n'existe pas sous forme de paquet pour Debian. Il faut donc télécharger le code, réaliser la configuration et créer la configuration Apache.

Pour notre cas, et pour respecter la FHS ainsi que la charte Debian, et nous allons tout mettre dans /opt/baikal/.

Néanmoins, Baikal impose d'installer quelques paquets au minimum pour fonctionner: php5, sqlite3 pour le backup des bases SQLite.

# aptitude install php5 php5-sqlite sqlite3
# cd /opt/
# wget http://baikal-server.com/get/baikal-regular-0.2.7.tgz
# tar -xzf baikal-regular-0.2.7.tgz
# mv baikal-regular baikal

Ensuite, nous devons appliquer un patch pour gérer l'authentification HTTP pour CalDAV et CardDAV. Voici le contenu de baikal-httpauth.patch:

--- baikal/Core/Frameworks/Baikal/WWWRoot/cal.php  2014-02-03 21:46:11.000000000 +0100
+++ baikal-httpauth/Core/Frameworks/Baikal/WWWRoot/cal.php    2015-03-27 16:48:30.430385115 +0100
@@ -74,7 +74,11 @@
 $server->setBaseUri(BAIKAL_CAL_BASEURI);

 # Server Plugins
-$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, BAIKAL_AUTH_REALM));
+## Setting Apache HTTP Authentication
+$apacheBackend = new \Sabre\DAV\Auth\Backend\Apache();
+$server->addPlugin(new \Sabre\DAV\Auth\Plugin($apacheBackend, BAIKAL_AUTH_REALM));
+
+#$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, BAIKAL_AUTH_REALM));
 $server->addPlugin(new \Sabre\DAVACL\Plugin());
 $server->addPlugin(new \Sabre\CalDAV\Plugin());

--- baikal/Core/Frameworks/Baikal/WWWRoot/card.php 2014-02-03 21:46:11.000000000 +0100
+++ baikal-httpauth/Core/Frameworks/Baikal/WWWRoot/card.php       2015-03-27 16:52:29.366387339 +0100
@@ -71,7 +71,11 @@
 $server->setBaseUri(BAIKAL_CARD_BASEURI);

 # Plugins 
-$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, BAIKAL_AUTH_REALM));
+## Setting Apache HTTP Authentication
+$apacheBackend = new \Sabre\DAV\Auth\Backend\Apache();
+$server->addPlugin(new \Sabre\DAV\Auth\Plugin($apacheBackend, BAIKAL_AUTH_REALM));
+
+#$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, BAIKAL_AUTH_REALM));
 $server->addPlugin(new \Sabre\CardDAV\Plugin());
 $server->addPlugin(new \Sabre\DAVACL\Plugin());

Il reste maintenant à appliquer les patchs aux bons fichiers et à gérer les droits d'accès:

# cd /opt/
# patch -p0 < baikal-httpauth.patch
# chown -R www-data:www-data /opt/baikal

Maintenant, nous pouvons enchaîner avec la configuration d'Apache.

Modification de la configuration d'Apache

La documentation officielle de Baïkal recommande l'utilisation d'un Virtualhost dédié (sous la forme dav.mydomain.com). Pour ma part, ce n'est pas ce que je souhaite. En effet, créer un sous-domaine dédié est un truc hasbeen ! Si si, les trucs qui commencent par www. quelquechose en 2015 ont peu d'intérêt. Si je tape une URL dans un navigateur web, c'est bien pour accéder au service web qui se fait sur un port dédié (le 80 ou le 443). Pas besoin de préciser quel service je veux. Pourquoi pas un mail.mydomain.com, un imap.mydomain.com, un pendant qu'on y est ? Pour ma part, il faut que le service soit disponible sur une URL du type: http://mydomain.com/calendar/. Pour y parvenir, nous allons donc mettre en place un Alias sous Apache.

Il faut également retenir que Baïkal recourre fortement aux fichiers d'Override (.htaccess). Cette gestion n'est pas terrible. En effet, sur un serveur avec des ressources limitées, il vaut mieux ne pas avoir de fichier .htaccess qui doivent être analysés à chaque ouverture de page. De plus, pour ma part, je préferre les configurations centralisées: tout dans le minimum de fichiers. Ça permet de retenir simplement dans quel fichier va se trouver le problème et de plus, tout est disponible dans un simple éditeur de texte, directement sous la main, sans avoir à faire de grep pour trouver dans quel fichier on a foutu la conf de telle partie du site web.

Bien sûr, vous me direz: "Mais comment je fais pour désactiver uniquement le service CalDAV et pas le reste ?". C'est simple, tu lis l'unique fichier de conf et tu mets à jour les lignes concernées ! C'est plus long que de taper a2dissite baikal certes mais ça te permet de te plonger dans la conf globale d'Apache. En plus, on peut trouver des solutions moins élégantes pour gérer un Alias dans un fichier dédié.

Pour la partie TLS, je vais considérer que vous mettez à jour un fichier de configuration d'Apache qui gère déjà ça !

Voici la configuration d'Apache à ajouter dans votre fichier de configuration centralisé, dans un virtualhost qui gère TLS !

        # Gestion de Baikal Caldav Server
        Alias /caldav /opt/baikal/html
        <Directory /opt/baikal/html>
          # Au cas où, on impose d'être dans un truc chiffré.
      # Si vous avez bien configuré Apache, vous ne devriez pas en avoir besoin
      SSLRequireSSL
          Options -Indexes FollowSymLinks
          AllowOverride All
          Order allow,deny
          Allow from all
          AuthType Basic
          AuthName "Baikal authentication"
          AuthUserFile /etc/apache2/webdav-users
          Require valid-user
        </Directory>

Pour la gestion des comptes, il vous faut le fameux fichier /etc/apache2/webdav-users. Vous pouvez le produire en utilisant htpasswd et surtout, en lisant la documentation officielle d'Apache 2.2 sur le sujet...

N'oubliez pas de relancer le service pour prendre en compte la configuration:

# service apache2 restart

Configuration

La configuration de Baikal s'effectue en ligne en utilisant un questionnaire accessible directement via le serveur HTTP sous forme de page HTML dédiée. Je déteste ce mode d'installation car il ne me semble pas sécurisé et qu'en plus, il faut que le développeur du logiciel code un "Wizard" d'installation. En plus, cette méthode brouille les cartes de l'administrateur système. En effet, rien ne vaut la création du fichier de configuration directement à la main: ça permet d'abord de s'en souvenir pour plus tard. Ça permet également de documenter la manière de faire. La méthode "Wizard" doit documenter en plus, l'activation du mode Wizard ainsi que présenter les différents éléments de configuration de l'interface d'installation.

Pour accéder à l'interface d'administration, vous devez d'abord créer un fichier spécifique dans l'arborescence de l'installation:

# touch /opt/baikal/Specific/ENABLE_INSTALL

Ensuite, rendez-vous sur l'URL d'installation: https://votreserveur.votredomaine/caldav/admin/

Une fois sur la page renseignez les valeurs adaptées. Voici celles que j'ai adopté en fonction du cahier des charges sus-cité:

  • Server TimeZone: Europe/Paris
  • Enable CalDAV: True
  • Enable CardDAV: True
  • WebDAV authentication type: Basic (en fait ça n'a aucune importance)
  • Admin password: mettez un mot de passe dédié, son hash est stocké dans le fichier de configuration.

La deuxième page vous demande où stocker le fichier de base de données SQLite. Pour ma part, je place toutes les DB dans un répertoire spécifique de mon serveur: /var/local/db. L'emplacement sera donc /var/local/db/baikal.sqlite.

Ensuite, vous devez copier le fichier de base de données au bon endroit (Baikal ne le fait pas pour vous, cette feignasse !):

# cp /opt/baikal/Core/Resources/Db/SQLite/db.sqlite /var/local/db/baikal.sqlite
# chown www-data:www-data /var/local/db/baikal.sqlite

Vous pouvez continuer l'installation en retournant dans la page d'administration. Vous devez aller modifier des variables dans la page intitulée "System settings". Pour que votre installation fonctionne correctement, il reste un dernier facteur à modifier. En effet, je vous avais dit que Baikal se basait sur une installation par VirtualHost. Or, ce n'est pas ce que nous avons configuré. Il faut donc modifier encore un paramètre de configuration pour prendre en compte cet élément. Ce paramètre est l'URI de base des services CalDAV et CardDAV. (CalDAV base URI et CardDAV base URI). Les valeurs doivent refléter l'URI d'accès à notre service soit:

  • CalDAV base URI: "/caldav/cal.php"
  • CardDAV base URI: "/caldav/card.php"

Pour la gestion des comptes, nous allons nous appuyer sur l'interface administrateur. En effet, c'est un moyen simple de créer des comptes en initialisant les éléments dans la base de données. Vous devez faire en sorte que les identifiants des comptes soient identiques aux comptes que vous avez créé dans le fichier /etc/apache2/webdav-users. Pour le mot de passe, vous pouvez mettre le même mot de passe que pour Apache mais ce n'est pas obligatoire. En effet, le patch que nous avons appliqué permet d'accéder aux données en utilisant simplement l'identifiant du compte, Baikal ne vérifie pas le mot de passe.

Une fois que les créations de comptes sont effectuées, il faut désactiver l'accès administrateur. On peut le faire depuis l'interface d'administration mais vous pouvez simplement modifier le contenu de /opt/baikal/Specific/config.php avec ce qui suit (le hash pour le mot de passe administrateur sera forcément différent chez vous):

# Timezone of your users, if unsure, check http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
define("PROJECT_TIMEZONE", 'Europe/Paris');

# CardDAV ON/OFF switch; default TRUE
define("BAIKAL_CARD_ENABLED", TRUE);

# CalDAV ON/OFF switch; default TRUE
define("BAIKAL_CAL_ENABLED", TRUE);

# WebDAV authentication type; default Digest
define("BAIKAL_DAV_AUTH_TYPE", 'Basic');

# Baïkal Web Admin ON/OFF switch; default TRUE
define("BAIKAL_ADMIN_ENABLED", FALSE);

# Baïkal Web Admin autolock ON/OFF switch; default FALSE
define("BAIKAL_ADMIN_AUTOLOCKENABLED", TRUE);

# Baïkal Web admin password hash; Set via Baïkal Web Admin
define("BAIKAL_ADMIN_PASSWORDHASH", '76dee1f1b9bc3e6de0ab63eb706a4ae7');

Pour mémoire, voici le contenu de /opt/baikal/Specific/config.system.php:

# PATH to SabreDAV
define("BAIKAL_PATH_SABREDAV", PROJECT_PATH_FRAMEWORKS . "SabreDAV/lib/Sabre/");

# If you change this value, you'll have to re-generate passwords for all your users
define("BAIKAL_AUTH_REALM", 'BaikalDAV');

# Should begin and end with a "/"
define("BAIKAL_CARD_BASEURI", "/caldav/card.php/");

# Should begin and end with a "/"
define("BAIKAL_CAL_BASEURI", "/caldav/cal.php/");

# Define path to Baïkal Database SQLite file
define("PROJECT_SQLITE_FILE", "/var/local/db/baikal.sqlite");

# MySQL > Use MySQL instead of SQLite ?
define("PROJECT_DB_MYSQL", FALSE);

# MySQL > Host, including ':portnumber' if port is not the default one (3306)
define("PROJECT_DB_MYSQL_HOST", '');

# MySQL > Database name
define("PROJECT_DB_MYSQL_DBNAME", '');

# MySQL > Username
define("PROJECT_DB_MYSQL_USERNAME", '');

# MySQL > Password
define("PROJECT_DB_MYSQL_PASSWORD", '');

# A random 32 bytes key that will be used to encrypt data
define("BAIKAL_ENCRYPTION_KEY", 'a178a328f480c55bff702e60a3579c93');

# The currently configured Baïkal version
define("BAIKAL_CONFIGURED_VERSION", '0.2.7');

On y retrouve bien nos URI ainsi que l'emplacement de la base de données.

Sécurisation de l'authentification

Je ne vais pas revenir sur ce sujet plus en détails, il suffit de lire le cahier des charges et la partie sur l'installation de baikal pour voir comment faire pour donner l'accès uniquement aux client autorisés par une authentification HTTP.

Mais, il nous faut également lutter contre les attaques d'authentification par la force brute. Pour ce point précis, j'utilise Fail2ban qui s'en sort plutôt pas mal même s'il présente de vrais problèmes (pas de support IPv6 notamment).

Voici le fichier de règles fail2ban à appliquer. Il se nomme /etc/fail2ban/filter.d/apache-auth.conf et il c'est celui par défaut du paquet fail2ban de Debian.

# Fail2Ban configuration file
#
# Author: Cyril Jaquier
#
# $Revision$
#

[INCLUDES]

# Read common prefixes. If any customizations available -- read them from
# common.local
before = apache-common.conf

[Definition]

# Option:  failregex
# Notes.:  regex to match the password failure messages in the logfile. The
#          host must be matched by a group named "host". The tag "<HOST>" can
#          be used for standard IP/hostname matching and is only an alias for
#          (?:::f{4,6}:)?(?P<host>[\w\-.^_]+)
# Values:  TEXT
#
failregex = ^%(_apache_error_client)s user .* (authentication failure|not found|password mismatch)\s*$

# Option:  ignoreregex
# Notes.:  regex to ignore. If this regex matches, the line is ignored.
# Values:  TEXT
#
ignoreregex =

On voit bien que tout problème sur l'authentification HTTP Apache sera pris en compte, que ce soit par rapport à un utilisateur non existant ou un problème de mot de passe. Ce filtre est générique pour l'authentification Apache, il ne cible pas le service CalDAV en particulier. On pourrait créer un fichier dédié en ajoutant le motif de recherche de l'URL de votre service CalDAV mais pour ma part, je pense qu'il vaut mieux ne pas créer un énième fichier de filtre si peu différent de l'original (pour ne pas compliquer la maintenance).

Et voici maintenant comment appliquer la surveillance en se basant sur le filtre précédent, à mettre dans le fichier /etc/fail2ban/jail.local:

[apache-http-auth]
enabled  = true
port     = http,https
filter   = apache-auth
logpath  = /var/log/apache2/error.log
maxretry = 3
action   = %(action_mwl)s

Pour ma part, l'action action_mwl envoie un email avec les lignes de logs incriminées.

Sauvegarde et restauration

Dans la sauvegarde, il faut prévoir beaucoup de choses. D'abord, il faut gérer la configuration de l'application. Pour notre cas, elle se trouve dans /opt/baikal/Specific/. Mais vu que Baïkal occupe peu de volume, on peut se retrancher assez facilement vers la sauvegarde de l'ensemble du répertoire /opt/baikal/.

La configuration Apache sera sauvegardée en copiant le fichier de configuration.

Il reste les bases de données SQLite. On pourrait être tenté de faire une copie directe des fichiers, après tout, SQLite n'est jamais qu'un fichier. Mais ce n'est pas la bonne manière de le faire. Il faut utiliser la méthode de backup online. Pour se faire, un simple script Bash armé de l'utilitaire sqlite3 fait l'affaire:

    # sqlite3 /var/local/db/baikal.sqlite ".backup /var/local/db/backup/baikal.sqlite"

Bien entendu, étant donné que j'ai plusieurs bases de données SQLite sur cette machine, je dispose du script suivant qui gère toute la chaîne de sauvegarde (/usr/local/bin/sqlite_backup.sh):

#!/bin/bash

# Script to correctly backup SQLite3 databases
# Copyright 2015, Médéric RIBREUX <mederic.ribreux@medspx.fr>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.This script is in GPLv3

# Some variables
SQLITEBIN="/usr/bin/sqlite3"
CONFFILE="/etc/sqlite_backup.conf"

# Verify we have everything
CMDS="sqlite3 awk"
for i in $CMDS
do
  command -v $i >/dev/null && continue || { echo "You need to install $i to launch this script..."; exit 1; }
done

# Verify if config file exists
if [ ! -f "$CONFFILE" ]; then
    echo "Configuration file cannot be found !"
    exit 2
fi

# Verify if SQLite3 exists
if [ ! -f "$SQLITEBIN" ]; then
    echo "SQLite3 is not the right path to sqlite3 !"
    exit 2
fi

# Parse config file and do main loop
ERRORCODE=0
while read v; do
    case $v in
        '#'*) continue;;
        *) src=${v%% *}
       dst=${v#* }
       $SQLITEBIN ${src} ".backup ${dst}"
       RETURNVAL=$?
       if [ "$RETURNVAL" -gt "0" ]; then
            ERRORCODE=$RETURNVAL
       fi
       ;;
    esac
done < "$CONFFILE"

# End
exit "$ERRORCODE"

Le script va puiser dans un fichier de configuration la liste des bases de données à sauvegarder. Ce fichier se nomme /etc/sqlite_bakcup.conf. Voici un exemple de contenu:

# SQLite Databases to backup
/var/local/db/baikal.sqlite /var/local/db/backup/baikal.sqlite

Pour automatiser la sauvegarde (ce qui est indispensable: sauvegarde non automatisée = sauvegarde non faite), on peut ajouter une crontab. Pour ma part, je la place dans la crontab du système, c'est-à-dire: /etc/crontab:

# SQLite backups on every saturdays at 9am
0 9 * * 6   root    /usr/local/bin/sqlite_backup.sh

Configuration pour des clients Firefox OS

J'ai un smartphone sous Firefox OS 2.0 (un geeksphone Revolution). Il peut se connecter à un serveur CalDAV. Mais comme tous les smartphones, il est forcément limité. Dans notre cas, il ne gère pas l'authentification en mode Digest.

Désactiver l'authentification Digest et utiliser l'authentification Basic. Comme nous travaillons dans une session TLS, le mot de passe sera envoyé de manière chiffrée. Comme d'habitude, le mode Digest n'est pas bien géré par les clients Web et c'est bien dommage.

Pour se connecter, il suffit d'utiliser l'URL suivante: https://votreserveur.votredomaine/caldav/cal.php/calendars/identifiant_utilisateur/default/

Attention, le client CalDAV de FirefoxOS est assez basique: il ne gère pas la synchronisation avec le mode offline. Donc pour créer un évènement sur le serveur Baikal, il faut absolument pouvoir s'y connecter. Espérons qu'avec le temps on puisse transférer facilement des évènements du calendrier local vers un calendrier en ligne...

Conclusion

Baikal permet de monter un service CalDAV/CardDAV de manière assez simple en très peu de temps. Ses besoins en termes de performances permettent de le faire tourner sur une configuration légère telle que celles qu'on peut retrouver sur les plugcomputers ou sur les cartes SOC embarquées comme le Raspberry Pi ou ses nombreux clones plus performants.

Nous avons vu que l'aspect sécurité ne doit pas être négligé et qu'il implique de modifier très légèrement le source de Baikal. Muni d'une authentification HTTP effectuée par le serveur Web, on peut espérer qu'il soit plus résistant aux attaques venus d'Internet et qui sont fortement susceptibles de se produire.

Dans les faits, il est donc possible de bénéficier d'un service d'agenda en ligne sans recourrir au cloud ! En plus, ce serveur CalDAV fonctionne bien avec FirefoxOS, ce qui est, en 2015, plutôt une bonne nouvelle.

Posted mar. 17 mars 2015 20:15:14

Un Plugcomputer est décidément un matériel bien spécial... Depuis le début, j'ai toujours eu des problèmes de boot sur mon Sheevaplug. Concrètement cela se manifeste par le fait qu'après le reboot de la machine, un certain nombre de services ne sont pas lancés. Il faut se connecter sur la machine pour relancer manuellement les services. Cette relance manuelle ne pose aucun problème: tous les services se lancent correctement. Mais c'est assez pénible et on se met à craindre chaque reboot de la machine qui, chez moi, indique une panne de courant.

De plus, ce problème est très variable: parfois, deux services sont non lancés, parfois, c'est juste un seul... Pas facile à débugger ! J'ai essayé de jouer sur les priorités de lancement mais sans succès (du moins, avec un succès restreint).

Dernièrement, je me suis battu avec Apache. A chaque reboot, le service n'était pas lancé. Rien dans les logs... la relance manuelle via SSH ne posait jamais de problème. J'ai essayé d'y voir un peu plus clair en trickant le fichier d'init (cette machine ne bénéficie pas de systemd) mais tout ce que je pouvais constater, c'est que le service ne se lance pas et ce, sans aucun message d'erreur (même en balançant toute les sorties vers un fichier de log). Le symptôme est le suivant: parfois /usr//lib/apache2/mpm-prefork/apache2 échoue sans erreur. A ce stade, on ne pas faire grand chose: impossible d'avoir des informations sur ce qui plante.

Ce symptôme qui est un vrai problème est, à mon avis, dû au fait qu'une machine basée sur un SOC et qui utilise un périphérique de masse lent comme une carte SD est assez mal ordonnancée. Je suppose que certains systèmes indispensables à Apache ne sont pas correctement initialisés au moment du boot. Comme il m'est impossible de diagnostiquer plus avant, j'ai imaginé une solution de contournement à peu de frais...

Pourquoi pas retarder le lancement des services utilisateurs avec une simple commande sleep ? J'ai donc conçu un script sysv qui fait tout simplement ça. En voici le contenu (je l'ai mis dans /etc/init.d/slowboot):

#!/bin/sh

### BEGIN INIT INFO
# Provides:        slowboot
# Required-Start:  $network
# Required-Stop:   $network
# Default-Start:   S
# Default-Stop:    0 6
### END INIT INFO

PATH=/sbin:/bin:/usr/sbin:/usr/bin

. /lib/lsb/init-functions

# Sheevaplug boots too fast for himself.
# Some services cannot be launched correctly if you don't
# make a pause to the booting process.
# This scripts just wait 15 seconds !
NAME=slowboot

case $1 in
     start|force-reload|restart)
    log_action_begin_msg "Slowing done boot !"
        sleep 15
        log_action_end_msg 0
        ;;
     stop)
    exit 0
        ;;
     status)
    exit 0
     ;;
     *)
        echo "Usage: $0 {start|stop|restart}"
    exit 2
    ;;
esac

Pour l'activer, lancer la commande suivante:

# update-rc.d slowboot start 14 S 

Cela permet de l'activer au moment du lancement des scripts /etc/rcS.d (les scripts systèmes lancés avant le niveau précisé par inittab) à la position 14 (chez moi, ça correspond à peu près après le lancement du réseau).

Une fois activé, mes problèmes ont disparu. 15 secondes sur une séquence de boot d'environ 2 minutes, ce n'est franchement pas la mer à boire. En tout cas, je préfère une relancement retardé de 15 secondes où les services sont tous lancés que d'avoir à me reconnecter à distance pour les réactiver manuellement.

Posted dim. 14 déc. 2014 12:15:50 Tags:

Un des points les plus importants pour améliorer la sécurité de votre système GNU/Linux est d'être sûr d'appliquer les correctifs de sécurité fournis par votre distribution.

Si vous administrez une distribution sous Debian stable, ces mises à jours sont peu fréquentes. Néanmoins, elles peuvent survenir de manière inopinée. Cette année, il y a eu ShellShock et quelques problèmes sur OpenSSL. Bien entendu, si vous faîtes un peu de veille technologique, vous serez au courant. Pour autant, difficile de se concentrer sur toutes les failles de sécurité qui concernent vos machines de production qui peuvent avoir des paquets différents d'une machine à l'autre.

Certains diront qu'on peut configurer des mises à jour automatiques sans interaction. Pourtant, sur des machines de production, je ne recommande pas cette stratégie. En effet, une machine de production doit être stable et la mise à jour des paquets doit se faire uniquement après des tests concluants. Sous Debian, il est très rare qu'une mise à jour de sécurité pose problème mais ça peut arriver et là, c'est le drame.

Le vrai défi est donc d'avoir une alerte qui indique lorsqu'il y a une mise à jour et ensuite, on peut réaliser les tests sur des machines de pré-production pour voir s'il n'y a pas de régressions. Donc, ce qu'on veut, c'est juste avoir l'information qu'une mise à jour existe et non d'appliquer aveuglément la mise à jour.

Une des solutions que j'ai retenue pour ce problème est d'utiliser le paquet apticron. Ce dernier est un simple script cron qui se lance tous les jours et qui va réaliser un apt-get update et voir s'il y a des paquets à installer. Si c'est le cas, il vous envoie un mail. Le principe est donc simple et très efficace.

Sa mise en oeuvre l'est également. Il suffit d'installer le paquet apticron de la manière suivante:

# aptitude install -R apticron

Par défaut, apticron recommande apt-listchanges mais à l'usage, ce paquet n'est pas nécéssaire. Voilà pourquoi je fais une installation minimaliste.

Ensuite, il faut juste renseigner l'adresse email qui va recevoir l'alerte dans le fichier de configuration situé dans /etc/apticron/apticron.conf:

# apticron.conf
#
# set EMAIL to a space separated list of addresses which will be notified of
# impending updates
#
EMAIL="mederic.ribreux@medspx.fr"
...

Le reste du fichier de configuration sert à customiser un peu le comportement d'apticron ou le contenu du mail qui sera envoyé. Pour ma part, je n'ai rien changé.

Une fois mis en service, le script cron est disponible dans /etc/cron.d/apticron. Son contenu est très simple à comprendre:

44 * * * * root if test -x /usr/sbin/apticron; then /usr/sbin/apticron --cron; else true; fi

Toutes les heures, lors de la 44ème minute, le script se lance. Par défaut, l'option --cron permet à apticron de ne pas envoyer de courriel plus d'une fois par jour. Cette configuration par défaut me convient bien.

A chaque mise à jour disponible sur les paquets de mes machines, je reçois une alerte par courrier électronique. Après, c'est à moi de voir ce que je dois faire... Mais au moins, j'ai l'information dans un délai raisonnable.

Posted sam. 13 déc. 2014 19:32:00 Tags: