Enabling Brute Force Detection at the realm, strengthening security headers, protecting sessions and tokens from hijacking, and establishing security auditing routines to keep Keycloak safe in production.

In episode 23 you installed MFA at the authentication door: TOTP, WebAuthn, and recovery codes. Episode 24 completes the defense from a different side — protecting that door itself from brute force attacks, then strengthening the often-forgotten areas: security headers, sessions, tokens, and security audit routines. If episode 22 discussed what gets recorded in logs, this episode discusses how to prevent bad things from happening in the first place.
Brute force is an attack that guesses credentials by trying millions of password combinations. Keycloak provides a built-in mechanism called Brute Force Detection, enabled per realm via Realm Settings → Security Defenses → Brute Force Detection. Once enabled, Keycloak records every failed login attempt and holds back authentication once the count crosses the threshold.
Each failed login raises the counter for that user, and Keycloak also keeps a separate count by source IP. Two kinds of lockout result:
failureFactor is reached; the account is held for waitIncrementSeconds before the next attempt is allowed.maxFailureWaitSeconds, creating a practically permanent lockout effect.Key settings you need to master:
| Setting | Function |
|---|---|
failureFactor | number of failed attempts before the account is temporarily held |
waitIncrementSeconds | hold duration, increasing with each continued attempt |
maxFailureWaitSeconds | upper cap on the hold, creating a prolonged lockout effect |
maxDeltaTimeSeconds | time window during which failed attempts still count |
quickLoginCheckMilliSeconds | gap between attempts considered too fast |
minimumQuickLoginWaitSeconds | extra penalty for super-fast attempts |
failureFactor is the most important threshold: with a value of 5, the account is held temporarily after five failed attempts. waitIncrementSeconds determines the hold duration, and its value rises with each continued failed attempt — until finally maxFailureWaitSeconds creates a prolonged lockout. This combination is what makes the attacker run out of time before managing to guess.
Tip
Start with moderate values: failureFactor 5, waitIncrementSeconds 60, and maxFailureWaitSeconds 900. Values that are too aggressive can lock out your real users when a script mistypes a password — and spawn unnecessary helpdesk tickets.
Keycloak counts failed attempts per user and per source IP. For the per-IP count to be accurate, Keycloak must recognize the client's real IP address. With a reverse proxy in front, what's visible is the proxy IP — unless the X-Forwarded-For header is used:
kc.sh start --proxy-headers xforwardedkc.sh start --proxy-headers xforwarded makes Keycloak trust the headers forwarded by the proxy. Without it, attacks from many IPs are counted as if they came from the same single proxy IP, and per-IP lockout loses its meaning.
For an extra layer, Keycloak has an Insert CAPTCHA step in the browser authentication flow. In the Authentication → Flows menu, copy the browser flow with the Copy button, give it a new name (for example browser captcha), then add the Insert CAPTCHA execution and place it right after the Password step. When a suspicious pattern is detected, humans can pass by typing the code, while bots will stumble.
Keycloak sends several security headers by default. You should still understand what they mean, because a reverse proxy in front can override or add to them:
| Header | Function |
|---|---|
| Content-Security-Policy | limits the resource sources the browser may load |
| X-Frame-Options | prevents Keycloak pages from being framed by other sites (clickjacking) |
| X-Content-Type-Options | prevents the browser from guessing content types (MIME sniffing) |
| Strict-Transport-Security | forces the browser to use HTTPS for this domain |
| X-XSS-Protection | legacy defense against reflected XSS, now obsolete in modern browsers |
If the default policy isn't strict enough, strengthen it at the proxy layer — an NGINX example:
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;An important note: X-XSS-Protection isn't a replacement for CSP. Focus your effort on a correct Content-Security-Policy, not a header many browsers already ignore.
Sessions are the softest target: once a session is hijacked, the attacker becomes the user. Layered defense:
Secure attribute so they don't leak over HTTP, and SameSite to limit cross-site cookie sending.HttpOnly, Secure, and SameSite appropriate to your architecture.state parameter to prove a request comes from the same application; never discard it.Tokens are identity in motion. Protect them in layers:
All the controls above need routine maintenance, not a set-and-forget:
The admin events from episode 22 are the raw material for auditing: combine them with rejected login attempts to see attack patterns earlier.
| Threat | Mitigation |
|---|---|
| Password brute force | Brute Force Detection with a reasonable failureFactor |
| Attacks from many IPs | IP-based blocking based on X-Forwarded-For |
| Automated bots | CAPTCHA on the browser flow |
| Clickjacking | X-Frame-Options and a strict CSP |
| Hijacked session | HttpOnly + Secure + SameSite cookies, HTTPS |
| Stolen token | Revoke endpoint, key rotation, signature verification |
| Unpatched vulnerability | Routine auditing, patching, penetration test |
Episode 24 taught you to close the gaps in the layer closest to the attacker: Brute Force Detection with failureFactor, waitIncrementSeconds, and maxFailureWaitSeconds; IP-based blocking that depends on X-Forwarded-For; CAPTCHA on the browser flow; security headers; session and token security; and auditing routines that keep everything fresh.
Key takeaways:
maxFailureWaitSeconds makes attacks uneconomical.--proxy-headers xforwarded, all per-IP protections go blind.In the next episode (episode 25), you move from protecting the entrance to governing what users may do inside: fine-grained authorization with Keycloak's Authorization Services — deciding access per resource, not just per role.