Php-fpm est une interface entre le moteur d’exécution PHP et les serveurs web comme Apache ou Nginx. Sa rapidité d’exécution en fait une brique incontournable de l’hébergement hautes performances. Cet article se focalise sur la sécurité avec la mise en place d’environnements chroot, qui nécessite un minimum de rigueur. Nous utilisons cet environnement exigeant pour notre activité d’hébergement.

Php-fpm et apache 2.4

Installation

Php-fpm est tellement souvent associé au serveur Nginx qu’on en oublierait qu’il fonctionne également comme un charme avec Apache 2.4 en remplacement de mod_php. Sur ce point Nginx est plus limité puisque on n’a pas le choix dans le moteur PHP.

Sur un système debian son installation n’a rien d’extraordinaire, si ce n’est le paquet libapache2-mod-fastcgi qui se trouve dans les dépôts non-free pour des raisons de licence.

Ensuite, cette interface PHP se présente sous la forme d’un daemon qui tourne en tâche de fond au même titre qu’apache. L’interconnexion entre les deux se fait pas un socket ou bien une interface réseau (locale ou non). C’est la différence majeure avec mod_php qui intègre un moteur d’exécution PHP à chacun des threads d’Apache, même pour les fichier statiques. On comprend bien que la plus grande légèreté se traduit par des performances accrues.

Avantages

  • Fonctionnement en mode daemon connecté en socket ou interface réseau. Cela permet d’envisager une ferme d’exécution PHP pour la scalabilité en hébergement hautes performances.
  • Meilleures performances car apache utilise fastcgi pour exécuter le code PHP uniquement lorsque c’est nécessaire (pour les .php et pas les .jpg par exemple).
  • Adaptabilité à des environnements différents en créant un pool par site, et donc un fichier de configuration PHP par site.
  • Sécurité accrue si utilisation de chroot pour compartimenter les différents sites.

Inconvénients

Le module apache mod_headers ne pourra pas être utilisé en conjonction avec fastcgi comme spécifié dans ce rapport de bug. Si vous utilisez ce module notamment pour ajouter des en-têtes X-Robots-Tag alors il faudra changer de méthode : soit l’ajout des en-têtes en PHP, soit l’utilisation de redirections 301.

Configuration de php-fpm

Aperçu des fichiers de configuration

Sur Debian sa configuration se fait dans /etc/php/fpm . Le fichier php-fpm.conf permet de configurer le daemon (PID, logs, processus) tandis que le php.ini est tout à fait classique, c’est un fichier potentiellement différent pour php5-cli, mod_php et php-fpm.

Le répertoire conf.d contient des liens vers /etc/php/mods-available/ donc une fois configurés, les modules (opcache, mysqli, …) agissent pour toute exécution PHP sur le système. C’est le plus simple.

Le répertoire pool.d est spécifique à php-fpm, il permet de spécifier une configuration spécifique pour certains sites web. Chaque fichier donne lieu à un pool php-fpm qui peut être configuré différemment des autres en ce qui concerne les logs, le chroot, ainsi que tout autre réglage se trouvant habituellement dans php.ini.

Outre ses excellentes performances, php-fpm se démarque par une sécurité accrue en permettant de cloisonner chaque environnement d’exécution PHP dans un chroot.

Mise en place de php-fpm avec Apache

Pour utiliser php-fpm avec Apache  il faut activer trois modules, et faire attention à ce que mod_php ne soit pas activé par inadvertance. Il est théoriquement possible de faire tourner mod_php et php-fpm ensemble sur le même serveur web mais on n’entrera pas ici dans les spécificités de cette configuration particulière, vu l’intérêt limité.

Ensuite on peut modifier le fichier de configuration /etc/apache2/mods-enabled/fastcgi.conf pour activer PHP sur l’ensemble du serveur web. On retrouve cet extrait partout sur internet.

La ligne la plus importante est celle qui commence par « FastCgiExternalServer » avec le socket /var/run/php5-fpm.sock qui permet la connexion au pool configuré dans /etc/php/fpm/pool.d/www.conf.

Pour utiliser des pool différents, on va plutôt mettre ces lignes de configuration à l’intérieur d’un VirtualHost dans /etc/apache2/sites-enabled/site.

Si vous rencontrez l’erreur You don't have permission to access /php5-fcgi-site/index.php on this server , c’est peut-être qu’il manque l’ouverture de /usr/lib/cgi-bin .

Création d’un pool avec chroot

Pour créer un pool, on va créer un fichier dans pool.d qui se base sur celui existant.

On va ensuite modifier la configuration, les lignes les plus importantes sont reproduites ici sur le fichier qui en comporte plus de 400.

Les trois dernières permettent de spécifier des réglages différents sur ce pool. Ici on a forcé le répertoire temporaire et répertoire d’upload dans tmp à la racine du site « / ». Le chemin à inscrire ici sera interprété par PHP comme étant préfixé par le chemin de chroot. Donc ce /tmp sera interprété comme /var/www/site/tmp/.

