NetBSD + nginx + naxsi + MySQL + PHP == 3NMP

GNU/Linux Magazine n° 159 | avril 2013 | Emile (iMil) Heitor
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
LAMP[1] par-ci, LAMP par-là, nous sommes en 2013 que diable ! Les technologies ont évolué, les besoins aussi, et si le standard de fait reste encore et toujours Apache muni du module mod_php[2], l'interprétation et l'affichage de pages Web programmées en PHP peuvent s'effectuer de façon bien plus élégante. Dans cet article, nous allons sortir des sentiers battus, et paramétrer un système NetBSD 6.0 afin d'en faire une plateforme web simple et sûre.

1. Environnement

Pour mettre en place notre plateforme, nous aurons besoin d'un système NetBSD 6.0 capable de joindre l'Internet.

Avant de démarrer la configuration de notre serveur Web, sachez que deux options s'offrent à vous pour installer les logiciels dont nous aurons besoin :

- pkgin dans le cas d'une installation de paquets binaires

- pkgsrc si vous souhaitez utiliser des paquets source

Dans ce dernier cas, vous devrez vous munir de l'arbre pkgsrc :

$ cd /usr

$ sudo cvs -d anoncvs@anoncvs.fr.NetBSD.org:/cvsroot co -rpkgsrc-2012Q4 pkgsrc

Cette commande aura pour effet de télécharger l'arbre pkgsrc 2012Q4, qui sera alors disponible dans le répertoire /usr/pkgsrc. Notez qu'il est probable qu'à l'heure où vous lirez cet article, pkgsrc 2013Q1 sera disponible, il suffira alors de remplacer 2012Q4 par 2013Q1.

La première étape de notre périple consiste à installer les logiciels nécessaires, dans l'ordre :

- nginx muni du module naxsi[3]

- php-fpm [4]

- MySQL

1.1 nginx + naxsi

On ne présente plus nginx, le serveur Web / Reverse Proxy HTTP / Proxy POP3/IMAP dont la réactivité et la faible empreinte mémoire ont fait de lui le second serveur Web le plus utilisé selon Netcraft[5]. Adossé à nginx, naxsi est un logiciel tierce partie dont un article faisait la présentation dans ces colonnes voici quelques mois. Pour rappel, naxsi est un module de nginx, développé par des employés de la société NBS-System dont je suis le directeur des opérations. Ce module est ce qu'il convient d'appeler un WAF, pour Web Application Firewall, dont l'objectif est de stopper les attaques classiques permettant de compromettre un site Web. Au contraire des WAF connus, naxsi ne se base pas sur une liste de signatures d'attaques, mais propose une liste de requêtes anormales qu'on trouve dans la quasi-totalité des tentatives d'intrusions. Par exemple, on peut lire dans le fichier naxsi_core.rules des règles très simples qui affectent un score lorsqu'une requête présente des caractères ou chaînes de caractères inhabituels. Il revient alors à l'administrateur d'ajouter des listes de règles (listes blanches) si l'utilisation d'un caractère suspect devait être autorisée. Nous reviendrons plus loin sur l'écriture des listes blanches.

Dans les dépôts officiels de paquets binaires NetBSD, nginx est compilé avec le strict minimum, cependant, NetBSDfr[7] met à disposition des utilisateurs des dépôts maison où l'on trouvera entre autres une version packagée de nginx disposant du module naxsi mais également real ip, ce dernier permettant d'inscrire des logs munis de l'IP réelle du client lorsque l'on se trouve derrière un Reverse Proxy[9].

Pour utiliser ce dépôt, il suffira de renseigner le fichier /usr/pkg/etc/pkgin/repositories.conf avec la ligne suivante :

http://packages.netbsdfr.org/latest/6.0/amd64/packages/All

Bien entendu, si votre plateforme est de type i386, il conviendra de remplacer amd64 par i386. Notez que NetBSDfr ne construit des paquets « que » pour i386 et amd64 par simple manque d'espace et d'architectures différentes (appel à contribution à peine masqué).

