Account Lockout Is a Denial-of-Service Vector

Lock the account after five failed attempts. It's in the hardening guide, the audit checklist, the framework, and the default configuration of nearly every directory product shipped in the last thirty years. Nobody defends it because nobody attacks it.

Lock the account after five failed attempts. It's in the hardening guide, the audit checklist, the framework, and the default configuration of nearly every directory product shipped in the last thirty years. Nobody defends it because nobody attacks it.

Here is what it means, stated without the euphemism: you have built a feature that lets any anonymous party on the internet disable any account, on demand, for free, by knowing nothing but the username.

That's the whole thing. Everything else in this article is detail.

The attack

Usernames are usually email addresses, and email addresses are semi-public. They're on LinkedIn, in commit history, in the From: header of every message anyone in the company has sent, and derivable from firstname.lastname@ once you've seen two examples. Enumeration isn't a prerequisite; it's a formality.

So: script a list, send five wrong passwords per account, and the workforce is locked out. No credentials, no exploit, no privilege. The cost to the attacker is a few thousand HTTP requests. The cost to you is every employee unable to log in on the morning of a release, a close, or a filing deadline.

The nastiest property isn't the disruption. It's that the attack is shaped exactly like the thing lockout is supposed to stop. A mass-lockout event and a large brute-force attempt produce nearly identical telemetry. The instinctive response — tighten the threshold, extend the duration — is precisely wrong, because it hands the attacker a bigger weapon. Five attempts to disable an account is bad; three attempts for a 24-hour lock is a better weapon.

Why the control was right when it was written

It's worth being fair to it, because it wasn't stupid — it was the only brake available.

Picture authentication in 1998. A local SAM database or a LAN directory. There is no central rate limiter, because there is no central anything — authentication happens at whichever domain controller answers. There is no per-IP telemetry worth the name. No anomaly detection, no device fingerprinting, no risk engine. Certainly no breach-corpus check, because there are no public breach corpora. And passwords sit under hashes — LM, unsalted NT — that make offline cracking arithmetic once someone gets the file.

Under those constraints, "count failures on the account and stop at N" is the only mechanism you can implement. It needs no infrastructure, no correlation, no shared state beyond the account record. It's a purely local control, which is what you build when you have no global view.

That premise expired. Most systems now authenticate through a central service that sees every attempt, from every source, across every account. The control stayed anyway — because nothing about a control announces that its assumptions are gone.

The attacks moved sideways

Lockout defends against vertical brute force: many passwords, one account. That was the dominant attack, and it is now a minor one.

What replaced it is horizontal, and lockout is structurally blind to it.

Password spraying inverts the loop. Instead of ten thousand passwords against one account, you try one password — Autumn2025!, CompanyName1, Welcome@123 — against ten thousand accounts. Each account sees exactly one failure. At a 1% hit rate that's a hundred compromised accounts and not a single lockout anywhere. The threshold is never approached, let alone crossed.

Credential stuffing is worse, because it doesn't fail at all. The attacker has [email protected]:hunter2 from an unrelated breach; if Alice reused it, the first attempt succeeds. There is no failure count to accumulate. Lockout isn't merely ineffective here — it's not in the code path.

The control is tuned against the attack that stopped being common, while remaining fully available as a weapon against you.

The part most people haven't worked out

Lockout policies have a second parameter that gets far less attention than the threshold: the counter reset window — "Reset account lockout counter after" in Active Directory, with an equivalent everywhere else, usually 15 or 30 minutes.

Do the arithmetic from the attacker's side. Threshold 5, reset window 15 minutes. Four attempts, wait fifteen minutes, four more. The counter never reaches five, so lockout never fires. That's 16 guesses per hour, per account, indefinitely — roughly 380 guesses per day, per account, forever, with a mathematical guarantee of never being locked out. Across ten thousand accounts that's 3.8 million guesses a day, with the policy functioning exactly as configured throughout.

