Sécurité WordPress : la checklist de durcissement qui ne bloque pas les connexions

Cet article vous a aidé ?

Votre site WordPress va « bien » jusqu’à 3h07 du matin et le téléphone d’astreinte vibre comme s’il voulait creuser à travers la table de nuit. Le symptôme est toujours le même : échecs de connexion, CPU saturé, connexions base de données empilées, et une ligne dans la timeline d’incident qui indique « quelqu’un a changé quelque chose lié à la sécurité ».

Le durcissement n’est pas l’ennemi. Le mauvais durcissement l’est. Voici une checklist opérationnelle pour renforcer la sécurité WordPress sans transformer les éditeurs légitimes, clients et utilisateurs SSO en dommages collatéraux.

Modèle de menace en une page : ce que vous défendez réellement

Le durcissement WordPress devient raisonnable quand vous cessez de le traiter comme une ambiance et que vous le traitez comme un modèle de menace. Les « attaquants » que vous verrez le plus souvent ne sont pas des hackers de film. Ce sont des bots, des malwares de commodité et des opportunistes qui parcourent Internet à la recherche de points faibles connus. Votre travail consiste à rendre votre site ennuyeux à attaquer et résilient quand il l’est.

Ce qui est le plus attaqué

  • /wp-login.php brute force (credential stuffing, password spraying, abus de tokens API).
  • /xmlrpc.php (historiquement utilisé pour les pingbacks et la publication distante ; souvent abusé pour amplification et brute force).
  • Vulnérabilités plugins/thèmes (RCE, upload de fichiers arbitraires, contournement d’auth, CSRF).
  • Webroot inscriptible (un bug d’upload devient persistance et défacement).
  • Dérive de la chaîne d’approvisionnement (plugins mis à jour « n’importe quand », sans rollback, sans canary, sans staging).

Ce que « ne bloque pas les connexions » signifie réellement

La sécurité des connexions n’est pas juste « je peux me connecter maintenant ». Elle inclut :

  • Des humains sur des IP dynamiques (Internet résidentiel, déplacements, hotspot mobile).
  • Automatisations légitimes : probes d’uptime, publication sans interface, clients API WooCommerce, callbacks SSO, checks de santé du reverse proxy.
  • Flux de travail des éditeurs avec plusieurs rôles : administrateurs, auteurs, contributeurs.
  • Accès d’urgence lorsque votre WAF ou service d’auth est dégradé.

Le durcissement doit être réversible, observable et en couches. Si un changement peut verrouiller votre propre équipe, il doit avoir un plan de contournement (de préférence du type que vous pouvez exécuter à 3h du matin avec un œil ouvert et sans héroïsme).

Une citation fiabilité à coller sur l’écran : « L’espoir n’est pas une stratégie. »Gordon R. Sullivan.

Faits intéressants et courte histoire (ce qui explique le bazar actuel)

  1. WordPress a commencé en 2003 comme fork de b2/cafelog ; son architecture de plugins lui a permis de gagner, et a aussi donné aux attaquants une énorme surface d’attaque.
  2. xmlrpc.php précède les APIs REST ; il permettait la publication distante bien avant que les modèles modernes d’authentification soient courants dans les CMS.
  3. Le brute force est passé du ciblé à l’ambient une fois que les botnets et les dumps d’identifiants ont été banalisés ; on ne vous attaque pas parce que vous êtes spécial, on vous attaque parce que vous existez.
  4. Les mises à jour automatiques du core WordPress (pour les versions mineures) sont devenues un filet de sécurité courant, mais les plugins restent le risque dominant.
  5. Les « plugins de sécurité » sont devenus une catégorie en grande partie parce que l’hébergement mutualisé rendait difficiles les contrôles au niveau serveur pour les propriétaires de sites normaux.
  6. Les salts dans wp-config.php existent parce que cookies et sessions ont besoin d’un caractère cryptographique unique par site ; les anciens sites conservent parfois des salts antiques lors de migrations.
  7. Les recommandations sur les permissions de fichiers ont évolué parce que les premières installations WordPress exécutaient souvent PHP en tant que même utilisateur que le propriétaire des fichiers, encourageant des répertoires modifiables par tous.
  8. Les CDN ont modifié l’histoire des connexions : la limitation de taux et la mitigation des bots se sont déplacées vers l’extérieur, mais une mauvaise config de l’origine laisse encore un accès direct à /wp-login.php.

Principes de durcissement qui évitent les verrouillages

1) Réduisez la surface d’attaque avant d’augmenter l’application des règles

Bloquer le monde entier depuis /wp-admin donne un sentiment de satisfaction. Cela casse aussi les éditeurs mobiles, les clients REST et quiconque a une IP changeante. Commencez plutôt par retirer ce que vous n’utilisez pas (xmlrpc, plugins inutilisés, vieux thèmes), puis ajoutez authentification et throttling, puis ajoutez des règles « deny » là où c’est sûr.

2) Chaque contrôle de sécurité nécessite de l’observabilité

