Skip to content

Choose a tag to compare

@github-actions github-actions released this 27 Aug 18:09
· 7 commits to master since this release

The static trails now live in their own repository, stamparm/trails, and reach a deployment as one assembled file instead of shipping in the source tree.

Everything else in this release is secondary to that, so read the upgrade note first.


Upgrading from 3.1.x — do this

A maltrail.conf written before 3.2 has no STATIC_TRAILS_URL, and without it the static trail set is not loaded at all. Add:

STATIC_TRAILS_URL https://github.com/stamparm/trails/releases/latest/download/trails.csv.gz

…or re-run install.sh, which writes it for you.

Maltrail will not let this pass quietly. The updater prints a banner across the terminal, server --doctor fails with exit 1, and maltrail-sensor -T fails its preflight — so the systemd unit's ExecStartPre catches it before the sensor ever runs. Losing the trail set is indistinguishable from a quiet network, and that is not a thing to discover in a month.

There is deliberately no silent fallback to a default URL. A deployment that fetches its detection content from an address the operator never configured is a worse answer than one that says what is missing.

Why the split

trails/static was 98.7% of every blob byte in this repository's history and 9,169 of 9,549 commits. Two consequences: git log, git blame and git bisect were useless on the code, and updating detection content meant pulling a new version of the software.

The repository went from 9,578 commits and a 110 MiB pack to 353 commits and 3.3 MiB. Content history was not deleted — it moved, with authorship intact.

What changes for a deployment

Static trails fetched from STATIC_TRAILS_URL, cached next to TRAILS_FILE
Update cost ~11 MB gzipped, or 65 bytes when the published digest says nothing changed
Content freshness published up to 4×/day; new indicators reach you within hours instead of on your next git pull
Pinning point STATIC_TRAILS_URL at a dated content-YYYYMMDD-HHMM release so a bad publish is not immediately global
Offline / air-gapped trails-bootstrap.csv.gz ships with this release and install.sh seeds the cache from it, so a first start detects immediately even with no connectivity
Integrity a payload that does not match its published sha256 is refused in favour of the cache
Feeds unchanged — still fetched by your deployment directly from each publisher
Custom trails unchanged, but CUSTOM_TRAILS_DIR no longer defaults inside the installation directory

trails/feeds/ moved to feeds/. trails/static and trails/custom stopped pretending to be feed plugins — they only ever were so update_trails() could run one loop, which is why DISABLED_FEEDS static used to silently disable 1.6M trails.

Also in this release

Everything that accumulated since 3.1.1:

  • Alerting: ALERT_WEBHOOK_URL posts events at or above a severity threshold, with throttling. Slack, Mattermost, Discord, a SIEM collector, or your own endpoint.
  • Event index: a per-day SQLite sidecar. /counts is exact rather than estimated, and /hunt skips non-matching lines instead of reading every one.
  • JA3/JA4 client fingerprints matched as trails, byte-identical to the Python implementation and pinned by generated vectors.
  • Beaconing heuristic — timer-regular reconnects to one destination, rated suspicious, never malware, because uptime monitors beacon too.
  • Trail confidence: how strongly sources agree, surfaced in /check and the trail drawer.
  • server --doctor preflight, and both config parsers now flag an unknown option instead of ignoring a typo.
  • Longest-match whitelist precedence — an exact static trail on a more specific name than its whitelisted ancestor now fires. 3,082 trails on shared platforms were loaded, counted, and could never match.
  • Source citations survive the split: each release of the trail set carries a provenance sidecar, so the trail drawer still shows the report an indicator was extracted from.

Fixes worth naming: UDP event intake was losing 24% at 10k events/s (a thread and an open() per datagram — now lossless to 20k/s); /hunt's IP search was 9× slower than a plain substring hunt; --detect-test told a healthy install its detection was broken; --smoke-test printed nothing and exited 1; every IPv6 event was unmapped on the attack map. The server also refuses to start with the private key Maltrail used to ship, and RIPEstat is proxied through /ripe so script-src is back to 'self'.

See the CHANGELOG for the full list.

Verifying what you downloaded

sha256sum -c maltrail-sensor-3.2-x86_64-unknown-linux-gnu.tar.gz.sha256
gh attestation verify oci://ghcr.io/stamparm/maltrail:3.2 --repo stamparm/maltrail

Binaries are built inside AlmaLinux 8 with libpcap linked statically. Checked on the published x86_64 binary: highest glibc symbol required is 2.28, and libpcap is not among its dynamic dependencies — so it starts on RHEL 8/9, Debian 12, Ubuntu and openSUSE Leap 15 whether or not libpcap is installed. Alpine/musl has no prebuilt sensor; build from source there.

Container: ghcr.io/stamparm/maltrail:3.2