JA3 TLS Fingerprinting#
InfraGuard has a built-in JA3 implementation that fingerprints TLS clients by parsing the raw ClientHello message before Python’s ssl module processes the handshake. Automated scanners, bots, and known-bad TLS stacks are detected without any external dependencies.
How It Works#
ClientHello Interception#
The JA3InjectingProtocol wraps uvicorn’s SSL protocol layer. When a TCP connection arrives, InfraGuard reads the raw bytes of the TLS ClientHello before passing them to OpenSSL:
Client ──TLS ClientHello──▶ JA3InjectingProtocol ──▶ ssl module
│
parse ClientHello
compute JA3 hash
store in registryThe JA3 hash is stored in a global registry keyed by (client_ip, client_port) and is available to the filter pipeline when the HTTP request arrives.
JA3 Hash Computation#
The JA3 string is computed from five fields extracted from the ClientHello:
- TLS version - the version advertised in the handshake
- Cipher suites - the ordered list of cipher suite IDs
- Extensions - the ordered list of extension type IDs
- Elliptic curves - supported curve IDs (from the
supported_groupsextension) - EC point formats - supported point format IDs
These fields are joined with commas, and dashes separate values within each field:
769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0The MD5 hash of this string is the JA3 fingerprint.
GREASE Filtering#
Per RFC 8701, TLS clients may insert GREASE (Generate Random Extensions And Sustain Extensibility) values to test server robustness. These values follow a pattern (0x0a0a, 0x1a1a, etc.) and are filtered out before computing the JA3 hash, since they are randomized and would make every fingerprint unique.
Registry#
JA3 hashes are stored in an in-memory registry with a cap of 10,000 entries. Entries are keyed by (ip, port) tuple and are looked up when the HTTP request arrives on the same connection.
Pipeline Filter#
The tls_filter in the pipeline checks the JA3 hash against configured rules:
pipeline:
tls_filter:
enabled: true
block_unknown: false # block connections with no JA3 match (aggressive)
allowed_ja3: [] # allowlist of known-good JA3 hashes
blocked_ja3: [] # additional hashes to block beyond the seed listSeed Blocklist#
The TLS filter ships with a built-in blocklist of JA3 hashes for common scanning tools:
| Tool | Description |
|---|---|
| Masscan | High-speed port scanner |
Python requests | Default Python HTTP client (no browser TLS stack) |
| curl | Command-line HTTP client |
| ZGrab2 | Internet-wide application-layer scanner |
| Nmap | Network scanner |
| Shodan | Search engine for internet-connected devices |
Filter Modes#
| Mode | Behavior |
|---|---|
| Blocklist only | Blocks connections matching known scanner JA3 hashes. Default. |
| Blocklist + allowlist | Blocks scanners AND requires JA3 to match an allowed hash. |
block_unknown: true | Blocks any connection whose JA3 is not in the allowlist. Most restrictive. |
Scoring#
JA3 filter results contribute to the pipeline’s cumulative score:
- Blocked JA3 match: score
1.0(terminal block) - Unknown JA3 with
block_unknown: true: score1.0(terminal block) - Allowed JA3 match: score
0.0(pass)