Durcir Nginx sur HestiaCP : bloquer les DDoS de couche 7 et accélérer les sites grâce au cache

Guide pratique pour créer un template Nginx personnalisé sur HestiaCP qui atténue les attaques DDoS de couche applicative (couche 7), tout en activant la mise en cache du contenu dynamique pour accélérer les sites.

Durcir Nginx sur HestiaCP : bloquer les DDoS de couche 7 et accélérer les sites grâce au cache

Une attaque DDoS « classique » vise à saturer la bande passante ou les connexions réseau du serveur (les attaques dites de couche 3/4, au niveau réseau et transport). Mais il existe une autre catégorie, souvent plus insidieuse car elle nécessite beaucoup moins de bande passante pour faire des dégâts : les attaques de couche 7, au niveau applicatif. Ici, la cible n'est pas la connexion réseau elle-même, mais le serveur web et les applications qui y tournent : des milliers de requêtes HTTP « légitimes » dans leur forme, mais envoyées si rapidement qu'elles épuisent le CPU, la mémoire et les connexions à la base de données, mettant à genoux même un serveur dont la connexion internet est loin d'être saturée.

Pour ceux qui gèrent leurs propres sites avec HestiaCP, le panneau de contrôle serveur open source basé sur Nginx (seul, ou en reverse proxy devant Apache), il existe un outil puissant mais peu connu pour se défendre contre ce type d'attaque : les templates Nginx personnalisés. Dans ce guide, nous verrons comment créer un template sur mesure qui ajoute des règles de mitigation des DDoS applicatifs, tout en exploitant le cache de Nginx pour accélérer le contenu dynamique (PHP, applications Node.js, conteneurs Docker), réduisant encore la charge du serveur, même en l'absence d'attaque.

Comment fonctionnent les templates Nginx dans HestiaCP

HestiaCP n'écrit pas « à la main » la configuration de chaque site : pour chaque domaine, il applique un template, c'est-à-dire un fichier modèle avec des espaces réservés (placeholders) que le panneau remplace par les valeurs réelles du domaine (nom de domaine, chemin des fichiers, utilisateur système, etc.) chaque fois que le domaine est créé ou « reconstruit ». Les templates par défaut se trouvent dans :

/usr/local/hestia/data/templates/web/nginx/          # Nginx utilisé en reverse proxy devant Apache
/usr/local/hestia/data/templates/web/nginx/php-fpm/  # Nginx avec PHP-FPM (sans Apache)

Chaque template est en réalité un trio de fichiers portant le même nom avec des extensions différentes : .tpl (configuration HTTP), .stpl (configuration HTTPS) et .sh (un script optionnel exécuté lors de la génération). Par exemple, le template « default » est composé de default.tpl, default.stpl et default.sh.

Règle d'or : ne jamais modifier les templates d'origine. Chaque mise à jour de HestiaCP écrase les fichiers par défaut, et toute personnalisation faite directement sur ceux-ci serait perdue. La bonne procédure consiste à les copier sous un nouveau nom et à ne travailler que sur la copie :

cd /usr/local/hestia/data/templates/web/nginx/php-fpm/
cp default.tpl hardened.tpl
cp default.stpl hardened.stpl
cp default.sh hardened.sh 2>/dev/null || true

(l'exemple utilise les templates pour Nginx + PHP-FPM ; si le site tourne derrière Nginx + Apache, la même procédure s'applique dans le dossier nginx/ plutôt que nginx/php-fpm/).

Dans les fichiers copiés, on trouve des espaces réservés comme %domain%, %docroot%, %user%, %home% et %backend_lsnr% : HestiaCP les remplace automatiquement par les valeurs correctes pour chaque domaine utilisant le template, donc il faut les laisser intacts tout en ajoutant les nouvelles directives autour.

Première règle de durcissement : limiter la fréquence des requêtes (rate limiting)

