unmask

docs

JA4 acquisition, LB / CDN setup, supported distros, FAQ.

A JA4 fingerprint is a client "fingerprint" extracted from the TLS handshake (ClientHello). It is unique to each attack tool and does not change even when the IP / country / VPN / User-Agent is switched.

However, whether JA4 can be captured depends on "who receives the TLS handshake first (where TLS is terminated)". This page lists the feasibility for each representative setup pattern.

Conclusion: it depends on where TLS is terminated

[Client] ──TLS handshake──▶ [ only whoever saw the ClientHello can compute JA4 ]

Computing JA4 requires the raw ClientHello byte sequence. A process that only sees the plaintext HTTP after TLS decryption cannot reconstruct JA4 afterward.

JA4 feasibility by setup pattern

Computing JA4 needs the raw ClientHello bytes, so feasibility is decided solely by where TLS is terminated (= who sees the client's ClientHello first) and unmask's mode (native / forward-auth). The backend app is irrelevant.

= node where unmask is installed   = TLS termination (where the ClientHello is seen)

Setup (client → …) JA4 unmask config
1. Front LB (co-located with unmask) + backend app — unmask terminates TLS
★●LB nginx (native) → httpdyesnone (plugin computes it)
★●LB nginx (forward-auth) → httpdno— nothing parses the ClientHello
★●LB apache (forward-auth) → httpdno
2. App-direct — unmask terminates TLS
★●nginx (native)yesnone
★●nginx (forward-auth)no
★●apache (forward-auth)no
3. Front CDN / LB terminates TLS (Cloudflare / GCP HTTPS LB / AWS ALB·CloudFront, …) + backend app
★CDN → ●LB nginx (native) → httpdyesselect under “trusted LB / CDN”
★CDN → ●LB nginx (forward-auth) → httpdyesselect under “trusted LB / CDN”
★CDN → ●LB apache (forward-auth) → httpdyesselect under “trusted LB / CDN”
4. Front CDN / LB terminates TLS + unmask direct
★CDN → ●nginx (native)yesselect under “trusted LB / CDN”
★CDN → ●nginx (forward-auth)yesselect under “trusted LB / CDN”
★CDN → ●apache (forward-auth)yesselect under “trusted LB / CDN”
  • native = nginx + unmask-plugin-nginx. The C module parses the ClientHello directly and computes JA4 (fastest, no subrequest, runs even on old distros like CentOS 6). Use this if you want JA4.
  • forward-auth = no plugin; verdicts delegated via auth_request / Apache mod_lua. It doesn't see TLS itself, so JA4 is empty unless the front CDN / LB forwards it (runs on UA / IP / rate / honeypot).
  • Apache has no native plugin (2.4's mod_ssl exposes no JA4 metadata; 2.6+ exposes metadata but unmask doesn't use it yet). For JA4 on Apache, the only path is forwarding from a front CDN / LB.
  • For forward-auth in rows 3–4, pick your front CDN / LB under “trusted LB / CDN” (same as native). unmask then renders a gate that adopts X-Client-JA4 only from that LB's IPs, and the shipped server.inc forwards the gated value automatically — no toggle, no manual wiring. See LB / CDN setup.

HTTP/3 (QUIC) — the q prefix

The first letter of a JA4 fingerprint names the transport the ClientHello arrived over: t = TLS over TCP, q = QUIC. The nginx plugin computes JA4 for HTTP/3 the same way it does for TCP — when nginx is built with --with-http_v3_module and listens on a listen … quic port, the QUIC ClientHello passes through the same TLS-library callback the module already hooks, so no extra configuration is needed. Verified against both TLS backends nginx HTTP/3 builds commonly use: the OpenSSL 3.5+ native QUIC API and the quictls patch set.

TCP  (HTTP/1.1, HTTP/2)  →  t13i3514h2_bfa337485184_3ad6d895dd8c
QUIC (HTTP/3)            →  q13i0306h3_55b375c5d22e_1cecd519fee8
  • The same client presents a different string over HTTP/3 than over TCP — the transport letter, the ALPN and the hello contents all change. When writing a custom JA4 rule for a site that serves HTTP/3, match both forms.
  • unmask's cookie rebinding already treats an HTTP/2 ↔ HTTP/3 switch by the same visitor as normal — it does not count as a fingerprint mismatch.
  • Behind a front CDN / LB that terminates HTTP/3 and forwards JA4, the forwarded value arrives with a q prefix; the trusted-LB gate passes it through unchanged.

What happens if JA4 cannot be captured?

Even with an empty JA4, unmask still works. Bot detection continues with the following signals:

  • User-Agent classification — identifies known bots via a crawler / bot UA list of 600+ patterns
  • Official IP ranges — registers official ranges (Googlebot / Bingbot etc.) on the bypass list to avoid ranking accidents. Vendors that publish ranges are verified by IP, not UA, so a spoofed UA is caught (crawlers without a range pass on UA)
  • rate-limit — escalates to a challenge when the per-IP × path threshold is exceeded
  • honeypot — bans clients that hit registered trap paths (e.g. CMS paths your site doesn't run)
  • behavioral CAPTCHA — a human-likeness score across 5 axes: mouseTrail / scroll / keyboard etc.
What you lose without JA4 is detection accuracy for stealth bots that impersonate legitimate browsers. A bot that cleanly fakes both its UA and IP can only be spotted by its TLS quirks (JA4). Conversely, as long as you can capture JA4, you can track the same tool through IP rotation and UA spoofing.

Summary — which setup to choose

  • Want to use JA4 fully → nginx + unmask-plugin-nginx (native mode). nginx must be terminating TLS
  • Have a front CDN / LB → extract JA4 there → X-Client-JA4 forward
  • Apache / plugin not possible → forward-auth mode. Run without JA4 using UA / IP / rate / honeypot

For install steps, see the install guide.