Si vous ne pouvez pas répondre à « qui a été bloqué ? » et « pourquoi ? » à partir des logs, vous ne faites pas de la sécurité. Vous faites du rituel. Mettez les décisions de refus dans les logs avec suffisamment de champs exploitables : IP, chemin, statut, user agent, request id et temps de réponse en amont.

3) Préférez les limitations de débit et les challenges aux listes d’autorisation IP

Les allowlists fonctionnent pour les réseaux de bureau et VPN. Elles échouent dans la réalité distante/hybride. Limiter le taux + une authentification forte vous protège sans hypothèses fragiles sur la stabilité des IP.

4) Séparez identité et autorisation

L’authentification à deux facteurs (ou SSO) renforce l’identité. Elle ne remplace pas l’autorisation. Gardez les rôles minimaux. Ne donnez pas le rôle « Administrator » pour résoudre des frictions de workflow. C’est comme ajouter plus d’essence parce que le moteur fait du bruit.

5) Réduisez le rayon d’explosion

Exécutez PHP-FPM avec un utilisateur dédié. Verrouillez la propriété des fichiers. Si une vulnérabilité de plugin survient, votre objectif est « dommages limités » plutôt que « tout le système de fichiers devient une toile ».

Blague #1 : La seule chose plus persistante que les malwares WordPress est la personne qui insiste pour que le mot de passe admin soit « CompanyName2024! ».

Playbook de diagnostic rapide : trouvez le goulot avant de « corriger »

Quand des problèmes de connexion apparaissent après un « durcissement », traitez cela comme un incident. Ne devinez pas. Ne faites pas de rollback à l’aveugle. Triez dans cet ordre parce que cela réduit rapidement le champ et vous évite d’accuser la mauvaise couche.

Premier : confirmez ce qui échoue (navigateur vs serveur vs amont)

  • Est-ce que /wp-login.php renvoie 200 avec un formulaire, ou 403/429/503 ?
  • L’échec concerne-t-il seulement certains utilisateurs (par rôle, IP, géolocalisation) ?
  • L’origine est-elle atteinte directement, en contournant le CDN/WAF ?

Deuxième : vérifiez les décisions de rate limiting / WAF / Fail2ban

  • Cherchez des pics de 403/429 sur les endpoints de connexion.
  • Confirmez si vos blocklists incluent la sortie de votre bureau/VPN ou vos IP de monitoring.
  • Validez qu’un « challenge » ne casse pas les clients non-navigateur (appels REST, apps mobiles).

Troisième : vérifiez les performances applicatives et la base de données

  • La connexion peut être lente parce que la base est lente (bloating de la table options, options autoload, indexes manquants).
  • Ou parce que les workers PHP sont saturés (trop peu d’enfants PHP-FPM, plugins lents trop nombreux).
  • Ou parce que le disque est malheureux (pics IO wait ; logs et uploads sur le même volume).

Quatrième : vérifiez les contrôles de correction

  • Permissions/propriété empêchant les écritures de session, uploads, mises à jour de plugins.
  • Mauvaise configuration du cache objet causant des boucles d’auth cookie.
  • En-têtes de sécurité ou flags de cookies mal réglés aux frontières HTTP/HTTPS.

Si vous ne pouvez pas identifier le goulot en 10–15 minutes, gèle les changements, faites un snapshot de l’état (configs + logs), puis procédez à un rollback contrôlé. L’adrénaline est un terrible système de gestion des changements.

Tâches pratiques de durcissement (commandes, sorties, décisions)

Ce sont des tâches opérationnelles réelles que vous pouvez exécuter sur un hôte Linux typique (saveur Debian/Ubuntu). Chaque tâche inclut : la commande, ce à quoi ressemble une sortie « bonne », et la décision à prendre.

Task 1: Identify who is actually serving WordPress (Nginx/Apache, PHP-FPM user)

cr0x@server:~$ ps -eo user,comm,args | egrep 'nginx:|apache2|php-fpm' | head
root     nginx:  nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data nginx:  nginx: worker process
www-data php-fpm8.2 php-fpm: pool www
www-data php-fpm8.2 php-fpm: pool www

Ce que cela signifie : Les workers Nginx et PHP-FPM tournent sous www-data. Cela informe les choix de propriété et permissions des fichiers.

Décision : Standardisez sur un utilisateur de service dédié (www-data convient) et assurez-vous que les fichiers WordPress sont détenus par un groupe contrôlé par les administrateurs, non modifiables par l’utilisateur web sauf si vous voulez explicitement des mises à jour in-place.

Task 2: Confirm WordPress path and detect accidental multiple installs

cr0x@server:~$ sudo find /var/www -maxdepth 3 -name wp-config.php -print
/var/www/site1/wp-config.php

Ce que cela signifie : Un seul wp-config.php trouvé sous les web roots attendus. Plusieurs configs signifient souvent des sites de staging oubliés exposés.

Décision : Si vous trouvez des installations supplémentaires, supprimez-les, firewallisez-les ou protégez-les par mot de passe. Les sites de staging cachés sont des lieux de shopping pour attaquants.