Le module ngx_http_limit_req_module, inclus par défaut dans Nginx, permet de compter les requêtes par adresse IP (ou toute autre variable) et de rejeter celles qui dépassent un seuil, en renvoyant une erreur HTTP 429 Too Many Requests. C'est la défense la plus simple et la plus efficace contre les inondations applicatives : un bot qui envoie des centaines de requêtes par seconde vers la page de connexion ou le moteur de recherche interne du site est bloqué avant de pouvoir consommer des ressources sur le backend PHP.

La zone de comptage ne doit être déclarée qu'une seule fois, globalement, dans le bloc http de la configuration principale de Nginx (/etc/nginx/nginx.conf, ou un fichier séparé inclus depuis celui-ci, par exemple /etc/nginx/conf.d/ddos-mitigation.conf) :

# 10 Mo de mémoire partagée suffisent pour suivre environ 160 000 IP
limit_req_zone $binary_remote_addr zone=req_zone_general:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=req_zone_login:10m rate=5r/m;

limit_conn_zone $binary_remote_addr zone=conn_zone:10m;

Deux zones de rate limiting distinctes sont créées ici : une générique (req_zone_general, 20 requêtes par seconde par IP) pour tout le site, et une beaucoup plus stricte (req_zone_login, 5 requêtes par minute) appliquée uniquement à la page de connexion, cible typique des attaques par force brute comme des DDoS. Une zone pour limit_conn a également été ajoutée : elle limite non pas la fréquence mais le nombre de connexions simultanées par IP, utile contre les tentatives de saturer le pool de connexions du backend.

Dans le template personnalisé (hardened.tpl et hardened.stpl), à l'intérieur du bloc server { ... }, on applique les zones qui viennent d'être définies :

server {
    listen      %ip%:%web_port%;
    server_name %domain_idn% %alias_idn%;
    root        %docroot%;

    # Limite les connexions simultanées par IP sur tout le site
    limit_conn conn_zone 20;

    location / {
        # Limite générale : autorise de courts pics (burst) sans
        # bloquer immédiatement un utilisateur qui clique rapidement
        limit_req zone=req_zone_general burst=40 nodelay;
        limit_req_status 429;
        try_files $uri $uri/ /index.php?$args;
    }

    location = /wp-login.php {
        limit_req zone=req_zone_login burst=5 nodelay;
        limit_req_status 429;
        include /home/%user%/conf/web/%domain%/nginx.hosting.conf*;
    }

    # ... le reste du template (gestion PHP-FPM, pages d'erreur, etc.)
    # reste identique au template « default » d'origine.
}

