Skip to main content

TLS fingerprinting with JA3 and JA4

TLS fingerprinting identifies clients by the parameters exchanged during the TLS handshake rather than by IP address or the User-Agent header. These parameters — cipher suites, protocol version, and extensions — reflect the client's underlying network stack. Because they remain consistent across IP changes and are difficult to randomize at scale, they're effective for identifying botnets that rotate addresses or imitate browsers.

WAAP supports two fingerprinting algorithms:

JA3JA4
AlgorithmMD5Truncated SHA-256
Ordering sensitivitySensitive — values can differ between sessionsStable — sorts fields before hashing
Output format32-character hex stringThree-part string (prefix_hex12_hex12)
Example valuee2925c27149b0d0dc34373d55040dde1t13d1516h2_8daaf6152771_b1ff8ab2d16f

JA3 dominates threat intelligence feeds and external security tools, making it the right choice when cross-referencing fingerprints across systems. JA4 is better suited for modern TLS clients — it normalizes handshake fields that browsers may reorder between sessions, so fingerprints stay consistent.

For every incoming request, WAAP computes and logs both the JA3 and JA4 fingerprints automatically. No configuration is required.

View events by fingerprint

Every event logged by WAAP includes both the JA3 and JA4 fingerprints of the originating request — visible in the event list and in the request details panel. Filter by hash to isolate all traffic from a specific client library:

  1. In the CDB Technical Web Portal, navigate to WAAP > Events.
  2. In the filter bar, click the filter icon and select JA3.
  3. Enter the 32-character hex hash and click Apply.
Events page with the JA3 filter selected in the filter bar

Create an Advanced Rule with a fingerprint condition

Advanced Rules evaluate fingerprint values against conditions and apply an action — block, allow, or rate-limit — to matching requests. A single rule matching a specific fingerprint covers an entire botnet regardless of how many IP addresses it uses.

After identifying a suspicious fingerprint in the event log, use it directly as a rule condition:

  1. In the Customer Portal, navigate to WAAP > Custom Rules.
  2. Click Add custom rule and select Advanced Rule as the rule type.
  3. In the Define rule section, set the object to request and the attribute to ja3 or ja4.
  4. Enter the fingerprint value and set the desired action, then click Save.
Advanced rule builder with the request object and ja4 attribute selected, showing the attribute dropdown with ja3 and ja4 options visible

The rule builder generates the CEL expression automatically. To match multiple fingerprints, click + to add conditions, or click Edit manually to write a combined expression:

request.ja3 == 'e2925c27149b0d0dc34373d55040dde1' or
request.ja3 == '3e9b20610098b6c9bff953856e58016a' or
request.ja3 == '7d671906ed4a1edac3262a54676dacfa'

Malicious TLS Fingerprint tag

Explicit fingerprint matching isn't the only option.

WAAP automatically assigns the malicioustlsfp predefined tag to requests whose JA3 fingerprint consistently shows malicious behavior across CDB's traffic. A tag-based rule targeting this tag applies an action without requiring knowledge of a specific hash value:

tags.exists('malicioustlsfp')

When a fingerprint hasn't yet crossed the behavioral threshold, an explicit request.ja3 or request.ja4 condition in an Advanced Rule is the fallback — as the incident below illustrates.

Incident example

A gaming service received approximately 1.5 billion requests from around 2,200 source IPs. The attack used only five User-Agent strings and mimicked legitimate browser behavior, making blocking by IP or User-Agent impractical.

Analysis showed that 99% of the attack traffic shared a single JA3 fingerprint. One Advanced Rule matching that fingerprint mitigated the entire attack without touching IP allowlists or rate limits.

The WAAP API supports two workflows: querying events by fingerprint and creating Advanced Rules with fingerprint conditions.

All requests require an API token. Set the required environment variables before running the examples:

export GCORE_API_KEY="{YOUR_API_KEY}"
export WAAP_DOMAIN_ID="{DOMAIN_ID}"

Query events by fingerprint

Pass the fingerprint as a ja3 or ja4 query parameter. The API returns only events where the originating client matched that hash — regardless of how many IP addresses the client used.

curl "https://api.cdb-staging.cdn.orange.com/waap/v1/analytics/requests?domain_ids=${WAAP_DOMAIN_ID}&start=2026-01-01T00:00:00Z&ja3=e2925c27149b0d0dc34373d55040dde1" \
  -H "Authorization: APIKey ${GCORE_API_KEY}"

Each event in the response includes both a ja3 field and a ja4 field. To filter by JA4, use the ja4 query parameter with a value in the prefix_hex12_hex12 format.

Once you've identified the fingerprint to block, use it in an Advanced Rule condition.

Create an Advanced Rule with a fingerprint condition

Advanced Rules accept request.ja3 and request.ja4 as condition fields in the CEL source expression. The Python and Go SDKs cover this endpoint.

import gcore
import os

from dotenv import load_dotenv

load_dotenv()

client = gcore.CDB(api_key=os.environ["GCORE_API_KEY"])
domain_id = int(os.environ["WAAP_DOMAIN_ID"])

rule = client.waap.domains.advanced_rules.create(
    domain_id,
    name="Block known botnet fingerprint",
    enabled=True,
    source="request.ja3 == 'e2925c27149b0d0dc34373d55040dde1'",
    action={"block": {"status_code": 403}},
)

print(f"Rule created: id={rule.id} name={rule.name!r}")
package main

import (
    "context"
    "fmt"
    "os"
    "strconv"

    "github.com/G-Core/gcore-go"
    "github.com/G-Core/gcore-go/option"
    "github.com/G-Core/gcore-go/waap"
)

func main() {
    client := gcore.NewClient(option.WithAPIKey(os.Getenv("GCORE_API_KEY")))
    domainID, _ := strconv.ParseInt(os.Getenv("WAAP_DOMAIN_ID"), 10, 64)

    rule, _ := client.Waap.Domains.AdvancedRules.New(
        context.Background(),
        domainID,
        waap.DomainAdvancedRuleNewParams{
            Name:    "Block known botnet fingerprint",
            Enabled: true,
            Source:  "request.ja3 == 'e2925c27149b0d0dc34373d55040dde1'",
            Action: waap.DomainAdvancedRuleNewParamsAction{
                Block: waap.DomainAdvancedRuleNewParamsActionBlock{
                    StatusCode: 403,
                },
            },
        },
    )

    fmt.Printf("Rule created: id=%d name=%q\n", rule.ID, rule.Name)
}
curl -X POST "https://api.cdb-staging.cdn.orange.com/waap/v1/domains/${WAAP_DOMAIN_ID}/advanced-rules" \
  -H "Authorization: APIKey ${GCORE_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Block known botnet fingerprint",
    "enabled": true,
    "source": "request.ja3 == '\''e2925c27149b0d0dc34373d55040dde1'\''",
    "action": {"block": {"status_code": 403}}
  }'

For JA4, swap request.ja3 for request.ja4 and use the prefix_hex12_hex12 format. To cover multiple known fingerprints, chain conditions with or in the source string.