Protéger WordPress du bruteforce sans plugin (avec NGINX et fail2ban)

Dans cette documentation, je vais vous montrer comment protéger WordPress du brute force sans utiliser aucun plugin WordPress.

Qu’est-ce que le brute force ?

Rien de tel qu’une article Wikipédia pour nous l’expliquer :

L’attaque par force brute est une méthode utilisée en cryptanalyse pour trouver un mot de passe ou une clé. Il s’agit de tester, une à une, toutes les combinaisons possibles.

Attaque par force brute – Wikipédia

C’est assez simpliste comme attaque, il suffit de tester un à un la liste de mot de passe compatible. Bien évidemment, des logiciels sont en place pour que l’opération soit automatisée.

Et aujourd’hui, elle est, la plupart du temps, opéré depuis des serveurs piratés/vérolés (ce qui permet de faire beaucoup de requêtes en parallèle et cibler ainsi plus de sites et/ou tester beaucoup plus de mot de passe par seconde) ou depuis des PC ou autres appareils du quotidiens qui sont piratés ou également vérolés.

La deuxième technique est fortement répandue du fait qu’aujourd’hui, la plupart des pages de connexion ne sont pas accessibles depuis des adresses IPs de datacenter : pour se préserver de ce type d’attaque, certains administrateurs systèmes bloquent systématiquement l’accès aux pages de connexion dès que l’adresse IP est détectée comme provenant d’un datacenter.

Et pour finir le tour de ce type d’attaque, sachez qu’aujourd’hui les hackers ne testent pas toutes les combinaisons, mais « juste » les combinaisons les plus probables : les mots de passes les plus utilisés, les mots de passes possible en exploitant des informations depuis le site lui-même (url du site, nom du compte administrateur, …) ou depuis vos données personnelles visible en ligne (votre date de naissance, retrouvé depuis un réseau social, par exemple).

Ne pas se protéger, la meilleure solution

C’est interloquant comme titre, non ? Oui, ne pas se protéger est la meilleure solution. Ceci consiste tout simplement à fermer la porte pour tout le monde et sans exception. De cette façon, vous ne dépensez pas inutilement de ressources pour identifier si tel ou tel requête fait partie d’une attaque ou non.

Ainsi, restreindre l’accès à wp-login.php et wp-admin à seulement votre adresse IP reste la meilleure solution. Mais ce n’est malheureusement toujours pas possible. C’est notamment le cas quand vous avez des adresses IPs dynamiques, quand vous êtes plusieurs à travailler sur le site, … Bref, faire disparaître la porte peut causer des tas d’inconforts du côté des administrateurs du site.

Pourquoi ne pas utiliser un plugin ?

Là, vite fait, j’ai trois arguments à vous proposer :

  • Moins de plugin = moins d’emmerdes sur WordPress. Je ne sais pas si vous avez déjà fait le test mais l’ajout d’un plugin (n’importe lequel) augmente le temps de réponse de WordPress et diminue donc sa vitesse de chargement. Et installer un plugin WordPress c’est aussi un peu plus de risque de bugs et d’instabilité, même si franchement les développeurs de plugins de sécurité WordPress font actuellement un travail très propre. J’estime que le jeu n’en vaut pas la chandelle.
  • Faire des actions sur WordPress n’est pas toujours possible. Notamment si on est dans la position d’hébergeur web. Modifier le site d’un client serait très mal vu, même si c’est pour mieux lui protéger, car vous risquez fortement d’induire des problèmes dans le site lui-même (incompatibilité avec votre plugin de sécurité, modifications particulières faites sur WordPress par le développeur, …).
  • Un plugin WordPress n’a pas les outils nécessaires pour agir en amont. Le plugin WordPress ne peut rien exécuter tant que WordPress n’est pas démarré. Donc, à chaque requête, WordPress démarre d’abord, avant qu’un plugin de sécurité ne puisse finalement annuler la requête d’un attaquant. Agir à un niveau plus haut permet d’empêcher WordPress de gaspiller un temps de chargement et des ressources pour les attaquants.
  • Bonus ou argument numéro 3 bis (à vous de voir), je vois beaucoup de plugin de sécurité de faire l’erreur de stocker les adresses IPs bloqués dans MySQL. Le souci se manifeste quand vous commencez à avoir 40 000 ou 70 000 IPs bloqués. C’est un nombre tout à fait raisonnable vu le fléau d’attaque sur internet, mais c’est très « amateur » de vouloir rechercher des données sur 70 000 lignes sur MySQL à chaque ouverture de page. Et encore pire quand on sait que l’adresse IP est stocké dans un champ non indexé ou un champ VARCHAR ou TEXT. Et loguer tout cela, en prime, c’est l’enfer. Or, avec des outils comme fail2ban, il y a des moyens de loguer cela en asynchrone.