Quelques explications sur les paramètres utilisés, pour ceux qui ne travaillent pas tous les jours avec Nginx :

  • rate=20r/s : le débit « de croisière » autorisé, ici 20 requêtes par seconde par IP.
  • burst=40 : une sorte de « réservoir » de requêtes supplémentaires qu'une IP peut accumuler et épuiser d'un coup, pour ne pas pénaliser un véritable utilisateur qui navigue rapidement entre plusieurs pages (chaque image, chaque fichier CSS ou JS d'une page génère sa propre requête).
  • nodelay : sans cette option, Nginx mettrait en file d'attente (en retardant) les requêtes dépassant le seuil ; avec nodelay, les requêtes au-delà du burst sont au contraire rejetées immédiatement avec une erreur 429, évitant d'accumuler des connexions en attente qui consommeraient elles-mêmes des ressources.
  • limit_req_status 429 : définit explicitement le code d'erreur renvoyé (le défaut serait sinon 503, mais 429 est sémantiquement plus correct et mieux reconnu par les CDN et les outils de supervision).

Deuxième règle de durcissement : couper les connexions lentes (anti-Slowloris)

Une autre famille d'attaques de couche 7, connue sous le nom de Slowloris, n'envoie pas beaucoup de requêtes mais très peu, tout en maintenant chaque connexion ouverte le plus longtemps possible en envoyant les données par fragments infimes : elle épuise ainsi le nombre maximal de connexions simultanées que le serveur peut gérer, sans jamais devoir générer un trafic élevé. Le remède consiste à réduire les délais maximum que Nginx accorde à un client pour terminer l'envoi de sa requête :

client_body_timeout   10s;
client_header_timeout 10s;
keepalive_timeout     15s;
send_timeout          10s;

# Limite aussi les tailles, pour éviter des uploads anormaux utilisés comme vecteur d'inondation
client_max_body_size  10m;
large_client_header_buffers 4 8k;

Ces valeurs (bien inférieures aux 60 secondes par défaut) doivent elles aussi être placées dans le bloc server { ... } du template personnalisé, ou dans le bloc global http { ... } si l'on souhaite les appliquer à tous les domaines du serveur. Attention à ne pas exagérer : des délais trop agressifs peuvent interrompre des envois légitimes de fichiers volumineux depuis des connexions lentes ou mobiles ; 10 secondes constituent généralement un bon compromis pour des sites ordinaires.

Optimiser la mise en cache du contenu dynamique

Atténuer les DDoS applicatifs ne consiste pas seulement à « bloquer les méchants » : il s'agit aussi de rendre très peu coûteux le fait de répondre à une requête, afin qu'un volume élevé de trafic (malveillant, ou simplement un pic de popularité soudain) ne mette pas le serveur à genoux. Le cache de Nginx pour le contenu dynamique sert exactement à cela : les pages générées par PHP (ou par une application Node.js derrière un proxy) sont temporairement enregistrées sur disque et renvoyées à l'identique aux requêtes suivantes, sans devoir réexécuter le code applicatif et les requêtes à la base de données à chaque fois.

HestiaCP distingue deux cas, selon la pile technique choisie pour le domaine :

Nginx + PHP-FPM : cache FastCGI

Pour les domaines utilisant Nginx avec PHP-FPM (sans Apache), on utilise le cache FastCGI. Dans la configuration globale (le même fichier /etc/nginx/conf.d/ddos-mitigation.conf utilisé plus haut, ou nginx.conf), on définit le cache :

fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=FASTCGICACHE:100m inactive=60m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

Dans le template personnalisé, dans le bloc qui gère PHP-FPM, il faut ajouter l'inclusion d'activation après la ligne include /etc/nginx/fastcgi_params; déjà présente dans le template d'origine :

location ~ \.php$ {
    try_files $uri =404;
    fastcgi_pass   %backend_lsnr%;
    fastcgi_index  index.php;
    include        /etc/nginx/fastcgi_params;

    # Active le cache pour ce domaine (le fichier est créé par HestiaCP
    # lorsque le cache FastCGI est activé depuis le panneau pour ce domaine)
    include /home/%user%/conf/web/%domain%/nginx.fastcgi_cache.conf*;

    fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

Toutes les pages ne doivent cependant pas être mises en cache : un panier e-commerce, un espace personnel après connexion, ou un endpoint générant un contenu différent pour chaque utilisateur ne doivent pas servir à un visiteur la page enregistrée pour un autre. Ces chemins sont exclus en définissant la variable $no_cache :

set $no_cache 0;
if ($request_uri ~* "/(panier|checkout|wp-admin|my-account)") {
    set $no_cache 1;
}
if ($http_cookie ~* "wordpress_logged_in") {
    set $no_cache 1;
}

Une fois le template créé, le cache FastCGI pour un domaine spécifique s'active depuis la page de modification du domaine dans le panneau HestiaCP (section dédiée au template Nginx/backend), en sélectionnant le template nouvellement créé. Pour vider le cache à tout moment (par exemple après une mise à jour du site), il suffit d'utiliser le bouton « Purge Nginx Cache » sur la même page, ou en ligne de commande :

v-purge-nginx-cache utilisateur domaine.tld

Nginx + Apache : cache proxy

Lorsque le domaine utilise plutôt Nginx comme reverse proxy devant Apache (configuration très courante sur HestiaCP pour prendre en charge .htaccess et mod_php), le cache FastCGI n'est pas disponible : on utilise à la place le cache proxy de Nginx, qui fonctionne de manière conceptuellement identique mais s'applique au trafic proxifié (il fonctionne aussi très bien pour des applications Node.js ou des conteneurs Docker derrière le proxy). Les templates personnalisés dédiés à cet usage suivent une convention de nommage particulière : caching-nompersonnalise.tpl, caching-nompersonnalise.stpl et caching-nompersonnalise.sh, toujours créés en copiant le template de départ équivalent fourni par HestiaCP dans le dossier nginx/ (pas nginx/php-fpm/), puis sélectionnables comme « template de cache » dédié depuis la page du domaine.

Un avertissement concernant les WebSockets

Le template standard comme les templates de cache proxy incluent par défaut la directive proxy_hide_header Upgrade;, ajoutée à l'origine pour contourner un problème de compatibilité avec Safari. Cette ligne bloque cependant aussi les connexions WebSocket (utilisées par exemple par les chats en temps réel, les notifications push côté navigateur, ou certains panneaux d'administration) : si l'application hébergée en a besoin, il faut la supprimer du template personnalisé.

Appliquer le template et vérifier son fonctionnement

Après avoir enregistré les modifications des fichiers .tpl/.stpl, il faut sélectionner le nouveau template pour le domaine visé : dans le panneau HestiaCP, on ouvre la page de modification du domaine (Web > nom de domaine > Modifier), on déploie les options avancées et on choisit le template nouvellement créé (hardened, dans notre exemple) dans le menu déroulant du template Nginx/Proxy. Alternativement, en ligne de commande, on peut forcer la reconstruction de la configuration pour tous les domaines d'un utilisateur avec :

v-rebuild-user nomutilisateur

Avant de tout mettre en production, il est recommandé de valider la syntaxe et de recharger Nginx manuellement :

nginx -t && systemctl reload nginx

Pour vérifier que le rate limiting fonctionne réellement, on peut simuler un petit flux de requêtes répétées depuis sa propre machine (à ne pas confondre avec un véritable test de charge, qui doit être mené avec précaution et uniquement sur des systèmes dont on est propriétaire) :

for i in $(seq 1 80); do
  curl -o /dev/null -s -w "%{http_code}\n" https://domaine.tld/
done | sort | uniq -c

Si la configuration est correcte, les premières requêtes (dans la limite du burst configuré) renverront 200, tandis que les suivantes, une fois le seuil dépassé, répondront 429. C'est le signe que la zone de rate limiting fait bien son travail.

Quelques précautions avant d'activer tout cela en production

Le rate limiting basé sur l'adresse IP a une limite intrinsèque à garder à l'esprit : de nombreux utilisateurs naviguant depuis des réseaux d'entreprise, des universités, ou certains réseaux mobiles partagent la même IP publique via NAT, et des seuils trop bas risquent de bloquer des utilisateurs légitimes en même temps que d'éventuels bots. C'est pourquoi il est conseillé de commencer avec des valeurs prudentes (comme les 20 r/s de l'exemple), de surveiller les journaux d'erreurs 429 pendant quelques jours, et de ne resserrer les seuils qu'ensuite si nécessaire. Il est également recommandé d'exclure du rate limiting les adresses IP de ses propres outils de supervision (contrôleurs de disponibilité, CDN, services de sauvegarde), à l'aide d'une directive geo ou d'un simple bloc map placé avant la zone de limit_req, pour éviter les fausses alertes.

Enfin, le rate limiting applicatif décrit dans ce guide constitue une première ligne de défense efficace contre les DDoS de couche 7, mais il ne remplace pas une protection dédiée au niveau réseau (couche 3/4) contre les attaques volumétriques très importantes : pour celles-ci, des services de mitigation en amont du serveur restent indispensables, comme un CDN avec protection anti-DDoS ou le filtrage proposé par certains hébergeurs et fournisseurs de connectivité.

Sources officielles

Partager

Quelle est votre réaction ?

Aimer Aimer 0
Je n'aime pas Je n'aime pas 0
Amour Amour 0
Drôle Drôle 0
En colère En colère 0
Triste Triste 0
Waouh Waouh 0