Three login attacks at once, zero logins: the accounts held because they could not be guessed

Photo by Kevin Horvat on Unsplash

During a two-day period in October 2026, three login attacks ran at once against the same page on my self-managed WordPress box, four sites I moved there in a blue-green migration. None got in. They failed because the accounts could not be guessed, not because a rate limit held. That distinction is what Fast, secure, self-managed WordPress covers.

TL;DR

All 101 attempts arrived on a username that does not exist, so no password was compared and two-factor authentication was never reached. A refused request looks identical whatever layer issued it, so the log, not the narrative, tells which one acted. The accounts held by design; the counters only cut load.

The campaign

Three streams hit the page together, each meeting a different layer.

  • A slow single address. One client tried 78 times, spread out over time, and the local lockout never fired at that spacing.
  • A distributed rotation. Seventeen addresses tried once each against the public byline and failed, because the login name there differs from it.
  • Short bursts. Six junk-name attempts in all, cut by fail2ban.

The diagnosis

A refused login request names no layer. The slow stream got a plain 403, which reads the same whether the local lockout or the network-level block issued it. Only the plugin’s log distinguishes them: each rejected attempt leaves a row naming the module that acted.

The sysadmin agent I run credited Solid Security‘s local lockout for that stream and recommended enabling the network-level brute-force protection. It was already active on all four sites and had done the blocking. My first read rested on the story, not the log’s own field; I corrected the agent’s skill the same day, which is the gap deterministic guards exist to close.

The layers by design

Four layers stand between the internet and one of my accounts. The sites share a box, each its own process and filesystem world, so each layer applies per site while the network-level one spans them.

  • The login names protect the accounts. Every admin login is unique per site and never a pseudo I use in public, so a guess from breach data cannot land.
  • Two-factor authentication protects the accounts. A correct password alone is not enough.
  • Solid Security’s Network Brute Force cuts traffic. It watches across sites and blocks an address that makes too many attempts, so a flood from one address is dropped before WordPress renders it.
  • Solid Security’s local lockout and the fail2ban jails cut load. They count failures and lock an address out, keeping the log and the page from absorbing a flood.

The first two are account design, the last two rate counters. Counters cut load and noise; they do not make an account unguessable.

What the review found

The same review that misread the layer found what a refused request cannot show: gaps in the design.

  • Two-factor authentication was live on only two of the four sites, so two admin accounts sat one password from a takeover.
  • An admin login on three sites was a pseudo known from public breach data, the exact guess the design defeats.
  • The repeat-offender jail had never banned anything, because it read the wrong backend.

I closed all three the same day and verified each live on the following day. Only the site’s own log could have shown them.

The load side

Every counter above counts a failed login, and a request that never tries a password is not one. One address loaded the login page roughly every two seconds, for minutes at a time. Each load is an uncached PHP render, about 20% of one processor core, and no jail banned it: the guards count failures, and a page load answered 200 is not one. The address returned within a day of each ban, which the repeat-offender jail could not see.

“Zero logins” is not “defended” when the actor never tries to log in. A failure counter answers a guessing campaign and says nothing about a client that only reads the page. These run unattended, and a counter that never fires is no proof of safety, the discipline behind daily automation that does not silently break.

The fix

A request counter closes the gap. I added a separate rate jail on the login page: it counts every request, whatever its status or form, bans an address that crosses its rate, holds the ban past a daily return, and closes its open connection. It protects load, not accounts, beside the failure counters.

As a configuration change, it passed the same gate as every web-server change: the test precedes the reload, the discipline that keeps a bad reload from taking four sites down. The jail is verified on its configuration only; its first real ban is still pending.

The reflex is to trust the story an incident tells. Here the accounts were never in danger, and the layer my review credited had not acted. The habit that settled both is one: read what the system recorded, not the narrative around it. The trade-offs I run this stack with are on the about page. If this is your problem, let’s talk.