Durcir un hébergement mutualisé sous Apache avec nginx et suPHP
Lorsqu'on souhaite fournir un service d'hébergement mutualisé – que ce soit pour des dizaines d'utilisateurs ou simplement pour fournir un espace de développement Web pour sa bien aimée sur son propre serveur – il incombe à l'administrateur système de mettre en place un système d'hébergement qui soit à la fois flexible et sécurisé.
Apache et nginx sont deux serveurs Web qui sont radicalement différents : le
premier est souvent le serveur de choix pour faire de l'hébergement mutualisé,
en particulier grâce au contrôle qu'on laisse à l'utilisateur avec les fichiers
.htaccess
, et l'autre est plus petit, plus léger, plus minimaliste, et fait
généralement office de « reverse
proxy » pour aider à
« durcir » un Apache.
Pourquoi alors vouloir associer ces deux serveurs, s'ils sont tous les deux censés servir des pages Web ? Simplement parce que :
- j'héberge déjà ce site sur nginx et que je n'ai pas envie de tout casser, même si c'est pour que quelqu'un d'autre puisse faire mumuse sur le même serveur ;
- le mécanisme de « reverse proxy » permet de mettre en cache des résultats et de filtrer sans trop de difficulté les requêtes à destination du serveur Apache. On applique le principe de la défense en profondeur : pour trouer le serveur, il faudrait exploiter une faille aussi bien dans nginx que dans Apache. Bien entendu, cela ne protège pas forcément contre les failles des scripts PHP des utilisateurs.
Il y a également d'autres avantages, en particulier la compression, le chiffrement SSL ou la répartition de charge, mais dont la pertinence est discutable dans le cadre de l'auto-hébergement.
Ainsi, dans ce modeste billet, je me propose de vous montrer les grandes lignes de l'installation d'Apache, PHP et nginx sur Gentoo (le tout étant bien entendu adaptable à n'importe quelle distribution Linux, voire même BSD, Mac OS ou Windows). Au menu :
- installation de Apache et PHP avec les bons useflags ;
- configuration de nginx comme proxy inversé pour uniquement les sous-domaines que l'on souhaite ;
- configuration de suPHP afin de cloisonner les scripts PHP des utilisateurs.
Il y a bien entendu d'autres techniques, comme le lancement des serveurs Apache
et nginx dans des chroot
, dont je ne parlerai pas ici (ou seulement plus tard,
dans un autre billet). Après tout, résumer la sécurisation d'un serveur Web de
manière exhaustive dans un billet est une entreprise ambitieuse.
Installation de apache, mod_suphp et mod_rpaf
En tant que root :
# emerge -va apache mod_suphp mod_rpaf
Avant de valider, je vous recommande fortement de jeter un œil aux useflags à la compilation de apache ; en particulier, suexec est intéressant à activer.
Le module mod_suphp est un module qui va forcer l'exécution des scripts PHP
sous l'utilisateur et le groupe propriétaires du fichier, plutôt que sous
l'utilisateur apache
. Cela permet de faire du cloisonnement, c'est-à-dire
d'éviter que l'ensemble des utilisateurs subisse les conséquences d'une faille de
sécurité dans un script d'une seule personne.
Le module mod_rpaf, quant à lui, permet de logguer la bonne adresse IP
source lorsqu'Apache sera derrière le reverse proxy. Sans cela, tout le trafic
Web semblera provenir de localhost
, ce qui rendrait le traçabilité des actions
d'un méchant pirate / hacker / cracker / script kiddie / ce que vous voulez
sacrément plus délicate.
Mise en place du reverse proxy
nginx
Dans /etc/nginx/nginx.conf
(ou dans un fichier à part inclus par nginx.conf
si vous voulez faire les choses proprement), ajoutez une directive
server
supplémentaire (deux si vous voulez aussi faire du HTTPS). Le fichier
de configuration devrait ressembler à celui-ci :
http {
# trucs d'origine
server {
# ...
}
# Agir comme mandataire inversé pour tous les domaines listés dans
# la directive server_name.
server {
server_name user1.example.com user2.example.com;
listen 80;
listen [::]:80;
location / {
proxy_pass http://localhost:81/;
include /etc/nginx/proxy.conf;
}
}
# Même chose, mais en HTTPS
server {
server_name user1.example.com user2.example.com;
listen 443;
listen [::]:443;
ssl_certificate /etc/nginx/ssl/wildcard.example.com.crt;
ssl_certificate_key /etc/nginx/ssl/wildcard.example.com.key;
location / {
proxy_pass http://localhost:81/;
include /etc/nginx/proxy.conf;
}
}
}
Même sur un site en HTTPS, la directive proxy_pass pointe vers une URL en HTTP. En effet, derrière un reverse proxy, c'est nginx qui se charge du chiffrement. Idéalement, le certificat serveur présenté pour chaque vhost devrait être différent, mais comme je n'ai pour le moment qu'une seule personne qui profite de cette configuration, je n'ai pas eu l'occasion de tester si CAcert accepte de signer des certificats « wildcard ».
Le fichier /etc/nginx/proxy.conf
, s'il n'existe pas déjà, contiendra des
directives spécifiques aux cas où nginx agit comme reverse proxy. Son contenu
est le suivant :
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffers 32 4k;
On remarquera les directives proxy_set_header
, qui permettent de modifier ou
d'ajouter des en-têtes HTTP à la volée, à destination d'Apache. L'en-tête
Host est nécessaire pour qu'Apache puisse faire la différence entre ses
propres vhosts ; l'en-tête X-Real-IP sert à logguer la bonne IP source, pour
mod_rpaf.
Apache
Du côté d'Apache, il faut lui préciser d'écouter uniquement sur la boucle locale
(pour que le serveur ne soit accessible qu'en passant par nginx), sur un port
inutilisé (en l'occurrence, le port 81). Pour cela, dans
/etc/apache2/vhosts.d/00_default_vhost.conf
, remplacer le contenu par :
<IfDefine DEFAULT_VHOST>
Listen 127.0.0.1:81
Listen [::1]:81
NameVirtualHost *:81
<VirtualHost *:81>
ServerName localhost
Include /etc/apache2/vhosts.d/default_vhost.include
<IfModule mpm_peruser_module>
ServerEnvironment apache apache
</IfModule>
</VirtualHost>
</IfDefine>
À présent, on peut définir des VirtualHosts à sa guise, comme avec un hébergement mutualisé classique.
Enfin, dans /etc/conf.d/apache2
, n'oubliez pas d'ajouter -D PHP5
, -D RPAF
et -D SUPHP
pour activer les modules du même nom, et de retirer -D
SSL_DEFAULT_VHOST
, car on n'a pas besoin d'écouter en SSL. Sous d'autres
distributions comme Debian, regarder du côté des commandes a2enmod,
a2dismod, a2ensite et a2dissite.
Mise en place de PHP et de suPHP
Installez PHP (emerge php
), au moins avec les useflags apache2 et cgi.
Choisissez les autres flags à votre guise, en fonction des fonctionnalités que
vous souhaitez activer.
Dans chaque déclaration de VirtualHost Apache, il suffit d'ajouter la ligne :
suPHP_UserGroup user1 users
Évidemment, cette ligne est à adapter pour que cette ligne reflète l'utilisateur et le groupe principal du propriétaire du vhost.
Ensuite, dans /etc/suphp.conf
, modifiez la variable docroot
. Par exemple, si
tous les utilisateurs ont leurs pages HTML dans ~/html
:
;Path all scripts have to be in
docroot=/home/*/html
Tout script PHP exécuté en-dehors de ce répertoire sera bloqué par suPHP.
Enfin, dans le même fichier, adapter la section [handlers]
pour que les
chemins soient corrects :
[handlers]
;Handler for php-scripts
x-httpd-php="php:/usr/lib/php5.4/bin/php-cgi"
x-httpd-php5="php:/usr/lib/php5.4/bin/php-cgi"
x-httpd-php4="php:/usr/lib/php4/bin/php-cgi"
x-httpd-phtml="php:/usr/lib/php5.4/bin/php-cgi"
Finalisation
Démarrer Apache et redémarrer nginx pour que les nouvelles configurations soient prises en compte :
# /etc/init.d/apache2 start
# /etc/init.d/nginx restart
Il ne reste plus qu'à tester. En particulier, vérifier que suPHP fonctionne bien avec une petite page PHP :
<?php echo "PHP tourne sous l'utilisateur " . exec('/usr/bin/whoami') ?>
ou encore avec un bon vieux phpinfo()
.
Conclusion
Cette technique est une bonne démonstration des différences entre la configuration de nginx et Apache. Bien qu'Apache soit le serveur Web le plus répandu, en particulier pour ses fonctions d'hébergement mutualisé, ce serait intéressant de mesurer les performances d'un couple Apache-nginx ainsi configuré.
Une chose qui me met relativement mal à l'aise est le fait que suPHP semble
lancer les scripts PHP des utilisateurs de la même manière qu'Apache exécute des
scripts CGI ; cela signifie que chaque visite d'une URL implique le lancement
d'un interpréteur PHP entier, le chargement de toutes ses bibliothèques, sans
évidemment parler des include()
et autres require()
que font les scripts
eux-mêmes. Tout ceci ajoute une énorme charge supplémentaire, ce qui limite
donc beaucoup l'intérêt de cette solution dès qu'on a un peu plus de trafic.
Certains suggéreraient plutôt d'utiliser nginx directement avec PHP-FPM : sur
le forum de nginx ou le blog
d'Interfacelab en
particulier.
Et c'est pas pour dire mais... je ne peux pas parler de PHP sans mettre un lien vers phpsadness.com. Quoi, je trolle ?