# Isoler les sites WordPress sur un serveur : utilisateurs et pools séparés

> Sur un serveur mutualisé qui héberge quinze sites clients, un seul site piraté ne devrait jamais pouvoir en contaminer un autre. Voici l'architecture qui l'empêche.

- Auteur : Clément Hadrot
- Publié le : 2024-03-01
- Mis à jour le : 2024-03-01
- Catégorie : Sécurité
- URL : https://wpmoderne.dev.wordpress-developpement.fr/securite/isoler-sites-wordpress-serveur-utilisateurs-pools-separes/

## L’essentiel

- Un utilisateur système par site limite la portée d'une compromission
- Un pool PHP-FPM dédié isole la mémoire et les processus
- open_basedir empêche la lecture de fichiers hors du site

Un serveur que nous avons repris hébergeait quatorze sites WordPress de clients différents, tous exécutés sous le même utilisateur système `www-data`, avec un unique pool PHP-FPM partagé. Le jour où l'un de ces sites a été compromis via une extension vulnérable, l'attaquant a pu lire les fichiers `wp-config.php` des treize autres sites, simplement parce que rien au niveau du système n'empêchait un processus PHP d'un site d'accéder aux fichiers d'un autre.

Ce scénario, malheureusement courant sur les hébergements mutualisés bas de gamme, s'évite entièrement avec une architecture d'isolation correcte. Voici comment nous structurons désormais chaque nouveau serveur pour qu'un site compromis reste un incident isolé, pas une porte ouverte sur l'ensemble du parc.

## Un utilisateur système dédié par site

La base de l'isolation consiste à créer un utilisateur Unix distinct pour chaque site hébergé, propriétaire exclusif des fichiers de ce site, plutôt que de tout faire tourner sous un compte partagé comme `www-data`. Cette séparation permet d'exploiter ensuite les permissions du système de fichiers de façon significative : un utilisateur ne peut lire les fichiers d'un autre site que si les permissions le permettent explicitement, ce qui n'est jamais le cas par défaut.

```
useradd -m -d /home/site-client-a -s /usr/sbin/nologin site-client-a
chown -R site-client-a:site-client-a /home/site-client-a/www
```

Le shell `/usr/sbin/nologin` empêche toute connexion interactive directe avec ce compte : il sert uniquement de propriétaire de fichiers et d'identité pour le pool PHP-FPM associé, pas de compte utilisable pour se connecter en SSH.

## Un pool PHP-FPM séparé par site

> L'essentiel à retenir : Un utilisateur système par site limite la portée d'une compromission ; Un pool PHP-FPM dédié isole la mémoire et les processus ; open_basedir empêche la lecture de fichiers hors du site

PHP-FPM permet de définir plusieurs pools, chacun avec son propre utilisateur système d'exécution, sa propre configuration mémoire et son propre socket. Sur un serveur qui héberge plusieurs sites, un pool dédié par site garantit qu'un processus PHP tourne sous l'identité de ce site précis, et non sous une identité partagée qui donnerait accès à tous les autres.

```
; /etc/php/8.2/fpm/pool.d/site-client-a.conf
[site-client-a]
user = site-client-a
group = site-client-a
listen = /run/php/site-client-a.sock
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 10
php_admin_value[open_basedir] = /home/site-client-a/www:/tmp
```

Chaque bloc `server` nginx correspondant pointe vers le socket de son propre pool, ce qui garantit qu'une requête pour le site A est toujours exécutée sous l'utilisateur `site-client-a`, jamais sous celui d'un autre site.

## open_basedir : la dernière ligne de défense au niveau PHP

La directive `open_basedir`, réglée ici directement dans la configuration du pool, restreint les chemins que PHP est autorisé à ouvrir en lecture ou en écriture, même si un bug applicatif ou une faille dans une extension tentait d'accéder à un chemin arbitraire du système de fichiers. Un site compromis au niveau applicatif, même avec exécution de code PHP arbitraire, ne pourrait alors pas lire le `wp-config.php` d'un site voisin, car ce chemin sort du périmètre autorisé.

C'est une protection en profondeur qui complète, sans la remplacer, la séparation par utilisateur système : même si les permissions de fichiers étaient mal configurées quelque part, `open_basedir` bloquerait quand même la tentative d'accès.

## La base de données suit la même logique

L'isolation ne s'arrête pas au système de fichiers. Chaque site doit disposer de son propre utilisateur MySQL ou MariaDB, avec des droits limités à sa seule base de données, jamais un compte administrateur partagé entre plusieurs sites :

```
CREATE USER 'site_client_a'@'localhost' IDENTIFIED BY 'mot_de_passe_genere';
GRANT ALL PRIVILEGES ON site_client_a_db.* TO 'site_client_a'@'localhost';
FLUSH PRIVILEGES;
```

Ainsi, même en cas d'injection SQL exploitée sur un site, l'attaquant reste cantonné à la base de données de ce site, sans pouvoir explorer ou modifier celles des autres clients hébergés sur la même instance.

## Vue d'ensemble de l'architecture cible

Une fois ces trois niveaux mis bout à bout, l'architecture d'un serveur qui héberge plusieurs sites ressemble à ceci :

```
serveur mutualisé
├── nginx (un bloc server par site, socket dédié)
├── site-client-a (utilisateur système, nologin)
│   ├── pool PHP-FPM dédié (open_basedir restreint)
│   ├── /home/site-client-a/www (fichiers WordPress)
│   └── base MySQL site_client_a_db (utilisateur MySQL dédié)
├── site-client-b (utilisateur système, nologin)
│   ├── pool PHP-FPM dédié (open_basedir restreint)
│   ├── /home/site-client-b/www (fichiers WordPress)
│   └── base MySQL site_client_b_db (utilisateur MySQL dédié)
└── ... un bloc identique par site supplémentaire
```

Chaque branche est étanche vis-à-vis des autres : aucun chemin de fichier, aucun processus PHP et aucune connexion base de données ne traverse la frontière d'un site à l'autre, à supposer que chaque niveau soit correctement configuré.

## Ce que cette architecture n'empêche pas

L'isolation par utilisateur et pool protège contre la contamination latérale entre sites, mais ne remplace pas les bonnes pratiques applicatives sur chaque site individuellement : un site qui reste vulnérable le reste, il ne peut simplement plus servir de tremplin vers les autres. De la même façon, un noyau système compromis (élévation de privilèges au niveau du serveur lui-même) contourne toute cette isolation applicative ; elle relève d'un autre niveau de durcissement, celui du serveur d'hébergement dans son ensemble.

> Sur nos serveurs de production, cette isolation nous a déjà évité, à deux reprises en un an, qu'un site compromis via une extension abandonnée ne contamine les treize autres partageant le même serveur. Le coût de mise en place au provisionnement d'un nouveau site : quelques minutes supplémentaires, une fois le script de création automatisé.

## En résumé

Sur un serveur mutualisé qui héberge plusieurs sites WordPress, l'isolation par utilisateur système, pool PHP-FPM dédié, `open_basedir` et compte de base de données séparé transforme une compromission ponctuelle en incident isolé plutôt qu'en catastrophe généralisée. C'est un investissement de configuration initial modeste, largement rentabilisé dès le premier incident évité sur un parc de plusieurs sites.
