Sécuriser WordPress : configuration du fail2ban

Protéger WordPress, ce n’est pas seulement empiler des plugins et espérer que l’un d’eux bloque tout. Sur un site réel, la plupart des incidents commencent par quelque chose de très banal: des tentatives répétées sur la connexion (wp-login.php, XML-RPC), des scans qui touchent les mêmes URL encore et encore, et des bots qui testent des combinaisons de mots de passe à un rythme industriel. C’est exactement le terrain de jeu de fail2ban.

Fail2ban ne “répare” pas une faille, il ne remplace pas une bonne hygiène applicative. Il fait une chose utile et mesurable: surveiller des journaux, repérer des comportements suspects, puis bloquer temporairement une source sur le réseau. Bien configuré, c’est un filet de sécurité efficace, surtout contre le brute force et certains abus applicatifs.

Le rôle de fail2ban dans une stratégie WordPress

Sur WordPress, une défense solide repose sur plusieurs couches, et fail2ban en ajoute une qui agit au plus près des attaques:

    il réduit la fenêtre d’essais en bloquant les IP “bruyantes” au bout d’un certain nombre d’échecs, il limite la charge induite par des attaques répétitives, il complète la sécurité applicative (mots de passe, limitation de tentatives côté WordPress, WAF, reverse proxy).

J’ai vu des cas où le site n’avait “rien de cassé”, mais les logs rendaient l’exploitation difficile: des milliers de requêtes par minute sur les endpoints de connexion, et des pics de CPU dus à la génération de pages et à la communication avec le backend. Fail2ban a souvent permis https://gardewp.fr/securite-wordpress/ de retrouver une utilisation plus stable, même quand on n’avait pas encore corrigé tout le reste.

L’approche à garder en tête est simple: fail2ban n’a pas besoin de comprendre WordPress, il lui suffit d’identifier les lignes de log qui correspondent aux tentatives ratées, puis d’appliquer un blocage cohérent.

Vérifier le contexte avant d’écrire les règles

La configuration dépend entièrement de votre environnement. Avant de toucher à fail2ban, j’essaie toujours de répondre à trois questions:

1) quel serveur web est en face (nginx ou Apache, parfois un reverse proxy), 2) où sont les logs que fail2ban doit lire, 3) quels URI et codes HTTP apparaissent pendant les tentatives.

Sur un nginx, typiquement on voit location de connexion, et des codes comme 401, 403, 404 ou 200 selon la manière dont WordPress et le serveur réagissent. Sur Apache, c’est souvent error.log ou access.log, selon les options de votre configuration.

Si vous utilisez un reverse proxy (Cloudflare, un load balancer, un CDN), le plus gros piège est la notion d’IP source. Dans ce cas, les logs peuvent contenir l’IP du proxy, pas celle du bot. Fail2ban bloquerait alors le proxy, ce qui est rarement une bonne idée. Dans ce scénario, il faut soit ajuster l’architecture (façon de journaliser l’IP réelle), soit choisir une autre couche de filtrage.

Installer fail2ban et préparer les journaux

Sans entrer dans le détail propre à chaque distribution, l’idée est de s’assurer que:

    fail2ban peut être lancé et lit correctement les fichiers de logs, la commande de blocage (iptables, nftables, ou un wrapper équivalent) est fonctionnelle, le système ne redémarre pas fail2ban sur un état “vide” qui casse la continuité.

En pratique, sur beaucoup de serveurs Linux, on utilise une structure de fichiers du style /etc/fail2ban/jail.d/ pour ajouter des règles sans toucher au fichier de base. C’est plus propre lors des mises à jour.

Pour WordPress, vous aurez besoin des journaux du serveur web. Deux formats sont courants:

    fichiers de logs d’accès, où figurent la requête et le code HTTP, fichiers d’erreurs, où figurent des messages plus “interprétables”.

Fail2ban sait faire avec l’un ou l’autre, mais les regex doivent correspondre exactement au format des lignes.

Avant d’écrire quoi que ce soit, prenez 10 minutes pour regarder le contenu réel. Un exemple de ligne (volontairement schématique) ressemble souvent à ceci côté nginx: une requête vers /wp-login.php avec un code 401 ou 404. Si votre serveur renvoie toujours 302 ou un mélange de codes, il faudra l’intégrer.

Construire une jail WordPress: la logique de base

Une “jail” fail2ban décrit trois choses:

1) quoi surveiller (fichier de log, filtre), 2) comment détecter un comportement suspect (regex), 3) quoi faire quand le seuil est atteint (action, durée de bannissement).

