Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JA4nginx

Nginx built with JA4(+) fingerprinting — module, OpenSSL/nginx patches, and an access-log format that records fingerprints on every request. Optionally pair with Suricata 8 for wire/L4 JA4 in eve.json.

What's in this repo

Path Purpose
ja4-nginx-module/ JA4 nginx module source + OpenSSL/nginx patches
configs/nginx-ja4-access.conf Access-log config using $http_ssl_ja4* variables
configs/suricata-ja4.yaml.snippet Suricata 8 JA4 / eve-log / af-packet snippets
configs/check-suricata-ja4.sh Verify JA4 (+ SNI) in /var/log/suricata/eve.json
scripts/install-system-nginx.sh Optional: install JA4 build as system /usr/sbin/nginx
scripts/ja4-dashboard.py Terminal dashboard: nginx + Suricata JA4 overview
README.md Build & run guide (nginx 1.28.0 + OpenSSL 3.0.13)

Local nginx/ and ja4-openssl/ build trees are not committed (too large). Download and patch them using the steps below.

Two layers of JA4

Layer Where Log
L5 (app) This nginx module /var/log/nginx/access.logja4=, ja4h=, …
L4 (wire) Suricata on the NIC /var/log/suricata/eve.jsontls.ja4, tls.sni

They are independent. Nginx sees HTTP(S) at the process; Suricata sees packets on an interface (e.g. enp0s3). Traffic to 127.0.0.1 will show up in nginx logs but not in Suricata if Suricata is not capturing lo.

JA4 in access logs

Custom log_format ja4_extended writes fingerprints alongside the usual request fields:

log_format ja4_extended '$remote_addr - $remote_user [$time_local] "$request" '
                        '$status $body_bytes_sent "$http_referer" '
                        '"$http_user_agent" "$http_x_forwarded_for" '
                        'ja4="$http_ssl_ja4" '
                        'ja4_string="$http_ssl_ja4_string" '
                        'ja4one="$http_ssl_ja4one" '
                        'ja4s="$http_ssl_ja4s" '
                        'ja4s_string="$http_ssl_ja4s_string" '
                        'ja4h="$http_ssl_ja4h" '
                        'ja4h_string="$http_ssl_ja4h_string" '
                        'ja4t="$http_ssl_ja4t" '
                        'ja4t_string="$http_ssl_ja4t_string" '
                        'ja4ts="$http_ssl_ja4ts" '
                        'ja4ts_string="$http_ssl_ja4ts_string" '
                        'ja4l="$http_ssl_ja4l" '
                        'ja4x="$https_ssl_ja4x" '
                        'ja4x_string="$https_ssl_ja4x_string" '
                        'ja4_l4="$http_x_ja4_l4"';

access_log  /var/log/nginx/access.log  ja4_extended;

Full example: [configs/nginx-ja4-access.conf](configs/nginx-ja4-access.conf).

Variable Fingerprint
$http_ssl_ja4 / _string Client TLS (JA4)
$http_ssl_ja4s / _string Server TLS (JA4S)
$http_ssl_ja4h / _string HTTP (JA4H)
$http_ssl_ja4t / _string TCP (JA4T)
$http_ssl_ja4ts / _string TCP server (JA4TS)
$http_ssl_ja4l Latency (JA4L)
$https_ssl_ja4x / _string Certificate (JA4X)
$http_x_ja4_l4 Optional L4 JA4 from Suricata (header)

TLS fields need HTTPS. ja4h works on plain HTTP.

Build

1. Working directory

mkdir ja4-nginx-build && cd ja4-nginx-build

2. Download nginx 1.28.0

wget http://nginx.org/download/nginx-1.28.0.tar.gz
tar -xzf nginx-1.28.0.tar.gz
mv nginx-1.28.0 nginx

3. Download OpenSSL 3.0.13

wget https://github.com/openssl/openssl/releases/download/openssl-3.0.13/openssl-3.0.13.tar.gz
tar -xzf openssl-3.0.13.tar.gz
mv openssl-3.0.13 ja4-openssl