Lockout doesn't cap guessing. It publishes a guessing budget and certifies that anyone staying inside it will never be interrupted. Worse, its presence is routinely the reason nobody builds real rate limiting — the box is ticked, move on.

Also under-appreciated: a large share of real lockouts aren't attacks. They're a stale credential in a mail client, a service account in a scheduled task, a mapped drive on a laptop in a drawer, retrying every few minutes against a password changed last week. Most organizations' lockout telemetry is mostly noise from their own estate — a good way to miss the signal when it matters.

The costs that land on someone else's budget

Helpdesk volume. Lockouts sit next to password resets at the top of enterprise ticket categories, and each ticket is a human verifying identity under time pressure — a social-engineering surface you generate on demand.

It pushes users into recovery, which is almost always a weaker path than the login they were just blocked from. Blocking the front door to force traffic through the side door is a strange posture.

Lockout state is an enumeration oracle. If a locked account returns a distinguishable response — different message, status, or latency — from a nonexistent one, an attacker enumerates your entire user base by deliberately locking accounts and reading the difference. The control designed to resist guessing hands over the list of things worth guessing.

What to do instead

There's no single replacement — a stack, each layer covering another's failure.

Strategy Defends against Fails against
Progressive delay on the account (exponential backoff) Vertical brute force Slow spraying; attacker can still degrade an account, though not hard-lock it
IP-scoped throttling Naive single-source volume Distributed botnets, residential proxies; punishes shared corporate NAT egress
Device / session reputation New-device credential stuffing First legitimate login from a genuinely new device
Breach-corpus checking at set-time Credential stuffing at the root Passwords not yet in any corpus
CAPTCHA / proof-of-work as escalation Cheap automation at scale Solver services; and it's a tax on real users if used as a gate
Cross-account correlation Spraying, stuffing, distributed campaigns Nothing structural — it's the layer most people don't have

Progressive delay is the direct swap for lockout: an attacker can still make an account slow, but can't take it away, and a legitimate user self-recovers by waiting rather than by filing a ticket. That asymmetry is the whole point.

The important row is the last one. Spraying is invisible in per-account metrics and blatant in cross-account ones. Ask different questions of the same log stream:

  • How many distinct accounts has this IP or ASN failed against in the last hour?
  • Is one password being attempted across many accounts? (You can count this over a keyed hash, without retaining candidates.)
  • What's the tenant-wide failure rate against its own baseline?
  • Are failures clustering on a small set of usernames right before a deadline?

A spray producing one failure per account produces thousands per source. The signal exists; almost nobody queries for it, because the metric everyone built is scoped to the account.

When lockout is correct

It isn't always wrong, and the cases are specific.

High-value administrative accounts. Break-glass, domain admin, root-equivalent. The trade genuinely inverts here: unavailability is a nuisance, compromise is an incident, and there are few enough of these that a targeted DoS against them is itself a loud, actionable signal.

Accounts with a guaranteed, fast human recovery path. If a verified in-person unlock is available in minutes, lockout's cost is bounded and known.

Regulated environments where the framework mandates it. Be honest: the auditor enforces the framework, not the reasoning. Losing that argument is normal.

Where it's mandated, make it survivable. Use a short auto-unlock window — 15 minutes, not indefinite — which preserves the audit answer while capping the DoS to a rolling annoyance rather than a kill. Don't let lockout fire on attempts from unauthenticated sources that can't demonstrate they reached the right account. And instrument lockouts by source, so the mass-lockout attack stays distinguishable from the brute force it imitates.

The question to ask instead

"How many failures before we lock" assumes the control is correct and asks only for a constant.

The better question: what attack are we actually defending against, and does this control touch it? For vertical brute force against a single account, lockout touches it — and so does a delay that doesn't hand attackers a kill switch. For spraying and stuffing, which is what's hitting your login endpoint this morning, it does not touch it at all.

A control that stops the attack you no longer face, while enabling one you do, isn't a security measure with a tuning problem. It's a feature request from your adversary that you implemented by default.