Pour WordPress, les cibles les plus utiles sont généralement:

    le point d’entrée du login, wp-login.php, la consultation XML-RPC, xmlrpc.php (souvent bruteforcée), parfois les URL d’administration selon votre thème et vos endpoints.

Il faut toutefois rester raisonnable. Bloquer “trop large” peut générer des faux positifs, par exemple si votre application effectue des requêtes internes, ou si un outil de monitoring passe par les pages d’administration.

L’autre point clé: fail2ban ne bloque pas un navigateur, il bloque une IP. Si vous avez une audience derrière une CGNAT (beaucoup d’utilisateurs partagent une IP), vous pouvez pénaliser des gens innocents. Dans ce cas, je recommande soit une durée plus courte, soit des seuils plus permissifs pour limiter les bannissements.

Exemple concret: jail pour wp-login.php et xmlrpc.php (nginx ou Apache)

Je vous propose un exemple “cadre” à adapter à vos logs. L’idée n’est pas d’imposer une regex universelle, mais de montrer une méthode de configuration robuste.

Commencez par créer un fichier dans /etc/fail2ban/jail.d/ (par exemple wordpress.conf). Dans ce fichier, vous définissez une ou plusieurs jails. Sur certains systèmes, il est aussi possible de mettre tout directement dans jail.local, mais j’aime mieux isoler.

Voici un modèle de base, à ajuster selon votre serveur et vos chemins de logs:

[wordpress-login] Enabled = true Port = http,https Logpath = /var/log/nginx/access.log Backend = auto # seuil: 5 tentatives dans un intervalle de 10 secondes, puis bannir 1 heure Maxretry = 5 Findtime = 10 Bantime = 1h Filter = wordpress-login Action = iptables-multiport[name=wordpress-login, port="http,https"]

Ensuite, définissez le filtre. Soit dans /etc/fail2ban/filter.d/wordpress-login.conf, soit en inline selon votre installation. Un filtre ressemble à ceci:

[Definition] Failregex = ^.*"(GET|POST) /wp-login\.php.*" (401|403) Ignoreregex =

Ce filtre est volontairement restrictif: il ne “voit” que les codes 401 et 403 associés à /wp-login.php. Si, dans vos logs, WordPress renvoie plutôt 200 avec une page de login, ce regex ne matchera rien. Il faudra ajuster pour coller aux lignes réelles. Par exemple, si vous voyez surtout des 404 pour des tentatives sur des URL de type wp-login.php, on peut élargir, mais je le fais avec prudence, parce que 404 peut aussi être du trafic légitime.

Pour XML-RPC, j’ajoute souvent une seconde jail, pour séparer le signal. Les tentatives sur xmlrpc.php sont fréquemment très répétées et très automatisées.

[wordpress-xmlrpc] Enabled = true Port = http,https Logpath = /var/log/nginx/access.log Backend = auto Maxretry = 3 Findtime = 5 Bantime = 2h Filter = wordpress-xmlrpc Action = iptables-multiport[name=wordpress-xmlrpc, port="http,https"]

Et le filtre correspondant:

image

[Definition] Failregex = ^.*"(GET|POST) /xmlrpc\.php.*" (401|403|404) Ignoreregex =

Pourquoi un seuil plus bas pour XML-RPC? Parce que les bots aiment marteler ce endpoint. Mais je garde en tête le risque de faux positifs, notamment si votre site utilise un client XML-RPC légitime et que son comportement génère des codes d’erreur temporaires (auth manquante, configuration cassée, etc.). Si c’est votre cas, vous pouvez remonter maxretry, ou ajouter une règle de contournement pour vos IP d’outils.

Adapter aux logs réels: la méthode que j’utilise

Fail2ban est bon, mais il n’est pas magique. Les regex échouent le plus souvent pour une raison simple: elles ne correspondent pas au format de log.

Ma méthode de terrain consiste à:

    isoler quelques lignes réelles pendant une tentative (ou pendant la période où vous savez que des scans arrivent), tester le filtre avec la commande de vérification de fail2ban (selon votre version, l’outil s’appelle souvent fail2ban-regex), modifier la regex jusqu’à ce qu’elle match exactement les lignes pertinentes.

Concrètement, je récupère une poignée de lignes d’accès autour de /wp-login.php et je regarde le code HTTP, la méthode (GET ou POST), et les caractères qui suivent l’URL (parfois des paramètres, parfois un user-agent qui change tout).

Une regex trop étroite ne bannit rien. Une regex trop large bannit trop. Entre les deux, il y a un point d’équilibre, et il dépend de votre manière de journaliser.

Choisir la bonne durée et le bon seuil, sans pénaliser les humains

