30 août 2026 · Tommy Bordas

wp-dashboard, mon outil open source pour superviser un parc de sites WordPress

wordpressopen-sourcepythonmaintenancesecurite

J'ai développé wp-dashboard, un tableau de bord auto hébergé et open source pour superviser un parc de sites WordPress : inventaire par site, veille de vulnérabilités faite en local, mises à jour avec retour arrière automatique. Voici le besoin et la technique derrière.

Le problème, savoir à temps qu'une extension est vulnérable

Quand on maintient plusieurs sites WordPress pour des clients, on sait qu'une extension installée quelque part sur le parc finira par avoir une faille. Ce qu'on maîtrise mal, c'est le délai avec lequel on l'apprend. Entre la divulgation publique d'une vulnérabilité et son exploitation automatisée, la fenêtre s'est considérablement réduite, comme je le détaille dans Piratages WordPress de l'été 2026, wp2shell, supply chain et la fin des mises à jour mensuelles.

Le facteur limitant n'est plus la qualité du correctif publié en amont, les éditeurs sérieux corrigent vite. C'est le délai entre sa publication et son application chez vous. Ce délai se joue sur deux points. Il faut d'abord savoir que le site X fait tourner l'extension Y en version Z, à l'instant présent et pas d'après une note d'il y a trois semaines. Il faut ensuite pouvoir appliquer la mise à jour sans y passer une demi journée à redouter de casser une boutique en production.

La maintenance artisanale ne tient pas la distance

Ma première approche a été celle de tout le monde : se connecter à chaque administration WordPress, regarder la pastille des mises à jour, reporter les versions dans un tableur. Ça tient sur deux ou trois sites. Passé la dizaine, le tableur devient surtout faux, parce qu'on ne le remplit plus systématiquement et qu'on ne sait plus très bien à quelle date remonte chaque ligne.

Ce qui pose problème n'est pas tant la charge de travail que la fiabilité de l'information. Le jour où une faille sort sur une extension répandue, la seule question qui compte est de savoir si on l'a installée quelque part, et où. Un tableur rempli il y a trois semaines laisse croire qu'on peut y répondre, alors qu'il faut de toute façon aller vérifier site par site.

Pourquoi je n'ai pas pris une solution existante

Les outils du marché existent, et ils sont bons. ManageWP, MainWP, WP Umbrella font le travail, avec des interfaces soignées et des équipes derrière. Deux choses m'ont retenu. La première est le coût, le plus souvent facturé par site et par mois, qui grimpe vite dès qu'on ajoute les options de sauvegarde et de supervision.

La seconde compte davantage à mes yeux. Ces services supposent d'envoyer l'inventaire complet de son parc chez un tiers : la liste des sites de ses clients, leurs versions de cœur, leurs extensions et les versions exactes de chacune. C'est, très littéralement, la carte au trésor d'un attaquant. Je ne prétends pas que ces plateformes soient mal protégées. Je préfère simplement que cette carte n'existe qu'à un seul endroit, sur un serveur dont je tiens les clés.

C'est de là qu'est né wp-dashboard, publié en open source sur github.com/tommybds/wp-dashboard.

Le parti pris technique, sobriété et zéro dépendance

Le backend est écrit en Python 3.8 ou supérieur, en bibliothèque standard uniquement. Pas de paquet externe, donc pas d'environnement virtuel à entretenir ni de chaîne de dépendances à auditer. C'est une contrainte que je me suis imposée volontairement, pour éviter qu'un outil de maintenance réclame lui même une maintenance tous les trois mois. Le frontend suit la même logique, une application monopage sans build, un public/index.html servi tel quel par nginx.

Autour, deux briques d'infrastructure classiques : nginx en frontal avec un certificat TLS, et Uptime Kuma dans un conteneur Docker pour l'état de disponibilité. Kuma est une dépendance assumée : plutôt que de réécrire un moniteur médiocre, le dashboard lit sa base SQLite en lecture seule via docker exec pour relier chaque site à son moniteur, et proxifie sa status page.

Le point qui compte le plus pour moi : les données du parc ne sortent jamais du serveur. Y compris pour la veille de vulnérabilités. Le croisement se fait en local contre une base publique ouverte, et la requête posée est « quelles failles connues pour l'extension X ? », jamais « voici la liste de mes sites, dis moi ce qui est vulnérable ». La nuance paraît subtile, elle change tout sur le plan de la confidentialité.

Trois composants, et rien de plus

Tout vit dans /opt/wp-dashboard/.

Composant Fichier Rôle
Collecteur collect.py Interroge chaque serveur en SSH via wp-cli, et les sites sans SSH via l'agent. Produit data/fleet.json. Lancé par cron toutes les 30 minutes.
API actions_server.py Service HTTP local sur 127.0.0.1:8090 : authentification, actions de maintenance sur liste blanche, endpoints de l'interface. Exposé en HTTPS par nginx.
Interface public/index.html Application monopage sans build, servie par nginx.

S'y ajoutent quatre tâches de fond, elles aussi pilotées par cron :

Fichier Rôle Cadence
vulns.py Croise l'inventaire avec la base de vulnérabilités publique, en local. 6 h
phperrors.py Lit les journaux d'erreur PHP que les serveurs écrivent déjà (PHP-FPM sur Plesk, nginx sur les VPS) et regroupe les occurrences par fichier et par ligne. Aucun site n'est modifié. 2 h
digest.py Envoie sur Telegram le bilan quotidien des changements, s'il y en a. 8 h
rotate.py Rotation des journaux à rétention différenciée : courte pour le tout-venant, longue pour ce qui a valeur de preuve (création d'un administrateur, échec d'une action). hebdo

Chaque script s'exécute aussi à la main, ce qui rend le déboguage nettement moins pénible :