On peut vérifier que les pool d’exécution PHP sont bien en activité.

La ligne chroot est déjà configurée mais elle ne risque pas de fonctionner en l’état et vous allez vous retrouver avec une page servie « File not found. » et un log d’erreur stderr: Primary script unknown . C’est généralement le moment où on se dit que le chroot c’est compliqué.

Précisions sur le chroot

Notre ami wikipédia nous annonce :

 chroot (change root) est une commande des systèmes d’exploitation UNIX permettant de changer le répertoire racine d’un processus de la machine hôte.

En d’autres termes, c’est une commande de base qui n’a à l’origine rien à voir la sécurité. Ce sont ses conséquences sur la restriction des droits qui en font un outil de sécurité extrêmement puissant à condition de ne pas l’utiliser en tant que root.

La mise en place de chroot pour cloisonner un daemon (web, ftp, …) nécessite une grande rigueur dans la configuration du serveur, la gestion des droits et la création des fichiers et dossiers dont dépendent les exécutables. Quelle que soit son application finale, il est bon de se remémorer les règles d’or du chroot. Cet article semble avoir été écrit au siècle dernier mais il est toujours d’actualité.

De mon coté, je vous propose une synthèse des prérequis pour envisager un chroot :

  • La racine chroot ne doit pas appartenir à l’utilisateur qui va évoluer dans le chroot. Donc /var/www/site appartient à root:root. Cela est d’ailleurs obligatoire pour chrooter sftp dans un autre contexte.
  • Seul l’utilisateur root a le droit d’écrire, pas group ni others. Donc www-data:www-data ne peut pas écrire dans /var/www/site mais il peut écrire dans /var/www/site/sous-répertoire.

Contenu du chroot

Maintenant que nous avons dit que le chroot va avoir pour conséquence de modifier la racine du système pour le processus, à quoi doit ressembler le répertoire /var/www/site ? Certains fichiers et dossiers sont nécessaires pour la bonne exécution des scripts PHP. Comme le dit très justement cet article pour nginx, avec le chroot, il faut les recopier.

Pour bien comprendre la démarche, on est en train de reconstruire une racine à l’intérieur de /var/www/site. Et cette racine, comme toute racine Linux qui se respecte, contient un répertoire /var, /etc, /usr etc. Cet article propose un script tout-en-un pour reconstituer l’arborescence.

Les données du site web se trouvent par défaut dans /var/www/site et on retrouve aussi bien des fichiers php que les répertoires var, etc et usr. Cela fonctionne mais je préfère séparer les dossiers système d’une part et la racine du site web d’autre part.

Je déplace donc tout le site internet dans /var/www/site/var/www/site . Une modification du fichier de configuration apache est requise sur DocumentRoot /var/www/var/www/site . Le reste suit correctement.

En prenant un peu de recul on se rend compte que le DocumentRoot de Apache est de deux répertoires à l’intérieur de l’environnement d’exécution PHP chrooté. Cela serait sort vraiment de l’ordinaire par rapport à une configuration standard basée sur mod_php.

Modifications nécessaires avec le chroot

L’utilisation de chroot est susceptible de bloquer certaines fonctionnalités de PHP comme les dates, les zones horaires, la connexion à la base de données et l’envoi d’email.

Lien vers ../..

Une dernière touche est nécessaire à cause de php-fpm, c’est de créer un lien pointant vers ../.. dans /var/www/site/var/www/site/var/www/ comme suit.

Pourquoi trois niveau d’imbrication ? Le premier niveau (à gauche dans la commande) est la racine chroot. Le second niveau (au milieu) est facultatif, uniquement parce que je souhaite ranger ici les données du site web. Le troisième niveau (à droite) consiste à créer le lien symbolique dans DocumentRoot/var/www/site  et il est requis par php-fpm. Ce troisième niveau est considéré comme un bug par la communauté et notamment sur serverfault. L’absence du lien vers ../.. entraîne un message « File not found. » et une ligne de log FastCGI: server "/usr/lib/cgi-bin/php5-fcgi-site" stderr: Primary script unknown .

Envoi d’emails

Cet article décrit comment faire fonctionner la fonction PHP mail() en ayant recours à un exécutable sendmail minimal, ainsi qu’un shell permettant de satisfaire aux prérequis de PHP qui lance des appels system pour exécuter sendmail.

La méthode proposée fonctionne parfaitement, il faut bien prendre garde à compiler sh en static, et mini_sendmail également (c’est le cas par défaut).

Exécution de binaires

Les fonctions php d’éxécution de commandes comme exec() , shell_exec() , ou proc_open()  font appel à /bin/sh. Pour des commandes très simples le shell minimaliste proposé par KZNL.de peut suffire. Si jamais vous rencontrez un soucis d’exécution alors il faudra inclure dans le chroot un /bin/sh digne de ce nom (lien vers /bin/dash sur Debian).