4. Use this repo's module + patches

# Clone this repo, then:
ln -s /path/to/JA4nginx/ja4-nginx-module .
# or: cp -a /path/to/JA4nginx/ja4-nginx-module .

cd nginx

5. Configure nginx

./configure \
  --with-debug \
  --with-compat \
  --add-module=../ja4-nginx-module/src \
  --with-http_ssl_module \
  --with-openssl=$(pwd)/../ja4-openssl \
  --prefix=$(pwd)/nginx_local

6. Patch & build OpenSSL

cd ../ja4-openssl
./Configure linux-x86_64 --prefix=$(pwd)/.openssl --openssldir=$(pwd)/.openssl
patch -p1 < ../ja4-nginx-module/patches/openssl.patch
make -j$(nproc)
sudo make install_sw LIBDIR=lib

7. Patch & build nginx

cd ../nginx
patch -p1 < ../ja4-nginx-module/patches/nginx.patch
make -j$(nproc)
sudo make install
sudo chown "$(whoami)" ./nginx_local

8. Install JA4 access-log config

cp /path/to/JA4nginx/configs/nginx-ja4-access.conf ./nginx_local/conf/nginx.conf
# Enable the HTTPS server block + certs for TLS fingerprints

9. Run (local prefix)

./nginx_local/sbin/nginx
tail -f /var/log/nginx/access.log

10. Optional: install as system nginx

Destructive: replaces /usr/sbin/nginx and adds a systemd drop-in. Backs up first.

Ensure nginx_local/conf/nginx.conf has:

  • pid /run/nginx.pid; (required for systemd Type=forking)
  • access_log / error_log paths nginx can write (e.g. /var/log/nginx/)
  • HTTPS listen 443 ssl + cert.pem / cert.key if you want TLS JA4

Then (from the build tree, or pass the prefix explicitly):

# After build: prefix is ./nginx/nginx_local  (or wherever you installed)
sudo /path/to/JA4nginx/scripts/install-system-nginx.sh /path/to/nginx/nginx_local

# Verify
nginx -V 2>&1 | head -3
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1/
curl -skS -o /dev/null -w '%{http_code}\n' https://127.0.0.1/
sudo tail -2 /var/log/nginx/access.log

Install without starting: sudo SKIP_START=1 .../install-system-nginx.sh /path/to/nginx_local.

Restore notes are printed by the script (backup under /etc/nginx/backup_*).

Terminal JA4 dashboard

Live terminal view of nginx access-log fingerprints and Suricata IDS TLS JA4 from eve.json (top counts, recent events, rare JA4s, IPs with multiple JA4s).

# Needs read access to both logs (usually root)
sudo python3 /path/to/JA4nginx/scripts/ja4-dashboard.py

# One-shot (scripting / pipes)
sudo python3 /path/to/JA4nginx/scripts/ja4-dashboard.py --once --plain

