August 30, 2026 · Tommy Bordas

wp-dashboard, my open source tool to monitor a fleet of WordPress sites

wordpressopen-sourcepythonmaintenancesecurite

I built wp-dashboard, a self-hosted open source dashboard to monitor and maintain a fleet of WordPress sites: per site inventory, vulnerability watch cross referenced locally, and updates with automatic rollback. Here is the need behind it and the technique inside.

The problem, hearing in time that a plugin is vulnerable

When you maintain several WordPress sites for clients, you know a plugin installed somewhere on the fleet will eventually have a flaw. What you control badly is how long it takes you to hear about it. The window between the public disclosure of a vulnerability and its automated exploitation has narrowed considerably, as I detail in WordPress attacks in the summer of 2026, wp2shell, supply chain and the end of monthly updates.

The limiting factor is no longer the quality of the upstream patch, serious vendors ship fixes quickly. It is the delay between that release and its application on your side. That delay plays out on two fronts. You first need to know that site X runs plugin Y at version Z, right now, not according to a note from three weeks ago. You then need to be able to apply the update without spending half a day dreading that you will break a live shop.

Hand rolled maintenance does not scale

My first approach was everybody's approach: log into each WordPress admin, look at the update badge, copy the versions into a spreadsheet. That holds for two or three sites. Past ten, the spreadsheet is mostly wrong, because you stop filling it in systematically and you no longer quite know how old each row is.

What causes trouble is less the workload than the reliability of the information. The day a flaw lands on a widespread plugin, the only question that matters is whether you have it installed anywhere, and where. A spreadsheet filled in three weeks ago suggests you can answer, when in practice you still have to go and check site by site.

Why I did not pick an existing solution

The commercial tools exist, and they are good. ManageWP, MainWP and WP Umbrella do the job, with polished interfaces and teams behind them. Two things held me back. The first is cost, usually billed per site per month, which climbs fast once you add the backup and monitoring options.

The second matters more to me. These services require sending your full fleet inventory to a third party: the list of your clients' sites, their core versions, their plugins and the exact version of each one. That is, quite literally, an attacker's treasure map. I am not claiming these platforms are poorly protected. I simply prefer that map to exist in exactly one place, on a server whose keys I hold.

That is where wp-dashboard came from, published as open source on github.com/tommybds/wp-dashboard.

The technical stance, restraint and zero dependencies

The backend is written in Python 3.8 or newer, standard library only. No external package, so no virtual environment to maintain and no dependency chain to audit. It is a constraint I imposed on myself deliberately, to avoid a maintenance tool that demands maintenance of its own every three months. The frontend follows the same logic, a single page application with no build step, a public/index.html served as is by nginx.

Around it, two conventional infrastructure blocks: nginx in front with a TLS certificate, and Uptime Kuma in a Docker container for availability. Kuma is an acknowledged dependency: rather than rewrite a mediocre uptime monitor, the dashboard reads its SQLite database read only through docker exec to link each site to its monitor, and proxies its status page.

The point that matters most to me: fleet data never leaves the server. Vulnerability watch included. The cross referencing happens locally against an open public database, and the question asked is "which known flaws exist for plugin X?", never "here is the list of my sites, tell me what is vulnerable". The distinction sounds minor, but it is what keeps the fleet inventory private.

Three components, and nothing more

Everything lives in /opt/wp-dashboard/.

Component File Role
Collector collect.py Queries each server over SSH via wp-cli, and the sites without SSH through the agent. Produces data/fleet.json. Run by cron every 30 minutes.
API actions_server.py Local HTTP service on 127.0.0.1:8090: authentication, whitelisted maintenance actions, interface endpoints. Exposed over HTTPS by nginx.
Interface public/index.html Single page application with no build, served by nginx.

Four background tasks come on top, also driven by cron:

File Role Cadence
vulns.py Cross references the inventory against the public vulnerability database, locally. 6 h
phperrors.py Reads the PHP error logs the servers already write (PHP-FPM on Plesk, nginx on VPS) and groups occurrences by file and line. No site is modified. 2 h
digest.py Sends the daily summary of changes to Telegram, when there are any. 8 h
rotate.py Log rotation with differentiated retention: short for routine noise, long for anything with evidential value (admin account creation, failed action). weekly