C’est tentant de mettre maxretry = 1 et bantime = 24h. Sur le papier, ça “sécurise”. Sur le terrain, ça peut vous faire perdre du temps de support. Les faux positifs arrivent plus souvent qu’on ne l’imagine, surtout si:

    un partenaire essaie de se connecter avec un mot de passe périmé et insiste, vous avez un outil automatisé qui utilise le login (certains scripts de maintenance), vous subissez un scan qui interagit de manière imprévisible et génère des codes trompeurs.

Je raisonne comme suit: le bot est généralement “persistant”, l’humain a souvent “un ou deux essais” quand il y a un problème. Donc, je règle les seuils pour que le premier échec ne déclenche pas de bannissement, mais qu’une répétition rapide le fasse.

Sur un site à fort trafic, j’ai tendance à:

    utiliser un findtime court (5 à 15 secondes) pour qualifier la rapidité, régler maxretry à un chiffre qui tolère un peu d’erreur, utiliser un bantime modéré, puis compter sur l’effet cumulatif du comportement (si votre version/gestion fait évoluer le temps).

Si vous observez des bannissements de vos propres IP (accès administrateur), vous ajustez immédiatement les seuils, ou vous mettez vos IP en whitelist.

Gérer les IP légitimes: whitelists et précautions

Il y a un autre sujet qui revient systématiquement: les accès administratifs via une IP fixe (vous au bureau, ou un bastion). Si cette IP est bannie une fois, vous perdez vite le contrôle du site pendant la durée du bannissement.

La bonne pratique est de prévoir une règle d’exemption pour vos IP fiables, plutôt que d’espérer que fail2ban ne vous bannisse pas par hasard. Selon votre configuration, il existe des options pour ignorer certaines IP ou sous-réseaux.

Voici un mini plan pragmatique, pas un inventaire exhaustif:

    gardez une liste d’IP d’administration (vos postes, VPN, bastion), testez la configuration en recréant des tentatives contrôlées (en local ou avec un compte de test), observez la montée en bannissement sur une fenêtre de quelques heures, ajustez sans dramatiser, la plupart des regex et seuils se règlent en un ou deux cycles, gardez un accès de secours si vous utilisez un firewall qui coupe aussi l’accès SSH.

C’est le genre de discipline qui évite la situation classique: “ça bloquait tout, j’ai attendu que ça revienne, et entre-temps j’ai perdu la main”.

Surveiller et diagnostiquer: ce que vous devez regarder

Après activation, ne laissez pas fail2ban dans l’obscurité. Les deux choses que je consulte sont:

    les logs de fail2ban, pour voir quelles règles matchent et quelles IP sont bannies, les logs du serveur web, pour vérifier que l’activité suspecte correspond bien aux patterns attendus.

Une erreur fréquente: une regex qui ne match pas, donc aucune action n’est déclenchée. À l’opposé, une regex trop large déclenche des bannissements, mais vous ne comprenez pas quels motifs ont été retenus.

Si vous avez accès à l’interface de votre firewall, vérifiez également que l’action de bannissement correspond bien à votre mode de filtrage. Certaines installations n’utilisent plus exactement iptables, mais un backend compatible. L’idée reste identique: le bannissement doit réellement bloquer, pas seulement “être déclaré”.

Trade-offs spécifiques à WordPress

XML-RPC et compatibilités applicatives

xmlrpc.php est un endpoint historique, et beaucoup d’outils cherchent encore à l’utiliser. Si vous avez désactivé XML-RPC ou si vous utilisez un plugin qui modifie le comportement, les codes HTTP dans les logs peuvent changer.

Résultat: votre regex peut soit devenir trop stricte, soit devenir trop permissive si elle s’appuie sur des codes que WordPress produit dans un cas particulier.

Quand je configure fail2ban, je commence par observer 15 à 30 minutes de logs après activation. Je ne force pas immédiatement la politique la plus dure. Je préfère d’abord valider que les bans correspondent bien aux tentatives réellement problématiques.

Codes HTTP trompeurs

WordPress et les composants en amont peuvent renvoyer des codes qui ne racontent pas toute l’histoire. Parfois, un comportement de login échoué peut être masqué derrière une page de réponse qui ne fait pas apparaître un code 401 ou 403.

Dans ces cas, je privilégie des signaux plus riches si je peux les obtenir depuis les logs, par exemple des différences dans la requête, certains chemins exacts, ou un format d’URL spécifique. Sinon, je reste prudent et je limite la portée à des endpoints que les bots touchent de manière répétée.

Nginx, Apache, et format de log

Le même filtre ne se transpose pas à l’identique entre nginx et Apache. Même en nginx, un log_format personnalisé peut changer les colonnes, et donc casser la regex.