python3 vulns.py --fetch --scan     # veille de vulnérabilités
python3 phperrors.py --print        # erreurs PHP (--hours 72 pour élargir)
python3 digest.py --dry-run         # bilan Telegram, sans rien envoyer
python3 rotate.py --dry-run         # rotation des journaux, à blanc

Deux façons de brancher un site

Tous les sites d'un parc ne sont pas hébergés dans les mêmes conditions, et c'est le principal obstacle pratique à ce genre d'outil. J'ai donc prévu deux modes, combinables.

En SSH, avec wp-cli. On déclare le serveur dans servers.json avec son hôte, son port et les globs des docroots à scanner. Le collecteur découvre tout seul les WordPress présents en cherchant wp-load.php, puis les inventorie. C'est le mode complet : mises à jour du cœur, des extensions et des thèmes, sauvegarde UpdraftPlus, vidage des caches, wp core verify-checksums, gestion des auto mises à jour.

Via un agent, pour les sites sans SSH. C'est le cas fréquent du client dont l'hébergement mutualisé ne donne accès qu'à l'administration WordPress. Depuis l'interface, on saisit l'URL : le dashboard sonde l'API REST, guide l'installation de l'agent et génère un code d'appairage à usage unique. L'inventaire obtenu est identique à celui du mode SSH. Seules les actions d'écriture sont indisponibles, l'agent étant volontairement en lecture seule. En contrepartie, on gagne les évènements en temps réel : nouvel administrateur créé, extension activée, mise à jour terminée.

Les deux modes ne s'excluent pas. Installer l'agent sur un site déjà accessible en SSH est purement additif. L'agent lui même (agent/sumotori-dash-agent/) est un plugin WordPress autonome, compatible multisite, qui signe ses échanges en HMAC et n'embarque aucune adresse de service en dur, l'URL du dashboard étant saisie au moment de l'appairage.

La mise à jour sûre

Tout ce qui précède ne sert qu'à préparer cette étape. Savoir qu'une extension est vulnérable ne protège de rien tant qu'on ne l'a pas mise à jour, et si on repousse la mise à jour, c'est presque toujours par crainte de casser le site. Sur une boutique qui encaisse, la crainte est d'ailleurs assez rationnelle.

J'ai donc cherché à rendre l'opération réversible. Le bouton de MAJ sûre enchaîne une séquence complète :

  1. Contrôle préalable de l'état du site.
  2. Sauvegarde UpdraftPlus.
  3. Archivage des fichiers et dump de la base.
  4. Mise à jour proprement dite.
  5. Contrôles après coup : la page est elle servie, son poids ne s'est il pas effondré, WordPress répond il normalement, et scan visuel VizProof si la commande est disponible.
  6. Retour arrière automatique si l'un de ces contrôles échoue.

Deux garde-fous traduisent des choix assumés. D'abord, la base n'est jamais restaurée automatiquement : elle contient ce qui a été écrit pendant l'opération, une commande passée, un commentaire, une connexion. La restaurer sans réfléchir détruirait ces données, l'outil fournit donc la commande exacte et laisse la décision à un humain. Ensuite, une extension marquée comme gelée n'est jamais mise à jour, quel que soit le chemin emprunté, ce qui est indispensable dès qu'on a du code sur mesure accroché à une version précise.

Et pour ceux qui, comme moi, n'aiment pas déclencher une action irréversible sans l'avoir vue venir, la simulation déroule toute la séquence sans rien écrire :

POST /api/actions/safe_update {"dry_run": true}

Un dashboard qui a les clés du parc est une cible

Un outil qui connaît tous les sites, leurs versions et leurs accès est évidemment une cible de choix. Le concevoir sans en tenir compte reviendrait à transformer un outil de sécurité en point de défaillance unique.

L'API n'écoute donc que sur 127.0.0.1. Elle n'est joignable de l'extérieur que par nginx, qui porte le TLS et une zone de limitation de débit sur les endpoints d'authentification. La session repose sur un cookie signé en HMAC, le mot de passe du compte d'accès est haché en PBKDF2, et les échecs de connexion sont journalisés pour pouvoir être branchés sur fail2ban.

Côté exécution, seules les commandes présentes dans un dictionnaire d'actions sur liste blanche peuvent être lancées, et leurs cibles (serveur, domaine) sont validées par expression régulière. La lecture wp-cli tourne sous l'utilisateur du site plutôt qu'en root, avec --skip-plugins --skip-themes, ce qui évite d'exécuter le code d'extensions potentiellement déjà compromises pour aller lire un simple numéro de version.

Enfin, la liaison en un clic crée un compte administrateur dédié par site plutôt que d'emprunter un compte personnel. On y gagne l'attribution (savoir qui a fait quoi) et la révocabilité (retirer un accès sans toucher au reste). Sa création est journalisée et alertée, et le compte est supprimé quand on retire le site du parc.

Un projet jeune, ouvert aux retours

Sur la maturité du projet, wp-dashboard en est à une trentaine de commits. Il est né de mes propres besoins, il n'a pas la finition d'un produit commercial et sa licence reste à arrêter, même si l'agent compagnon est déjà publié en GPLv2+ comme l'exige l'écosystème WordPress. L'installation demande un serveur Linux, nginx, un certificat et un conteneur Uptime Kuma, donc plutôt à l'aise en ligne de commande que pressé de s'abonner à un service.

Je le publie quand même, pour la même raison que Miroir Local Sync, mon autre outil open source sur WordPress : il résout déjà un vrai problème pour moi, et le publier reste le meilleur moyen de le rendre plus solide. Le code et la documentation d'installation sont sur le dépôt GitHub. Si vous maintenez vous aussi un parc de sites WordPress, les retours, les remontées de bug et les critiques d'architecture sont les bienvenus.