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:

Introduction

Dès que vous posez un serveur sur Internet, vous vous récupérez un paquet de requêtes frauduleuses. Par frauduleuses, j'entends une transmission de paquets destinés à récupérer un accès non autorisé ou de faire un Denial-Of-Service. On peut aussi le résumer à une attaque venue du réseau Internet car c'est bien de cela dont il s'agit.

Si vous analysez vos logs, vous verrez une tonne de lignes qui rejettent des accès sur vos différents services publics. Sur les systèmes sous GNU/Linux, votre distribution se doit de maintenir la sécurité en vous fournissant des mises à jour adaptées. Et heureusement... Il n'y a pas si longtemps, la faille shellshock est sortie et à peine quelques heures après, je pouvais lire des lignes d'attaques dans mes logs Apache. Je peux également repérer des attaques qui tentent d'exploiter des failles de sécurité sur des logiciels, par exemple, les attaques sur Wordpress sont légion. Je peux même affirmer que près de 90% des erreurs que je rencontre sur mon serveur web sont des tentatives d'accès frauduleux.

Il faut relever que ces attaques sont portées sur vos services publics pour lesquels vous avez une porte ouverte au sein de votre pare-feu. Même si vous disposez de règles de pare-feu bien ciselées, il y a de grandes chances que vous soyez vulnérables à toute attaque de type brute force password cracking. Même si votre distribution va s'occuper pour vous (enfin, ses contributeurs) de corriger les failles de sécurité rapidement, sachez que vous ne serez pas toujours à l'abri, même si on peut considérer dans la pratique que vous êtes couverts dans 99% des cas. C'est que tout le monde a pu constater sur la faille Shellshock où la publication des correctifs a pris près d'une semaine avant de couvrir complètement le problème.

Il faut donc se prémunir de ce genre d'attaque. Une parade simple et efficace consiste à repérer ses tentatives d'accès frauduleux et de couper purement et simplement l'accès des machines distantes à votre machine publique via le pare-feu. De plus, il est bon de déterminer la fréquence d'attaque. A moins de surveiller en permanence vos logs manuellement, c'est une information difficile à obtenir. De plus, des attaques peuvent cibler votre machine pendant un moment donné comme dans le cas d'un denial of service et il vous faudra être alerté de ce problème pour pouvoir réagir en conséquence.

Bien entendu, comme ce problème d'attaque est devenu le quotidien pour les machines exposées sur Internet, des outils ont été élaborés pour tenter de le gérer. Aujourd'hui, je vais exposer l'outil fail2ban qui permet de répondre, au moins en partie, aux tentatives d'accès frauduleux.

Concepts de fail2ban

Pour résumer, fail2ban est un analyseur de logs à actions. Globalement, il lit vos fichiers de logs et en fonction de leur contenu, il lance des actions. Ces actions sont généralement des choses comme des modifications de règles de pare-feu pour éviter que l'adresse IP distante puisse disposer d'un quelconque accès à votre serveur public.

Pour détecter une attaque dans les logs, fail2ban utilise des filtres. Il s'agit d'expression régulières un peu spécifiques qui sont calquées sur les messages des logs. Elles permettent de remonter quel service est touché, quelle adresse IP tente un accès frauduleux, la date et l'heure, etc.

Quand fail2ban détecte une attaque, il lance des actions. Ces dernières sont définies dans des fichiers de configuration spécifiques. Elles peuvent se cumuler et prendre en compte des paramètres relevés dans les filtres (ou dans d'autres fichiers de configuration). L'action par défaut de fail2ban consiste à modifier les règles de pare-feu via iptables en interdisant à l'adresse IP de l'attaquant de se connecter sur le serveur requêté.

Bien entendu, il faut lier des filtres à des actions car on ne va pas réagir de la même manière à une tentative de relais de spam sur un serveur SMTP qu'à un denial-of-service sur un serveur web. Ce regroupement s'appelle un "jail". La plupart du temps, on définit un jail par type de service (un pour sshd, un pour httpd). On peut également définir un jail spécifique pour isoler par exemple les tentatives d'accès frauduleux sur une authentification HTTP et un autre pour réagir aux attaques sur une partie spécifique d'un site web. Dans ce cas, on définira deux jails différents car les filtres employés seront différents.

Petite précision pour terminer, fail2ban est écrit en Python. C'est un point intéressant car sur une machine aux performances limitées, le lancement de fail2ban aura une conséquence en termes de temps de traitement.

Spécificités de la configuration Debian

Par défaut, Debian livre des fichiers de configuration situés dans /etc/fail2ban/. Les filtres sont stockés dans des fichiers de configuration spécifiques hébergés dans /etc/fail2ban/filter.d/. Les actions sont rangées dans /etc/fail2ban/action.d/. Les jails sont définis dans un seul fichier de configuration nommé /etc/fail2ban/jail.conf. Enfin, il existe un fichier de configuration générale de fail2ban nommé /etc/fail2ban/fail2ban.conf.

Par ailleurs, les mainteneurs Debian du paquet fail2ban livrent un ensemble de fichiers de référence dont l'extension se termine par .conf. La recommandation est de ne pas toucher à ces fichiers et de construire votre propre configuration dans des fichiers dont l'extension se termine par .local. Ces fichiers se substituent aux fichiers .conf. Ainsi, si vous désirez configurer vos propres jails, il est recommandé de créer le fichier /etc/fail2ban/jail.local. Vous pouvez copier le contenu du fichier .conf qui correspond (jail.conf) et le customiser à votre sauce, seule votre configuration sera appliquée.

Vous pouvez faire la même chose pour les fichiers de filtre ou les fichiers d'action. Cette méthode élégante permet de customiser simplement fail2ban sans modifier la configuration des mainteneurs Debian du paquet.

Principes de constitution d'un jail fail2ban

Il est maintenant temps de visualiser comment se construit un jail fail2ban. Nous allons passer en revue un fichier de filtre, un fichier d'action et enfin la définition d'un jail.

Les filtres

Pour mieux comprendre, voici le contenu du fichier de filtre /etc/fail2ban/filter.d/sshd.conf:

# Fail2Ban configuration file
#
# Author: Cyril Jaquier
#
# $Revision$
#
[INCLUDES]
# Read common prefixes. If any customizations available -- read them from
# common.local
before = common.conf

[Definition]
_daemon = sshd
# Option: failregex
# Notes.: regex to match the password failures 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 = ^%(__prefix_line)s(?:error: PAM: )?Authentication failure for .* from <HOST>\s*$
            ^%(__prefix_line)s(?:error: PAM: )?User not known to the underlying authentication module for .* from <HOST>\s*$
            ^%(__prefix_line)sFailed (?:password|publickey) for .* from <HOST>(?: port \d*)?(?: ssh\d*)?$
            ^%(__prefix_line)sROOT LOGIN REFUSED.* FROM <HOST>\s*$
            ^%(__prefix_line)s[iI](?:llegal|nvalid) user .* from <HOST>\s*$
            ^%(__prefix_line)sUser .+ from <HOST> not allowed because not listed in AllowUsers$
            ^%(__prefix_line)sauthentication failure; logname=\S* uid=\S* euid=\S* tty=\S* ruser=\S* rhost=<HOST>(?:\s+user=.*)?\s*$
            ^%(__prefix_line)srefused connect from \S+ \(<HOST>\)\s*$
            ^%(__prefix_line)sAddress <HOST> .* POSSIBLE BREAK-IN ATTEMPT!*\s*$
            ^%(__prefix_line)sUser .+ from <HOST> not allowed because none of user's groups are listed in AllowGroups\s*$

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

On peut observer que la partie la plus importante du fichier de filtre est la définition de la variable failregex. C'est ici qu'est le cœur du filtre. La regexp est basée sur les expressions rationnelles Python et notamment sur les groupes de capture nommés. C'est le cas de tout ce qui est de la forme (?P<name>...). Globalement, cette expression capture le contenu entre parenthèse et le place dans un groupe nommé name.

Intéressons-nous à l'expression %(__prefix_line)s. Cette dernière est une chaîne de caractères dont le contenu est dynamique. Ici, c'est la variable nommée __prefix_line qui sera affichée à la place de l'expression. Cette dernière est au format Python ConfigParser.

Pour comprendre la totalité de l'expression, il faut disposer du contenu du fichier common.conf qui définit un certains nombre de variables par défaut dont __prefix_line. Une variable attire l'attention. Il s'agit de <HOST>. Il est tellement courant de filtrer sur une adresse IP que fail2ban inclue cette variable dont la valeur contient en fait (?:::f{4,6}:)?(?P<host>[\w\-.^_]+), ce qui permet de "matcher" une adresse IPv4 (y compris si elle est incluse dans une IPv6) ainsi qu'un nom de machine (FQDN ou local).

Enfin, on peut voir que l'expression contient plusieurs expressions à la suite des autres. Cela signifie que toute expression qui sera vérifiée validera le filtre (et déclenchera l'action).

Les actions

Voyons maintenant le contenu d'un fichier d'action. Il s'agit de /etc/fail2ban/action.d/iptables-multiport.conf qui est le fichier d'action par défaut, comme nous le verrons plus tard.

# Fail2Ban configuration file
#
# Author: Cyril Jaquier
# Modified by Yaroslav Halchenko for multiport banning
# $Revision$
#

[Definition]
# Option: actionstart
# Notes.: command executed once at the start of Fail2Ban.
# Values: CMD
#
actionstart = iptables -N fail2ban-<name>
              iptables -A fail2ban-<name> -j RETURN
              iptables -I <chain> -p <protocol> -m multiport --dports <port> -j fail2ban-<name>

# Option: actionstop
# Notes.: command executed once at the end of Fail2Ban
# Values: CMD
#
actionstop = iptables -D <chain> -p <protocol> -m multiport --dports <port> -j fail2ban-<name>
             iptables -F fail2ban-<name>
             iptables -X fail2ban-<name>

# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck = iptables -n -L <chain> | grep -q fail2ban-<name>

# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j DROP

# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
actionunban = iptables -D fail2ban-<name> -s <ip> -j DROP

[Init]
# Defaut name of the chain
#
name = default

# Option: port
# Notes.: specifies port to monitor
# Values: [ NUM | STRING ] Default:
#
port = ssh

# Option: protocol
# Notes.: internally used by config reader for interpolations.
# Values: [ tcp | udp | icmp | all ] Default: tcp
#
protocol = tcp

# Option: chain
# Notes specifies the iptables chain to which the fail2ban rules should be
# added
# Values: STRING Default: INPUT
chain = INPUT 

On commence par trouver la définition des actions-types. Ces dernières sont nommées actionstart, actionend, actionban, action. Voici leur signification

  • actionstart: Cette action est lancée au démarrage du jail (donc au démarrage de fail2ban). Dans notre cas, elle consiste à initialiser des chaînes de pare-feu spécifiques dans le cadre d'une future utilisation.
  • actionstop: Cette action est lancée lors de l'arrêt du jail (donc à l'arrêt de fail2ban). Dans notre cas, elle s'occupe de supprimer les chaînes de pare-feu spécifiquement créées pour fail2ban.
  • actioncheck: Cette action permet de vérifier que le jail est bien en fonction. Ici, on vérifie que les chaînes de pare-feu spécifiques à fail2ban existent.
  • actionban: C'est l'action principale de fail2ban. Ici, on demande à iptables de rejeter (DROP) tous les paquets de l'IP en faute.
  • actionunban: C'est l'action qui permet de supprimer le banissement d'une machine distante. Dans notre cas, on supprimer l'IP de la chaîne de blocage d'iptables.

Enfin, le fichier d'action contient des paramètres par défaut. Ici, il s'agit du nom de la chaîne iptables, du port, du protocole et de la chaîne d'entrée d'iptables. Ces variables peuvent être également utilisées dans le fichier de définition des jails.

Pour terminer le jail

Pour terminer, il faut relier les filtres aux actions. Etudions une petite partie du fichier /etc/fail2ban/jail.conf:

...
banaction = iptables-multiport

# Action shortcuts. To be used to define action parameter
# The simplest action to take: ban only
action_ = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]

# ban & send an e-mail with whois report to the destemail.
action_mw = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
              %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"]

# ban & send an e-mail with whois report and relevant log lines
# to the destemail.
action_mwl = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
               %(mta)s-whois-lines[name=%(__name__)s, dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"]

# Choose default action. To change, just override value of 'action' with the
# interpolation to the chosen action shortcut (e.g. action_mw, action_mwl, etc) in jail.local
# globally (section [DEFAULT]) or per specific section
action = %(action_)s

#
# JAILS
#
[ssh]

enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 6 

[xinetd-fail]

enabled = false
filter = xinetd-fail
port = all
banaction = iptables-multiport-log
logpath = /var/log/daemon.log
maxretry = 2

#
# HTTP servers
#
[apache]

enabled = false
port = http,https
filter = apache-auth
logpath = /var/log/apache*/*error.log
maxretry = 6 

On rencontre d'abord la création de quelques alias pour les actions par défaut. On trouve ainsi la définition de la variable banaction qui est affectée à iptables-multiport que nous avons étudié plus haut.

Etudions la syntaxe du premier alias:

 action_ = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]

On peut observer que l'alias définit une action qui consiste à lancer l'action banaction (qui contient en fait iptables-multiport) avec les paramètres d'action name, port, protocol et chain.

Suivent d'autres définitions d'alias d'action qui permettent de lancer iptables-multiport et également d'envoyer un courrier électronique via sendmail. C'est l'action sendmail-whois.conf (si l'on remplace les variables par le bon contenu).

Enfin, on trouve l'action par défaut: action = %(action_)s, qui consiste tout simplement à utiliser l'expression utilisée plus haut: iptables-multiport avec les bons arguments.

Ensuite, on rencontre trois définitions de jail différentes:

  • une pour SSH,
  • une pour les services xinetd
  • la dernière pour appache

De cet exemple, on peut en déduire qu'un jail se définit de la manière suivante:

[nom du jail]

enabled = true ou false suivant si on active ce jail ou pas
port = liste de ports sur lesquels porte l'action. Cette variable est transmise au fichier d'action.
filter = nom du filtre pour détecter l'attaque
logpath = chemin du fichier de log à surveiller pour ce jail.
maxretry = nombre minimum de "match" du filtre dans le fichier de log avant de déclencher le bannissement.
action = le nom du fichier d'action à lancer pour ce jail. si cette variable n'est pas remplie, on utilise celle définie plus haut dans la configuration générale. 
banaction = Dans notre cas, banaction est une variable utilisée pour définir la variable action. On peut donc l'utiliser ici, elle sera substituée lors de l'appel de la variable action.

On voit donc que créer un jail est assez simple. Attention aux variables par défaut qui sont déclarées en amont du fichier de jail.

Adapter fail2ban à votre configuration

Introduction

Pour ma part, je tiens à superviser et sécuriser l'ensemble de mes services publics. J'utilise la méthode suivante:

  • D'abord, il faut lister l'ensemble des services exposés à Internet. Je fais ça de mémoire, ça me permet de voir ce qui est prioritaire par rapport au reste (si je l'ai oublié, c'est que ce n'est pas prioritaire).
  • Ensuite, je regarde quels sont les services réseaux en écoute sur la machine à sécuriser. Cela permet de voir quels sont les services qui ont été oubliés.
  • Enfin, il faut lire les logs des services précédemment listés pour repérer les attaques les plus courantes. C'est la partie la plus fastidieuse mais également la plus intéressante. Ca ne fait jamais de mal de se plonger dans les logs Cette partie permet de déterminer les règles de filtrage dont on a besoin.
  • Pour terminer, il est bon de se plonger dans la lecture des fichiers de filtre livrés par défaut avec fail2ban pour éviter de réinventer la roue. En règle générale, les développeurs de fail2ban implémentent des expressions de filtre qui font référence dans le domaine, donc inutile de tenter de recréer difficilement ce qui existe déjà. En plus de ça, il y a de grandes chances que votre expression ne soit pas correcte. Autant utiliser ce qui est déjà testé et éprouvé. Néanmoins, ces fichiers de référence ne devraient pas vous empêcher de faire ce que bon vous semble avec fail2ban.

Pour ma part, voici la liste des services publics que j'héberge sur ma machine:

  • 22/ssh sur le réseau local uniquement (restreint par règle iptables sur ipv4 et Ipv6).
  • 25/smtp sur Internet. Il s'agit du MTA exim4.
  • 53/dns sur le réseau local uniquement (restreint par règle iptables sur ipv4 et ipv6).
  • 80/http sur Internet. Il s'agit d'Apache et de tout les modules qui vont avec.
  • 143/imap sur le réseau local uniquement (restreint par règle iptables sur ipv4 et ipv6). Il s'agit de dovecot.
  • 443/https sur Internet. Il s'agit d'Apache et de tout les modules qui vont avec. Dans mon cas, les services hébergés en HTTPS sont différents de ceux en HTTP.

Pour la majorité des cas, je veux empêcher les tentatives d'accès frauduleux par brute-force (ou par erreur de saisie). Pour certains services, il faudra aller plus loin comme détecter les attaques sur services web ou les dénis de service.

Verrouiller SSH

Ce premier point est primordial: si un compte POSIX est découvert via un brute-force-cracking SSH, votre serveur est vraiment compromis. Détecter ce genre d'attaque est donc essentiel. C'est d'ailleurs le seul jail lancé par défaut lorsque vous installez fail2ban.

Dans ce cadre, vous avez juste à utiliser le filtre /etc/fail2ban/filter.d/sshd.conf que je vous invite à lire avant d'utiliser.

Voici le code du jail:

[ssh]
enabled  = true
port     = ssh
filter   = sshd
logpath  = /var/log/auth.log
maxretry = 3
action   = %(action_mws)s

Attaques sur l'authentification HTTP

Ici, on veut juste bannir les machines distantes qui tentent de faire du cassage en force brute sur des comptes présent sur une authentification HTTP. En règle générale, placer une authentification HTTP règle une grande partie des problèmes d'attaque Apache. Si vous avez une application qui nécéssite une authentification qui tourne derrière un serveur web et dont vous ne savez pas quel est le niveau de sécurité du code qui la compose, je vous recommande de placer une authentification HTTP avant: ça évitera que quelqu'un puisse exploiter directement une faille de sécurité de l'application pour casser l'authentification. En effet, je fais plus confiance à Apache pour la sécurité d'une authentification HTTP qu'à un mécanisme inconnu d'une application PHP codée avec les pieds.

Dans notre cas, le filtre par défaut suffit largement. Il est situé dans /etc/fail2ban/filter.d/apache-auth.conf. Je vous invite à le lire pour mieux comprendre.

Voici le code du jail:

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

Attaques de failles de sécurité sur le serveur Web

Un serveur web sur Internet se prend un maximum de choses dans la gueule ! Voici ce que j'ai pu découvrir en lisant mes logs d'erreur Apache:

  • des attaques sur des failles de routeur Cisco (tmUnblock.cgi).
  • des attaques sur PhpMyAdmin (que je n'ai pas).
  • des attaques sur CGI
  • des attaques dédiées aux failles de IIS (que je n'aurai jamais).
  • des clients qui ne balancent pas leur nom.
  • des attaques sur vtigercrm
  • etc.

La réponse est assez directe: toute tentative d'accès sur ce genre d'URLs conduit à un bannissement immédiat !

Voici le code du filtre que j'ai placé dans /etc/fail2ban/filter.d/apache-noscript.local:

# Fail2Ban configuration file
#
# Authors: Cyril Jacquier, Médéric Ribreux
#
# $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 (File does not exist|script not found or unable to stat): /\S*(\.php|\.asp|\.exe|\.pl)\s*$
            ^%(_apache_error_client)s script '/\S*(\.php|\.asp|\.exe|\.pl)\S*' not found or unable to stat\s*$
            ^%(_apache_error_client)s (File does not exist|script not found or unable to stat): /\S*([mM]y[Aa]dmin|php|cgi-|administrator|w00t|vtigercrm|tmUnblock).*$
            ^%(_apache_error_client)s client sent HTTP/1.1 request without hostname.*$

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

Et voici le code du jail:

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

Tentatives de relais de spam et d'accès frauduleux sur MTA

Pour le relayage de spam, en règle générale, les fraudeurs font une seule tentative. Si le serveur ne permet pas le relais, ils s'arrêtent ou bien recommencent quelques heures plus tard. Dans ce cas, autant être impartial: toute tentative de relais de spam sera caution à bannissement !

D'autres attaques peuvent avoir lieu notamment des tentatives de confirmation d'adresse (VRFY) pour tenter de trouver des adresses email, ou encore des rejets de connexion.

Voici le code du filtre à mettre dans /etc/fail2ban/filter.d/exim-norelay.local:

# Fail2Ban configuration file
#
# Author: Médéric Ribreux
#
# $Revision$
#
[Definition]

# Option: failregex
# Notes.: regex to match the password failures 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
#
# In versions >= 0.8.11 below strings defined in exim-common.conf
host_info = H=([\w.-]+ )?(\(\S+\) )?\[<HOST>\](:\d+)? (I=\[\S+\]:\d+ )?(U=\S+ )?(P=e?smtp )?
pid = ( \[\d+\])?

failregex = ^%(pid)s %(host_info)ssender verify fail for <\S+>: Unrouteable address\s*$
            ^%(pid)s \S+ F=(<>|\S+@\S+) %(host_info)srejected by local_scan\(\): .{0,256}$
            ^%(pid)s %(host_info)s.*(?:relay not permitted).*$
            ^%(pid)s %(host_info)s.*rejected (EXPN|VRFY) root.*$
            ^%(pid)s rejected EHLO from \[<HOST>\]: syntactically invalid argument\(s\): \(no argument given\).*$
            ^%(pid)s.*rejected connection from H=\[<HOST>\].*$

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

Et voici le code du jail:

[exim-norelay]
enabled  = true
filter   = exim
port     = smtp,ssmtp
logpath  = /var/log/exim4/rejectlog
maxretry = 1

Tentatives d'accès frauduleux sur serveur IMAP

Mon serveur IMAP n'est pas public. Dans ce cadre, les seules attaques qui peuvent survenir viennent du LAN. En règle générale, un indicateur pertinent est celui d'une attaque en force brute sur un compte IMAP ou tenter d'accéder à un compte qui n'existe pas. Dans ce cas, le fichier de filtre par défaut de fail2ban joue pleinement son rôle (lisez-le et comparez-le à vos logs d'échec).

Voici le code du jail

[dovecot]
enabled = true
port = imap
filter = dovecot
logpath = /var/log/mail.log
maxretry = 3
action = %(action_mw)s 

Tester les règles de fail2ban

Vous avez maintenant défini l'ensemble de vos jails mais il reste encore à les tester. En effet, qui vous dit que vous n'avez pas fait d'erreur de saisie où que votre filtre est défectueux ? fail2ban dispose d'une commande bien pratique qui vous permet de vérifier vos filtres. Il s'agit de fail2ban-regex. Son utilisation est assez simple:

 $ fail2ban-regex nom_du_fichier_de_log nom_du_fichier_de_filtre

Pour tester notre filtre Apache no-script, on va utiliser la commande suivante (et je vous présente également une partie des résultats):

$ fail2ban-regex /var/log/apache2/error.log ./filter.d/apache-noscript.local

Results
=======

Failregex
|- Regular expressions:
|  [1] ^\span>^<span class="hl opt">+\] \[error\] \[client <HOST>\] (File does not exist|script not found or unable to stat): /\S*(\.php|\.asp|\.exe|\.pl|\.cgi)\s*$
|  [2] ^\span>^<span class="hl opt">+\] \[error\] \[client <HOST>\] script '/\S*(\.php|\.asp|\.exe|\.pl)\S*' not found or unable to stat\s*$
|  [3] ^\span>^<span class="hl opt">+\] \[error\] \[client <HOST>\] (File does not exist|script not found or unable to stat): /\S*([mM]y[Aa]dmin|php|cgi-).*$
|
`- Number of matches:
   [1] 0 match(es)
   [2] 5 match(es)
   [3] 86 match(es)

On obtient alors les stats de chaque regexp sur le fichier de log. Une expression à zéro peut vous faire douter de sa syntaxe. Il faudra creuser dans le fichier de log pour voir si rien ne déclenche le filtre.

Une fois que vous avez mis en place des règles de filtre, je vous conseille d'industrialiser les tests avec un fichier log spécialement fait pour. Une ligne suffit pour remplir les conditions de chaque test (soit une ligne par possibilité de match par expression de filtre, ce qui peut faire beaucoup. Effectivement, un fichier de log de cet accabit est assez pénible à mettre en place. Mais c'est le gage d'avoir un test fiable pour éprouver la configuration de fail2ban. Car quoi de plus dommage que d'avoir un fail2ban en mode passoire ?

Améliorer le fonctionnement par défaut de fail2ban

Correction du problème de date dans les courriels

Par défaut, la version de Debian de fail2ban formatte les dates d'envoi de courriel via la commande date. Si vous utilisez une locale différente de la locale C, votre date ne respecte pas les standards du courrier électronique et le mail reçu n'aura pas de date valide. La solution est assez triviale, il suffit de modifier l'appel à la commande dateen la préfixant avec LC_TIME=C. Cette correction a été effectuée en amont et elle est disponible sur la version 0.9 de fail2ban. Malheureusement, la version sous Debian stable de fail2ban est la version 0.8.6. Nous pouvons néanmoins corriger ce problème en portant la modification dans un fichier d'action local. Celui qui nous intéresse est /etc/fail2ban/action.d/sendmail-whois.local. Par ailleurs, je profite de ce fichier pour:

  • Franciser le message du courrier électronique car après tout, c'est moi qui vais le lire !
  • Supprimer les actions actionsstart et actionstop qui envoient un mail pour chacun des jails qui sont lancés par fail2ban (au démarrage et à la fin du service): je n'ai pas besoin de ces informations.

Voici le contenu de /etc/fail2ban/action.d/sendmail-whois.local:

# Fail2Ban configuration file
#
# Author: Médéric RIBREUX
#
# $Revision$
#

[Definition]

# Option: actionstart
# Notes.: command executed once at the start of Fail2Ban.
          We do not send anything: too much mail kill mail
# Values: CMD
#
actionstart =

# Option: actionstop
# Notes.: command executed once at the end of Fail2Ban
# Values: CMD
#
actionstop =

# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck =

# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
actionban = printf %%b "Subject: [Fail2Ban] service <name>: <ip> bannie...
            Date: `LC_TIME=C date -u +"%%a, %%d %%h %%Y %%T +0000"`
            From: Fail2Ban <<sender>>
            To: <dest>\n
            Alerte !\n
            L'adresse IP <ip> a été bannie par Fail2ban après
            <failures> tentatives d'accès à <name>.\n\n
            Quelques informations sur cet hôte:\n
            `/usr/bin/whois <ip>`\n\n
            A+,\n
            Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>

# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD 
actionunban =

[Init]

# Defaut name of the chain
#
name = default

# Destination/Addressee of the mail
#
dest = root

# Sender of the mail
#
sender = root 

IPv6

Bon, fail2ban, c'est bien mais ma machine dispose d'un accès IPv6. Certes, j'ai un ensemble de règles de pare-feu mais mes services publics sont également ouverts sur IPv6. Il me faut donc une configuration IPv6 pour fail2ban. Le problème c'est que la version de fail2ban de Debian stable (Wheezy au moment de la rédaction de cet article) ne le supporte pas. Il existe des patchs non officiels mais je n'ai pas vraiment envie de les appliquer pour l'instant. On verra lors d'une migration vers Jessie (qui devrait intervenir dans moins de 6 mois maintenant)...

TO BE continuated !

Alertes par SMS

Vous pouvez vouloir vous alerter autrement que par courrier électronique. Dans le cadre d'attaques sérieuses, vous pouvez vouloir être alerté sur votre téléphone mobile. Mon opérateur mobile me permet d'envoyer gratuitement des sms sur mon téléphone grâce à une API web activable grâce à des outils en ligne de commande tels que wget ou curl. Je vais donc utiliser cette API pour m'envoyer des SMS encas de ban SSH sur mon serveur (ce qui est le moins probable car le service SSH n'est pas exposé sur Internet).

D'abord, commençons par le script d'envoi de SMS. Il est générique. Je le place dans /usr/local/sbin/notify_sms.sh avec des droits uniquement pour root !

#!/bin/sh
# Script to send SMS via Free Mobile SMSAPI

USER="********" # The username for the API
PASS="*****" # The API key
URI="https://smsapi.free-mobile.fr"                                                                                                                                                           

# We grab stdin and convert newline to %0d to have multiline SMS
MESSAGE=$(cat | sed ':a;N;$!ba;s/\n/ %0d/g')

STATUS=$(wget -q -S --no-check-certificate -O- "${URI}/sendmsg?user=$USER&pass=$PASS&msg=$MESSAGE" 2>&1 | grep "HTTP/" | awk '/^ HTTP/{print $2}')

if [ "$STATUS" -ne "200" ]; then
  echo "Error (${STATUS}) in sending an SMS on $URI..." | logger -p security.error -t SMS
  exit $STATUS
fi

echo "An SMS has been sent to $URI..." | logger -p security.notice -t SMS

exit 0

Ensuite, il reste à intégrer ce script dans une action fail2ban. C'est assez simple, je vais m'inspirer de ce que j'ai trouvé dans la partie sur sendmail. Voici le contenu du fichier /etc/fail2ban/action.d/sendsms.local:

# Fail2Ban configuration file
#
# Author: Médéric RIBREUX
#
# $Revision$
#

[Definition]
# Option: actionstart
# Notes.: command executed once at the start of Fail2Ban.
# Values: CMD
#
actionstart =

# Option: actionstop
# Notes.: command executed once at the end of Fail2Ban
# Values: CMD
#
actionstop =

# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#

actioncheck =
# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#

actionban = printf %%b "Alerte Fail2Ban sur le service <name> : <ip> bannie...
                        Alerte !
                        L'adresse IP <ip> a été bannie par Fail2ban après
                        <failures> tentatives d'accès à <name>.\n
                        --
                        Envoyé depuis `hostname`." | /usr/local/sbin/notify_sms.sh

# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
actionunban =

[Init]
# Defaut name of the chain
#
name = default 

Ensuite, pour faciliter l'utilisation de cette action dans le jail, je définis une action via alias:

...
action_mws = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
             %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"]
             sendsms[name=%(__name__)s, logpath=%(logpath)s, chain="%(chain)s"] 
...
# plus loin dans la définition d'un jail:
action = %(action_mws)s

Voilà, ça marche et c'est bien formatté !

Conclusion

Nous avons pu constater que fail2ban permet de détecter un certain nombre d'attaques et de réagir automatiquement en bannissant les attaquants pour une certaine durée. Sa configuration sous Debian est spécifique mais semble assez logique et élégante. Par exemple, nous avons pu régler quelques problèmes sur le formattage des courriels ainsiq que créer nos propres filtres. Lors des prochaines mises à jour du paquet, vos corrections resteront disponibles. Fail2ban est également hautement customisable. On peut créer de nouveaux jails et rédiger des actions bien spécifiques comme celle qui consiste à envoyer un SMS en cas d'attaque.

Reste le point noir de la non gestion d'IPv6 qui, en 2014, ne devrait plus être rencontrée. Il existe des patchs et des pull-requests sur le code de Fail2ban sur ce sujet, espérons qu'ils seront bientôt versés dans le code officiel et qu'ils deviendront accessibles à l'ensemble des personnes qui veulent savoir ce qui se passe sur leurs machines exposées sur Internet.

Posted mer. 26 nov. 2014 19:24:00 Tags:

Introduction

Le boot, c'est parfois long. Souvent, on a pas que ça à faire ! C'est très vrai pour les ordinateurs portables ou les stations de travail. Mais d'une manière générale, y compris pour des grosses machines de production, j'aime bien les séquences de boot rapide. En effet, plus le boot est rapide et plus la remise en fonctionnement des services associés à la machine est rapide. Plus on gagne de secondes sur ce sujet et mieux c'est. De plus, avoir une séquence de boot rapide implique que vous ayez passé un peu de temps sur cette dernière. C'est généralement une bonne pratique d'administration système. Si vous n'avez installé que le strict nécéssaire, votre séquence de démarrage sera souvent assez rapide.

Néanmoins, il existe des outils techniques qui permettent de gagner de précieuses secondes. Le premier qui me vient à l'esprit est systemd qui permet d'accélérer la séquence de démarrage en parralélisant au maximum le lancement des services, y compris si le réseau n'est pas encore configuré. Sur mes stations de travail, systemd permet d'économiser entre 5 et 10 secondes.

L'autre possibilité est d'utiliser un disque SSD pour le système. C'est le cas d'une de mes stations de travail. Néanmoins pour tout un tas d'autres machines, ce n'est pas forcément possible. Par exemple, un serveur de récupération (c'est du green-IT) n'a pas besoin d'un disque SSD. Ou peut-être n'avez-vous tout simplement pas les moyens de vous offrir un SSD.

Optimiseur de système de fichiers

Pour les systèmes à disques durs classiques, on peut quand même gagner du temps en utilisant un optimiseur de système de fichiers. Il existe par exemple readahead qui fonctionne avec systemd. Sous GNU/Linux Debian Wheezy, il existe un paquet nommé readahead-fedora. Le principe de ce dernier est de lire des fichiers pour les placer en mémoire. readahead est prévu pour être lancé au démarrage de la machine: il utilise une liste de fichiers à lire en amont dans un ordre bien déterminé de manière à accélérer le temps de démarrage. En effet, si on concentre les opérations de lecture de fichiers dont on aura besoin dans tout le processus de démarrage, les accès disques seront beaucoup mieux optimisés.

Mais on peut encore aller plus loin. En effet,on peut agir au sein du système de fichiers. Si tous les fichiers à lire pour accélérer la séquence de démarrage sont placés au même endroit, les uns à la suite des autres, leur lecture pour les placer en mémoire sera beaucoup plus rapide, les têtes de lecture n'ayant pas à se déplacer de manière répartie. Il y a un paquet pour ça: e4rat. Voyons comment l'installer...

Principes d'e4rat

e4rat est un petit logiciel qui se charge de regrouper les fichiers préchargés du système dans un endroit donné, de manière linéaire. Il ne fonctionne que sur les systèmes ext4 mais, comme btrfs n'est pas encore implémenté en standard dans toutes les distributions (et qu'il n'est pas encore prêt pour la production), nous pouvons allègrement l'utiliser.

Son principe d'utilisation est finalement assez proche de celui de readahead à la différence qu'il n'est pas vraiment géré à la sauce Debian. Certes il existe un paquet debian sur le site web de l'utilitaire mais il n'est pas intégré à la distribution.

e4rat utilise 3 binaires différents:

  • e4rat-collect qui est un peu le readahead-collector: il fabrique une liste de fichiers ouverts de manière chronologique.
  • e4rat-realloc qui s'occupe de réallouer les fichiers au bon endroit.
  • enfin, on trouve e4rat-preload qui joue le rôle de readahead: ce binaire va charger les fichiers dans l'ordre requis.

Installation d'e4rat

Cette fois, apt-get ou aptitude ne peuvent pas nous aider. Il faut aller sur le site web du projet et télécharger le paquet debian qui va bien. Si vous utilisez autre chose que du x86 ou de l'amd64 (de l'armhf ou du mips), il faudra recompiler à la main !

 # dpkg -i e4rat_0.2.3_amd64.deb

Le paquet installe quelques binaires dans /sbin/ ainsi que les pages de manuel relatives.

Configuration d'e4rat

Pour la partie configuration, elle sort vraiment du lot Debian: il faut intervenir en amont du système et indiquer au noyau linux le nouveau nom du processus init. C'est une technique identique à celle de bootchart. Nous devrons donc gérer tout ça dans la configuration de GRUB2.

Pour résumer, voici ce que nous allons faire:

  • booter le système en ayant pris soin de mettre en processus init le binaire /sbin/e4rat-collect
  • après quelques vérifications, rebooter le système en mode autonome (single) pour effectuer le repositionnement des fichiers concernés.
  • enfin, il faudra indiquer dans la configuration de GRUB2 qu'on souhaite définitivement utiliser le binaire e4rat-preload en processus init pour tout le temps pré-charger les fichiers concernés.

Voici le déroulé de ces étapes.

  • Ajoutez les lignes suivantes dans le fichier /etc/grub.d/40_custom (à adapter en fonction de votre noyau):

    menuentry 'E4Rat collect Debian GNU/Linux,3.2.0-4-amd64' --class debian --class gnu-linux --class gnu --class os { insmod gzio insmod part_msdos insmod ext2 set root='(hd0,msdos5)' search --no-floppy --fs-uuid --set=root 9beb0590-6505-41af-8daa-cdf23a108473 echo 'Chargement de Linux 3.2.0-4-amd64 avec E4RAT collection...' linux /boot/vmlinuz-3.2.0-4-amd64 root=UUID=9beb0590-6505-41af-8daa-cdf23a108473 ro quiet init=/sbin/e4rat-collect echo 'Chargement du disque mémoire initial ...' initrd /boot/initrd.img-3.2.0-4-amd64 }

  • Lancer la commande '''update-grub2''' pour mettre à jour le menu GRUB. Lancer alors un reboot de la machine et prendre soin de choisir cette nouvelle entrée.

  • Le système va se lancer et créer une liste de fichiers à précharger en fonction de ce qui est lancé au niveau du système de fichiers. La liste des fichiers est stockée, par défaut, dans le fichier '''/var/lib/e4rat/startup.log'''.

  • N'hésitez pas à fouiller cette liste et à virer les fichiers qui ne sont pas importants. Pour ma part, j'ai supprimé toutes les entrées qui mobilisent des fichiers de thumbnails par exemple.

  • Une fois la vérification de ce fichier réalisée, il suffit de redémarrer le système en mode de dépannage (single). Lorsque le système vous donne la main (après avoir donné le mot de passe root), il suffit d'entrer la commande '''e4rat-realloc /var/lib/e4rat/startup.log'''.

  • Les fichiers concernés par la réallocation vont être alors déplacés de manière à minimiser leur temps d'accès. Une fois que e4rat-realloc a terminé son travail, il suffit de rebooter sur une instance normale du système (la première entrée dans le menu de GRUB2).

  • Enfin, contrairement à readahead, le pré-chargement des fichiers de e4rat est réalisé au niveau du processus init (readahead se base sur un script sysvinit). Il faut donc l'indiquer dans la configuration de GRUB2. Une méthode (un peu bourrine il faut bien le reconnaître), consiste à modifier le contenu d'une variable du fichier '''/etc/default/grub''':

    GRUB_CMDLINE_LINUX_DEFAULT="quiet init=/sbin/e4rat-preload"

  • Après un '''update-grub2''', à chaque reboot, le système va charger les fichiers et tout devrait automagiquement aller plus vite.

Bien entendu cette configuration fonctionne très bien avec systemd car elle est indépendante du système d'init de la distribution.

Conclusion

L'installation d'e4rat est un peu complexe. Sa configuration n'est pas vraiment dynamique: si vous installez de nouveaux paquets ou de nouvelles versions de paquets existants, il faudra recommencer l'étape de collecte des fichiers en bootant sur l'entrée GRUB E4Rat collect et relancer e4rat-realloc. Néanmoins sur un système en production où ces changements sont peu nombreux, c'est très adapté. Avec e4rat on peut gagner près de 5 à 10 secondes supplémentaires sur la séquence de démarrage. Donc, le jeu en vaut clairement la chandelle vu le faible temps d'administration système nécéssaire pour mettre en place la configuration (moins de 30 minutes).

Bien entendu, e4rat n'a pas vraiment d'intérêt sur des disques SSD qui n'ont pas les mêmes problèmes de latence que les disques durs classiques. C'est la même chose pour les machines virtuelles !

Pour ma part, je l'ai déployé sur des stations de travail et des ordinateurs portables avec beaucoup de succès. Je le recommande y compris pour les serveurs de production (physiques bien sûr) comme processus à la mise en production: sur un parc un peu fourni, on peut remettre en service avec un peu d'avance et minimiser les temps d'indisponibilité. Vos utilisateurs seront plus rapidement contents et vos administrateurs systèmes nerveux un peu moins longtemps lors des redémarrages.

Posted ven. 14 nov. 2014 19:45:00

Introduction

Ikiwiki permet d'éditer des pages en ligne. Jusqu'à présent, j'utilisais uniquement Git pour commiter des articles sur ma plate-forme Ikiwiki. C'est un moyen assez efficace pour publier des articles car j'utilise mon éditeur de texte favori (Emacs) pour le faire et ça ne pose aucun problème.

Néanmoins, parfois, on a besoin d'effectuer juste une petite correction et le faire dans un navigateur Web reste sans doute le moyen le plus effiace. Mais qui dit édition en ligne, dit sécurité ! Il ne faut pas en effet que n'importe qui puisse réaliser ces modifications sans contrôle.

Mon cas d'utilisation est assez simple: il n'y a qu'un seul utilisateur qui publie sur la plate-forme. Je n'ai donc pas besoin de mettre en place un système complexe basé sur une base de données. De plus, j'aimerais bien pouvoir réutiliser une des méthodes d'authentification dont je me sers fréquemment.

Ces besoins impliquent d'utiliser un plugin particulier d'Ikiwiki qui se nomme httpauth. Ce dernier permet de déléguer le mécanisme d'authentification au serveur Web. En cas d'authentification réussie, ce dernier passe la main au CGI d'Ikiwiki avec les bons paramètres.

Cet article tente d'illustrer la configuration de ce plugin dans Ikiwiki ainsi que la mise en place du mécanisme d'authentification de manière à disposer d'une édition en ligne sécurisée.

Installation des paquets nécessaires à l'authentification

Pour faire fonctionner le plugin httpauth, il est nécessaire de disposer des bons modules Perl. En effet, le CGI d'Ikiwiki ne permet pas grand chose sans les bons paquets. Si vous tentez d'utiliser le plugin httpauth sans ces modules, le CGI retournera une erreur.

# aptitude install libcgi-session-perl libcgi-formbuilder-perl

Avec ces modules, le CGI d'Ikiwiki est capable de générer des formulaires web ainsi que de gérer des sessions. C'est tout ce dont nous avons besoin à ce niveau.

Configuration d'Ikiwiki

si vous consultez la documentation d'Ikiwiki sur l'authentification, vous pourrez constater qu'il y a plusieurs méthodes autres que httpauth. Par défaut, Ikiwiki utilise passwordauth. Ce dernier créé une base de données utilisateur (dans le répertoire .ikiwiki/userdb) qui gère les accès. Par défaut, cette base est ouverte, c'est à dire qu'on peut s'y enregistrer simplement en remplissant un formulaire. En conclusion, si vous laissez Ikiwiki sans modifier sa configuration, n'importe qui pourra créer et modifier des articles en créant un compte bidon. Voilà pourquoi il faut enchaîner toute de suite avec la configuration du plugin httpauth.

Vous devez modifier votre fichier de configuration Ikiwiki (fichier .setup). Voici ce que j'ai fait au mien:

    ...
    # plugins to add to the default configuration
    add_plugins => [qw{goodstuff format highlight toggle img rawhtml favicon sidebar meta calendar search editpage httpauth}],
    # plugins to disable
    disable_plugins => [qw{passwordauth openid anonok}],
    ...
    # httpauth plugin
    # url to redirect to when authentication is needede
    cgiauthurl => 'https://example.org/auth/ikiwiki.cgi',
    ...

J'ai tout simplement ajouté le plugin editpage pour permettre l'édition de pages ainsi que le plugin httpauth pour la gestion de l'authentification. Ensuite, j'ai préventivement désactivé tous les autres plugins liés à l'authentification (passwordauth, openid et anonok).

Enfin, j'ai renseigné une variable nommée cgiauthurl. Cette dernière permet au CGI d'Ikiwiki d'être accédé depuis une autre URL lorsqu'aucune session valide n'a été repérée. C'est ce mécanisme qui va nous permettre de gérer l'authentification. Il suffira de déclencher une authentification lorsqu'un utilisateur désire accéder à https://example.org/auth/ et le tour sera joué. Remarquez que cette page est accédée via TLS (HTTPS) ce qui n'est pas un hasard.

Vous aurez sans doute besoin de créer le répertoire défini par cgiauthurl. Dans mon cas, j'ai juste créé le répertoire /var/www/auth avec les bons droits (ceux pour www-data) et réalisé un lien symbolique de /var/www/ikiwiki vers /var/www/auth/ikiwiki.cgi:

# mkdir /var/www/auth
# chown www-data:www-data /var/www/auth
# ln -s /var/www/ikiwiki.cgi /var/www/auth/ikiwiki.cgi

Il faudra bien sûr adapter ces chemins à votre installation d'Ikiwiki.

Pour prendre en compte ce changement de configuration vous aurez besoin de regénérer le CGI d'Ikiwiki. Cette opération est déclenchée lorsque vous reconstruisez l'ensemble du site:

$ ikiwiki --setup ikiwiki.setup

Configuration Apache

Maintenant que nous avons configuré Ikiwiki, il reste à s'occuper d'Apache. En effet, c'est bien le serveur HTTP qui va réaliser une authentification de type Basic et passer la main au CGI d'Ikiwiki si l'authentification s'est correctement déroulée. Le serveur Web envoie le nom de l'utilisateur authentifié au CGI via une variable d'environnement, nommée REMOTE_USER.

Puisque l'authentification se fera en mode Basic, une connexion chiffrée est indispensable ! C'est à cet effet que nous avons délibérément configuré la variable Ikiwiki cgiauthurl en https. Ainsi, lorsqu'un utilisateur tente d'éditer une page en mode HTTP standard, il est automatiquement redirigé vers la partie HTTPS du site et aucun mot de passe ne transitera en clair sur le réseau.

Il faut donc éditer cette partie au niveau de la configuration d'Apache pour déclencher une authentification. Je ne vais pas rentrer dans les détails car votre configuration peut être très différente de la mienne. Dans mon cas, j'ai juste défini un nouvel emplacement dans la configuration du VirtualHost dédié à HTTPS:

    # Ikiwiki authentication
    <Directory /var/www/auth/>
        Options -Indexes FollowSymLinks MultiViews +ExecCGI
        AllowOverride None
        Order allow,deny
        Allow from all
        AuthType Basic
        AuthName "Ikiwiki authentication"
        AuthUserFile /etc/apache2/my_userdb
        Require valid-user
    </Directory>

Attention, le CGI d'Ikiwiki utilise la variable d'environnement REMOTE_USER retournée par Apache pour déterminer le nom de l'utilisateur authentifié. Si vous avez défini cette variable (avec un SetEnv par exemple) dans votre configuration Apache et même si cette valeur est nulle, Ikiwiki l'utilisera et considérera que l'utilisateur est authentifié. Dans mon cas, j'avais un comportement assez particulier: Ikiwiki ne me demandait jamais de m'authentifier et le nom de l'utilisateur pour la session était vide. J'avais tout simplement initialisé la variable d'environnement REMOTE_USER par défaut car j'avais mis en place sur mon serveur un accès Git par HTTP (git-http-backend). Ce dernier définissait la variable de la manière suivante:

    SetEnv REMOTE_USER=$REDIRECT_REMOTE_USER

Lorsque j'accède à mes dépôts Git via HTTP(S), le client Git remonte la variable REDIRECT_REMOTE_USER. Mais lorsqu'on n'utilise pas Git (ce qui est le cas lorsqu'on édite une page avec le CGI d'Ikiwiki), la variable d'environnement REDIRECT_REMOTE_USER est vide. Cette configuration Apache créé la variable REMOTE_USER, peu importe le cas. Cette définition est trop générale (et risquée).

A la place, il faut définir autrement cette variable dans la configuration Apache:

    SetEnvIf Request_URI "^/git/" REMOTE_USER=$REDIRECT_REMOTE_USER

Dans ce cas, la variable REMOTE_USER n'est définie que pour les URLs qui commencent par /git/ (ce qui n'est pas le cas lorsqu'on édite une page avec le CGI d'Ikiwiki). Ne pas redéfinir cette variable en mode conditionnel conduit à un vrai trou de sécurité ! Alors faîtes bien attention à votre configuration Apache...

Conclusion

Dans cet article, on voit que la mise en place d'un système d'authentification est assez accessible. Il y a juste quelques modules Perl à installer ainsi qu'un minimum de configuration d'Ikiwiki et d'Apache. Je ne peux que vous recommander d'effectuer quelques tests en pré-production avant de valider la configuration car l'enjeu est important: si vous n'avez pas assez sécurisé cette partie, n'importe quel attaquant pourra vous pourrir vos articles (ou faire du DDOS).

Maintenant, je peux faire de l'édition en ligne de manière sécurisée. Cet article a d'ailleurs été complètement rédigé en ligne !

Posted ven. 17 oct. 2014 18:34:00