Hundreds of failed login attempts across ten WordPress sites. Bots from DigitalOcean Singapore and Google Cloud Oregon, hammering wp-login.php on repeat. But the real problem wasn't the bots — it was the doors we'd left open for them.

Three attack vectors, all wide open. The REST API at /wp-json/wp/v2/users was publicly returning usernames and IDs on four of five sites — a free directory of who to target. XML-RPC was returning 200 OK on every site, with system.multicall enabled, meaning a bot could try thousands of password combinations in a single HTTP request. And the author archive redirects were leaking usernames through URL enumeration — hit ?author=1 and the redirect tells you the admin's username.

One MU-plugin. Four layers. Zero dependencies.

The REST API user enumeration block runs at priority 99 on rest_pre_dispatch — not priority 10, because ACF's handler at priority 10 returns null implicitly, wiping out any WP_Error from earlier filters. A subtle framework interaction that took debugging to discover. The XML-RPC kill switch is comprehensive: filter disabled, methods array emptied, X-Pingback header stripped, RSD link removed, init-level 403 for anything that still gets through. The author archive block catches both the query parameter and the permalink pattern, strips author info from oEmbed responses, and sets a clean 404 instead of a redirect that leaks data. And a login rate limiter — ten failed attempts per IP per ten minutes, using transients, Cloudflare-aware so it reads the real IP from CF-Connecting-IP instead of seeing Cloudflare's edge server.

Deployed to all ten WordPress sites via SCP. Cloudflare WAF rules went out to all five DNS zones: block XML-RPC requests outright, managed challenge on wp-login.php POST. Two layers — the plugin catches what gets through the WAF, the WAF stops most of it before it touches PHP.

The same session standardized email across the fleet. ProComfort had no SMTP configured — raw PHP mail(), which means emails from form submissions were either landing in spam or not arriving at all. And all sixteen FluentForm notifications were disabled. Snell Automotive was running an expired Outlook OAuth token through GoSMTP Pro. Patton Plumbing had two conflicting SMTP plugins fighting over who sends the email. Safari was already on FluentSMTP and Brevo — the target state. THD had its own expired OAuth situation.

Three sites moved to FluentSMTP plus Brevo. DNS authentication configured on each: SPF records, DKIM CNAME pairs, DMARC policies, Brevo verification codes. ProComfort's sixteen form notifications got enabled with the correct recipient. Patton's contact forms had stale email addresses from former employees — all updated. Snell's admin email changed from a personal Gmail to the business address.

And a marketing landing page shipped for Zenthic.space — a self-contained MU-plugin rendering a static page. Warm earth tones, sage green, under 50KB, full JSON-LD schema, scroll animations via IntersectionObserver. The kind of thing that takes an hour to build when the pattern is solid.

Fleet hygiene isn't glamorous. Security hardening and email configuration don't produce screenshots or user-facing features. But a fleet where every site leaks admin usernames, accepts XML-RPC brute force, and drops form submissions into the void isn't a fleet — it's a liability. Now it isn't.