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. |
strix-milter — the Postfix/Sendmail front end
A second CGO-free binary that runs on the MTA host, buffers each message, POSTs it to strixd and stamps the verdict as a header. It always accepts: the MTA's milter_header_checks turns the verdict into policy, so a scanner outage can never block mail.
strix-milter -url http://127.0.0.1:8079 -listen inet:127.0.0.1:8081
| Flag | Default | Purpose |
|---|---|---|
-url | — (required) | Base URL of strixd (MAILSTRIX_URL). |
-listen | inet:127.0.0.1:8081 | inet:HOST:PORT or unix:/path.sock (MAILSTRIX_MILTER_LISTEN). Keep it on loopback or a unix socket: anyone who can reach it can have messages scanned. |
-token-file | none | File holding strixd's shared secret, keeping it out of the process list. |
-timeout | 20s | Hard per-message deadline. On expiry the message is accepted as unknown. |
-max-body | 8 MiB | A larger message is accepted unscanned rather than scanned as a truncated prefix, which would be a silent miss. |
-max-conns | 64 | Max concurrent MTA connections. Bounds memory at roughly max-conns × max-body × 2, so a flood of large messages cannot OOM the filter. |
-cape-policy | static-only | Delivery always follows the current static verdict; the CAPE adapter is report-only. Any other value is rejected at startup. |
-log-clean | off | Log clean verdicts too (noisy). |
It stamps X-Mailstrix-Status, X-Mailstrix-Rules and X-Mailstrix-Family. Hold anything called infected from /etc/postfix/milter_header_checks.
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_ICAP_MAX_CONNS | auto (8× in-flight) | Max live ICAP connections, capped at accept(). Over the cap the server answers 503 and hangs up, so a slow-loris pool cannot accumulate goroutines or fds. |
MAILSTRIX_CLAMD_UNIX_PATH | disabled | Absolute socket path for the clamd INSTREAM listener. |
MAILSTRIX_CLAMD_TCP_ADDR | disabled | Explicit host:port for clamd over TCP. A wildcard host needs MAILSTRIX_ALLOW_WILDCARD_BIND=1. |
MAILSTRIX_CLAMD_MAX_CONNS | 64 | Shared Unix/TCP connection cap, range 1–1024. |
MAILSTRIX_ALLOW_WILDCARD_BIND | 0 | Permit a wildcard (all-interface) ICAP or clamd TCP bind. These listeners do not check the /scan token, so keep them on a trusted network. |
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. |
MAILSTRIX_ARCHIVE_PW (or _FILE) | 0 (off) | Opt-in. Try to decrypt password-protected archive members (zip ZipCrypto and WinZip-AES, 7z, rar) with candidate passwords scraped from the mail subject, body and filename, plus an optional wordlist, then scan the recovered payload. |
MAILSTRIX_CAPE_CONFIG_FILE | disabled | Absolute path to the CAPE adapter's JSON config. With no file, no CAPE listener, state or scheduler is created. |
MAILSTRIX_CAPE_POLICY | static-only | The only accepted value. Mail always follows the static verdict, so a pending detonation never holds, quarantines or tempfails a message. Anything else exits 2 at startup. |
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. |
MAILSTRIX_RULES_POLL_INTERVAL | 86400 (image/deb) | Seconds between automatic verified update and reload. Explicit 0 disables it; an enabled interval must be at least 60 seconds. |
MAILSTRIX_RULES_FETCH_TIMEOUT | 300 | Seconds; shared deadline for the automatic update's network and cache-lock waits. |
MAILSTRIX_RULES_EXTRA_SIGNING_KEYS | — | Comma-separated extra trusted manifest signing public keys (raw 32-byte ed25519, base64). Additive only — the keys built into the binary stay trusted and cannot be removed, and verification cannot be disabled. |
MAILSTRIX_RULES_ALLOW_HTTP | off | Permit a plain-http rules URL. https is required otherwise, and https-to-http redirects are always refused. |
MAILSTRIX_RULES_ALLOW_COUNT_DROP | off | Install a verified bundle carrying zero rules, or under 50% of the current count. Refused by default. |
MAILSTRIX_PROFILE | mail | How much of the fetched rule breadth is baked. mail drops only rules that can never fire on a mail attachment's bytes. |
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. |
MAILSTRIX_REDIS_MAC_KEY (or _FILE) | — | Optional HMAC-SHA256 key (≥ 32 bytes) over Redis L2 values, bound to the Redis key. A missing or bad MAC is a cache miss, so a tampered or replayed value is never trusted. Roll it out on every replica. |
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.