Task 3: Check file ownership and writable surfaces

cr0x@server:~$ cd /var/www/site1
cr0x@server:~$ sudo find . -maxdepth 2 -type d -perm -0002 -print | head
./wp-content/uploads

Ce que cela signifie : Les répertoires modifiables par « others » sont des signaux d’alerte. WordPress a besoin que uploads soit modifiable, mais pas nécessairement world-writable.

Décision : Retirez les bits modifiables par tous. Gardez seulement ce qui doit être modifiable par l’utilisateur PHP (généralement wp-content/uploads et peut-être des répertoires cache).

Task 4: Apply sane permissions (without breaking media uploads)

cr0x@server:~$ sudo chown -R root:www-data /var/www/site1
cr0x@server:~$ sudo find /var/www/site1 -type d -exec chmod 0750 {} \;
cr0x@server:~$ sudo find /var/www/site1 -type f -exec chmod 0640 {} \;
cr0x@server:~$ sudo chown -R www-data:www-data /var/www/site1/wp-content/uploads
cr0x@server:~$ sudo find /var/www/site1/wp-content/uploads -type d -exec chmod 0750 {} \;
cr0x@server:~$ sudo find /var/www/site1/wp-content/uploads -type f -exec chmod 0640 {} \;

Ce que cela signifie : Les fichiers core ne sont pas modifiables par le runtime ; les uploads le sont. Cela bloque une grande classe de persistance « écrire un webshell dans le core ».

Décision : Si vous dépendez des mises à jour depuis le tableau de bord, acceptez soit un chemin modifiable contrôlé (et une surveillance stricte), soit passez à des mises à jour via déploiement (recommandé en environnements sérieux).

Task 5: Lock down wp-config.php specifically

cr0x@server:~$ sudo stat -c '%a %U:%G %n' /var/www/site1/wp-config.php
640 root:www-data /var/www/site1/wp-config.php

Ce que cela signifie : L’utilisateur web (dans le groupe www-data) peut lire la config, mais pas l’écrire. C’est généralement souhaitable.

Décision : Si le fichier est modifiable par www-data, corrigez-le. Si le serveur web ne peut pas le lire, votre site ne démarrera pas. Ne « corrigez » pas cela en mettant 777.

Task 6: Verify core and plugin integrity with WP-CLI

cr0x@server:~$ cd /var/www/site1
cr0x@server:~$ sudo -u www-data wp core verify-checksums
Success: WordPress installation verifies against checksums.

Ce que cela signifie : Les fichiers core correspondent aux checksums attendus. Si cela échoue, vous pouvez avoir une altération, ou vous êtes sur une version/build qui ne correspond pas aux checksums.

Décision : Les échecs nécessitent une investigation. Si vous n’avez pas patché le core intentionnellement, considérez la mismatch comme suspecte et réinstallez le core depuis une source de confiance.

Task 7: Enumerate plugins and kill what you don’t use

cr0x@server:~$ sudo -u www-data wp plugin list --status=inactive
+---------------------+----------+-----------+---------+
| name                | status   | update    | version |
+---------------------+----------+-----------+---------+
| hello-dolly         | inactive | none      | 1.7.2   |
| old-seo-plugin      | inactive | available | 2.1.0   |
+---------------------+----------+-----------+---------+

Ce que cela signifie : Les plugins inactifs sont toujours du code sur le disque. Les vulnérabilités ne se préoccupent pas que la case UI soit décochée.

Décision : Désinstallez les plugins inactifs sauf si vous avez une raison forte de les garder (et même alors, conservez un artefact packagé plutôt que du code vivant).

Task 8: Update safely with a “what will change” preview

cr0x@server:~$ sudo -u www-data wp plugin update --all --dry-run
Available plugin updates:
- woocommerce (7.9.0 -> 7.9.2)
- wordfence (7.10.1 -> 7.10.2)
Success: Checked available updates.

Ce que cela signifie : Vous voyez le rayon d’explosion avant de pousser les changements. Les dry runs sont sous-utilisés parce que les gens aiment apparemment les surprises.

Décision : Stagez les mises à jour, surtout sur WooCommerce ou sites à revenus. Avancez avec sauvegardes et plan de rollback.

Task 9: Detect if xmlrpc.php is still reachable (and decide what to do)

cr0x@server:~$ curl -s -o /dev/null -w '%{http_code}\n' https://example.com/xmlrpc.php
200

Ce que cela signifie : xmlrpc.php est atteignable. Ce n’est pas automatiquement mauvais, mais c’est couramment abusé.

Décision : Si vous n’utilisez pas Jetpack remote features, l’ancienne publication mobile ou les pingbacks, bloquez-le. Si vous l’utilisez, restreignez les méthodes ou appliquez un rate-limit agressif.

Task 10: Block xmlrpc.php at the web server (Nginx) without touching wp-login

Exemple de snippet Nginx (appliquer dans le server block) :