Et de l’autre côté, j’ai quelques 3 avantages à mettre en avant :

  • La mise en place peut se faire sans accès au tableau de bord du site. Ceci permet notamment de protéger en une opération tous les sites d’un même serveur.
  • Les logs ne sont pas dans MySQL. C’est un avantage non négligeable pour garder la base de données WordPress léger et rapide à accéder. wp_postmeta chauffe déjà assez. 😅
  • Le blocage se fait au niveau du service web voire même au niveau du kernel (iptables) ce qui permet d’économiser les ressources.

Mise en place de la protection bruteforce sur NGINX

Dans un premier temps, j’ai ajouté une zone de limite de requête dans NGINX :

echo "limit_req_zone $server_name$binary_remote_addr zone=wpratelimit:10m rate=10r/m;" > /etc/nginx/conf.d/wp-rate-limit.conf

Vous pouvez notamment constater :

  • limit_req_zone : la directive NGINX utilisée
  • $server_name$binary_remote_addr : la clé d’identification du client. Dans mon cas, j’ai mis à la fois $server_name (nom du site) et $binary_remote_addr (adresse IP du client en binaire) pour pouvoir identifier les clients différemment par site. Ceci me permet de ne pas bloquer un client parce qu’il accède à 10 sites WordPress sur le même serveur.
  • zone=wpratelimit:10m : création de la zone « wpratelimit » et qui a 10 Mo de taille
  • rate=10r/m : la limite est de 10 requêtes par minute.

Ensuite, il faut appliquer cette zone de limite aux fichiers wp-login.php et xmlrpc.php. Pour ma part, j’ai imbriqué les directives location étant donné que j’utilise fastcgi directement pour servir les fichiers PHP :

location ~ \.php$ {
    [...]

    location ~* (wp-login|xmlrpc)\.php$ {
      limit_req zone=wpratelimit;
      add_header RateLimit "yes";
    }
  }

Si vous utilisez NGINX comme reverse proxy, il faudra implémenter différemment, mais le principe reste le même. Il faudra également avoir un log d’erreur que fail2ban analysera régulièrement :

error_log /home/monsite/logs/error.log

N’oubliez pas de recharger NGINX après les modifications :

systemctl reload nginx

Bloquer les attaques répétés avec fail2ban

Une fois les attaques par bruteforce bloqués sur NGINX, il faudra bloquer les attaques répétés depuis iptables pour réduire le risque de sécurité (un attaquant pourrait potentiellement essayer autre chose que WordPress) et les ressources consommés.

Pour cela, il faut créer un nouveau filtre fail2ban dans /etc/fail2ban/filter.d/wp-rate-limit.conf :

[Definition]
ngx_limit_req_zones = wpratelimit
failregex = ^\s*\[[a-z]+\] \d+#\d+: \*\d+ limiting requests, excess: [\d\.]+ by zone "(?:%(ngx_limit_req_zones)s)", client: <HOST>,
ignoreregex =
datepattern = {^LN-BEG}

Notez que c’est basé sur le fichier etc/fail2ban/filter.d/nginx-limit-req.conf disponible sur fail2ban par défaut.

Ensuite, dans /etc/fail2ban/jail.local, j’ajoute un nouveau prison :

