English version is available here: PHP-FPM, application server made by PHP.
Introduction
Avant, faire tourner plusieurs application PHP sur un serveur Web vous laissait avec 2 options:
- Des performances correctes, mais pas de sécurité car tout les scripts sont exécutés avec le même UID
- Une bonne sécurité au prix de perfs pas forcément bonnes par l'utilisation de suexec pour changer l'UID
Aujourd'hui, PHP-FPM est en train de changer la donne. PHP-FPM, pour FastCGI Process Manager, permet d'avoir en permanence des processus PHP, sur le modèle d'Apache pre-fork. Ces processus ont la particularité d'être regroupés en pool. Ce regroupement permet d'exploiter un certain nombre de fonctionnalités:
- UID/GID particuliers
- chroot
- Nombre de processus minimum et maximum, nombre de processus en attente (spare)
Du coup, il est possible d'atteindre un niveau d'isolation des applications PHP intéressant. Il n'est pas question ici de faire l'analyse détaillée du modèle de sécurité de PHP-FPM, mais bien de vous montrer concrètement comment vous pourriez faire.
Préparation
Je ne vais pas expliquer dans le détail l'installation des composants. D'autres l'ont fait bien avant moi, et probablement mieux que je ne pourrais le faire moi-même.
Nous allons simplement voir concrètement comment utiliser au mieux PHP-FPM pour déployer des applications PHP. Avant de commencer, jetons un oeil au cahier des charges:
- Une application peut être spécifique à un domaine. Dans ce cas, elle est rattachée au pool PHP-FPM du domaine.
- Une application peut être mutualisée au niveau du serveur. Dans ce cas, elle aura son propre pool PHP-FPM.
- Chaque pool doit avoir ses propres UID / GID
- Chaque pool doit si possible tourner dans un chroot.
La question du chroot est délicate: lors de mes tests, une application chrootée ne pouvais plus résoudre de mon de domaines. Ceci est sans doute dû à une librairie absente de l'arborescence de chroot, donc pensez-y si vous vous lancez.
Dans tous les cas, nous avons besoin d'une arborescence basique et ce quelque soit l'application concernée. Imaginons que toute votre arborescence web se trouve dans /var/www/.
Alors, les domaines hébergés setrouveront dans /var/www/domains/, les applications mutualisées dans /var/www/apps/.
Pour plus de détails en ce qui concerne les domaines, je vous renvoie aux autres publications s'appliquant à Apache ou Nginx:
- Configuration d'hôtes virtuels sur Apache avec support automatique des sous-domaines, du SSL, de l'authentification et avec le support des Alias
- Configuration d'hôtes virtuels sur NGinx avec support automatique des sous-domaines, du SSL et de l'authentification - version 2
En résumé, voici à quoi doit ressembler l'arborescence de notre domain domain.tld:
Quant à nos applications mutualisées, pas de grande différence:
Quelques précisions tout de même:
- Le répertoire
configaccueillera les fichiers de configuration liés au serveur Web et à PHP-FPM - Le répertoire
privateaccueillera les fichiers propres à l'application. Il ne s'agit pas du code, il sera stocké dansdocroot, mais des fichiers éventuellement générés par l'application, comme les sessions PHP par exemple. Ce répertoire ne sera accessible que par l'application elle-même et par les admin du serveur. - Le répertoire
tmpsera le répertoire temporaire de l'application.
Installation de PHP-FPM
Pas de surprise, je m'appuye pour cela sur l'excellent travail de Guillaume Plessis qui met gracieusement à disposition des utilisateurs Debian des paquets PHP 5.3.3 intégrant PHP-FPM via le site DotDeb.
Une fois installé, il est temps de commencer la configuration. PHP-FPM fonctionne donc un peu à la manière d'un serveur d'application. Pour cela, un process père va démarrer et tourner en tant que root, ce qui lui permettra de forker d'autres processus avec des caractéristiques que nous préciserons par la suite.
Configuration de PHP-FPM
La configuration de PHP-FPM sous Debian se trouve dans le fichier /etc/php5/fpm/php5-fpm.conf
Comme vous pouvez le constater, la conf globale est des plus réduite. Vous aurez bien entendu noté les 2 directives include en fin de fichier. Ces 2 directives vont nous permettre:
- D'activer les pools de domaines
- D'activer les pools d'applications
Exemple de pool de domaine
Premier pool à être configuré, le pool de domaine. Celui-ci devra tourner avec des UID et GID particulier. Par défaut, il sera chrooté pour ne voir que l'arborescence du domaine. Enfin, un certain nombre de paramètres PHP seront personnalisés.
Les pools de domaine utiliseront un UID/GID compris entre 9001 et 9999. Ce sera également le port d'écoute de PHP-FPM.
Points particuliers:
- Le pool va se mettre en écoute sur l'interface loopback (127.0.0.1) port TCP 9001.
- Le pool tounera sous l'UID et le GID
domain.tld. - Une fois lancé, le processus père va le "chrooter" dans
/var/www/domains/domain.tld. - La racine web de PHP sera
/var/www/domains/domain.tld/docrootou plus exactement/docrootsi le pool est chrooté. - Le répertoire temporaire du pool sera
/var/www/domains/domain.tld/tmpou plus exactement/tmpsi le pool est chrooté. - Chaque requète ne pourra durer plus de 2 sec (directives
php_value[max_execution_time]et/ourequest_terminate_timeout. - Chaque requète ne pourra utiliser plus de 1M de RAM (directives
php_admin_value[memory_limit].
Vous aurez remarquer qu'une directive ne tient pas compte du chroot éventuel: slowlog.
En fait, ceci est tout à fait normal: elle est prise en compte par le processus père et, de ce fait, insensible au chroot auquel on soumet le processus fils.
L'avantage ici est que la mesure est plus précise car s'effectuant en dehors du contexte d'exécution PHP. Néanmoins, il est pratique de journaliser les requètes lentes
par domaine, notamment dans le cas où le propriétaire du domaine possède un accès FTP ou SSH qui lui donne de ce fait accès à ces logs.
C'est bien joli tout ça, mais notre utilisateur domain.tld n'existe pas. il faut y remédier:
L'option --force-badname est nécessaire car le nom d'utilisateur comporte un point. Je n'ai pas vu d'effets de bord indésirable juqu'à maintenant, mais libre à vous
de faire autrement.
Exemple de pool applicatif
Second type de pool à être configuré, le pool applicatif. Celui-ci devra également tourner avec des UID et GID particulier. Par défaut, il sera lui aussi chrooté pour ne voir que l'arborescence de l'application. Enfin, un certain nombre de paramètres PHP seront personnalisés.
Les applications partagées entre plusieurs domaines utiliseront un gid/uid compris entre 10001 et 10999. Ce sera également le port d'écoute de PHP-FPM
Rien de particulier qui n'ai pas déjà été expliqué à l'étape précédente. Là encore, la directive slowlog ne tient pas compte du chroot éventuel. Et, bien entendu, il faut créer l'utilisateur adéquat:
Bien. Nous avons installé PHP-FPM, paramétré le processus père, paramétré 2 processus, un de domaine, l'autre d'application. Il est à présent temps de les rendre accessibles. Pour cela, rien de tel qu'un petit NGinx.
Configuration de Nginx
Je passe sur l'installation de NGinx. Elle ne présente absolument aucune difficulté sous Debian. Tout au plus pourrez-vous vous faire des nœuds au cerveau pour installer la version backport au lieu de stable.
Bref, rien de vraiment bien méchant, et de toute façon, largement documentée sur le Net. As usual, Google is your best friend ever !
Pour faire simple, Nginx va être configuré de la manière suivante:
- Un vhost par défaut, qui accueillera les URL de monitoring de vos pool PHP-FPM
- Ben oui, parce que c'est pas parce que c'est du FastCGI, que c'est chrooté et que ça tourne sous un ID particulier que faut pas surveiller un tantinet.
- Les URL d'entrée de vos applications partagées
- Ces définitions d'URL, directive Location en jargon Nginx, seront incluses dans les vhosts concernés.
Comme vous pouvez le constater, le vhost par défaut charge toutes les URL de monitoring (fichiers nginx_status.conf) des pool PHP-FPM.
Le vhost du domaine "domain.tld" quant à lui va charger un par un les fichiers de config des pools applicatifs qui doivent être accessibles sur ce domaine.
En ce qui concerne les applications, rien de bien difficile, pour qui comprend un peu la syntaxe Nginx:
Comme vous pouvez le constater, rien de délicat. Mais vous aurez besoin du fichier de configuration FastCGI. Celui-ci regroupe tous les éléments communs de la configuration FastCGI. Les autres, qui demeure propres à chaque domaine, vous permettrons de jouer sur, par exemple, les timeout:
Tests
Une fois tout ceci mis en place, il est temps de tester. Pour cela, créez un fichier /var/www/domains/domain.tld/docroot/index.php et un autre /var/www/apps/appname/docroot/index.php avec le contenu suivant:
Puis, pour vérifier que tout fonctionne bien, ouvrez les URL suiventes dans votre navigateur:
- Pool de domaine
- http://domain.tld/
- Pool applicatif
- http://domain.tld/appname/
Vous devez voir apparaître la page classique de PHPInfo. Sinon, ben... respirez un grand coup, faites une pause, et reprenez depuis le début ;-)
Conclusion
Cette méthode, bien qu'un peu contraignante, vous apportera de la souplesse dans la gestion du déploiement de vos applications PHP. En outre, vous bénéficierez d'une sécurité accrue, ce qui est loin d'être négligeable quand on sait que la majeure partie des vulnérabilités actuelles se situent justement dans les applications Web.
Bien sûr, tout n'est pas idéal: le chroot ne vous protégera pas contre un défacement de site si une faille est trouvée dans l'application. En revanche, une application bien cloisonnée limitera l'attaquant à l'arborescence de l'application, et l'empêchera d'avoir acès au reste du disque dur.
Enfin, comme toujours, tout ceci ne doit pas vous dispenser de sécuriser le reste du serveur. N'oubliez pas que "Le niveau de sécurité d'un ensemble correspond au niveau de sécurité du maillon le plus faible".
