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
| Layer | Decides on | Cost |
|---|---|---|
| Access rules | Address, ASN, country — pre-resolved on the platform side | Kernel drop, before routing |
| Threat detection | Known-bad addresses and fingerprints from the feed | Kernel drop |
| ML models | Is this fingerprint malicious, and how serious | On-host inference |
| Smart Firewall | The fingerprint itself — block by who the client is | Kernel, after evaluation |
| WAF | The request itself — path, method, headers, body | After decryption |
WAF rate_limit | How often this client is asking | After decryption |
WAF challenge | A CAPTCHA, when you are unsure | Full 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.
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_proxiesbefore 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
- JA4+ — what each fingerprint captures
- Machine Learning Models — the classifier and its limits
- WAF — the expression language and its fields
- Live Traffic View — watch fingerprints arrive while you tune