Si vous avez customisé le format de log, faites-le correspondre à votre filtre. Fail2ban utilise souvent un placeholder , mais la façon dont le host est “positionné” dans la ligne compte.

En pratique: une configuration minimale mais utile

Si vous n’avez pas envie de sur-engineering, l’approche la plus simple que je recommande consiste à:

    créer une jail “login WordPress” centrée sur wp-login.php, créer une jail “xmlrpc” centrée sur xmlrpc.php, activer des seuils raisonnables, tester en observant les logs.

Un point important: restez concentré sur ce qui apporte de la valeur. Les sites WordPress n’ont pas besoin d’une centaine de regex. Quelques règles bien calibrées peuvent faire une énorme différence dès qu’une vague de brute force arrive.

Liste de contrôles avant de déployer en production

Avant de basculer sur le serveur “pour de vrai”, voici ce que je fais presque systématiquement:

1) vérifier que fail2ban lit bien le bon fichier de log, 2) vérifier que la regex match bien des lignes réelles (au moins pour un échantillon), 3) tester un bannissement sur une IP de test, puis vérifier que le blocage fonctionne, 4) s’assurer que vos IP d’administration ne sont pas bannies par erreur, 5) surveiller fail2ban et le serveur web pendant les premières heures.

Cette étape semble “bureaucratique”, mais elle évite les erreurs silencieuses, celles qui ne se voient que lorsque quelqu’un a besoin de se connecter.

Éléments qui complètent fail2ban (sans le remplacer)

Fail2ban s’intègre bien avec des mesures plus applicatives, parce que vous ne cherchez pas à tout faire à un seul endroit. Sans faire une liste exhaustive, je pense notamment à:

    la politique de mots de passe pour les comptes administrateurs, la limitation de tentatives côté WordPress quand c’est possible, une couche de filtrage plus en amont (selon votre architecture), un système d’alerting quand des échecs répétés apparaissent.

Le meilleur scénario que j’ai vu, c’est quand fail2ban et WordPress se renvoient le ballon: WordPress gère l’authentification et les réponses, fail2ban coupe les vagues à la source en limitant la répétition.

Problèmes courants et correctifs

Aucun bannissement ne se produit

Cause typique: regex qui ne match pas les lignes de logs. Corrigez d’abord le diagnostic: assurez-vous que les endpoints apparaissent bien dans le fichier de log et que le format correspond à votre filtre. Ensuite seulement, ajustez les seuils.

Trop de bannissements

Cause typique: regex trop large, ou seuil trop agressif pour votre contexte. Réduisez la portée, affinez les codes HTTP, ou augmentez maxretry et findtime. Si des utilisateurs légitimes partagent une IP, soyez encore plus prudent.

Bannissement de votre reverse proxy ou de votre serveur de sortie

Cause typique: fail2ban bloque l’IP vue dans les logs, qui n’est pas celle de l’attaquant. Ajustez votre configuration de journalisation (X-Forwarded-For ou équivalent selon votre stack) ou basculez la politique à un autre point du réseau.

Pour aller plus loin: rendre la configuration “vivante”

Une règle figée, même bien écrite, fatigue avec le temps. Les scans changent, les bots évoluent, et les réponses HTTP peuvent varier selon les mises à jour de WordPress, de vos thèmes, ou de votre serveur web.

Le bon réflexe consiste à revalider les journaux au fil des semaines, surtout après:

    changement de log_format nginx, migration Apache vers nginx, ou inverse, ajout d’un reverse proxy, modification de la configuration WordPress qui impacte les réponses à la page de login.

Fail2ban n’exige pas un entretien constant, mais il mérite un audit léger. Une fois que vous connaissez les motifs exacts de vos attaques réelles, votre configuration devient stable, efficace, et moins sujette aux faux positifs.

Dernier point: sécuriser WordPress, c’est d’abord réduire la surface

Fail2ban est une réponse pragmatique aux attaques répétitives. Mais la meilleure sécurité, c’est de réduire ce qui attire les robots. Sur un WordPress bien configuré, les endpoints ne “répondent” pas de la même manière, les comptes sont plus protégés, et les tentatives échouent sans créer une charge inutile.

Si vous voulez que votre configuration fail2ban soit vraiment utile, considérez-la comme un garde du corps. Il agit quand quelqu’un insiste, mais il ne remplace pas le verrou de la porte.

Si vous me dites votre stack exacte (nginx ou Apache, distribution Linux, format de log, et si vous êtes derrière un reverse proxy), je peux proposer une version de filtres plus directement adaptée à vos lignes réelles, avec des seuils cohérents pour votre trafic.