This Nginx module implements JA4, JA4H, JA4TCP, and JA4one fingerprinting standards for HTTP and TLS clients. It enables you to identify and fingerprint incoming requests based on their TLS handshake (Client Hello), HTTP headers, and TCP characteristics.
This module is designed for:
- Security: Detecting bots, scrapers, and malicious tools.
- Access Control: Blocking or allowing requests based on fingerprints.
- Analytics: Logging client types for traffic analysis.
Important: This implementation uses Full SHA256 Hashes (64 characters) for the fingerprint components instead of the standard truncated 12-character hashes defined in the original JA4 spec.
- Why? To provide maximum collision resistance and complete cryptographic detail as per user requirements.
- Result: Fingerprints generated by this module will be significantly longer than standard JA4 strings.
- JA4 (TLS): Fingerprints the TLS Client Hello (Version, Cipher Suites, Extensions, etc.).
- JA4H (HTTP): Fingerprints HTTP headers (Method, Version, Cookie, Header Order).
- JA4S (HTTP/2): Fingerprints HTTP/2 connection state (Window Size, Frame Size).
- JA4TCP (TCP): Fingerprints TCP characteristics (Window Size, Options, Flags, TTL).
- JA4one (Composite): A combined fingerprint (
JA4_JA4H) providing a holistic view of the client (TLS + HTTP). - Access Control Directives: Native Nginx directives to block/allow traffic.
- Variables: Exposes fingerprints as Nginx variables for logging or Lua scripting.
The following benchmark demonstrates the impact of the JA4 module on Nginx performance. Conclusion: The module is highly optimized and introduces negligible or no measurable overhead in typical scenarios. In some test runs, the module path may even appear faster due to system variance, proving it does not block or significantly slow down request processing.
Test Environment:
- Connections: 100 concurrent
- Duration: 30s
- Threads: 4
| Metric | Meaning (Goal) | Without Module (Baseline) | With Module | Performance Delta | Interpretation |
|---|---|---|---|---|---|
| Requests/sec | Capacity (Higher is Better) | 12,817 | 13,321 | +3.93% | ✅ No Degradation |
| Avg Latency | Speed (Lower is Better) | 9.34ms | 8.80ms | -0.54ms | ✅ No Degradation |
| p99 Latency | Slowest 1% (Lower is Better) | 24.33ms | 22.45ms | -1.88ms | ✅ No Degradation |
Note: Benchmarks run on a virtualized environment; minor variance (<5%) is expected.
- Nginx source code (tested with 1.27.3+)
- OpenSSL (development headers)
Configure Nginx with the --add-module flag pointing to this directory:
./configure --with-compat --add-module=/path/to/ja4-nginx-module --with-http_ssl_module
make
make installThe module exposes the following variables:
$http_ssl_ja4: The TLS fingerprint.$http_ssl_ja4h: The HTTP fingerprint.$http_ssl_ja4s: The HTTP/2 fingerprint (JA4S).$http_ssl_ja4tcp: The TCP fingerprint.$http_ssl_ja4one: The composite fingerprint.
Add them to your log_format:
http {
log_format ja4_log '$remote_addr - [$time_local] "$request" '
'JA4="$http_ssl_ja4" JA4H="$http_ssl_ja4h" '
'JA4S="$http_ssl_ja4s" JA4TCP="$http_ssl_ja4tcp" JA4one="$http_ssl_ja4one"';
access_log logs/ja4.log ja4_log;
}You can enforce rules in your server or location blocks.
Directives:
ja4_deny/ja4_allow: Check against JA4 (TLS) fingerprint.ja4h_deny/ja4h_allow: Check against JA4H (HTTP) fingerprint.ja4s_deny/ja4s_allow: Check against JA4S (HTTP/2) fingerprint.ja4tcp_deny/ja4tcp_allow: Check against JA4TCP (TCP) fingerprint.ja4one_deny/ja4one_allow: Check against JA4one composite fingerprint.
Example:
server {
listen 443 ssl;
# Enable JA4 module (optional, enabled implicitly by directives/variables)
ja4 on;
# Block a specific curl client via JA4H
ja4h_deny "ge11n03_fe444ad14866d725a8e22320d4ed56810819b0fae0efae0c09bedfdd";
# Block suspicious TCP fingerprints (example pattern)
ja4tcp_deny "29200_2-4-8-1-3_1460_7"; # Python-like small window
# Block specific HTTP/2 fingerprint (JA4S)
ja4s_deny "h204_0000000000000000000000000000000000000000000000000000000000000000_0_xxxx_0";
location / {
# Allow specific bot via JA4one
ja4one_allow "t13d3012_..._ge11n03_...";
# Add headers for debugging
add_header X-JA4-Fingerprint $http_ssl_ja4;
add_header X-JA4H-Fingerprint $http_ssl_ja4h;
add_header X-JA4TCP-Fingerprint $http_ssl_ja4tcp;
add_header X-JA4one-Fingerprint $http_ssl_ja4one;
add_header X-JA4S-Fingerprint $http_ssl_ja4s;
}
}Due to the full hash modification, the formats are:
- JA4:
t13d1516h2_<64-char-cipher-hash>_<64-char-extension-hash> - JA4H:
ge11n03_<64-char-header-hash>ge: GET Method11: HTTP 1.1n: No Cookie03: 3 Headers
- JA4S:
h2<ver>_<cnt>_<hash>_<window>_0- Example:
h220_02_70946d4d1524_10485760_0 ver: Protocol version (20 = HTTP/2.0)cnt: Count of settings (02 = constant in No-Patch mode)hash: SHA256 of Window & Frame Sizewindow: Initial Window Size
- Example:
- JA4TCP:
<window>_<option_kinds>_<mss>_<scale>- Example:
65535_2-4-8-3_1460_7 window: TCP window size (decimal)option_kinds: TCP option kinds in order (decimal, dash-separated, NOPs included)mss: Maximum Segment Size (decimal)scale: Window scale factor (decimal)
- Example:
- JA4one:
[JA4]_[JA4H](Concatenated)
Identify and block common bot fingerprints:
server {
listen 443 ssl;
server_name example.com;
# Block known bot JA4 fingerprints
ja4_deny "t13d1517h2_8daaf6152771...";
ja4_deny "t12d2917h2_7af8b3c9...";
# Block bot HTTP patterns
ja4h_deny "ge11n00_3b5f7...";
location / {
return 200 "OK";
}
}Create a whitelist of known good browsers:
server {
listen 443 ssl;
server_name secure-api.example.com;
# Allow Chrome
ja4_allow "t13d3016h2_8daaf6152771...";
# Allow Firefox
ja4_allow "t13d2217h2_a6bf3e8f...";
# Allow Safari
ja4_allow "t13d1516h2_9fe2a8c3...";
# Deny all other fingerprints
ja4_deny "all";
location /api/ {
proxy_pass http://backend;
}
}Combine TLS, HTTP, and TCP fingerprinting for maximum security:
server {
listen 443 ssl;
server_name api.example.com;
# Block suspicious TCP patterns (typical of bots/scanners)
ja4tcp_deny "1024_2-4-8_1460_0"; # Unusual small window
ja4tcp_deny "29200_2-4-8-1-3_1460_7"; # Python requests pattern
# Block known malicious JA4H patterns
ja4h_deny "po11n00_8b3f...";
location /sensitive/ {
# Additional validation using JA4one
ja4one_deny "t13d1515h2_..._ge10n00_...";
# Add all fingerprints to response headers for monitoring
add_header X-JA4 $http_ssl_ja4 always;
add_header X-JA4H $http_ssl_ja4h always;
add_header X-JA4TCP $http_ssl_ja4tcp always;
add_header X-JA4one $http_ssl_ja4one always;
proxy_pass http://sensitive-backend;
}
}Log all fingerprints for traffic analysis:
http {
# Define comprehensive logging format
log_format fingerprint_analytics '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'JA4="$http_ssl_ja4" '
'JA4H="$http_ssl_ja4h" '
'JA4TCP="$http_ssl_ja4tcp" '
'JA4one="$http_ssl_ja4one"';
server {
listen 443 ssl;
server_name analytics.example.com;
# Log to special file
access_log /var/log/nginx/fingerprints.log fingerprint_analytics;
location / {
return 200 "Logged";
}
}
}Use fingerprints with Lua/OpenResty for advanced rate limiting:
http {
lua_shared_dict fingerprint_limit 10m;
server {
listen 443 ssl;
location /api/ {
access_by_lua_block {
local limit = require "resty.limit.req"
local fingerprint = ngx.var.http_ssl_ja4one or "unknown"
-- Different limits based on fingerprint
local lim, err = limit.new("fingerprint_limit", 10, 5)
if not lim then
ngx.log(ngx.ERR, "failed to instantiate limit.req: ", err)
return ngx.exit(500)
end
local key = fingerprint
local delay, err = lim:incoming(key, true)
if not delay then
if err == "rejected" then
return ngx.exit(429)
end
return ngx.exit(500)
end
}
proxy_pass http://backend;
}
}
}Different behaviors for different client types:
map $http_ssl_ja4tcp $client_type {
default "unknown";
"~^65535_" "modern_client";
"~^1024_" "legacy_client";
"~^8192_" "suspicious";
}
server {
listen 443 ssl;
location / {
# Redirect suspicious clients to captcha
if ($client_type = "suspicious") {
return 302 /captcha;
}
# Limit features for legacy clients
if ($client_type = "legacy_client") {
return 301 /legacy-version;
}
# Full access for modern clients
proxy_pass http://backend;
}
location /captcha {
return 200 "Please verify you are human";
}
}- Missing JA4? Ensure you are connecting via HTTPS. Plain HTTP requests have no TLS handshake, so
$http_ssl_ja4will be empty. - Missing JA4S? Only available for HTTP/2 connections. Check if client supports H2 and NGINX
http2is enabled. - Missing JA4TCP? JA4TCP requires access to raw TCP connection data. Ensure NGINX is running with
--with-http_realip_moduleand proper network configuration. If using Docker, use--network hostmode for accurate TCP fingerprinting. - Short Codes? If you see 12-char hashes, you are running the standard version. Recompile with the modified generic hash length if full hashes are required.