You know the drill: Your nginx log shows a request from “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36”, a perfectly normal Chrome browser. Or is it? Spoiler: It’s a Python script trying to brute-force your /wp-login.php.
User-Agents are like business cards. Anyone can write whatever they want on them. But what if there was a way to identify clients by something they can’t easily fake?
Welcome to the world of TLS Fingerprinting.
What is TLS Fingerprinting?
“Show me your ClientHello, and I’ll tell you who you are”
When a client establishes an HTTPS connection, the first thing it sends is a TLS ClientHello. It’s basically saying “Hello, I’d like to communicate securely”, but with a ton of technical details:
| Parameter | What it reveals | Example |
|---|---|---|
| TLS Version | Supported protocol versions | TLSv1.2, TLSv1.3 |
| Cipher Suites | Which encryption the client supports | TLS_AES_128_GCM_SHA256 |
| Extensions | Additional features | SNI, ALPN, supported_groups |
| Elliptic Curves | Supported curves for ECDHE | secp256r1, x25519 |
| Signature Algorithms | How certificates are verified | RSA-PSS, ECDSA |
This combination is like a digital fingerprint. Different clients (browsers, bots, scripts) have different implementations and therefore different fingerprints.
Pro tip: The fingerprint is generated before encryption. You can see it without decrypting the TLS session.
JA3: The De-Facto Standard
“Invented by Salesforce, used by everyone”
JA3 (named after its creators John Althouse, Jeff Atkinson, and Josh Atkins) is the most popular method for hashing TLS fingerprints:
JA3 = MD5(
TLSVersion,
Ciphers,
Extensions,
EllipticCurves,
EllipticCurvePointFormats
)
The result is a 32-character MD5 hash like:
cd08e31494f9531f560d64c695473da9 → Chrome 114 (Windows)
b4da7f95e46bf2cf5285fd609cf726e4 → Googlebot
c199b43d41b470f8f68c5561f8f1ce3e → PetalBot
0149f47eabf9a20d0893e2a44e5a6323 → AhrefsBot
Fun fact: There’s also JA3S, the counterpart for server responses. You can use it to identify malware C2 servers.
Why is This Useful?
“Your script can lie, your TLS library can’t”
Use Case 1: Bot Detection
| Scenario | User-Agent says | JA3 says |
|---|---|---|
| Python requests | “Chrome 120” | cd09e31... (Python/urllib3) |
| curl | “Mozilla/5.0” | 473cd7c... (curl/OpenSSL) |
| Headless Chrome | “Chrome 120” | cd08e31... (real Chrome ✓) |
| Go http.Client | “Chrome 120” | ecdf4f4... (Go TLS Stack) |
Use Case 2: Client Categorization
You can sort clients into categories:
map $http_ssl_ja3_hash $ja3_client_type {
default "unknown";
1. Browsers
"cd08e31494f9531f560d64c695473da9" "browser"; # Chrome Windows
"68b3ecfaf0034bb9fcbecd518b5ab8d4" "browser"; # Chrome Android
"ecdf4f49dd59effc439639da29186671" "browser"; # Safari iOS
1. Known bots (allowed)
"b4da7f95e46bf2cf5285fd609cf726e4" "bot"; # Googlebot
"c199b43d41b470f8f68c5561f8f1ce3e" "bot"; # PetalBot
"0149f47eabf9a20d0893e2a44e5a6323" "bot"; # AhrefsBot
1. Malicious
"6734f37431670b3ab4292b8f60f29984" "malicious"; # Metasploit
"e7d705a3286e19ea42f587b344ee6865" "malicious"; # Cobalt Strike
}
Use Case 3: Blocking Without Collateral Damage
1. Block bots on login pages
location = /wp-login.php {
if ($ja3_client_type = "bot") {
return 403;
}
1. ...
}
Implementation with nginx
“Because stock nginx can’t do this”
For JA3 support in nginx, you need a patched nginx build. The project phuslu/nginx-ssl-fingerprint does exactly that:
Dockerfile (simplified)
FROM ubuntu:22.04
1. Patch OpenSSL for JA3 extraction
RUN git clone https://github.com/nickvdp/openssl.git && \
cd openssl && \
./config && make && make install
1. Build nginx with ssl_ja3 module
RUN git clone https://github.com/nickvdp/nginx.git && \
cd nginx && \
./auto/configure \
--with-http_ssl_module \
--with-http_v2_module \
--add-module=/path/to/nginx-ssl-fingerprint && \
make && make install
nginx.conf
1. Log format with JA3
log_format ja3_json escape=json '{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"request":"$request",'
'"status":$status,'
'"ja3_hash":"$http_ssl_ja3_hash",'
'"ja3_client_type":"$ja3_client_type",'
'"http_user_agent":"$http_user_agent"'
'}';
1. Include JA3 map
map_hash_max_size 4096;
include /etc/nginx/ja3/ja3_fingerprints.map;
server {
listen 443 ssl http2;
1. Forward JA3 as header to backend
proxy_set_header X-JA3-Hash $http_ssl_ja3_hash;
proxy_set_header X-JA3-Client-Type $ja3_client_type;
access_log /var/log/nginx/access.log ja3_json;
}
Limitations
“Nothing is perfect, not even fingerprints”
1. HTTP/3 (QUIC) = No JA3
This is the big catch. JA3 is based on the TLS handshake, but HTTP/3 uses QUIC, which has TLS 1.3 embedded. The handshake looks completely different.
1. This makes JA3 useless:
listen 443 quic; # HTTP/3
listen 443 ssl; # HTTP/2 + HTTP/1.1 (JA3 works here)
| Protocol | JA3 works? |
|---|---|
| HTTP/1.1 over TLS | ✅ Yes |
| HTTP/2 over TLS | ✅ Yes |
| HTTP/3 (QUIC) | ❌ No |
Pro tip: If you have HTTP/3 enabled, approximately 80 to 90 percent of your requests use QUIC and have no JA3 fingerprint.
2. Fingerprint Spoofing
Clever attackers can configure their TLS stack to look like Chrome. Tools like uTLS make this trivial:
// Go script with Chrome fingerprint
config := &utls.Config{
ServerName: "example.com",
}
conn := utls.UClient(rawConn, config, utls.HelloChrome_Auto)
Therefore: JA3 is an additional factor, not a silver bullet. Combine it with:
- IP reputation
- Rate limiting
- Behavioral analysis
- Request pattern matching
3. Legitimate Clients with Same Fingerprint
The fingerprint 68b3ecfaf0034bb9fcbecd518b5ab8d4 is used by:
- Real Chrome 116 Mobile users
- Azure based vulnerability scanners
You can’t block this hash without hitting real users.
Quick Reference Card
┌─────────────────────────────────────────────────────────────┐
│ TLS FINGERPRINTING │
├─────────────────────────────────────────────────────────────┤
│ │
│ JA3 = MD5(Version + Ciphers + Extensions + Curves) │
│ │
│ WORKS: HTTP/1.1, HTTP/2 over TLS │
│ DOESN'T WORK: HTTP/3 (QUIC) │
│ │
│ USE CASES: │
│ ✓ Bot detection (Python, curl, Go) │
│ ✓ Crawler categorization (Google, Bing, SEO) │
│ ✓ Malware detection (Metasploit, Cobalt Strike) │
│ ✓ Additional auth factor │
│ │
│ LIMITATIONS: │
│ ✗ HTTP/3 = no fingerprints │
│ ✗ Fingerprint spoofing possible (uTLS) │
│ ✗ Collisions (same FP, different clients) │
│ │
│ PROJECTS: │
│ • github.com/phuslu/nginx-ssl-fingerprint │
│ • github.com/salesforce/ja3 │
│ │
└─────────────────────────────────────────────────────────────┘
Appendix: Known Fingerprints
| JA3 Hash | Client | Type | Threat |
|---|---|---|---|
cd08e31494f9531f560d64c695473da9 |
Chrome 114 (Windows) | Browser | 🟢 |
68b3ecfaf0034bb9fcbecd518b5ab8d4 |
Chrome 116 (Android) | Browser | 🟢 |
ecdf4f49dd59effc439639da29186671 |
Safari iOS 18 | Browser | 🟢 |
b4da7f95e46bf2cf5285fd609cf726e4 |
Googlebot | Crawler | 🟢 |
a1180b5557791f9d36d36739d0d9b08a |
Bingbot | Crawler | 🟢 |
c199b43d41b470f8f68c5561f8f1ce3e |
PetalBot | Crawler | 🟡 |
0149f47eabf9a20d0893e2a44e5a6323 |
AhrefsBot | Crawler | 🟡 |
e4228d1fa6f13a432e4bc5ff54841b91 |
SemrushBot | Crawler | 🟡 |
473cd7cb9faa642487833865d516e578 |
curl/OpenSSL | Tool | 🟠 |
6734f37431670b3ab4292b8f60f29984 |
Metasploit | Malicious | 🔴 |
e7d705a3286e19ea42f587b344ee6865 |
Cobalt Strike | Malicious | 🔴 |
a0e9f5d64349fb13191bc781f81f42e1 |
Sliver C2 | Malicious | 🔴 |
Legend:
- 🟢 Trusted: allow through
- 🟡 Known: log, consider rate limiting
- 🟠 Suspicious: monitor closely
- 🔴 Malicious: block immediately
Conclusion
TLS Fingerprinting isn’t a replacement for other security measures, but it’s a powerful additional signal. It tells you which TLS library a client is really using, regardless of what the User-Agent claims.
Key Takeaways:
- JA3 is the standard for TLS fingerprinting
- Only works for HTTP/1.1 and HTTP/2 (not HTTP/3)
- Combine it with IP blocking and rate limiting
- Maintain a whitelist for legitimate crawlers (Googlebot, Bingbot, etc.)
- Expect fingerprint spoofing in targeted attacks
And remember: If someone configures their TLS stack to look like Chrome just to attack your /xmlrpc.php, that’s kind of a compliment to your security.
Stay fingerprinted, stay curious. 🔐
Further Reading:
- Salesforce JA3 Repository
- nginx-ssl-fingerprint
- uTLS: TLS Fingerprint Spoofing Library
Leave a Reply