Are you Token to me ?
Le but de cet article est d’aborder l’utilisation du mod_secdownload de Lighttpd. Ce module ne permet qu’un seul mode de protection basé sur l’expiration des URLs au bout d’une durée déterminée par l’administrateur. Une autre approche serait de créer des jetons qui expirent quand ils sont utilisés. Cette approche sera brièvement abordée à la fin de cet article.
Le principe de fonctionnement du module mod_secdownload est relativement simple, l’URL doit contenir : le chemin relatif du fichier à télécharger, la date de création de l’URL et une somme de contrôle servant à valider l’URL. Si la date de création est jugée trop loin dans le passé, un code d’erreur 410 est renvoyé. Si la somme de contrôle est invalide, un code d’erreur 403 est renvoyé. Si la date de création est suffisamment récente et que la somme de contrôle est correcte, le fichier est envoyé au client.
Gentlemen start our engines
Le module mod_secdownload est inclus dans la distribution de base de Lighttpd. Il faut néanmoins veiller à ce que le module soit chargé au lancement de Lighttpd. Ce qui donne dans lighttpd.conf :
server.modules = (
...
"mod_secdownload",
...
)
La configuration du module se fait grâce à 4 paramètres :
secdownload.secret = "MonSecret" secdownload.document-root = "/path/to/protected/files" secdownload.uri-prefix = "/telechargement/" secdownload.timeout = 30
- Le paramètre secret sert lors du calcul de la somme de contrôle. La sécurité de la protection est basée sur cette chaine de caractères, il est donc important de bien la choisir et de ne pas la divulguer.
- Le paramètre document-root sert à définir le chemin absolu du dossier contenant les données à proteger.
- Le paramètre uri-prefix est similaire aux directives Alias d’Apache. Chaque appel du type mondomaine.tld/uri-prefix sera traité par le module mod_secdownload
- Le paramètre timeout permet quand à lui de définir la durée en secondes pendant laquelle l’URL est valide
Et maintenant, je fais quoi ?
Une fois Lighttpd configuré, il faut que l’application Internet soit en mesure de générer des liens valides de téléchargements. Des implémentations simples sont disponibles sur le site de Lighttpd. Il est important de bien comprendre l’algorithme sous-jacent, une fois l’algorithme maîtrisé la création de liens de téléchargements est relativement aisée.
L’URL de téléchargement sera de la forme suivante :
<mondomaine.tld>/<uri-prefix>/<token>/<timestamp>/<chemin-relatif>
Le timestamp doit être au format Hexadecimal. Le token est la somme MD5 de la concaténation du secret définit dans la configuration Lighttpd, du chemin relatif du fichier (en y laissant le / du début du chemin) et du timestamp au format hexadecimal.
Le script générant les URLs de téléchargement a donc besoin de connaître le secret. Le token varie en fonction du temps et du fichier à télécharger. Il faut donc faire attention aux deux points suivant lors de l’implémentation du générateur d’URL de téléchargement :
- Le secret doit être stocké de manière sécurisée (fichier avec droits limités par exemple) afin de ne pas compromettre la protection
- Le générateur d’URLs doit avoir son horloge synchronisée avec le serveur de téléchargement
Et si je veux faire autrement ?
Une autre approche serait de créer un GUID (Global Unique ID) lors de la création de l’URL, stocker cet ID et de l’invalider une fois qu’il a été utilisé. Cette solution est plus complexe à mettre en place : il n’existe pas à ma connaissance de module Lighttpd permettant de mettre en place ce genre de scénario. Le téléchargement devra alors être géré par l’interpréteur PHP/Ruby/Python/… . De plus le stockage des IDs nécessite a priori l’utilisation d’une base de données.
Voici de manière synthétique les avantages et inconvénients des deux solutions :
- mod_secdownload : la vérification de la validité de l’URL est rapide et le téléchargement bénéficie des différentes optimisation de Lighttpd (IO asynchrones par exemple). Par contre, une URL est valide pour tout les client pendant le laps de temps défini dans la configuration. Cette solution est performante mais plus limitée. De plus la sécurité de l’ensemble de la solution est compromise dès lors que le secret est connu par un tiers.
- GUID par téléchargement : Cette solution est plus sécurisée, pour compromettre la solution, il faudrait obtenir un accès en écriture au système de stockage des IDs de téléchargements. Cependant, cette solution est plus lourde à mettre en place et moins performante car elle nécessite l’utilisation d’un interpréteur PHP/Ruby/Python/… lors du téléchargement du fichier.
Lighttpd est un très bon serveur web mais force est de constater qu’il est difficile de se passer d’apache (en particulier à cause des fichiers .htaccess qui ne sont pas supportés par Lighttpd). Nous allons donc voir comment mettre en place un serveur Lighttpd qui servira du contenu statique et dynamique tout en faisant office de proxy pour Apache quand l’utilisation de ce dernier est nécessaire.
Vue d’ensemble de la solution
- Lighttpd écoutera sur le port 80 et répondra donc à toutes les requêtes, il servira de reverse-proxy pour les requêtes destinées à Apache.
- Apache écoutera sur le port 8080, il répondra à toutes les requêtes sur ce port. En production, il ne devrait appelé que par Lighttpd mais il sera possible de l’appeller depuis n’importe quel navigateur pendant une phase de Debug.
- La configuration sera adaptable pour chaque VirtualHost, il sera envisageable de mettre en place 3 configurations différentes :
- uniquement Lighttpd, dans le cas d’un site statique ou dynamique pouvant fonctionner sous Lighttpd, cette configuration est à privilegier
- contenu statique servi par Lighttpd, contenu dynamique servi par Apache via le mod_proxy de Lighttpd. Cette solution est à utiliser pour améliorer les performances d’un site nécessitant Apache pour son contenu dynamique.
- site uniquement servi par Apache via le mod_proxy de Lighttpd. Cette solution n’est envisagée que pour des cas particuliers, elle est à éviter a priori.
Modification de la configuration d’Apache
Afin que le serveur Apache écoute sur le port 8080, il faut modifier la directive Listen 80 en Listen 8080 de son fichier de configuration. Il faura aussi modifier les configuration des différents VirtualHost pour qu’ils soient pris sur le port 8080. Enfin, vu qu’en fonctionnement nominal, le serveur ne sera interrogé que sur du contenu dynamique et page par page, on pourra désactiver le KeepAlive en plaçant la directive KeepAlive Off.
Configuration de base de Lighttpd
La configuration de Lighttpd est en général effectuée dans un seul fichier lighttpd.conf. Dans notre cas, nous avons besoin d’activer les modules suivants :
- mod_proxy : ce module permettra de faire fonctionner Lighttpd en tant que reverse-proxy.
- mod_status : ce module nous permettra de suivre l’état du serveur
- mod_auth : ce module permettra de limiter l’accès au server-status
Pour ajouter un module, il suffit de décommenter la ligne contenant le nom du module dans le fichier de configuration.
Pour activer le server-status, il suffit d’ajouter la directive suivante :
status.status-url = "/server-status"
Il est quand même plus sur de limiter l’accès au server-status, il faut créer un fichier .htpasswd avec les identifiants et mot de passes des utilisateurs autorisés à se connecter au server-status (l’executable htpasswd d’Apache convient parfaitement pour cette tâche). il faut ensuite ajouter les lignes suivantes à la configuration de Lighttpd (dans mon cas, le fichier .htpasswd est stocké dans le dossier /usr/local/www) :
auth.backend = "htpasswd"
auth.backend.htpasswd.userfile = "/usr/local/www/.htpasswd"
auth.require = ( "/server-status" =>
(
"method" => "basic",
"realm" => "status",
"require" => "valid-user"
)
)
On peut ajouter une petite optimisation : l’utilisation d’un cache pour les appels stat :
server.stat-cache-engine = "fam"
Ajout d’un VirtualHost servi uniquement par Lighttpd
C’est le cas le plus simple et a priori le plus performant, il faut néanmoins être sur que l’application web hébergée sur le VirtualHost est totalement compatible avec Lighttpd.
$HTTP"host" =~ "test.labs.fr" { server.document-root = "/home/majinboo/sites/test" accesslog.filename = "/var/log/lighttpd/test-access.log" }
Ajout d’un VirtualHost Mixte : contenu statique servi par Lighttpd et contenu dynamique sous Apache
Ce blog est propulsé par Dotclear, n’ayant que peu de retour sur la compatibilité de Dotclear avec Lighttpd, j’ai pour l’instant choisi de le faire fonctionner en configuration mixte. La stratégie est la suivante : pour qu’Apache soit appelé sur les URL non-statiques (qui sont la plupart du temps rewritées) on utilise un proxy uniquement pour les url qui ne terminent pas par une extension connue pour être celle d’un fichier statique.
Vu que le contenu sera servi pour moitié par Apache et pour l’autre moitié par Lighttpd, il faut configurer le virtualHost à la fois dans Apache et dans Lighttpd. Ce qui donne pour la configuration d’Apache quelque chose de relativement anodin :
<VirtualHost *:8080>
ServerAdmin majinboo@labs.fr
DocumentRoot "/home/majinboo/sites/blog.majinboo.org"
ServerName majinboo.org
ServerAlias www.majinboo.org
ServerAlias blog.majinboo.org
ErrorLog "/var/log/httpd/majinboo.org-error.log"
CustomLog "/var/log/httpd/majinboo.org-access_log" combined
</VirtualHost>
Pour Lighttpd, la configuration est un poil plus complexe en raison de l’utlisation du mod_proxy :
$HTTP"host" =~ "majinboo.org" { server.document-root = "/home/majinboo/sites/blog.majinboo.org" accesslog.filename = "/var/log/lighttpd/majinboo.org-access.log" $HTTP"url" !~ "\.(js|css|gif|jpg|png|ico|txt|swf|html|htm)$" { proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => 8080 ) ) ) } }
Ajout d’un VirtualHost entièrement managé par Apache
Cette configuration peut par exemple être utile dans le cas d’un WebService qui sert des images générées par un script (captcha ou autre). Dans ce cas il faudra mettre la configuration du VirtualHost dans la configuration d’Apache et se contenter de tout rediriger vers Apache dans la configuration de Lighttpd, ce qui donne :
$HTTP"host" =~ "test2.labs.fr" { server.document-root = "/home/majinboo/sites/captchaServer" accesslog.filename = "/var/log/lighttpd/captcha-access.log" proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => 8080 ) ) ) }
Petit test de performances
Le test consiste à lancer siege en mode benchmark avec une liste d’URLs correspondant à la page d’accueil de ce blog. Pour ce test, 150 utilisateurs concurrents déroulent la liste d’URLs de manière séquentielle pendant 10 minutes. Le serveur qui est testé est un Bi Xeon Dual Coeurs avec 4 GB de RAM.
Résultats avec Apache
- Nombre de requêtes par seconde : 148
- Load du serveur : 50
- Mémoire libre : 2GB
Résultats avec la configuration mixte Lighttpd / Apache
- Nombre de requêtes par seconde : 151
- Load du serveur : 20
- Mémoire libre : 2.3 GB
On remarque que les performances brutes ne sont pas significativement meilleures avec Lighttpd, cependant le serveur est bien moins chargé avec la configuration mixte. Il est possible que ce soit la machine de test qui limite (une dédibox première génération).
Possibilités d’amélioration qui feront l’objet d’un futur article
Pour l’instant, le mod_proxy de Lighttpd interroge Apache à chaque requête. Un patch permet d’ajouter un mod_cache à Lighttpd qui permettrait d’augmenter les performances de la solution en servant le résultats de certaines requêtes destinées à Apache depuis un cache.
De plus PHP et postGreSQL qui sont utilisés par le blog ne sont pour l’instant pas optimisés. Il serait opportun d’optimiser leur configuration et d’ajouter le module eAccelerator à PHP.
FreeBSD propose deux solutions de RAID software : gmirror et ZFS (uniquement depuis la version 7.0 pour ce dernier). La solution ZFS n’étant pas suffisamment mature selon moi, je ne parlerai dans cet article que de la mise en place de gmirror.
Situation de départ
- Un serveur ou un poste de travail sous FreeBSD 6.X ou 7.0 avec un disque dur qui contient les partitions sur lesquelles sont les données à passer en RAID et un deuxième disque dur au moins aussi spacieux
- Un accès à distance au serveur : cette solution fonctionne uniquement par SSH (testé avec succès par votre serviteur). Il faut cependant faire attention car en cas d’erreur, il faudra un accès physique pour redemarrer serveur.
La stratégie
- Passage du disque actuellement utilisé en mirroring
- Reboot sur une grappe RAID à un seul disque
- Ajout du deuxième disque à la grappe RAID
Les détails de la mise en place
- Pour être en mesure de modifier des disques qui sont en cours d’utilisation, il faut utiliser la commande suivante :
sysctl kern.geom.debugflags=16
- Il faut ensuite créer une grappe RAID an l’initialisant avec le disque en cours d’utilisation (dans le cas présent : ad0) :
gmirror label -v -b round-robin gm0 /dev/ad0
- Il faut maintenant modifier le fichier /boot/loader.conf (en le créant si nécessaire). Afin que gmirror soit chargé au démarrage, il faut ajouter la directive suivante :
geom_mirror_load="YES"
- Il faut ensuite modifier le fichier /etc/fstab, les modifications dépondrons de votre installation. Il faut remplacer les références à votre disque physique (eg /dev/ad0) et les remplacer par la grappe RAID (eg /dev/mirror/gm0)
- L’étape suivante est critique, il faut redémarrer. S’il y a une erreur dans le fichier /boot/loader.conf ou dans le fichier /etc/fstab, le serveur risque fort de ne pas redémarrer correctement. Les erreurs les plus fréquentes sont l’oubli des ‘_’ dans geom_mirror_load ou l’utilisation de /dev/gm0 à la place de /dev/mirror/gm0. Il faut donc être le plus vigilant possible avant de redémarrer le serveur.
- Une fois le serveur redemarré et le bon fonctionnement de la grappe RAID validé, nous pouvons ajouter le deuxième disque à la grappe avec la commande suivante (dans notre cas, le deuxième disque est ad1) :
gmirror insert gm0 /dev/ad1
- Il est possible de connaitre l’état de la grappe à tout moment avec la commande :
gmirror status
. Dans notre cas, la grappe sera en état dégradée tant que les données du premier disques n’auront pas été copiées sur le deuxième disque.