Pour la sécurité, on remarque que l’absence de /bin/sh empêchera l’exécution de commandes système, ce qui peut être intéressant.

Le shell minimaliste m’a déjà posé des soucis pour des redirections 2>&1 , ou encore lorsque le binaire était encadré par des simples quotes (ce que fait la fonction PHP escapeshellarg()  par exemple). Exemple '/usr/local/bin/wkhtmltopdf' /tmp/tmp_WkHtmlToPdf_8bIfSD.html /tmp/tmp_WkHtmlToPdf_ovifp4 . A en croire KZNL, PHP utilise le binaire sh selon le schéma suivant : /bin/sh -c <command> .

Fonctions avancées et /proc

Des fonctionnalités avancées comme l’utilisation de wkhtmltopdf ou encore la librairie curl peuvent nécessiter un accès à /proc. Notamment l’extension WordPress Broken Link Checker est connue pour utiliser /proc/loadavg en lecture. Pour cela je mounte /procdans /var/www/site/proc  en mode bind et lecture seule.

Socket mysql

Pour permettre l’accès au serveur mysql via le fichier socket (le plus performant) il faut que le répertoire de chroot contienne var/run/mysql/ avec le socket habituellement situé dans /var/run/mysql/. Pour sela on utilise mount en mode bind.

Ainsi le fichier de socket agit comme une passerelle qui permet d’exploiter le serveur mysql sans le mettre lui-même dans le chroot (lui et ses nombreuses librairies).

Pour mysql on peut s’affranchir de cette manipulation en utilisant l’interface loopback 127.0.0.1, plutôt que le fichier socket qui est utilisé lorsqu’on spécifie « localhost ».

Résolution DNS

Dans le chroot, PHP n’arrive plus à effectuer la résolution DNS. Cela empêche tout téléchargement depuis un nom de domaine (non IP) ainsi que l’envoi d’emails. Certains librairies email comme swift ou phpmailer doivent vérifier le MX.

Il faut commencer par inclure le fichier hosts dans le chroot. L’utilisation du fichier hosts seul permettrait de contrôler les noms de domaine qui ont le droit à la résolution avec PHP.

Puis ensuite on trouve souvent sur internet l’astuce d’utiliser nscd pour faire l’intermédiaire entre l’environnement chrooté et la résolution système.

Après mise en production du socket nscd avec php7.0-fpm pendant plusieurs années je déconseille d’utiliser nscd. Le daemon nscd est un vieux machin tout pourri qui marche mais pas tout le temps, une vrai galère à utiliser avec une conséquence fâcheuse : cURL et gethostbyaddr() marchent aléatoirement. En réalité PHP n’a pas besoin du socket nscd tant que toutes ses librairies soient accessibles depuis le chroot. En l’occurrence les dossiers /lib, /lib64 et /usr/lib doivent êtres montés en mode bind,ro dans le chroot (utiliser ldd sur l’exécutable php pour vérifier chez vous).

Pour vérifier le bon fonctionnement on peut utiliser le fichier PHP minimal suivant et voir ce qui sort. Cela dit, les lignes ci-dessous semblent fonctionner même si nscd est éteint, ce qui ne sera pas le cas d’une requête cURL en PHP.

Le service nscd doit être configuré pour ne mettre en cache que les données de type « host » et en aucun cas « passwd » car sinon, un script PHP serait capable de lister les utilisateurs du système en utilisant cette méthode.

Sessions PHP

Les sessions PHP sont habituellement stockées dans /var/lib/php5/sessions donc il faut créer ce dossier dans le chroot. Les dossiers var, lib, php5 seront en chmod 555 pour root:root tandis que sessions sera en chmod 755 pour www-data:www-data.

Il est conseillé de régler  session.gc_probability = 1 dans php.ini afin de supprimer automatiquement le contenu de ce répertoire sessions.

Iconv

La fonction iconv()  a besoin de  /usr/lib/x86_64-linux-gnu/gconv  qu’il suffira de replacer dans le chroot (source), ainsi que de /usr/lib/locale . Sans cela, on obtiendra un log du type PHP Notice: iconv(): Wrong charset, conversion from `UTF-8' to `CP1252' is not allowed .

Curl

Avec WordPress les mises à jour doivent être téléchargées depuis un serveur extérieur. J’ignore comment le CMS procède d’habitude mais avec la configuration décrite ci-dessus le paquet php5-curl est bien utile pour éviter le message « Le certificat SSL de l’hôte n’a pas pu être vérifié. ».

Conclusion

Maintenant que votre serveur web est configuré aux petits oignons, il ne reste plus qu’à migrer des sites internet dessus. Pour aller plus loin dans la recherche de la sécurité, je vous propose de lire mon article détaillé sur la sécurisation de WordPress. Les mesures de sécurité qui sont proposées sont également valables pour tout autre CMS écrit en PHP comme Drupal, Prestashop, etc.

Ressources externes