# Options
sudo python3 /path/to/JA4nginx/scripts/ja4-dashboard.py \
  --nginx-log /var/log/nginx/access.log \
  --eve /var/log/suricata/eve.json \
  --interval 2 \
  --tail 5000 \
  --top 10 \
  --recent 8
  • Press q then Enter (or Ctrl-C) to quit live mode.
  • Optional prettier UI if [rich](https://github.com/Textualize/rich) is installed (pip install rich); otherwise plain text.
  • Same-host curls may appear in nginx but not Suricata (enp0s3 only) — hit the LAN IP from another machine for L4 rows.

Example report (live)

Illustrative live view (SNIs anonymized). Nginx rows are local test clients; Suricata rows are outbound TLS on the NIC — often different flows until a remote client hits this host.

╭──────────────────────────────── status ────────────────────────────────╮
│ JA4 dashboard                        2026-07-24 18:20:46 UTC           │
│ nginx                                OK   /var/log/nginx/access.log    │
│ eve                                  OK   /var/log/suricata/eve.json   │
│ events nginx=6  suricata_tls+ja4=86  L5 nginx + L4 Suricata IDS        │
╰────────────────────────────────────────────────────────────────────────╯
                  Nginx top ja4
┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  count ┃ ja4                                  ┃
┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│      3 │ t13i3011h2_1d37bd780c83_b26ce05bbdd6 │
└────────┴──────────────────────────────────────┘
                         Nginx top ja4h
┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  count ┃ ja4h                                                ┃
┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│      6 │ ge11nn030000_b51846f30ce9_e3b0c44298fc_e3b0c44298fc │
└────────┴─────────────────────────────────────────────────────┘
                                                        Nginx recent
┏━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ src             ┃ st  ┃ req                    ┃ ja4                                  ┃ ja4h                             ┃
┡━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 127.0.0.1       │ 200 │ GET / HTTP/1.1         │ -                                    │ ge11nn030000_b51846f30ce9_e3b... │
│ 127.0.0.1       │ 200 │ GET / HTTP/1.1         │ t13i3011h2_1d37bd780c83_b26ce05bbdd6 │ ge11nn030000_b51846f30ce9_e3b... │
│ 127.0.0.1       │ 200 │ GET / HTTP/1.1         │ -                                    │ ge11nn030000_b51846f30ce9_e3b... │
│ 127.0.0.1       │ 200 │ GET / HTTP/1.1         │ t13i3011h2_1d37bd780c83_b26ce05bbdd6 │ ge11nn030000_b51846f30ce9_e3b... │
│ 192.168.110.33  │ 200 │ GET / HTTP/1.1         │ -                                    │ ge11nn030000_b51846f30ce9_e3b... │
│ 192.168.110.33  │ 200 │ GET / HTTP/1.1         │ t13i3011h2_1d37bd780c83_b26ce05bbdd6 │ ge11nn030000_b51846f30ce9_e3b... │
└─────────────────┴─────┴────────────────────────┴──────────────────────────────────────┴──────────────────────────────────┘
                                 Suricata top ja4 + sni
┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  count ┃ ja4                                      ┃ sni                              ┃
┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│     33 │ t13d1011h1_61a7ad8aa9b6_d4929ced8316     │ api.example.com                  │
│     31 │ t13d521200_b262b3658495_66863fb0a24c     │ api.example.com                  │
│     22 │ t13d5212h2_b262b3658495_8e6e362c5eac     │ cdn.example.com                  │
└────────┴──────────────────────────────────────────┴──────────────────────────────────┘
                                                        Suricata recent tls
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ time                     ┃ src            ┃ dst                  ┃ ja4                                ┃ sni                      ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 2026-07-24T18:16:56.5... │ 192.168.110.33 │ 54.156.149.72:443    │ t13d521200_b262b3658495_66863fb... │ api.example.com          │
│ 2026-07-24T18:19:10.9... │ 192.168.110.33 │ 52.6.16.241:443      │ t13d521200_b262b3658495_66863fb... │ api.example.com          │
│ 2026-07-24T18:19:12.3... │ 192.168.110.33 │ 52.6.16.241:443      │ t13d521200_b262b3658495_66863fb... │ api.example.com          │
│ 2026-07-24T18:19:14.7... │ 192.168.110.33 │ 54.80.240.195:443    │ t13d521200_b262b3658495_66863fb... │ api.example.com          │
│ 2026-07-24T18:19:15.7... │ 192.168.110.33 │ 3.228.152.244:443    │ t13d521200_b262b3658495_66863fb... │ api.example.com          │
│ 2026-07-24T18:19:17.0... │ 192.168.110.33 │ 34.226.129.221:443   │ t13d521200_b262b3658495_66863fb... │ api.example.com          │
│ 2026-07-24T18:20:05.7... │ 192.168.110.33 │ 34.199.61.39:443     │ t13d5212h2_b262b3658495_8e6e362... │ cdn.example.com          │
│ 2026-07-24T18:20:34.0... │ 192.168.110.33 │ 54.235.102.83:443    │ t13d1011h1_61a7ad8aa9b6_d4929ce... │ api.example.com          │
└──────────────────────────┴────────────────┴──────────────────────┴────────────────────────────────────┴──────────────────────────┘
   Triage: rare ja4 (count=1)
┏━━━━━━━━━━━━┳━━━━━━━━┓
┃ source     ┃ ja4    ┃
┡━━━━━━━━━━━━╇━━━━━━━━┩
│ -          │ (none) │
└────────────┴────────┘
      Triage: IPs with multiple ja4
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━┓
┃ source     ┃ ip                 ┃    n ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━┩
│ suricata   │ 192.168.110.33     │    3 │
└────────────┴────────────────────┴──────┘
q + Enter quit | Ctrl-C quit

Requires pip install rich (otherwise the dashboard falls back to plain text).

One-shot Suricata-only check remains: [configs/check-suricata-ja4.sh](configs/check-suricata-ja4.sh).

Suricata JA4 (optional L4) — IDS / AF_PACKET

Suricata 8.x in IDS mode (passive AF_PACKET — not inline IPS) with HAVE_JA4 writes client JA4 into TLS eve events. Snippets: [configs/suricata-ja4.yaml.snippet](configs/suricata-ja4.yaml.snippet).

Install

sudo apt update
sudo apt install -y suricata   # 8.0+ recommended
suricata --build-info | grep -i JA4   # expect: JA4 support: yes

Enable JA4 in /etc/suricata/suricata.yaml

  1. Generate fingerprints under app-layer.protocols.tls:
app-layer:
  protocols:
    tls:
      enabled: yes
      ja3-fingerprints: yes
      ja4-fingerprints: yes
  1. Log them under outputseve-logtypes:
- eve-log:
    enabled: yes
    filetype: regular
    filename: eve.json
    types:
      - tls:
          extended: yes   # includes ja4 + sni (among other fields)
  1. Capture NIC under af-packet (replace with your interface):
af-packet:
  - interface: enp0s3
  - interface: default
  1. Restart:
sudo systemctl restart suricata
sudo systemctl status suricata

Verify

sudo bash /path/to/JA4nginx/configs/check-suricata-ja4.sh
# or:
sudo grep -o '"ja4":"[^"]*"' /var/log/suricata/eve.json | sort | uniq -c | sort -rn | head

JA4 + SNI counts:

sudo jq -r 'select(.event_type=="tls") | "\(.tls.ja4 // "-")\t\(.tls.sni // "-")"' /var/log/suricata/eve.json \
  | sort | uniq -c | sort -rn | head

Unix socket permission (optional fix)

If logs show failed to create socket directory /var/run/suricata/: Permission denied, Suricata still captures but suricatasc / clean stops break. Fix:

sudo mkdir -p /etc/systemd/system/suricata.service.d
sudo tee /etc/systemd/system/suricata.service.d/runtime.conf >/dev/null <<'EOF'
[Service]
RuntimeDirectory=suricata
RuntimeDirectoryMode=0755
EOF
sudo systemctl daemon-reload
sudo systemctl restart suricata

Optional: Suricata JA4 into nginx access log

The nginx log_format includes ja4_l4="$http_x_ja4_l4". That only fills if something (e.g. a local forwarder) sets request header X-JA4-L4 from Suricata. Out of the box, nginx L5 and Suricata L4 stay in separate logs.

Patches

  • ja4-nginx-module/patches/nginx.patch — ClientHello early callback / extension capture for JA4
  • ja4-nginx-module/patches/openssl.patch — OpenSSL helpers needed by the module

Attribution

Based on FoxIO-LLC/ja4-nginx-module (FoxIO License 1.1).
nginx and OpenSSL remain under their respective licenses.

License

Private — all rights reserved for this repo’s config and docs.
Module/patches are subject to FoxIO License 1.1.

About

Nginx + Suricata JA4(+) — L5 fingerprints in nginx access logs, L4 JA4 in Suricata eve.json (IDS / AF_PACKET)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages