Mettre en place un service CalDAV sécurisé avec Baikal🔗

Posted by Médéric Ribreux 🗓 In blog/ Sysadmin/

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.

À propos de Baikal et de notre cahier des charges

Commençons par les choses qui fâchent: 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:

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. quelque-chose 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 recoure 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éfère 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é:

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:

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 clients 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 venues 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 recourir au cloud ! En plus, ce serveur CalDAV fonctionne bien avec FirefoxOS, ce qui est, en 2015, plutôt une bonne nouvelle.