docs: add Fingerprinting page covering sniffer, prober, and Caddy fp module

2026-05-10 04:10:47 -04:00
parent d4b88c68ef
commit d45fb08b6d
2 changed files with 166 additions and 0 deletions

165
Fingerprinting.md Normal file

@@ -0,0 +1,165 @@
# Fingerprinting
DECNET builds a multi-layer fingerprint of every attacker from three
independent sources: **passive wire capture**, **active probing**, and
**inline HTTP inspection**. Each layer contributes distinct evidence;
together they let you tell a curl script from a Metasploit operator from a
nation-state implant even when the source IP changes.
All fingerprint data is stored as `bounty` rows in the DECNET database and
surfaces in the **Attacker detail** page under the *Fingerprints* tab.
---
## Layer 1 — Passive sniffer (network layer)
The sniffer runs fleet-wide on the host interface and reads raw packets
without touching any decky service. It fires on the first packet of each
connection, so it captures the attacker's stack signature before any
application-level exchange.
| Fingerprint | What it captures | Algorithm |
|---|---|---|
| **JA3 / JA3S** | TLS ClientHello / ServerHello cipher suite and extension order | MD5 of normalised fields per Salesforce spec |
| **JA4 / JA4S / JA4L** | TLS 1.3-aware version; JA4L adds latency timing | FoxIO JA4 spec |
| **TCP SYN OS** | MSS, window scale, TCP option order from the SYN | Mini-p0f classifier (`decnet/sniffer/p0f.py`) |
| **JA4-QUIC** | QUIC Initial ClientHello — QUIC-specific extensions and transport params | FoxIO JA4-QUIC spec |
| **Flow timing** | Round-trip latency and inter-packet timing | Raw timestamps from the sniffer |
Sniffer events land as `attacker.observed` or `attacker.fingerprinted` bus
events consumed by the correlator and ingester.
> **Limitation:** the sniffer only sees the TLS handshake — it cannot read
> HTTP headers or QUIC stream frames inside an encrypted session. Layers 2
> and 3 fill that gap.
---
## Layer 2 — Active prober (application layer)
After a new attacker is first observed, the prober worker reaches back
out to the attacker's IP on a set of default ports to collect
application-level fingerprints.
| Fingerprint | Protocol | Ports probed |
|---|---|---|
| **JARM** | TLS (any HTTPS-ish service) | 443, 8443, 8080, 4443, 50050, 2222, 993, 995, 8888, 9001 |
| **HASSH** | SSH server | 22, 2222, 22222, 2022 |
| **TCP fingerprint** | TCP SYN response analysis | 22, 80, 443, 8080, 8443, 445, 3389 |
Active probes are stealthy: they look like ordinary clients, carry no
DECNET-specific banner, and use the same port-rotation patterns an
informed scanner would use. See [Security-and-Stealth](Security-and-Stealth).
When a fingerprint changes between probes, a `attacker.fingerprint_rotated`
bus event fires — that is a strong signal of infrastructure churn (VPS
swap, cert rotation, banner rewrite).
---
## Layer 3 — Inline HTTP fingerprinting (Caddy fp module)
The `http` and `https` decky templates ship with a custom Caddy module
(`decnet_fp`) that intercepts connections at the byte level, before
Caddy's HTTP parser sees them. This gives wire-accurate fingerprints
that cannot be faked by HTTP-level header manipulation.
### JA4H (HTTP request header order)
The `decnet_fp` listener wrapper taps the raw TLS stream and buffers the
first request headers of each connection before replaying them to Caddy's
parser.
- **h1:** headers are split by `\r\n` in arrival order.
- **h2:** a per-connection HPACK decoder maintains the dynamic table and
emits headers in HPACK decode order — pseudo-headers
(`:method`, `:path`, `:scheme`, `:authority`) appear first, then regular
headers in the order the client encoded them.
The ordered list feeds `_compute_ja4h` in `syslog_bridge.py`, which
produces a JA4H hash per the FoxIO spec.
> Map-iteration order in Go is randomised; DECNET captures order at the
> *byte level*, not from `http.Header`, so the JA4H is reproducible and
> meaningful.
### H2 SETTINGS
During the h2 connection preface, the client sends a `SETTINGS` frame
listing its implementation parameters. The fp module parses the raw
6-byte `(id, value)` tuples in wire order and records:
- `settings` — map of setting name → value
(e.g. `HEADER_TABLE_SIZE`, `MAX_CONCURRENT_STREAMS`, `INITIAL_WINDOW_SIZE`)
- `frame_order` — setting IDs in the exact order the client sent them
Different HTTP/2 implementations (curl, Chrome, Firefox, Go net/http,
Java HttpClient) have characteristic SETTINGS maps and orderings.
### H3 SETTINGS
For HTTP/3, the QUIC server is Caddy with native h3 support. Caddy
exposes the client's h3 SETTINGS frame via the `http3.Settingser`
interface on the `ResponseWriter`. The fp module captures:
- `EnableDatagrams` — whether the client advertised H3 datagram support
- `EnableExtendedConnect` — extended CONNECT (used by WebTransport)
- `Other` — any additional settings (including GREASE entries)
### Source port as fingerprint signal
`remote_addr` in every fp record is the full `host:port` string from
Go's network layer. The collector strips the port before resolving
attacker identity (so 50 connections from the same IP do not produce 50
attackers), but preserves it as `remote_port` in the structured fields.
An attacker whose tooling consistently originates from the same source
port (or a narrow range) is a meaningful signal — some NAT devices, VPN
clients, and C2 frameworks exhibit this behaviour. `remote_port` is
stored in the `fingerprint` bounty payload and visible in the Attacker
detail page.
---
## Where fingerprints are stored
Every fingerprint event produces a `bounty` row:
| Bounty `fingerprint_type` | Source | Key discriminating fields |
|---|---|---|
| `ja3` / `ja4` / `ja4s` | Sniffer | `hash`, `tls_version`, `ciphers` |
| `ja4_quic` | Sniffer | `ja4_quic`, `sni`, `alpn` |
| `tcp_os` | Sniffer | `os_guess`, `mss`, `window_scale` |
| `jarm` | Prober | `jarm_hash`, `port` |
| `hassh` | Prober | `hassh_server`, `port` |
| `tcpfp` | Prober | `tcp_fp_hash`, `port` |
| `ja4h` | Caddy fp module | `ja4h`, `protocol`, `method`, `remote_port` |
| `http2_settings` | Caddy fp module | `settings`, `frame_order`, `remote_port` |
| `http3_settings` | Caddy fp module | `settings`, `remote_port` |
Bounties are deduplicated per `(attacker_uuid, fingerprint_type, hash)` so
repeated connections from the same attacker produce one row, not thousands.
---
## Enabling inline HTTP fingerprinting
The Caddy fp module is **built into the `http` and `https` decky templates
automatically** — no extra configuration is needed. The module activates
when the template is deployed.
For HTTP/3, ensure `http/3` is listed in the service's `http_versions`
setting. Caddy's native h3 stack handles UDP/443; the fp module hooks into
it via the `http3.Settingser` interface.
---
## Related pages
- [Identity-Resolution](Identity-Resolution) — how fingerprints are
clustered into attacker identities
- [OS-Fingerprint-Spoofing](OS-Fingerprint-Spoofing) — how DECNET spoofs
*its own* OS fingerprint to look like the target OS
- [Security-and-Stealth](Security-and-Stealth) — probe stealth measures
- [Logging-and-Syslog](Logging-and-Syslog) — how fp socket records flow
through syslog_bridge to the collector

@@ -27,6 +27,7 @@
- [Database-Drivers](Database-Drivers)
- [Systemd-Setup](Systemd-Setup)
- [Logging-and-Syslog](Logging-and-Syslog)
- [Fingerprinting](Fingerprinting)
- [Service-Bus](Service-Bus)
- [Realism](Realism)
- [Web-Dashboard](Web-Dashboard)