Catching Auth Attacks: Brute Force, Credential Stuffing, MFA Fatigue

Good authentication decides who gets in. Detection decides whether you notice the people trying who shouldn't. The two failure modes look nothing alike in the logs, which is the part that trips up most home-grown alerting. If your only auth alert is "too many failed logins," you're catching the loudest, dumbest attack and missing the ones that actually work.

Brute force is the easy one, and the one everyone builds first. Many failures against one account, from one source, in a short window: it has a clean frequency signature, and a threshold rule catches it. I run exactly that in my SIEM and it auto-blocks the source. The problem is that serious attackers stopped doing this years ago precisely because it's trivial to detect. It's still worth alerting on, but if it's your whole program you're guarding the one door nobody serious uses.

flowchart LR BF["Brute force"] -->|"many fails · one account · one source"| S1["Threshold rule → auto-block"] CS["Credential stuffing"] -->|"1 try/account · many accounts"| S2["Aggregate by source → distinct-account spike"] MF["MFA fatigue"] -->|"push burst then approve"| S3["Sequence rule → alert on the 'yes'"] ATO["Working credential"] -->|"anomaly on SUCCESS"| S4["Impossible travel · new ASN · new geo"] S1 --> R["Investigate / respond"] S2 --> R S3 --> R S4 --> R
Catching Auth Attacks: Brute Force, Credential Stuffing, MFA Fatigue

Credential stuffing is the same tool turned sideways, and it's the one that quietly works. Attackers take username/password pairs from someone else's breach and try each pair once against you. From a single account's point of view there's one failed login. Nothing. The signal isn't per-account failures, it's the shape across accounts: one source, or a botnet spread across many, touching hundreds of different usernames with a low per-account failure rate and the occasional success. You detect it by aggregating the other way: distinct accounts per source, failures spread across the tenant, not by watching any single login. And the successes are what matter, because a stuffed password that works produces a valid login that no failure-based rule will ever flag.

That's why the highest-signal detections watch successful auth, not failed auth. Impossible travel, the same account authenticating from two places too far apart to be real, catches a working credential no matter how it was obtained. A first-seen login from a new country, a new ASN, a headless user-agent: none of these are failures, they're anomalies on success, and they're where takeovers actually surface. MFA fatigue lives here too. The tell is a burst of push challenges to one user followed by an approval, so the thing to alert on isn't a failed factor. It's many prompts then a yes. A rhythm, not an error.

flowchart LR subgraph Fail["Failure-based rules"] direction TB F1["Count failed logins"] --> F2["Catches brute force"] F2 --> F3["Blind to stuffing's<br/>successful logins"] end subgraph Win["Success-based detection"] direction TB W1["Watch successful auth"] --> W2["Impossible travel · new ASN · new geo"] W2 --> W3["Catches working credentials"] end
Catching Auth Attacks: Brute Force, Credential Stuffing, MFA Fatigue

The through-line is the same as the rest of my detection work: the loud attacks are easy and mostly harmless, and the quiet ones hide inside successful logins. So I spend the alerting budget accordingly: a cheap threshold rule for brute force, and the real attention on cross-account patterns and anomalies on the logins that worked. The attacker's entire goal is to become a normal successful login. Your detections have to be looking there when he arrives.