Every script also runs by hand, which makes debugging far less painful:

python3 vulns.py --fetch --scan     # vulnerability watch
python3 phperrors.py --print        # PHP errors (--hours 72 to widen)
python3 digest.py --dry-run         # Telegram summary, without sending
python3 rotate.py --dry-run         # log rotation, dry run

Two ways to connect a site

Not every site in a fleet is hosted under the same conditions, and that is the main practical obstacle for this kind of tool. So I planned two modes, which can be combined.

Over SSH, with wp-cli. You declare the server in servers.json with its host, its port and the globs of the docroots to scan. The collector discovers the WordPress installs on its own by looking for wp-load.php, then inventories them. This is the full mode: core, plugin and theme updates, UpdraftPlus backup, cache flushing, wp core verify-checksums, auto update management.

Through an agent, for sites without SSH. This is the common case of a client whose shared hosting only gives access to the WordPress admin. From the interface you enter the URL: the dashboard probes the REST API, walks you through installing the agent and generates a single use pairing code. The inventory you get is identical to the SSH mode. Only write actions are unavailable, since the agent is deliberately read only. In exchange, you gain real time events: a new administrator created, a plugin activated, an update completed.

The two modes are not exclusive. Installing the agent on a site already reachable over SSH is purely additive. The agent itself (agent/sumotori-dash-agent/) is a standalone WordPress plugin, multisite compatible, which signs its exchanges with HMAC and hardcodes no service address, the dashboard URL being entered at pairing time.

The safe update

Everything above only serves to prepare this step. Knowing that a plugin is vulnerable protects nothing until you have updated it, and when people put the update off, it is almost always for fear of breaking the site. On a shop that takes money, the fear is fairly rational.

So I tried to make the operation reversible. The safe update button chains a complete sequence:

  1. Pre-flight check on the state of the site.
  2. UpdraftPlus backup.
  3. File archival and database dump.
  4. The update itself.
  5. Post checks: is the page still served, has its weight not collapsed, does WordPress respond normally, plus a VizProof visual scan when the command is available.
  6. Automatic rollback if any of those checks fails.

Two guardrails reflect deliberate choices. First, the database is never restored automatically: it holds whatever was written during the operation, an order placed, a comment, a login. Restoring it without thinking would destroy that data, so the tool supplies the exact command and leaves the decision to a human. Second, a plugin marked as frozen is never updated, whatever path is taken, which is essential as soon as custom code is pinned to a specific version.

And for those who, like me, dislike triggering an irreversible action without seeing it coming, the simulation walks the whole sequence without writing anything:

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

A dashboard holding the fleet keys is a target

A tool that knows every site, their versions and their access is obviously a prime target. Designing it without accounting for that would turn a security tool into a single point of failure.

The API therefore listens only on 127.0.0.1. It is reachable from outside only through nginx, which carries the TLS and a rate limiting zone on the authentication endpoints. The session relies on an HMAC signed cookie, the access account password is hashed with PBKDF2, and failed logins are recorded so they can be wired into fail2ban.

On the execution side, only the commands present in a whitelisted action dictionary can be run, and their targets (server, domain) are validated by regular expression. The wp-cli reads run under the site user rather than root, with --skip-plugins --skip-themes, which avoids executing the code of possibly already compromised plugins just to read a version number.

Finally, the one click linking creates a dedicated administrator account per site instead of borrowing a personal one. You gain attribution (knowing who did what) and revocability (removing one access without touching the rest). Its creation is logged and alerted, and the account is deleted when the site leaves the fleet.

A young project, open to feedback

On maturity, wp-dashboard sits at around thirty commits. It grew out of my own needs, it does not have the finish of a commercial product and its licence is still to be settled, even though the companion agent is already published under GPLv2+ as the WordPress ecosystem requires. Installing it takes a Linux server, nginx, a certificate and an Uptime Kuma container, so it suits someone comfortable on the command line rather than someone looking for a subscription.

I am publishing it anyway, for the same reason as Miroir Local Sync, my other open source WordPress tool. It already solves a real problem for me, and open source remains the best way to make it sturdier. The code and the installation documentation are on the GitHub repository. If you too maintain a fleet of WordPress sites, feedback, bug reports and architecture criticism are all welcome.