Single binary.
Zero dependencies.
Real intrusion prevention.
Predictable behavior under hostile input.
Less runtime. Smaller TCB. Same job.
fail2zig keeps the operational model familiar while cutting runtime bulk, bounding memory use, and moving parsing into a smaller, more explicit execution path.
Smaller attack surface
A single static Zig binary versus a Python runtime plus CPython, the standard library, and C extension modules. A smaller trusted computing base means fewer places for bugs to hide.
Bounded memory
A configurable ceiling bounds the state tracker — the dominant consumer — regardless of attack volume. When it fills, the oldest un-banned entry is evicted and active bans are kept. No garbage collector in the loop.
Higher throughput
Comptime-generated parsers with a zero-allocation hot path. Measured at 5.96M lines/sec — far beyond what busy SSH, postfix, and nginx log streams produce.
Instant readiness
No interpreter startup, no import chain. The daemon is accepting log events within milliseconds of exec. After a reboot, the gap where nothing is banning is as short as the kernel allows.
Runs anywhere
Static musl binary. Works on distroless containers, scratch images, hardened minimal servers, embedded Linux, OpenWrt routers — anywhere a Python runtime is unacceptable.
Clean config
Native TOML with strict validation — unknown keys are errors, not silent no-ops. --import-config migrates your existing fail2ban tree, translating jails and filters automatically.
Measured, not marketing.
fail2zig's figures are measured on real hardware under real load — the benchmark suite ships with the source and is reproducible. Competitor figures are approximate, shown for order-of-magnitude context.
Parse throughput · auth.log
ReleaseSafe build,
tests/benchmark/parse_throughput.zig; bars scale linearly. Competitor rates
are approximate, for context. The benchmark suite ships with the source.
Memory under attack · 50K unique IPs · sustained
Six components. One binary. Every allocation accounted for.
A straight pipeline: log in, ban out. Explicit allocator boundaries at every stage. No hidden work, no implicit runtime, no third-party code in the critical path.
Event-driven, rotation-aware
inotify for log files, plus the systemd journal read through a journalctl poll. Handles rename, truncate, and new-file creation without missing events.
Comptime filters, zero-copy
Built-in filters compile to specialised parsers at build time — no runtime regex engine in process. Scalar, single-pass IP extraction, bounds-checked on every byte.
Entry-capped, never resizes
A ceiling-derived entry cap, allocated once and never resized under load. When it fills, the oldest un-banned entry is evicted and active bans are kept. Tracked memory is bounded regardless of attack volume.
Pluggable firewall backends
nftables via direct netlink (default), with iptables and ipset as fallbacks. eBPF/XDP is planned for Phase 2 — dropping packets at the NIC driver level before they reach the kernel TCP stack.
Native + fail2ban-compatible
Native fail2zig.toml for new deployments. A dedicated parser reads jail.conf,
jail.local, jail.d, and filter.d. The --import-config flag converts your existing fail2ban tree.
Prometheus, out of the box
Built-in HTTP server — bind-restricted, read-only. Prometheus /metrics, a JSON /api/status endpoint, and a WebSocket event stream for bans and unbans.
How it compares.
Same category, different tradeoffs. fail2zig ships as a static binary, stays local-first, and avoids cloud dependency in the critical path.
| Language | Deployment | Banning | Throughput | Footprint | |
|---|---|---|---|---|---|
| fail2ban | Python 3 | pkg + runtime | iptables / nftables | ~hundreds l/s | GC · unbounded |
| SSHGuard | C | single binary | pf / iptables / nftables | medium | small, SSH-focused |
| CrowdSec | Go | binary + cloud API | iptables / nftables | medium-high | heavy · cloud dep |
| fail2zig | Zig | static binary, musl | nft · iptables · ipset · XDP (ph.2) | 5.96M l/s (measured) | fixed ceiling · 0 deps |
Drop-in migration, two commands.
Fetch the binary, import the existing config, swap the service. That is the migration path.
$ curl -fsSL https://fail2zig.com/install.sh | sh
# 2. migrate from fail2ban, review the generated config
$ fail2zig --import-config /etc/fail2ban/
→ translated 14 jails · 2 warnings · wrote /etc/fail2zig.toml
# 3. swap the service
$ systemctl disable --now fail2ban
$ systemctl enable --now fail2zig
Reasonable skepticism, addressed.
Why Zig? Why not Rust?
Zig gives us explicit allocator control end-to-end, comptime code generation for built-in filters, trivial cross-compilation to musl targets, and a language small enough to audit. Rust would work. Zig fits the problem.
Is this really a drop-in replacement?
fail2zig ships 15 built-in filters covering the most common services, and its import tool translates a fail2ban jail.conf + filter.d tree. Custom Python filter code and sendmail actions are explicitly out of scope — they need to be rewritten or replaced.
Can attacker-controlled log lines crash or OOM the daemon?
That's the threat model we designed to. Bounds-checked parsing with a strict line-length limit, no regex engine in process, a state tracker bounded by a hard entry cap, and fuzz-tested parsers. Full write-up →
What about distroless / scratch containers?
First-class target. The binary is static, musl-linked, and has zero runtime dependencies. Drop it into a scratch image, give it CAP_NET_ADMIN, done.
Is this a SIEM or a WAF?
Neither. It reads logs, acts on them, and gets out of the way. It does not aggregate, correlate, or inspect HTTP bodies.
Read the code before it reads your logs.
Small enough to audit. Fast enough to build. Simple enough to ship.