Une note avant de commencer. Toutes les commandes et configurations sont disponibles sur ce Gist également
Table des matières
Le contexte
Depuis quelques semaines je m’amuse avec PHP-FPM et notamment son option permettant de lancer PHP dans un environnement chrooté.
Initialement je m’étais dit que c’était une bonne idée du point de vue sécurité, mais en faisant quelques recherches, je me suis vite rendu compte que le chroot n’est pas une sécurité.
Pour preuve, c’est très simple de s’évader d’un chroot comme le montre cet article très instructif.
Bref, le soucis n’est pas là. La difficulté avec le chroot, est qu’il faut copier des bibliothèques systèmes et d’autres exécutables dans le répertoire « chrooté » afin que PHP puisse s’exécuter correctement. Il faut re-concevoir une sorte de « / » minimaliste, permettant de faire fonctionner juste ce que l’on souhaite.
Le problème, c’est qu’il y a toute la partie DNS et SSL à remettre en place dans cet environnement chrooté et ce n’est pas forcément évident, car il faut trouver les bons fichiers à copier par exemple puis faire la liste des dépendances manuellement.
Un premier exemple de gestion des dépendances
Pour illustrer cela, commençons par un exemple simple : chrooter l’exécutable de bash dans un répertoire « chroot_bash ». C’est simple car les dépendances de bash ne sont pas très nombreuses.
Le fait de simplement copier l’exécutable de bash ne suffit pas, car bash à besoin de certaines bibliothèques qui ne sont pas installés dans l’environnement chrooté. Pour voir les bibliothèques qu’utilise bash, on peut utiliser la commande « ldd » :
La plupart des bibliothèques sont dans /lib64, on va donc recréer ce dossier dans l’environnement chrooté puis copier les bibliothèques nécessaires à bash :
Maintenant, si on teste à nouveau de lancer bash en chroot, on obtient un shell minimaliste :
La commande « ls » n’est même pas installée, on peut l’installer en copier le binaire dans le dossier « bash_chroot » puis en utilisant « ldd » de la même manière pour installer les dépendances.
Voilà le genre de choses qu’il faut faire créer un environnement chrooté. Pour le moment, c’est assez simple car ldd donne tout ce qu’il faut. Cependant, dés que ça touche un peu au réseau, aux résolutions DNS et aux SSL, ça se complique.
Création d’un envionnement chrooté pour PHP
Abordons le vif du sujet, la préparation d’un environnement chrooté pour le bon fonctionnement d’une application PHP. On va se mettre pour objectif de faire fonctionner correctement WordPress dans un environnement chrooté avec php-fpm. Ca permet d’avoir un exemple concret.
On commence par créer un utilisateur, créer un dossier pour accueillir le site puis mettre la base de WordPress dedans pour commencer à travailler :
Ensuite, on génère un pool php-fpm (dans ma configuration, j’utilise un utilisateur par site et un pool par utilisateur) et la configuration nginx (minimaliste, largement améliorable).
La configuration PHP-FPM permettant le chroot est la ligne « chroot » surligné ci dessus. Les logs seront conservés dans l’environnement chrooté, tout comme les sessions PHP.
Quelques détails sur la configuration :
- J’utilise des sockets pour PHP-FPM, je trouve cela plus élégant que d’avoir différents ports d’écoute.
- Dans nginx, la subtilité se trouve dans le fastcgi_param, le « SCRIPT_FILENAME » est modifié de telle sorte à ce que Nginx génère un chemin à partir /home/tuto et non à partir du « / » tout court (donc du point de vue de PHP, le / sera directement /home/tuto en réalité).
Maintenant, on peut commencer à peupler /home/tuto avec quelques fichiers nécessaires :
A ce stade, nous avons un WordPress à peu près fonctionnel. Quelques notes supplémentaires :
- Pour la base de données, la solution la plus élégante que j’ai trouvé était un « mount –bind » sur le socket de mysql sinon le chroot n’y a pas accès
- La plupart des fichiers copiés sont pour les accès réseau, sans cela, le « localhost » du fichier de configuration de wordpress pour la base de données ne fonctionne pas par exemple (car contenu dans /etc/hosts)
- Vous remarquez la dernière commande qui permet de créer un /dev/null dans l’envrionnement chrooté. Je pense que le /dev/null est trop utilisé pour être ignoré. Je n’ai pas testé sans, mais ça doit causer des problèmes « bizarres ».
Les choses qui ne fonctionnent pas
En apparence WordPress est fonctionnel, mais si vous allez dans la gestion des extensions et notamment l’ajout, vous aurez l’erreur suivante :
En activant le debug PHP et le debug de WordPress, on obtient :
Warning: Une erreur inattendue s’est produite. Quelque chose semble ne pas fonctionner avec WordPress.org ou la configuration de ce serveur. Si vous continuez à rencontrer des problèmes, veuillez essayer les forums de support. (WordPress n’a pas pu établir de connexion sécurisée vers WordPress.org. Veuillez contacter l’administrateur de votre serveur.) in /www/wp-admin/includes/plugin-install.php on line 83
Si on regarde le fichier PHP, on arrive sur le bloc de code suivant :
Même problème avec Akismet si vous essayez d’activer votre clé d’api. Ces deux erreurs sont surtout présentes si votre blog est en SSL, car WordPress va faire des requêtes vers ces deux services (api wordpress.org et akismet) en SSL de préférence.
Je ne suis pas un expert WordPress, mais de ce que je vois, ça fait une requête, sans doute via CURL sur une API distante. En bidouillant un peu le code et notamment en faisant affiché le « $url » et « $http_args », j’ai vu que la requête était sur : https://api.wordpress.org/plugins/info/1.0/
A ce stade, il y a deux soucis :
- La résolution pour api.wordress.org échoue
- Le HTTPS ne fonctionne pas
Le problème de résolution DNS
Il manque la plupart des bibliothèques DNS nécessaires à PHP pour faire fonctionner « gethostbyname » par exemple. Après des recherches, j’ai trouvé que les fichiers suivants étaient manquant :
Les noms peuvent changer un peu, sachant que je travaille sous Centos 7.1 lors de la rédaction de ce billet. Globalement, il faut reprendre /lib64/*dns* et refaire les liens symboliques aussi.
Désormais, la partie DNS devrait fonctionner, c’est testable avec ce bout de code :
Il faut bien relancer php-fpm après avoir fait tout cela, j’ai passé 20 minutes à ne pas comprendre pourquoi la requête DNS ne se faisait pas à cause de cet oubli …
Le problème avec CURL sur des liens HTTPS (SSL)
C’est le problème avec lequel j’ai passé le plus de temps. Si vous souhaitez juste la solution, sautez quelques paragraphes et lisez en diagonales les commandes.
Déjà, pour comprendre que le problème venait de SSL (car j’avais toujours des doutes sur la partie résolution DNS), j’ai dû modifier un peu le fichier de WordPress (celui cité plus haut) pour forcer le passage en HTTP classique (en modifiant la variable PHP $url directement).
J’ai vu que la requête passait en HTTP mais pas en HTTPS.
Pour comprendre d’où cela venait, j’ai activé le debug de PHP, mais je n’avais pas d’erreurs. J’ai donc utilisé un outil magique qui est : strace.
Cet outil permet d’avoir une idée de ce que fait un processus en cours d’exécution et est très utile dans le genre de cas ou les logs ne disent pas grand choses et qu’on doit observer une « boite noire » en quelque sorte.
Pour faire cela, j’ai donc diminué le nombre de worker php-fpm à 1, afin d’avoir un seul PID. Le strace fonctionne en donnant le PID du processus et avec plusieurs worker c’est délicat d’intercepter le bon.
On rafraîchie la page WordPress qui plante et on analyse le fichier de log généré. Généralement je commence par le bas du fichier (souvent ça « plante » vers la fin).
En regardant les logs, des IP sautent aux yeux (donc la résolution DNS se fait bien), voici la partie intéressante isolé du reste :
connect(5, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr(« 66.155.40.203 »)}, 16) = -1 EINPROGRESS (Operation now in progress)
poll([{fd=5, events=POLLOUT|POLLWRNORM}], 1, 0) = 0 (Timeout)
poll([{fd=5, events=POLLOUT}], 1, 1000) = 1 ([{fd=5, revents=POLLOUT}])
poll([{fd=5, events=POLLOUT|POLLWRNORM}], 1, 0) = 1 ([{fd=5, revents=POLLOUT|POLLWRNORM}])
getsockopt(5, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
getpeername(5, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr(« 66.155.40.203 »)}, [16]) = 0
getsockname(5, {sa_family=AF_INET, sin_port=htons(59211), sin_addr=inet_addr(« 163.172.12.146 »)}, [16]) = 0
stat(« /etc/pki/nssdb », 0x7ffd8fded480) = -1 ENOENT (No such file or directory)
Premier réflexe, on copie tout ce dossier « pki » puis on fait des recherches sur « nss » également.
Pour simplifier un peu les tests et comprendre les dépendances nécessaires à curl, utilisé par WordPress, on va copier l’exécutable curl et ces bibliothèques dans le chroot.
Les dépendances sont plus complexes cette fois, je vous recommande d’utiliser un script pour gérer cela automatiquement (facilement trouvable sur internet, par exemple celui de cyberciti est souvent recommandé). De mon côté, j’ai codé le script car j’avais envie de le faire en bash.
Une fois que cela est fait, on peut déboguer cela en ligne de commande directement :
On a déjà un peu plus d’information qu’avec PHP.
Dans ce strace, quelques éléments sont intéressants :
open(« /proc/sys/crypto/fips_enabled », O_RDONLY) = -1 ENOENT (No such file or directory)
open(« /lib64/libsoftokn3.so », O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
readlink(« /lib64/libnss3.so », 0x137e400, 1023) = -1 EINVAL (Invalid argument)
On ajoute les libs manquantes :
Maintenant, vient l’erreur vicieuse qui m’a fait perdre beaucoup de temps. En l’état, ça ne fonctionne toujours pas, car il manque une bibliothèque mais qui n’a aucun rapport avec SSL.
En relance une nouvelle fois un strace et en lisant les logs, on observe que ça progresse, car les erreurs précédentes ne sont plus présentes, par contre, deux nouvelles erreurs apparaissent :
Il manque la bibliothèque « libsqllite3 » qui est nécessaire à NSS car ce dernier manipule des fichiers .db du répertoire /etc/pki/nssdb. J’étais passé totalement à côté de ce détail, pourtant évidant dès qu’on y pense. J’étais concentré sur SSL en me disant qu’il manquait encore des choses.
Conclusion
J’espère vous avoir montré deux choses dans cet article, la première étant de voir comment il est possible d’avoir un SSL fonctionnel dans un environnement chrooté et la deuxième, la plus importante pour moi, est l’utilisation de strace. C’est un outil à connaitre et à utiliser lorsque les logs ne parlent pas beaucoup. Il est très verbeux par contre, vous avez des options pour limiter l’affichage éventuellement.
J’ai tout fait « manuellement » dans cet article car je suis encore dans une phase d’apprentissage pour php-fpm et son option de chroot, je veux bien comprendre le mécanisme et éviter de copier/coller bêtement des choses trouvables sur internet.
Si vous voulez faire cela, sachez qu’il existe pas mal d’outils pour automatiser tout cela, notamment pour la partie avec « ldd » (par exemple celui-ci).
Si vous avez des idées, des remarques sur la partie SSL, chroot (encore un peu nouveau pour moi !) ou strace n’hésitez pas à partagez cela dans les commentaires.