cr0x@server:~$ sudo nginx -T 2>/dev/null | sed -n '1,120p'
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Ce que cela signifie : La syntaxe est valide. Ajoutez maintenant un bloc location explicite (montré ci‑dessous) et retestez.

cr0x@server:~$ sudo tee /etc/nginx/snippets/wordpress-xmlrpc-block.conf >/dev/null <<'EOF'
location = /xmlrpc.php {
  deny all;
  access_log /var/log/nginx/xmlrpc-block.log;
  return 403;
}
EOF
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
cr0x@server:~$ sudo systemctl reload nginx

Décision : Si vous découvrez plus tard une dépendance métier à XML-RPC, revenez sur ce snippet et passez à du rate limiting au lieu d’un deny total.

Task 11: Rate-limit wp-login.php and wp-admin endpoints (throttle, don’t brick)

Les rate limits doivent cibler les patterns abusifs, pas le travail éditorial normal. Gardez des limites modestes et surveillez.

cr0x@server:~$ sudo tee /etc/nginx/snippets/wordpress-login-ratelimit.conf >/dev/null <<'EOF'
limit_req_zone $binary_remote_addr zone=wp_login:10m rate=10r/m;

location = /wp-login.php {
  limit_req zone=wp_login burst=20 nodelay;
  include snippets/fastcgi-php.conf;
  fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}

location ~* ^/wp-admin/ {
  limit_req zone=wp_login burst=40 nodelay;
  try_files $uri $uri/ /index.php?$args;
}
EOF
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Ce que cela signifie : Vous appliquez une limitation de requêtes par IP sur les chemins de connexion/admin. Le burst permet de courtes pointes légitimes (chargements de page) sans punir les humains.

Décision : Si vous voyez beaucoup de 429 pour des utilisateurs réels, desserrez la rate ou augmentez le burst. Si vous subissez des attaques soutenues, serrez et ajoutez Fail2ban par-dessus.

Task 12: Confirm headers and cookie behavior aren’t sabotaging logins

cr0x@server:~$ curl -I https://example.com/wp-login.php | egrep -i 'set-cookie|strict-transport|content-security|x-frame|x-content-type'
strict-transport-security: max-age=31536000; includeSubDomains
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff

Ce que cela signifie : Vous avez des en-têtes de sécurité de base. Des en-têtes manquants ne cassent pas toujours la connexion, mais des flags de cookie mal configurés entre HTTP/HTTPS peuvent le faire.

Décision : Si les connexions bouclent ou que les cookies ne persistent pas, validez la terminaison TLS, WP_HOME/WP_SITEURL, et les en-têtes proxy (voir erreurs courantes).

Task 13: Check whether wp-login is being hammered (access logs)

cr0x@server:~$ sudo awk '$7 ~ /wp-login.php/ {print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
  842 203.0.113.50
  611 198.51.100.24
  402 192.0.2.10

Ce que cela signifie : IP sources les plus fréquentes frappant la page de connexion. De grands comptes suggèrent brute force ou credential stuffing.

Décision : Si quelques IP dominent, Fail2ban peut aider. Si des milliers d’IP répartissent la charge, préférez les challenges CDN/WAF et les rate limits.

Task 14: Install and validate Fail2ban for WordPress (Nginx example)

cr0x@server:~$ sudo apt-get update
Hit:1 http://archive.ubuntu.com/ubuntu jammy InRelease
Reading package lists... Done
cr0x@server:~$ sudo apt-get install -y fail2ban
Setting up fail2ban (0.11.2-6) ...

Créez un filtre correspondant aux tentatives de connexions échouées répétées (vous aurez besoin d’un log_format incluant le chemin de requête et le statut ; adaptez à votre format). Exemple de jail :

