> ## Documentation Index
> Fetch the complete documentation index at: https://gcore.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# TLS fingerprinting with JA3 and JA4

export const MethodSection = ({children}) => children ?? null;

export const MethodSwitch = ({children}) => {
  const tabs = React.Children.toArray(children).map(c => {
    if (!c || !c.props) return null;
    if (c.props.id) return c;
    const inner = c.props.children;
    if (inner && inner.props && inner.props.id) return inner;
    return null;
  }).filter(Boolean);
  const firstId = tabs.length > 0 ? tabs[0].props.id : "";
  const [active, setActive] = React.useState(firstId);
  React.useEffect(() => {
    try {
      const saved = localStorage.getItem("gcore_docs_method");
      if (saved && tabs.find(t => t.props.id === saved)) {
        setActive(saved);
      }
    } catch (_) {}
  }, []);
  React.useEffect(() => {
    try {
      document.querySelectorAll("h2[id], h3[id]").forEach(heading => {
        const visible = heading.offsetParent !== null;
        document.querySelectorAll(`a[href="#${heading.id}"]`).forEach(link => {
          if (link.closest("h1,h2,h3,h4,h5,h6")) return;
          const li = link.closest("li");
          if (li) li.style.display = visible ? "" : "none";
        });
      });
    } catch (_) {}
    window.dispatchEvent(new Event("scroll"));
  }, [active]);
  const handleClick = id => {
    setActive(id);
    try {
      localStorage.setItem("gcore_docs_method", id);
    } catch (_) {}
  };
  return <div>
      <div className="not-prose flex gap-0 border-b border-zinc-200 dark:border-zinc-800 mb-8 mt-2" role="tablist">
        {tabs.map(tab => {
    const isActive = active === tab.props.id;
    return <button key={tab.props.id} role="tab" aria-selected={isActive} onClick={() => handleClick(tab.props.id)} className={["px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors cursor-pointer", isActive ? "border-primary text-primary" : "border-transparent text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200"].join(" ")}>
              {tab.props.label}
            </button>;
  })}
      </div>

      {tabs.map(tab => <div key={tab.props.id} style={{
    display: active === tab.props.id ? "" : "none"
  }}>
          {tab.props.children}
        </div>)}
    </div>;
};

<MethodSwitch>
  <MethodSection id="portal" label="Customer Portal">
    <p>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.</p>

    <p>WAAP supports two fingerprinting algorithms:</p>

    |                      | JA3                                            | JA4                                      |
    | -------------------- | ---------------------------------------------- | ---------------------------------------- |
    | Algorithm            | MD5                                            | Truncated SHA-256                        |
    | Ordering sensitivity | Sensitive — values can differ between sessions | Stable — sorts fields before hashing     |
    | Output format        | 32-character hex string                        | Three-part string (`prefix_hex12_hex12`) |
    | Example value        | `e2925c27149b0d0dc34373d55040dde1`             | `t13d1516h2_8daaf6152771_b1ff8ab2d16f`   |

    <p>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.</p>

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

    ## View events by fingerprint

    <p>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:</p>

    1. In the [Gcore Customer Portal](https://portal.gcore.com), 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**.

    <Frame>
      <img src="https://mintcdn.com/gcore/B34IbT2MOPX1Mg9e/images/docs/waap/threat-intelligence/tls-fingerprinting/ja3-events-filter.png?fit=max&auto=format&n=B34IbT2MOPX1Mg9e&q=85&s=95dbf83cc7a76dd9425108fc06ff80f7" alt="Events page with the JA3 filter selected in the filter bar" width="1400" height="336" data-path="images/docs/waap/threat-intelligence/tls-fingerprinting/ja3-events-filter.png" />
    </Frame>

    ## Create an Advanced Rule with a fingerprint condition

    <p>[Advanced Rules](/waap/waap-rules/advanced-rules/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.</p>

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

    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**.

    <Frame>
      <img src="https://mintcdn.com/gcore/B34IbT2MOPX1Mg9e/images/docs/waap/threat-intelligence/tls-fingerprinting/ja4-in-rule-builder.png?fit=max&auto=format&n=B34IbT2MOPX1Mg9e&q=85&s=1395234ccb769dc877318b993b763ff6" alt="Advanced rule builder with the request object and ja4 attribute selected, showing the attribute dropdown with ja3 and ja4 options visible" width="1400" height="900" data-path="images/docs/waap/threat-intelligence/tls-fingerprinting/ja4-in-rule-builder.png" />
    </Frame>

    <p>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:</p>

    ```text theme={null}
    request.ja3 == 'e2925c27149b0d0dc34373d55040dde1' or
    request.ja3 == '3e9b20610098b6c9bff953856e58016a' or
    request.ja3 == '7d671906ed4a1edac3262a54676dacfa'
    ```

    ## Malicious TLS Fingerprint tag

    <p>Explicit fingerprint matching isn't the only option.</p>

    <p>WAAP automatically assigns the `malicioustlsfp` [predefined tag](/waap/waap-rules/custom-rules/tag-rules/predefined-tags) to requests whose JA3 fingerprint consistently shows malicious behavior across Gcore's traffic. A [tag-based rule](/waap/waap-rules/custom-rules/tag-rules) targeting this tag applies an action without requiring knowledge of a specific hash value:</p>

    ```text theme={null}
    tags.exists('malicioustlsfp')
    ```

    <p>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.</p>

    ### Incident example

    <p>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.</p>

    <p>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.</p>
  </MethodSection>

  <MethodSection id="api" label="REST API">
    <p>The WAAP API supports two workflows: querying events by fingerprint and creating Advanced Rules with fingerprint conditions.</p>

    <p>All requests require an [API token](/account-settings/api-tokens). Set the required environment variables before running the examples:</p>

    ```bash theme={null}
    export GCORE_API_KEY="{YOUR_API_KEY}"
    export WAAP_DOMAIN_ID="{DOMAIN_ID}"
    ```

    ## Query events by fingerprint

    <p>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.</p>

    ```bash theme={null}
    curl "https://api.gcore.com/waap/v1/analytics/requests?domain_ids=${WAAP_DOMAIN_ID}&start=2026-01-01T00:00:00Z&ja3=e2925c27149b0d0dc34373d55040dde1" \
      -H "Authorization: APIKey ${GCORE_API_KEY}"
    ```

    <p>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.</p>

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

    ## Create an Advanced Rule with a fingerprint condition

    <p>Advanced Rules accept `request.ja3` and `request.ja4` as condition fields in the CEL `source` expression. The [Python](/developer-tools/sdks/python) and [Go](/developer-tools/sdks/go) SDKs cover this endpoint.</p>

    <Tabs>
      <Tab title="Python SDK">
        ```python theme={null}
        import gcore
        import os

        from dotenv import load_dotenv

        load_dotenv()

        client = gcore.Gcore(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}")
        ```
      </Tab>

      <Tab title="Go SDK">
        ```go theme={null}
        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)
        }
        ```
      </Tab>

      <Tab title="curl">
        ```bash theme={null}
        curl -X POST "https://api.gcore.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}}
          }'
        ```
      </Tab>
    </Tabs>

    <p>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.</p>
  </MethodSection>
</MethodSwitch>
