Olympus Docs
CookbookDefensive security

Bot detection strategies

Tell humans from bots without harming UX

Bot traffic on auth endpoints: credential stuffing, account creation, password resets for reconnaissance. Detect and mitigate without making real users click 15 traffic-light CAPTCHAs.

Signal layers

Layer 1: Rate-based

Just rate limiting catches the dumbest bots:

@login_post path /self-service/login method POST
rate_limit @login_post {
  zone login-burst
  events 5
  window 1m
  key {http.request.remote_host}
}

Effective against single-IP brute-force.

Layer 2: Behavioral

Bots tend to:

  • Fill forms in <100ms (humans take >1s).
  • Have no mouse movement (events).
  • Tab order skipping (filling fields out of order).

Capture via JS:

window.addEventListener("DOMContentLoaded", () => {
  const start = Date.now();
  document.querySelector("form").addEventListener("submit", (e) => {
    const fillTime = Date.now() - start;
    e.target.dataset.fillTime = fillTime;
  });
});

Server-side: if fill_time < 500, treat as suspicious.

Layer 3: Honeypot fields

Add an invisible field. Real users won't fill it; bots will:

<input type="text" name="email_confirm" tabindex="-1" autocomplete="off" 
       style="position: absolute; left: -9999px;">

Server-side:

if (req.body.email_confirm) {
  return res.status(400).json({ error: "spam_detected" });
}

Catches indiscriminate form-fillers. Sophisticated bots ignore.

Layer 4: Risk scoring

Combine signals into a score:

let score = 0;
if (fillTime < 500) score += 30;
if (!hasMouseEvents) score += 20;
if (newIpAddress) score += 10;
if (knownBadAsn(ip)) score += 40;
if (failedLastNAttempts(ip) >= 5) score += 30;

if (score >= 50) showCaptcha = true;
if (score >= 80) reject();

Tune thresholds against your own data.

Layer 5: CAPTCHA

Only triggered when risk score is high. Don't show on every signup.

{showCaptcha && <Turnstile sitekey={SITE_KEY} onVerify={setToken} />}

Cloudflare Turnstile is the user-friendliest. Doesn't ask user to do anything most of the time.

Layer 6: ML-based services

Cloudflare Bot Management, DataDome, hCaptcha Enterprise, etc. They handle the layers above and more sophisticated stuff:

  • TLS fingerprinting (different from browser fingerprinting).
  • Browser env consistency checks.
  • Real-time IP reputation.

Cost: ~$200-500/mo at moderate scale.

What to do with a detected bot

Soft response: rate limit aggressively

if (isBotSuspect) {
  await rateLimiter.add(ip, "bot", "1/min");
}

Bot retries but is throttled to crawl pace.

Medium response: shadow ban

Accept the registration but flag the identity. Don't actually log them in or send emails:

if (isBot) {
  await kratos.adminCreateIdentity({
    traits,
    metadata_admin: { shadow_banned: true, reason: "bot_detected" },
    state: "inactive",
  });
  // Return success but DON'T sign in
  return Response.json({ session: null });
}

Bot doesn't realize it failed; retries less.

Hard response: reject

if (isBot) return Response.json({ error: "blocked" }, 403);

Bot reads 403 and either gives up or rotates IPs.

Common bot patterns to detect

Credential stuffing

  • Many different emails, each tried once with a password.
  • Source IPs widely distributed (residential proxies).
  • High failure rate.

Mitigate:

  • Have I Been Pwned check on password.
  • CAPTCHA on >3 failed attempts per IP/per email.
  • Account lockout after N attempts (per identity).

Account farming

  • Many account creations in short time.
  • Similar email patterns (alex234@gmail.com, alex235@gmail.com).
  • Disposable email domains.

Mitigate:

  • Email-domain reputation.
  • Disposable-domain block list.
  • Behavioral: time to fill form.

Account enumeration

  • Probe for which emails exist.
  • Differentiated responses (timing, error messages).

Mitigate:

  • Constant-time login responses (Olympus does this).
  • Generic error messages ("Invalid email or password", not "no such user").
  • Rate limit probing.

Don't over-block

False positives cost users. Conservatively:

  • Tier 1 (block): only with very high confidence.
  • Tier 2 (CAPTCHA): for medium-confidence.
  • Tier 3 (allow with logging): for low-confidence.

Monitor:

  • Block rate per cohort (real users shouldn't see CAPTCHA on first try).
  • Customer complaints (someone said "I can't sign up?").

Logging

Log every bot detection so you can analyze:

await db.insert(botEvents).values({
  ip: ip,
  user_agent: ua,
  score: score,
  signals: { fillTime, honeypot, hasMouseEvents },
  action: result,  // "allow" | "captcha" | "block"
  endpoint: req.path,
});

Weekly review:

  • Which IPs are repeat offenders?
  • Are real users being blocked?
  • New attack patterns to detect?

On this page