wpfleet: monitoring WordPress fleets from the inside
On-host monitoring for WordPress fleets. It watches from the inside, where the real signals are.
Highlights
- Runs on the host, not against a URL, so it sees what a scanner never can: PHP in the uploads folder, mu-plugins that were not there yesterday, core versions years out of date
- Surfaces the size of autoloaded options, read on every request and shown nowhere in WordPress, including the autoload values WordPress 6.6 changed that older scripts quietly miss
- Cached background collection, so a walk across hundreds of installs never stalls a Prometheus scrape
The problem
I built wpfleet after a server kept falling over from the same thing. Sites were getting reinfected after every cleanup, and with dozens of installs on one box, working out what had changed since the last sweep was slow and done by hand. You clean a site, it looks fine, and two days later it is serving spam again. The frustrating part is never the cleanup. It is not knowing what came back, or how.
Bulwark answers one question: is this site compromised right now. wpfleet answers the one that comes next: what changed, and when. It watches the whole fleet over time, so when something reappears I can see exactly what moved and go straight at the culprit instead of hunting through hundreds of directories hoping to trip over it.
What it does
It runs on the server itself. That is the whole idea, and it is the thing that separates it from every scanner that hits your site from the outside and reports that it is up. Being up was never the question. wpfleet has filesystem and database access, so it can see the things that actually matter.
On each pass it finds every WordPress install on the host and reads a small set of facts about each one, straight off the disk. The core version. How many plugins and themes are installed. Whether there is a PHP file sitting in the uploads directory, which there never should be, and which is almost always a backdoor when there is. The size of the mu-plugins directory, which WordPress loads automatically on every request and never shows you in the admin, and which is therefore a favourite place to hide.
None of that touches PHP or wp-cli. It is all read directly, which is the only way a sweep of a busy server stays cheap enough to run every few minutes.
The autoload thing
With database access turned on it reads one more number, and it is the one I am most pleased with, because nothing inside WordPress will ever show it to you.
Every option row marked to autoload is read and unserialised on every single request. Plugins write to it freely, and plenty of them never clean up after themselves when you uninstall them. So it grows. A site accumulates megabytes of autoloaded junk and nobody notices, because there is no screen anywhere that adds it up, right up until the site is mysteriously slow and no one can say why.
wpfleet measures it, which means for the first time you can alert on it before a client does.
There is a detail here that most scripts get wrong. WordPress 6.6 stopped
treating autoload as a plain yes or no and added several new values. Anything
written before that still checks for yes and silently undercounts on every
modern install. wpfleet matches the full set, so the number it gives you is the
real one.
The one decision that matters
Walking hundreds of installs takes a few seconds. Prometheus gives a scrape ten seconds before it writes the whole thing off as a failure. Put those two facts together and the naive version of this tool falls over on exactly the busy server you most needed it on.
So collection never happens during a scrape. A background loop does the expensive walk on its own clock and drops the result into a snapshot. When Prometheus comes knocking, it gets that snapshot instantly. The thing being measured is never allowed to slow down the measuring. It is the same lesson I learned building sysmon-gov, just applied to a fleet instead of a single box.
That design is also the reason this became more than a scanner. Because Prometheus keeps every snapshot with a timestamp, the question stops being “is this site infected” and becomes “what changed on this site overnight.” That second question is the one I actually needed answered, and it is the one that turned an unfindable reinfection into something I could catch in the act.
Where it is right now
Early, and honest about it. The filesystem collection and the autoload metric work and are tested. It deploys the way node_exporter does: one binary per host, scraped by Prometheus, alerting handled by rules rather than by someone watching a screen.
What is not built yet is the rest of the performance picture. php-fpm pool saturation, nginx request rates, and per-plugin version checking so it can tell you not just how many plugins a site runs but how many are out of date. Those are the next three collectors, roughly in that order.
It is not going to replace a commercial security suite and I would not point it at a site I do not run. It does one specific job for the specific kind of server I work on every day, and it does the part those suites skip, which is telling you what moved since yesterday.
The final note is that more changes are to come and the options to successfully deploy across a cluster. Will keep this one updated.