cr0x@server:~$ sudo tee /etc/fail2ban/jail.d/wordpress-login.conf >/dev/null <<'EOF'
[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/nginx/access.log
findtime = 600
bantime = 3600
maxretry = 20
EOF
cr0x@server:~$ sudo tee /etc/fail2ban/filter.d/wordpress-login.conf >/dev/null <<'EOF'
[Definition]
failregex = ^<HOST> .* "(GET|POST) /wp-login\.php.*" (200|401|403|404) .*
ignoreregex =
EOF
cr0x@server:~$ sudo systemctl restart fail2ban
cr0x@server:~$ sudo fail2ban-client status wordpress-login
Status for the jail: wordpress-login
|- Filter
|  |- Currently failed: 0
|  |- Total failed: 0
|  `- File list:        /var/log/nginx/access.log
`- Actions
   |- Currently banned: 0
   |- Total banned: 0
   `- Banned IP list:

Ce que cela signifie : Le jail est actif. « Currently failed » s’incrémente quand le regex matche. « Currently banned » montre les bans actifs.

Décision : Ajustez le failregex à votre format de logs réel. Ne déployez pas des règles Fail2ban non testées, sauf si vous aimez bannir le Wi‑Fi d’un directeur commercial en déplacement.

Task 15: Verify TLS termination and proxy headers (a frequent login loop culprit)

cr0x@server:~$ sudo -u www-data wp option get siteurl
https://example.com
cr0x@server:~$ sudo -u www-data wp option get home
https://example.com

Ce que cela signifie : WordPress pense qu’il est en HTTPS. Si vous terminez TLS sur un load balancer et forwardez HTTP vers l’origine, vous devez aussi forwarder X-Forwarded-Proto et configurer WordPress en conséquence.

Décision : Si ces options sont en http:// alors que les utilisateurs accèdent en https://, corrigez-les et assurez-vous que votre reverse proxy définit X-Forwarded-Proto https.

Task 16: Check PHP-FPM saturation (login failures under load)

cr0x@server:~$ sudo tail -n 20 /var/log/php8.2-fpm.log
[04-Feb-2026 03:12:41] WARNING: [pool www] server reached pm.max_children setting (10), consider raising it

Ce que cela signifie : Les workers PHP sont épuisés. Sous attaque, cela ressemble à des « connexions cassées », mais c’est de la famine de ressources.

Décision : Augmentez pm.max_children seulement après avoir confirmé la marge CPU/RAM. Réduisez aussi le travail par requête (cache, limiter plugins coûteux, ajouter WAF/limits).

Task 17: Confirm database health (slow auth queries can look like security blocks)

cr0x@server:~$ mysql -e "SHOW PROCESSLIST\G" | sed -n '1,40p'
*************************** 1. row ***************************
     Id: 41
   User: wpuser
   Host: 127.0.0.1:48822
     db: wordpress
Command: Query
   Time: 12
  State: Sending data
   Info: SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes'

Ce que cela signifie : La requête des options autoload prend du temps. Cela gonfle souvent au fil des années quand des plugins stockent des déchets dans les options.

Décision : Auditez la taille des autoloads et réduisez-la. Le durcissement n’aide pas si l’endpoin de connexion passe des secondes à tracter un container d’options à chaque requête.

Task 18: Check disk pressure (because logins need IO too)

cr0x@server:~$ df -h /var/www /var/log | tail -n 2
/dev/sda1        80G   74G  2.1G  98% /
/dev/sda1        80G   74G  2.1G  98% /

Ce que cela signifie : Le système de fichiers est presque plein. Cela casse les sessions, les uploads, la journalisation, les mises à jour et parfois les écritures en base. Cela rend aussi chaque outil de sécurité plus bruyant.

Décision : Libérez de l’espace immédiatement. Ensuite séparez les logs/uploads du filesystem racine, et ajoutez des alertes avant que cela n’atteigne 90%.

Blague #2 : Disque à 98% : le serveur ne dit pas « je suis furieux », il dit « je suis juste déçu ».

Trois mini-récits d’entreprise depuis le terrain

Incident #1: A wrong assumption (IP allowlisting “for security”) caused a lockout spiral

Une entreprise de taille moyenne gérait un site WordPress pour du contenu support et de la génération de leads. Après une petite alerte—quelques tentatives de connexion suspectes dans les logs—un ingénieur a fait ce que beaucoup d’entre nous ont fait sous pression : il a verrouillé /wp-admin et /wp-login.php sur la plage d’IP du bureau. Ça a marché instantanément. Le bruit des connexions a cessé. Le tableau de bord est devenu paisible.

Puis l’équipe commerciale est repartie sur la route. Wi‑Fi domestique, aéroports, hôtels, tethering mobile. Soudain, les rapports « WordPress est en panne » ont explosé, parce que, de leur point de vue, il l’était. L’équipe d’ingénierie a supposé une erreur utilisateur, puis le VPN, puis l’IdP. Pendant ce temps, l’équipe marketing ops a commencé à partager un compte admin « juste pour avancer », parce qu’une seule personne conservait l’accès.

Le mode de défaillance s’est empiré : une agence externe qui gérait le SEO n’accédait plus au site. On a ouvert temporairement l’allowlist à « n’importe où » pour leur donner accès, puis oublié de la refermer, et l’endpoint de connexion s’est retrouvé à nouveau assailli. L’équipe a fini avec le pire des deux mondes : des verrouillages pour les vrais utilisateurs et une exposition au risque initial.

La solution n’a pas été héroïque. Ils ont remplacé l’allowlisting par des contrôles en couches : mots de passe robustes + 2FA, une limite modeste Nginx sur wp-login.php, et Fail2ban pour tentatives répétées. Ils ont aussi veillé à ce que l’origine ne soit accessible que via le CDN/WAF. Les accès se sont stabilisés et le bruit d’attaque est retombé à un bourdonnement gérable et observable.

La leçon : les listes IP ne sont pas de la « sécurité ». Elles sont un système d’identité fragile. Utilisez-les pour des panneaux d’administration infrastructurels et des staging privés. Pour les connexions WordPress en 2026, partez sur du throttling et une authentification forte.

Incident #2: An optimization that backfired (aggressive caching broke authentication)

Une autre organisation exécutait WordPress derrière Nginx avec une couche de cache. La performance était un KPI majeur ; l’équipe tenait plus au temps de chargement que la plupart tiennent aux week‑ends. Un ingénieur a ajouté des règles de cache pour accélérer les pages dynamiques. Le hit ratio a augmenté. Le CPU est tombé. Des high fives partout.

En quelques heures, des tickets support sont arrivés : les utilisateurs ne pouvaient pas se connecter, ou se connectaient puis étaient immédiatement déconnectés. Certains voyaient la barre admin d’autres utilisateurs clignoter brièvement—rare, mais terrifiant. L’équipe a d’abord suspecté du XSS ou du vol de session. La panique est bruyante.

Le coupable réel était ennuyeux : la couche de cache mettait en cache des réponses qu’elle n’aurait jamais dû. Certaines pages étaient servies avec des cookies inappropriés, et le cache ne variait pas correctement selon l’état d’authentification. L’optimisation était correcte pour le contenu anonyme et catastrophique pour tout ce qui touchait aux sessions.

La récupération a été une réécriture prudente des règles de cache : ne jamais cache /wp-login.php, ne jamais cache /wp-admin, et éviter de cacher les pages quand des cookies d’auth sont présents. Ils ont aussi ajouté des conditions explicites de bypass pour WooCommerce et les cookies des sites membres. Après ça, la stabilité des connexions est revenue, et les gains de performance sont restés—juste pas sur les endpoints où la justesse est non négociable.

La leçon : l’optimisation peut devenir un incident de sécurité si elle casse l’isolation. Cachez comme un adulte : définissez ce qui ne doit pas être mis en cache, puis vérifiez avec des tests et des en‑têtes.

Incident #3: A boring but correct practice saved the day (least privilege + restore drills)

Une entreprise SaaS hébergeait un site marketing WordPress qui paraissait simple mais avait une vraie valeur business : pages de tarification, flows d’inscription et témoignages clients utilisés dans des deals enterprise. Ils le traitaient comme de la production, parce que c’en était. L’équipe avait une politique qui semblait ennuyeuse : propriété de fichiers verrouillée, pas d’installation de plugins in‑place en production, et exercices mensuels de restauration de la base et des uploads.

Un weekend, une vulnérabilité de plugin est sortie. Les bots ont bougé vite. Leur site a commencé à recevoir des requêtes malveillantes ciblant le endpoint d’upload de ce plugin. Certaines requêtes ont atteint PHP, mais l’exploit n’a pas persisté parce que l’utilisateur web ne pouvait pas écrire dans les répertoires core ou du plugin. Les attaquants pouvaient fouiller ; ils ne pouvaient pas s’installer.

Pour autant, ils n’ont pas déclaré victoire. Ils ont supposé que « des tentatives ont réussi quelque part ». Ils ont exécuté des vérifications d’intégrité, comparé les répertoires plugins à des artefacts connus bons, et inspecté les logs pour des POSTs anormaux. Ils ont tourné les clés/salts et forcé des réinitialisations de mots de passe pour les admins par précaution.

Le vrai gain est venu de la pratique ennuyeuse : l’équipe avait une procédure de restauration testée. Ils ont pris un snapshot, restauré dans un environnement isolé, et validé le comportement du site et l’arborescence des fichiers. Ce n’était pas nécessaire pour récupérer cette fois, mais cela a transformé la peur en une étape de vérification contrôlée. Pas de supposition, pas de superstition.

La leçon : quand vous pouvez restaurer de façon fiable, vous pouvez durcir agressivement. Pas parce que vous prévoyez l’échec, mais parce que vous refusez d’être piégé par l’échec.

Erreurs courantes : symptômes → cause racine → correctif

La page de connexion renvoie 403 pour tout le monde

Syndromes : Tous les utilisateurs voient 403 sur /wp-login.php. Accès admin mort. Les éditeurs hurlent.

Cause racine : Règle deny trop large (Nginx/Apache), jeu de règles WAF en mode « block » plutôt que « challenge », ou restriction geo/IP appliquée au endpoint de connexion.

Correctif : Revenir sur la règle en ciblant seulement /xmlrpc.php ou des chemins connus mauvais ; passer au rate limiting ; ajouter un chemin de contournement d’urgence accessible seulement depuis le VPN.

Les utilisateurs peuvent charger wp-login, mais les identifiants ne tiennent jamais (boucle de connexion)

Syndromes : L’utilisateur soumet le bon mot de passe, est redirigé vers le login. Les cookies ne persistent pas.

Cause racine : Mismatch de terminaison HTTPS (siteurl/home incorrects), X-Forwarded-Proto manquant, ou mise en cache des réponses de connexion.

Correctif : Corriger home et siteurl en HTTPS, configurer les en-têtes proxy, désactiver le caching pour les endpoints d’auth, vérifier COOKIE_DOMAIN et l’hôte canonique.

Certain·e·s utilisateur·rice·s obtiennent 429 Too Many Requests tandis que d’autres vont bien

Syndromes : Les équipes distantes se plaignent ; les utilisateurs de bureau sont ok. L’app mobile échoue de manière intermittente.

Cause racine : Rate limiting clefée incorrectement (ex : par IP NAT partagée, ou par un en‑tête qui regroupe des utilisateurs distincts dans un même bucket).

Correctif : Cleflez les limites par $binary_remote_addr en périphérie ; si vous êtes derrière un proxy, assurez-vous que la vraie IP client est correctement définie (module real_ip). Augmentez le burst. Ajoutez CAPTCHA/challenges seulement là où des humains sont attendus.

Les pages admin sont lentes, les connexions expirent sous charge

Syndromes : Erreurs 504/502, échecs intermittents de connexion, CPU et IO wait qui montent.

Cause racine : Saturation PHP-FPM, requêtes lentes en base, ou disque presque plein. Parfois une attaque brute force amplifie une inefficacité sous-jacente.

Correctif : Augmentez la capacité PHP-FPM si les ressources le permettent, ajoutez du cache correctement, tunez la base, réduisez les options autoload, et ajoutez une protection en amont (WAF/limits).

Après le « durcissement », les mises à jour de plugins et uploads médias échouent

Syndromes : « Could not create directory », erreurs de mise à jour, erreurs de permissions.

Cause racine : Propriété/permissions trop strictes ou inconsistantes entre le webroot et wp-content.

Correctif : Choisissez : mises à jour via déploiement (préféré) ou autoriser un accès en écriture contrôlé à wp-content. Ne rendez pas le core inscriptible pour résoudre un workflow plugin.

Fail2ban bannit de vrais utilisateurs à répétition

Syndromes : Utilisateurs verrouillés après quelques tentatives ; bans corrélés au NAT bureau/VPN.

Cause racine : Regex trop large, maxretry trop bas, le NAT concentre beaucoup d’utilisateurs sur une même IP.

Correctif : Augmentez maxretry, raccourcissez bantime, ajustez le regex aux échecs réels (ex : 200 sur wp-login n’est pas toujours un « échec »), et préférez les challenges WAF quand possible.

Checklists / plan étape par étape (ne pas improviser en prod)

Phase 0 : Pré‑vol (faites ceci avant de toucher aux contrôles de sécurité)

  1. Inventaire des dépendances : Utilisez‑vous XML‑RPC ? Jetpack ? Publication mobile ? Des systèmes externes qui appellent des endpoints REST ?
  2. Décidez où l’application des règles vit : CDN/WAF d’abord, puis serveur web, puis plugins applicatifs. Ne mettez pas tout dans WordPress si vous pouvez l’éviter.
  3. Établissez un accès d’urgence : Un chemin VPN, un bastion, ou une procédure de maintenance temporaire qui ne dépend pas du login WordPress lui‑même.
  4. Sauvegardes + test de restauration : Vérifiez que vous pouvez restaurer la base et wp-content/uploads dans un environnement propre.
  5. Fenêtre de changement et rollback : Sachez exactement quels fichiers de config et règles vous changez, et comment revenir vite en arrière.

Phase 1 : Supprimez les gains faciles (risque faible, rendement élevé)

  1. Supprimez plugins et thèmes inutilisés. Inactif n’est pas sûr ; c’est juste du code inactif.
  2. Mettez à jour WordPress core et plugins. Faites du staging si le site génère des revenus.
  3. Définissez permissions et propriété correctes. Gardez le core en lecture seule pour le runtime ; gardez les uploads inscriptibles.
  4. Régénérez salts/keys si vous suspectez une compromission ou si le site a transité par plusieurs mains.

Phase 2 : Protégez le login sans le casser

  1. Limitez le débit de /wp-login.php au niveau du serveur web ou en périphérie. Évitez les refus d’entrée tant que vous n’êtes pas sûr.
  2. Activez la 2FA pour les admins (ou SSO avec politique forte). C’est le plus gros réducteur de risque spécifique aux connexions.
  3. Désactivez XML‑RPC si inutilisé ; sinon restreignez et surveillez.
  4. Limitez les chemins de création d’admin (ne laissez pas des plugins escalader les rôles ; auditez régulièrement les utilisateurs).

Phase 3 : Ajoutez des garde‑fous et de la visibilité

  1. Centralisez les logs (au minimum accès Nginx/Apache + error logs, logs PHP‑FPM, et logs applicatifs liés à l’auth).
  2. Alertez sur les anomalies : pics de requêtes /wp-login.php, augmentation des 403/429, usage disque, saturation PHP‑FPM, taux de requêtes lentes en DB.
  3. Surveillance d’intégrité des fichiers pour core et répertoires plugins (hashes ou checksums). Si ça change hors déploiement, c’est une alerte.
  4. Exercez la réponse à incident : isoler l’origine, tourner les identifiants, restaurer dans un environnement propre, vérifier.

Liste « Ne faites pas ça » (parce que vous serez tenté)

  • Ne cachez pas /wp-admin avec des plugins d’obscuration et appelez ça « fini ». Les bots crawlent et les humains oublient.
  • Ne mettez pas les permissions à 777 pour « réparer » les mises à jour. Ce n’est pas réparer ; c’est se rendre.
  • Ne comptez pas sur des allowlists IP pour des utilisateurs qui voyagent. C’est comme ça que vous créez des comptes admin de l’ombre.
  • Ne déployez pas des règles de cache non testées sur des endpoints d’auth. Si vous devez le faire, testez avec plusieurs sessions utilisateurs et cookies.

FAQ

1) Dois‑je désactiver xmlrpc.php ?

Si vous n’utilisez pas de fonctionnalités qui en dépendent, oui—bloquez‑le au niveau du serveur web. Si vous l’utilisez (Jetpack, publication legacy), gardez‑le mais throttlez et surveillez.

2) Changer l’URL de connexion est‑ce un vrai contrôle de sécurité ?

C’est une réduction du bruit, pas un contrôle principal. Cela réduit les bots stupides, mais n’arrêtera pas les attaques ciblées ou les plugins exploités. Utilisez‑le seulement si cela ne casse pas les intégrations et que vous avez toujours des rate limits et une auth forte.

3) La limitation de taux va‑t‑elle casser les utilisateurs derrière un NAT ?

Ça peut. C’est pour cela que vous fixez des taux et bursts raisonnables, et que vous keyez correctement sur la vraie IP client. Commencez permissif, surveillez les 429, puis resserrez.

4) Fail2ban suffit‑il sans WAF/CDN ?

Fail2ban aide quand les attaques se concentrent sur des IP répétées. Le credential stuffing se répartit souvent sur de nombreuses IPs, où les protections en périphérie sont plus efficaces. En pratique : utilisez les deux si le site compte.

5) Quelles permissions de fichiers WordPress devrais‑je appliquer ?

Le core doit être en lecture seule pour le runtime. Les uploads doivent être inscriptibles. Un schéma courant : core possédé par root, groupe www-data, fichiers 0640 et répertoires 0750 ; uploads possédés par www-data.

6) Dois‑je autoriser WordPress à mettre à jour les plugins depuis le tableau de bord ?

Pour les sites perso, peut‑être. Pour des sites business en production, préférez des mises à jour via déploiement pour que le runtime web ne puisse pas écrire du code exécutable. Si vous devez l’autoriser, contraignez les permissions à wp-content et surveillez l’intégrité.

7) Pourquoi les connexions échouent après l’activation d’une politique d’en‑têtes de sécurité ?

Une Content Security Policy (CSP) trop stricte peut bloquer des scripts sur la page de connexion, surtout avec des thèmes ou plugins qui injectent des assets. Déployez la CSP en mode rapport d’abord, puis serre‑z graduellement.

8) Comment savoir si l’origine est exposée derrière le CDN ?

Vérifiez le DNS et le firewall. Si l’IP d’origine est atteignable et sert WordPress directement, les attaquants peuvent contourner vos contrôles en périphérie. Verrouillez l’origine aux plages IP CDN ou à un chemin réseau privé.

9) Ai‑je besoin d’un plugin de sécurité ?

Pas toujours. Les contrôles au niveau serveur (rate limits, WAF, permissions, mises à jour, monitoring) font la majorité du travail. Les plugins de sécurité ajoutent 2FA et fonctionnalités pratiques, mais ils ajoutent aussi du code dans votre appli.

10) Quel est le minimum à faire aujourd’hui si je suis débordé ?

Mettez à jour core/plugins, supprimez plugins/thèmes inutilisés, verrouillez les permissions, ajoutez une limitation de taux sur /wp-login.php, et activez la 2FA pour les admins. Puis ajoutez de la journalisation/alertes pour voir ce qui se passe.

Conclusion : prochaines étapes qui font vraiment la différence

Durcir WordPress sans casser les connexions, c’est surtout de la retenue. Ne sortez pas le marteau de bannissement en premier. Réduisez la surface d’attaque, verrouillez ce qui est inscriptible, throttlez les abus et ajoutez une authentification forte. Ensuite instrumentez tout pour que vos contrôles de sécurité ne deviennent pas la prochaine panne.

Faites ceci ensuite :

  1. Exécutez les vérifications d’intégrité et d’inventaire (checksums WP‑CLI, liste plugins, permissions).
  2. Implémentez la limitation de débit côté serveur pour /wp-login.php et bloquez /xmlrpc.php si inutilisé.
  3. Activez la 2FA pour les admins (ou appliquez une politique SSO) et auditez les comptes admin.
  4. Vérifiez la correcte configuration proxy/TLS pour éviter les boucles de connexion.
  5. Ajoutez des alertes : pics 403/429, warnings PHP‑FPM pm.max_children, usage disque, et taux de requêtes de connexion.
  6. Planifiez un exercice de restauration. Pas parce que vous êtes paranoïaque—parce que vous aimez dormir.
← Précédent
MSI/MSI-X + Remappage des interruptions : le correctif de 5 minutes pour les saccades aléatoires des VM
Suivant →
NVMe Passthrough vs VirtIO : le gagnant de performance que personne ne mentionne

Laisser un commentaire