[wp-rate-limit]
enabled = true
bantime = 86400
maxretry = 3
logpath = /home/*/logs/error.log tail

Comme vous voyez, je bloque pour 86 400 secondes (24h si vous voulez) au bout de 3 blocages détectés sur NGINX. Et j’analyse les fichiers d’erreurs sur /home/*/logs/error.log. Le tail à la fin de logpath me permet de ne pas recharger les fichiers en entier au démarrage de fail2ban, ce qui diminue fortement la charge I/O engendrée (ça se voit beaucoup sur un serveur à 1000 sites).

N’oubliez pas de redémarrer fail2ban par la suite :

systemctl restart fail2ban

Place aux tests

Pour tester, j’ai utilisé Apache Benchmark (ab) pour effectuer « beaucoup » de requêtes sur wp-login.php :

ab -n 20 https://www.monsite.com/

Au bout de 10 requêtes, l’IP est bloquée. On peut constater cela depuis le fail2ban-client :

fail2ban-client status wp-rate-limit

Et les blocages faites par NGINX sont logués :

[root@serveur ~] tail /home/monsite/logs/error.log
2022/05/06 20:49:03 [error] 14560#14560: *312 limiting requests, excess: 0.991 by zone "wpratelimit", client: 1.2.3.4, server: www.monsite.com, request: "GET /wp-login.php HTTP/1.0", host: "www.monsite.com"
2022/05/06 20:49:03 [error] 14560#14560: *312 limiting requests, excess: 0.991 by zone "wpratelimit", client: 1.2.3.4, server: www.monsite.com, request: "GET /wp-login.php HTTP/1.0", host: "www.monsite.com"
2022/05/06 20:49:03 [error] 14560#14560: *312 limiting requests, excess: 0.991 by zone "wpratelimit", client: 1.2.3.4, server: www.monsite.com, request: "GET /wp-login.php HTTP/1.0", host: "www.monsite.com"
2022/05/06 20:49:03 [error] 14560#14560: *312 limiting requests, excess: 0.991 by zone "wpratelimit", client: 1.2.3.4, server: www.monsite.com, request: "GET /wp-login.php HTTP/1.0", host: "www.monsite.com"
2022/05/06 20:49:03 [error] 14560#14560: *312 limiting requests, excess: 0.991 by zone "wpratelimit", client: 1.2.3.4, server: www.monsite.com, request: "GET /wp-login.php HTTP/1.0", host: "www.monsite.com"
2022/05/06 20:49:03 [error] 14560#14560: *312 limiting requests, excess: 0.991 by zone "wpratelimit", client: 1.2.3.4, server: www.monsite.com, request: "GET /wp-login.php HTTP/1.0", host: "www.monsite.com"
2022/05/06 20:49:03 [error] 14560#14560: *312 limiting requests, excess: 0.991 by zone "wpratelimit", client: 1.2.3.4, server: www.monsite.com, request: "GET /wp-login.php HTTP/1.0", host: "www.monsite.com"
2022/05/06 20:49:03 [error] 14560#14560: *312 limiting requests, excess: 0.991 by zone "wpratelimit", client: 1.2.3.4, server: www.monsite.com, request: "GET /wp-login.php HTTP/1.0", host: "www.monsite.com"
2022/05/06 20:49:03 [error] 14560#14560: *312 limiting requests, excess: 0.991 by zone "wpratelimit", client: 1.2.3.4, server: www.monsite.com, request: "GET /wp-login.php HTTP/1.0", host: "www.monsite.com"
2022/05/06 20:49:03 [error] 14560#14560: *312 limiting requests, excess: 0.991 by zone "wpratelimit", client: 1.2.3.4, server: www.monsite.com, request: "GET /wp-login.php HTTP/1.0", host: "www.monsite.com"
2022/05/06 20:49:03 [error] 14560#14560: *312 limiting requests, excess: 0.991 by zone "wpratelimit", client: 1.2.3.4, server: www.monsite.com, request: "GET /wp-login.php HTTP/1.0", host: "www.monsite.com"

Laisser un commentaire