Durcir un hébergement mutualisé sous Apache avec nginx et suPHP

Auteur :  x0r Publié le   Nombre de commentaires : 0
Mots-clefs : sysadmin nginx apache reverseproxy 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 ?

Commentaires

Poster un commentaire

Poster un commentaire