Après avoir mis à jour la base de données de paquets disponibles via la commande :

# pkgin up

On peut installer le couple nginx/naxsi en tapant simplement :

# pkgin in nginx

Pour les plus téméraires d'entre vous, si vous souhaitez installer ces logiciels au travers de pkgsrc, prenez soin d'ajouter les lignes suivantes au fichier /etc/mk.conf :

PKG_OPTIONS.nginx+=     naxsi realip

Ce qui aura pour effet de provoquer l'inclusion des modules naxsi et realip dans nginx lors de sa compilation. L’installation du paquet via pkgsrc se fait de cette façon :

$ cd /usr/pkgsrc/www/nginx

$ sudo make install clean

On s'assure que le serveur sera exécuté à chaque démarrage de la machine hôte :

# cp /usr/pkg/share/examples/rc.d/nginx /etc/rc.d/

# echo "nginx=YES" >> /etc/rc.conf

Il est désormais possible de démarrer le serveur Web :

# /etc/rc.d/nginx start

1.2 php-fpm

Il existe deux méthodes très connues pour faire interpréter le langage PHP afin d'en faire rendre le contenu par un serveur Web : mod_php et un des multiples systèmes de type FastCGI. Dans notre cas, mod_php est à proscrire, car il n'existe simplement pas d'implémentation de ce module pour nginx. De plus, l'exécution de script PHP déportée sur un programme tiers n'est certainement pas une mauvaise idée, car si la légende selon laquelle l'exécution de PHP serait plus rapide en passant par une socket (FastCGI) que par un pointeur (mod_php) est grotesque, il n'en reste pas moins que mod_php produit des processus Apache gigantesques. Notez par ailleurs que les documentations officielles, sur php.net, déconseillent fortement l'utilisation du mode Worker (threadé) couplé au module mod_php pour des raisons de réentrance[10] (rassurant, n'est-ce pas ?).

Pour toutes ces raisons, nous utiliserons PHP-FPM pour l'interprétation du code PHP qui viendra peupler notre DocumentRoot. Ce logiciel est livré avec PHP depuis la version 5.3.3 et dispose d'un paquet dédié dans pkgsrc, aussi, son installation est triviale :

# pkgin in php54-fpm

Ou si vous souhaitez rester en version 5.3 :

# pkgin in php53-fpm

Si l'on désire passer par pkgsrc et compiler toute la chaîne, on s'assurera en premier lieu de spécifier la version de PHP préférée dans le fichier /etc/mk.conf :

$ grep PHP /etc/mk.conf

PHP_VERSION_DEFAULT= 54

Puis on lance la chaîne d'installation :

# cd /usr/pkgsrc/www/php-fpm

# make install clean

Comme pour tous les démons, on demande son exécution au démarrage de cette façon :

# cp /usr/pkg/share/examples/rc.d/php_fpm /etc/rc.d

# echo "php_fpm=YES" >> /etc/rc.conf

Le paramétrage de PHP-FPM s'effectue dans le fichier /usr/pkg/etc/php-fpm.conf, et les différentes directives destinées à PHP se trouvent classiquement dans /usr/pkg/etc/php.ini. Un paramétrage par défaut suffira largement à notre mise en place, il conviendra cependant de s'assurer que PHP-FPM fonctionne avec le même utilisateur que nginx. Il faudra donc vérifier que les valeurs de user et group dans le fichier php-fpm.conf sont identiques à la ligne user du fichier nginx.conf.

1.3 MySQL

L'installation du serveur de bases de données MySQL est triviale, mais dans le cas où vous avez choisi d'installer les versions binaires des logiciels que nous allons manipuler, il est important de correctement choisir sa version de MySQL. En effet, le module php-mysql binaire livré par les dépôts NetBSDfr est lié à la version 5.1, aussi, l'installation s'effectue comme suit :

# pkgin in mysql-server-5.1

Dans le cas d'une installation via pkgsrc, il suffira de se rendre dans le répertoire de la version souhaitée, par exemple :

$ cd /usr/pkgsrc/databases/mysql55-server

$ sudo make install clean

2. Now Kiss

Les briques essentielles de notre serveur-à-chatons étant posées, nous allons nous assurer de la bonne communication entre nginx et PHP-FPM. Pour ce faire, nous créons une section location dans le fichier nginx.conf qui aura pour tâche de prendre en charge les URI[11] de type .php :

location ~ \.php$ {

  fastcgi_pass   unix:/tmp/fcgi-php.sock;

  fastcgi_index index.php;

  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

  fastcgi_buffers 256 4k;

  include        /usr/pkg/etc/nginx/fastcgi_params;

}

Nous plaçons cette location dans la section server déjà présente dans le fichier nginx.conf installé par le paquet. Dans cet exemple, nous passons les fichiers PHP à interpréter via une socket UNIX que met en place PHP-FPM. Le chemin de cette socket est défini dans le fichier php-fpm.conf :

listen = /tmp/fcgi-php.sock

Il est également possible d'utiliser un port TCP, par exemple pour déporter l'exécution des scripts sur une machine tierce, la syntaxe serait alors :

listen = 192.168.0.1:9000

Ou encore, pour une écoute locale :

listen = 127.0.0.1:9000

Il convient alors de modifier, dans la section location de notre fichier d'exemple nginx.conf, la valeur du paramètre fastcgi_pass de cette façon :

fastcgi_pass 127.0.0.1:9000;

En ayant préalablement démarré PHP-FPM de cette façon :

# /etc/rc.d/php-fpm start

Nous vérifions que la syntaxe du fichier nginx.conf est correcte grâce à cette commande :

# nginx -t

nginx: the configuration file /usr/pkg/etc/nginx/nginx.conf syntax is ok

nginx: configuration file /usr/pkg/etc/nginx/nginx.conf test is successful

On peut alors recharger sereinement notre nouvelle configuration :

# nginx -s reload

Reste à vérifier le bon fonctionnement d'un script PHP, en créant dans l'arborescence connue comme root par nginx, un simple fichier PHP :

$ cat > test.php

<?php phpinfo(); ?>

^D

Et de constater :

NetBSD + nginx + PHP-FPM : check.

3. Des p'tits trous

Aaaah, PHP. Aaaah la magie du développement permissif… Soyons clairs, je suis une quiche en PHP. Je ne peux l'exprimer autrement. Vraiment. Non, c'est pas mon truc. Après une soirée anormalement arrosée, je serais certainement en mesure d'écrire quelque chose de cet acabit :

<?php

  $content = file($_GET['path']);

  foreach($content as $line) {

    echo $line;

  }

?>

Et laisser ainsi la porte ouverte à l'ensemble des informations présentes sur le système de fichiers du serveur Web où serait situé ce vilain script.

Cet exemple est grossier et simpliste, mais malheureusement, non seulement ce type d'erreur fait légion, mais dans d'innombrables cas de figures, des machineries complexes telles que WordPress, Drupal, Magento, PrestaShop ou encore Joomla (rires) sont inconsciemment truffées d'exploits potentiels. Il suffit pour s'en convaincre de parcourir rapidement les CVE (Common Vulnerabilities and Exposures) de ces divers applicatifs pour qu'un frisson parcoure votre échine d'administrateur système hébergeant des dizaines, centaines, milliers de sites PHP hors de contrôle.

C'est dans cette scène angoissante qu'entre en jeu le WAF naxsi.

L'intégration de naxsi dans nginx n'est pas une tâche complexe. L'installation du paquet nginx a provoqué la création d'un fichier d'exemple /usr/pkg/share/examples/nginx/conf/naxsi_core.rules. Ce fichier, comprenant les règles de base de naxsi, est à copier dans le répertoire de configuration de nginx :

# cp /usr/pkg/share/examples/nginx/conf/naxsi_core.rules \

/usr/pkg/etc/nginx/

On inclut ce fichier dans la section http du fichier nginx.conf :

include /usr/pkg/etc/nginx/naxsi_core.rules;

Reste maintenant à préparer les locations que nous souhaitons sécuriser. On crée pour cela un fichier, appelons-le naxsi.rules dans lequel on spécifiera :

- l'activation des règles

- l'URL de redirection en cas de rejet

- les scores associés aux différentes détections

- d'éventuelles listes d'exception

Dans son aspect minimal, le fichier sera de cette forme :

SecRulesEnabled;

DeniedUrl "/denied";

## check rules

CheckRule "$SQL >= 8" BLOCK;

CheckRule "$RFI >= 8" BLOCK;

CheckRule "$TRAVERSAL >= 4" BLOCK;

CheckRule "$EVADE >= 4" BLOCK;

CheckRule "$XSS >= 8" BLOCK;

Il convient de créer une location denied, par exemple :

location /denied {

        rewrite ^ http://foobar.net/503.gif break;

}

Pour « activer » naxsi, la simple inclusion du fichier naxsi.rules précédemment créé suffit. Par exemple, pour sécuriser nos scripts PHP, nous ajoutons à notre location ~ \.php$ :

include /usr/pkg/etc/nginx/naxsi.rules;

Un rechargement de nginx plus loin, voici le résultat d'une tentative d'exploitation de notre backdoor :

$ curl -I -o- http://coruscant/own.php?path=/etc/passwd

HTTP/1.1 302 Moved Temporarily

Server: nginx/1.2.6

Date: Sun, 17 Feb 2013 13:43:32 GMT

Content-Type: text/html

Content-Length: 160

Connection: keep-alive

Location: http://foobar.net/503.gif?ip=192.168.0.1&server=coruscant&uri=/own.php&learning=0&total_processed=4&total_blocked=4&zone0=ARGS&id0=1202&var_name0=path

4. Monsieur Propre

Bien que fonctionnelle, cette configuration peut s'avérer pénible à copier/coller, et rendre l'administration de notre serveur Web fastidieuse, alors que ce dernier est syntaxiquement beaucoup plus ordonné et simple que notre bon vieux Apache. Nous allons utiliser et abuser des fonctionnalités d'inclusion que nous propose nginx.

Bien loin de l'expérimentation, l'organisation que vous allez découvrir est celle utilisée sur le serveur Web affichant les sites du groupe GCU-Squad[11]. Le groupe propose plusieurs services, certains statiques, certains dynamiques, nous allons voir comment proprement disposer ces différents sites, en protégeant ceux qui doivent l'être.

Il m'est fastidieux de parcourir des dizaines de fichiers de logs d'erreur, aussi n'aurais-je qu'un seul et même error.log pour tous les hôtes virtuels que sert la machine (virtuelle, elle aussi) :

error_log /var/log/nginx/error.log;

Les différents sites sont en réalité servis par l'excellent proxy inverse varnish (voir les non moins excellents articles sur ce sujet sur le site UnixGarden[13][14]), ainsi, ce ne sont pas directement les IPs des clients que voit arriver nginx, mais celle du proxy. Ce problème se contourne aisément à l'aide du module real_ip :

        set_real_ip_from 192.168.0.254;

        real_ip_header X-Forwarded-For;

Aucune autre directive concernant nos hôtes virtuels ne viendra polluer le fichier de base nginx.conf, nous inclurons le reste :

include /usr/pkg/etc/nginx/sites/*;

Notre fichier se résume alors à ces quelques lignes :

user   www www;

worker_processes 1;

error_log /var/log/nginx/error.log;

events {

    worker_connections 1024;

}

http {

    include /usr/pkg/etc/nginx/mime.types;

    default_type text/plain;

    sendfile on;

    keepalive_timeout 65;

    set_real_ip_from 192.168.100.254;

    real_ip_header X-Forwarded-For;

    include /usr/pkg/etc/nginx/naxsi_core.rules;

    include /usr/pkg/etc/nginx/sites/*;

}

Dans le répertoire sites/, nous distinguons deux fichiers :

ls sites/

dynamic   static

Qui correspondent aux deux types de services que nous trouvons sur ce serveur Web, des sites statiques, et des sites dynamiques. Nous mettrons l'emphase sur la structure d'un vhost dynamique :

server {

        server_name gcu.info www.gcu.info gcu-squad.org www.gcu-squad.org;

        root /chemin/vers/gcu/www;

        include /usr/pkg/etc/nginx/php.conf;

}

Là encore, peu de directives, la liste des FQDN autorisés, un simple DocumentRoot et une inclusion. Voyons cette dernière de plus près :

client_max_body_size    20M;

include /usr/pkg/etc/nginx/logs.conf;

include /usr/pkg/etc/nginx/denied;

include /usr/pkg/etc/nginx/global.conf;

location / {

        index index.php index.html;

        try_files $uri $uri/ /index.php?$args;

}

location ~ \.php$ {

        include /usr/pkg/etc/nginx/naxsi.rules;

        include /usr/pkg/etc/nginx/fastcgi_params;

}

Nous avons parlé de la location ~ \.php$ plus haut dans cet article, aussi voyons le contenu des 3 inclusions :

$ cat /usr/pkg/etc/nginx/logs.conf

if ($host ~ gcu) {

    set $log_fqdn $host;

}

access_log /var/log/nginx/$log_fqdn.access_log;

Nginx permet l'utilisation de certaines variables dans des paramètres comme access_log, ainsi, chaque vhost disposera de son fichier de log personnalisé, à condition que le FQDN contienne le mot-clé « gcu ». Attention, cette astuce ne fonctionne étrangement pas pour l'error_log

Nous avons déjà étudié la location /denied, mais notez qu'il s'agit désormais d'un fichier séparé qu'il est possible d'inclure au lieu d'en copier-coller le contenu.

Le fichier global.conf regroupe des paramètres que nous souhaitons inclure dans chacun des vhosts :

listen 80;

location = /favicon.ico {

        log_not_found off;

        access_log off;

}

location = /robots.txt {

        allow all;

        log_not_found off;

        access_log off;

}

location ~ /\.ht.* {

        deny all;

}

On écoute sur le port 80, on ne souhaite pas logger les erreurs 404 liées aux recherches automatiques de fichiers favicon ni de robots.txt. Enfin, nous interdisons la lecture de potentiels fichiers de type htaccess / htpasswd.

La structure d'un site statique est similaire aux hôtes dynamiques :

server {

    server_name tuhs.gcu.info tuhs.gcu-squad.org;

    root /home/ftp/mirrors/tuhs;

    autoindex on;

    include /usr/pkg/etc/nginx/logs.conf;

    include /usr/pkg/etc/nginx/denied;

    include /usr/pkg/etc/nginx/global.conf;

    include /usr/pkg/etc/nginx/static_location;

}

On autorise, pour certains vhosts, l'auto-indexation afin de montrer le contenu d'un répertoire à l'aide de la directive autoindex on. Le contenu du fichier static_location quant à lui, se résume à la déclaration d'une location /:

$ cat static_location

location / {

        include /usr/pkg/etc/nginx/naxsi.rules;

        index   index.html;

}

Bien découpée, notre configuration nous permet de manipuler très simplement nos multiples vhosts, en épargnant au maximum nos fragiles petits doigts de rébarbatifs copier-coller.

5. La blanche règle

Serveur en place et configuré, ne reste plus qu'à spécifier à naxsi les exceptions qu'il devra faire. En effet, le site GCU-Squad est propulsé par WordPress, et ce dernier utilise de façon intensive de multiples caractères douteux dans un contexte parfaitement licite. Pour présenter le fonctionnement des whitelists, je partirai d'un exemple simple ; WordPress, parmi les innombrables variables qu'il utilise, fait passer en POST (appelé “BODY” dans la terminologie naxsi) la valeur update dans une variable action. Cela lèvera un blocage qui se traduira dans le fichier error.log par quelque chose de ce genre :

2013/02/02 23:57:19 [error] 17370#0: *535 NAXSI_FMT: ip=192.168.0.254&server=www.gcu-squad.org&uri=/wp-login.php&total_processed=3&total_blocked=1&zone0=BODY&id0=1000&var_name0=action, client: 192.168.100.254, server: gcu.info, request: "POST /wp-login.php?action=update HTTP/1.0", host: "www.gcu-squad.org"

On constate ici que la variable de type BODY et de valeur action a provoqué un blocage correspondant à l'id 1000. Dans le fichier de règles de base, on peut lire :

MainRule "rx:select|union|update|delete|insert|table|from|ascii|hex|unhex|drop" "msg:sql keywords" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:4" id:1000;

Le mot update est souvent utilisé dans le cadre d'injections SQL, mais ici, il est inoffensif, nous allons donc l'ajouter en whitelist. Ceci est réalisé dans notre fichier naxsi.rules, ou éventuellement un fichier tiers inclus, en déclarant une BasicRule. Dans le cas qui nous occupe, cette règle aura la forme suivante :

BasicRule wl:1000 "mz:$BODY_VAR:action";

Décryptage : nous plaçons une whitelist (wl) pour l'id 1000, la zone de match (mz, match zone) est le BODY sur la variable action.

And voila !

Notez que ces règles pouvant d'avérer pénibles à maintenir, l'équipe de développement de naxsi propose sur un dépôt public des règles mises à jour pour plusieurs applicatifs PHP connus[15].

6. Keep control

Une nouvelle génération d'hébergement s'offre à nous, toujours bien heureusement articulée autour de Logiciels Libres, nous gagnons ici plus de contrôle, de lisibilité, mais aussi de sécurité. Bien que cela ne doive pas motiver plus de nonchalance dans les méthodes de développement, la conjonction d'un démon HTTP léger et puissant, d'un moteur de sécurité simple et compréhensible, et d'un système d'interprétation décorrélé du service de rendu, permet une granularité plus marquée, et par conséquent une isolation fort agréable des rôles.
Bien que cette démonstration a pris comme socle le système NetBSD, GNU/Linux n'est pas en reste puisque toutes les briques citées dans cet article sont bien évidemment disponibles dans les distributions majeures, quoique parfois moins à jour. De plus, il est à noter que nous sommes bien loin de l’expérimentation, puisque cette configuration est actuellement en place et en production sur de nombreux sites professionnels dans le cadre d'offres d'hébergement sécurisées, entre autres articulées autour du duo
nginx/naxsi.

Références

- [1] http://fr.wikipedia.org/wiki/LAMP

- [2] http://php.net/manual/en/install.unix.apache2.php

- [3] http://code.google.com/p/naxsi/

- [4] http://php-fpm.org/

- [5] http://news.netcraft.com/archives/2013/02/01/february-2013-web-server-survey.html

- [6] http://www.nbs-system.com/

- [7] http://www.netbsdfr.org/

- [8] http://wiki.nginx.org/HttpRealipModule

- [9] http://fr.wikipedia.org/wiki/Proxy_inverse

- [10] http://fr.wikipedia.org/wiki/R%C3%A9entrance

- [11] http://www.gcu-squad.org/

- [12] https://www.varnish-cache.org/

- [13] http://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-138/Varnish-un-proxy-qui-vous-veut-du-bien

- [14] http://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-140/Plus-loin-avec-Varnish

- [15] https://github.com/nbs-system/naxsi-rules