strixd — the scanning backend
One binary, several subcommands. With no command it defaults to serve. Run strixd <command> -h for that command's own flags.
strixd <command> [flags]
serve run the HTTP backend (/scan, /metrics, /health) — the default
scan scan files/dirs/stdin against the rules in-process
fetch-rules download an updated compiled rule bundle into the cache
check-rules compile the rules, print the count, exit non-zero on failure
extract dump what the container extractor carves from a file (no scan)
info print build, libyara, and loaded-rules-bundle identity
health probe the local /health endpoint (container HEALTHCHECK)
version print the version
extract is the debugging window into the moat: it shows every layer Mailstrix carves out of a file — OLE/OOXML parts, decompressed VBA, archive members, carved PE/ELF — without running YARA, so you can see exactly what the matcher will be handed.
strixd serve — flags
Each flag mirrors an environment variable (shown in parentheses); the flag overrides it.
| Flag | Env | Default | Purpose |
|---|---|---|---|
-host | MAILSTRIX_HOST | all interfaces | HTTP bind host. Bind to loopback/private only. |
-port | MAILSTRIX_PORT | 8079 | HTTP bind port. |
-backend-timeout | MAILSTRIX_BACKEND_TIMEOUT | 1s | Whole-request backend budget. |
-scan-timeout | MAILSTRIX_SCAN_TIMEOUT | 8s | Per-scan libyara timeout. |
-max-concurrent | MAILSTRIX_MAX_CONCURRENT | CPU count | Max simultaneous libyara scans (CPU gate). |
-max-body | MAILSTRIX_MAX_BODY | 8 MiB | Max request body; oversized is rejected, never truncated. |
-token | MAILSTRIX_TOKEN[_FILE] | none | Shared secret for /scan. Prefer the _FILE form (keeps it out of the process list). |
-rules-dir | MAILSTRIX_RULES_DIR | none | Directory of *.yar/*.yara to compile at start. |
-rules | MAILSTRIX_RULES | none | Precompiled .yac bundle; wins over -rules-dir. |
-cache-dir | MAILSTRIX_CACHE_DIR | none | Writable dir for the live bundle; seeded from -seed-rules when empty/unreadable. |
-seed-rules | MAILSTRIX_SEED_RULES | none | Baked read-only .yac used to (re)seed the cache. |
-verbose | MAILSTRIX_VERBOSE | off | Per-request logging. |
-log-stdout | MAILSTRIX_LOG_STDOUT | off | Info/access logs to stdout; errors stay on stderr. |
strix-scan — the CGO-free client
A tiny client with no libyara dependency, for the Sieve/LDA path and for boxes that can't link a C library. It reads a file argument (or stdin), POSTs it to a running strixd, and signals the verdict through its exit code — 0 = clean/no-match (and on a transport error when -fail-open), non-zero = match or hard error.
strix-scan [flags] [file|-]
| Flag | Default | Purpose |
|---|---|---|
-url | — (required) | Base URL of strixd, e.g. http://127.0.0.1:8079 (MAILSTRIX_URL). |
-token-file | none | File holding the shared secret (preferred — not in the process list). Falls back to MAILSTRIX_TOKEN. |
-token | none | Shared secret inline. Optional; omit for a token-less strixd. |
-filename | none | Attachment name, sent as X-MAILSTRIX-Filename so name/extension-keyed rules fire. |
-timeout | 10s | HTTP request timeout. |
-max-body | 8 MiB | Max bytes read from input; oversized fails open without scanning a truncated prefix. |
-fail-open | true | On a transport/HTTP error treat the message as CLEAN (exit 0) so a scanner outage never blocks delivery. =false surfaces the error (exit 2). |
-quiet | false | Print nothing on a match — rely on the exit code only. |
-version | — | Print version and exit. |
Environment variables
Every MAILSTRIX_* knob, grouped. Booleans accept any non-empty value as true. Durations like the refresh/TTL/timeout values are seconds unless a serve flag accepts Go duration syntax (500ms, 2s). All values are clamped to safe ranges at startup, so a non-positive value cannot disable a guard.
Server & admission
| Variable | Default | Meaning |
|---|---|---|
MAILSTRIX_HOST | all ifaces | HTTP bind host. |
MAILSTRIX_PORT | 8079 | HTTP bind port. |
MAILSTRIX_ICAP_ADDR | disabled | When set, also start an ICAP listener on this address. |
MAILSTRIX_MAX_BODY | 8388608 (8 MiB) | Max request body in bytes. |
MAILSTRIX_MAX_CONCURRENT | CPU count | CPU gate — simultaneous libyara scans. |
MAILSTRIX_MAX_INFLIGHT | 0 → 2× concurrent | Admission gate — in-flight buffers held for the whole request. |
MAILSTRIX_BACKEND_TIMEOUT | 1s | Whole-request backend budget. |
MAILSTRIX_SCAN_TIMEOUT | 8s | Per-scan libyara timeout. |
MAILSTRIX_PPROF | off | Enable the net/http/pprof endpoints (debug only). |
Effort
| Variable | Default | Meaning |
|---|---|---|
MAILSTRIX_EFFORT | auto / max | Extraction depth: 1 = raw bytes + shallow, higher = deeper recursion, auto derives it from admission-gate pressure. Per-request override via the header. |
MAILSTRIX_EFFORT_MAX | build default | Ceiling the effort level (and the X-MAILSTRIX-Effort header) is clamped to. |
Authentication
| Variable | Default | Meaning |
|---|---|---|
MAILSTRIX_TOKEN | none | Shared secret for /scan. Empty = token-less (open) backend. |
MAILSTRIX_TOKEN_FILE | none | Read the secret from a file (preferred; not in the process list). |
MAILSTRIX_TOKEN_NEXT | none | Second valid token, for zero-downtime rotation — both accepted during the overlap. |
MAILSTRIX_METRICS_AUTH | off | Require the token on /metrics too (otherwise metrics are open). |
Rules & cache
| Variable | Default | Meaning |
|---|---|---|
MAILSTRIX_RULES | none | Precompiled .yac bundle (wins over RULES_DIR). |
MAILSTRIX_RULES_DIR | none | Directory of source rules to compile at start. |
MAILSTRIX_RULES_URL | none | Bundle URL for fetch-rules (the daily rules-current release). |
MAILSTRIX_CACHE_DIR | none | Writable dir for the live bundle; self-heals from the seed. |
MAILSTRIX_SEED_RULES | none | Baked read-only .yac used to (re)seed the cache. |
MAILSTRIX_RULES_MAX_AGE | 0 (off) | Mark the bundle stale after N seconds; stale → /ready reports 200 but logs a warning. |
MAILSTRIX_BIGFILE_RULES | none | Separate bundle applied only to inputs over the threshold. |
MAILSTRIX_BIGFILE_THRESHOLD | 6291456 (6 MiB) | Size above which the big-file bundle is used. |
MAILSTRIX_RULE_ALLOWLIST | none | Only these rules may produce a verdict (tags-only). |
MAILSTRIX_RULE_DENYLIST | none | Inline denylist of rule names to suppress. |
MAILSTRIX_DENYLIST_FILE | none | File of denylisted rule names; reloaded on SIGHUP. |
MAILSTRIX_CANARY | off | Shadow mode — tag every match mailstrix_canary=1 instead of acting; for safe rollout. |
Verdict cache
| Variable | Default | Meaning |
|---|---|---|
MAILSTRIX_CACHE_SIZE | 65536 | In-memory L1 verdict entries (LRU+TTL). |
MAILSTRIX_CACHE_TTL | 3600s | Verdict cache TTL. |
MAILSTRIX_REDIS_URL | none | Optional L2 verdict cache, shared across replicas. |
MAILSTRIX_REDIS_PREFIX | default | Key prefix for the Redis L2 cache. |
Threat-intel feeds (abuse.ch)
All feeds are optional and refresh in the background. Refresh values are seconds.
| Variable | Default | Meaning |
|---|---|---|
MAILSTRIX_URLHAUS_KEY | none | URLhaus auth key; enables malware-URL lookups on extracted links. |
MAILSTRIX_URLHAUS_MAX_URLS | 64 | Max URLs checked per message. |
MAILSTRIX_URLHAUS_REFRESH | 21600s | Feed refresh interval. |
MAILSTRIX_MBAZAAR_KEY | none | MalwareBazaar key; enables attachment SHA-256 lookups. |
MAILSTRIX_MBAZAAR_FEED | none | MalwareBazaar feed source/path. |
MAILSTRIX_MBAZAAR_REFRESH | 86400s | Feed refresh interval. |
MAILSTRIX_THREATFOX_KEY | none | ThreatFox key; IOC URL lookups. |
MAILSTRIX_THREATFOX_MAX_URLS | 64 | Max URLs checked per message. |
MAILSTRIX_THREATFOX_REFRESH | 21600s | Feed refresh interval. |
MAILSTRIX_FEODO | off | Enable the Feodo Tracker botnet-C2 feed. |
MAILSTRIX_FEODO_REFRESH | 21600s | Feed refresh interval. |
Logging
| Variable | Default | Meaning |
|---|---|---|
MAILSTRIX_VERBOSE | off | Per-request logging. |
MAILSTRIX_LOG_STDOUT | off | Info/access logs to stdout; errors stay on stderr. |
The strix-scan client reads MAILSTRIX_URL and MAILSTRIX_TOKEN from the environment too.
HTTP API
The serve backend exposes five routes. Only /scan (and optionally /metrics) require the token; the health probes are always open so orchestrators can probe without a secret.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /scan | token | Scan the raw request body; returns the JSON verdict. |
GET | /metrics | optional | Prometheus exposition — scans, matches, errors, cache hits, feed stats. |
GET | /health | open | Liveness — the process is up. |
GET | /ready | open | Readiness — rules loaded (warns, not fails, on a stale bundle). |
GET | /version | open | Build, libyara and loaded-bundle identity. |
# scan a file, token in a Bearer header
curl -sS -X POST http://127.0.0.1:8079/scan \
-H "Authorization: Bearer $MAILSTRIX_TOKEN" \
-H "X-MAILSTRIX-Filename: invoice.docm" \
--data-binary @invoice.docm
Request headers
| Header | Purpose |
|---|---|
Authorization: Bearer <token> | Shared secret. The X-MAILSTRIX-Token header is accepted as an equivalent. |
X-MAILSTRIX-Token | Alternative way to pass the shared secret. |
X-MAILSTRIX-Filename | Attachment/message name, mapped to the YARA filename/extension externals so name-keyed THOR/Loki rules fire. |
X-MAILSTRIX-Effort | Per-request extraction depth, clamped to [1, EFFORT_MAX]. Overrides MAILSTRIX_EFFORT=auto. |
X-MAILSTRIX-Cache | Per-request cache control (e.g. bypass the verdict cache for a re-scan). |
JSON verdict
A 200 with a matches array. The array is empty, never null, on a clean message, so a caller can branch on length alone. Each match names the rule and the source ruleset namespace, plus any tags and metadata.
{
"matches": [
{
"rule": "SUSP_VBA_Shell_Exec",
"namespace": "sigbase-gen_office.yar",
"tags": ["macro", "exec"],
"meta": { "author": "Florian Roth", "score": "75" }
}
]
}
| Field | Meaning |
|---|---|
rule | The YARA rule name that fired. |
namespace | Source ruleset file (which set fired, not just the rule). Empty for synthetic matches such as URLhaus. |
tags | Rule tags (omitted when none). |
meta | Rule metadata key/value map (omitted when none). |
A scan error, timeout or libyara panic returns an empty matches array — Mailstrix fails open by design, so the scanner can never become a mail-delivery outage. The error is still counted in /metrics.
Rules & bundles
Mailstrix doesn't author rules — it packages roughly 10,000 public rules from eight curated upstreams, precompiled to a .yac bundle and rebuilt daily. The full list and per-set licenses are on the front page FAQ.
- Each rule file compiles into its own libyara namespace named after the file, so the verdict's
namespacetells you which ruleset fired. - A writable
CACHE_DIRholds the live bundle and self-heals fromSEED_RULESon a fresh deploy;fetch-rulespulls the dailyrules-currentrelease into it. - Any upstream can be pinned or disabled at build time with a per-source build arg (e.g.
DIDIER=0). - Allowlist/denylist narrow the active set without recompiling; the denylist reloads on
SIGHUPwith no downtime.
Deploy quickstarts
Docker
docker run -d --name mailstrix \
-p 127.0.0.1:8079:8079 \
-e MAILSTRIX_TOKEN_FILE=/run/secrets/mailstrix \
eilandert/mailstrix:latest
strix-scan from a Sieve/LDA pipe
strix-scan -url http://127.0.0.1:8079 \
-token-file /etc/mailstrix/token \
-filename "$ATTACHMENT_NAME" message.eml
# exit 0 = clean, non-zero = match
The five integration paths (rspamd, SpamAssassin, Sieve, ICAP, standalone) each have a ready-made config under contrib/; a Debian package, a Helm chart and full deployment docs live in the GitHub repository.