Skip to main content

Stop bots and credential stuffing

Someone is driving your login form with a script. The requests are well-formed, the user agent says Chrome, the addresses rotate through a residential proxy pool, and rate limiting alone either misses them or starts catching real users.

The reason this is hard with conventional tooling is that every field a bot controls, it lies about. What it cannot easily change is how its TLS and TCP stack behaves — and that is what Synapse matches on.

What identifies the client

JA4+ fingerprints the connection itself: the TLS ClientHello, the TCP options and timing, the HTTP header order. A Go program, a headless Chrome, a curl build and a real browser produce visibly different fingerprints even when every header is identical.

That gives you a handle that survives address rotation, because the fingerprint travels with the client, not with its IP. Acting on it is the Smart Firewall: a fingerprint match installs a kernel block, so a scanner that changes address every request is still stopped by the thing that does not change.

The layers, cheapest first

LayerDecides onCost
Access rulesAddress, ASN, country — pre-resolved on the platform sideKernel drop, before routing
Threat detectionKnown-bad addresses and fingerprints from the feedKernel drop
ML modelsIs this fingerprint malicious, and how seriousOn-host inference
Smart FirewallThe fingerprint itself — block by who the client isKernel, after evaluation
WAFThe request itself — path, method, headers, bodyAfter decryption
WAF rate_limitHow often this client is askingAfter decryption
WAF challengeA CAPTCHA, when you are unsureFull round trip

The fingerprint classifier is the piece that does the work here. It answers is this client's JA4+ fingerprint malicious with a probability, and the reported accuracy is high — but read the section on using the output responsibly before you wire it straight to a block.

Turning it on

Fingerprinting runs in both modes; the classifier needs an -ml build, and CAPTCHA and the WAF need proxy mode.

# /etc/synapse/config.yaml
mode: "proxy"

classifier:
enabled: true

proxy:
captcha:
provider: "turnstile" # hcaptcha, recaptcha, turnstile, prosopo

Then express the policy as a WAF rule. The scheme exposes threat intelligence alongside the request, so a rule can combine both:

threat.score > 80 and threat.advice == "block"

Full field list and the operators available are on the WAF page.

Rate limiting is a WAF action rather than a separate subsystem, which matters here: the same rule that identifies the client decides what happens to it. A credential-stuffing rule usually wants rate_limit rather than block — the login endpoint is legitimate, it is the rate that is not.

Counters are per rule and per client address, with three settings on the rule: requests allowed per period seconds, then duration seconds at 429 once the threshold is crossed. duration is the penalty, not the window.

See it happen first

The playground has a Brute Force scenario, plus Botnet Traffic, Tor Exit Node and VPN/Proxy Traffic — four ways an automated client arrives, and which layer catches each. Send the Clean Request first so you know what a pass looks like.

Where this stops working

  • A fingerprint is not an identity. Every user of a given browser build shares one. Blocking a fingerprint outright blocks a population, which is why the classifier returns a probability and why a challenge is often the better response than a drop.
  • Shared egress addresses carry shared reputation. A CGNAT or corporate NAT address represents many people. Prefer the fingerprint, or a combination of signals, before blocking an address outright.
  • A determined adversary can mimic a fingerprint. This raises the cost of automation; it does not make it impossible.
  • Rate limiting behind a load balancer counts the wrong address. The bucket keys on the L4 peer, so an LB that rotates source addresses scatters one attacker across buckets that never reach the threshold, and an LB that collapses clients onto a few addresses makes real users limit each other. Set proxy.trusted_proxies before relying on a rate-limit rule in that topology.
  • The agent sees less than the proxy. Without terminating TLS you get the client-side fingerprints but not the request, so no WAF and no CAPTCHA.

See also