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
~12K lines of Zig versus ~45K lines of Python + CPython runtime + stdlib + C extensions. A smaller trusted computing base means fewer places for bugs to hide.
Bounded memory
A hard, configurable ceiling the daemon never exceeds — regardless of attack volume. Eviction policy is operator-defined. No garbage collector in the loop.
Higher throughput
Comptime-generated parsers with SIMD acceleration. Measured at 5.96M lines/sec — keeping pace with busy SSH, postfix, and nginx log streams.
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.
Every figure measured on real hardware under real load. Not synthetic, not modeled. The benchmark suite ships with the source and is fully reproducible.
Parse throughput · auth.log
ReleaseSafe build,
tests/benchmark/parse_throughput.zig. Bars scale linearly. Reproduction
guide in tests/benchmark/README.md.
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 on Linux, kqueue on BSD, sd_journal for systemd, io_uring for high-throughput batched I/O on 5.1+. 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. SIMD-accelerated IP extraction and timestamp parsing where the CPU supports it.
Fixed-size arena, hard ceiling
Pre-allocated buckets, no dynamic resizing under load, configurable max with explicit eviction policy. Memory usage has a hard cap regardless of attack volume.
Pluggable firewall backends
nftables, iptables, ipset on day one. eBPF/XDP in Phase 2 — drops packets at the NIC driver level before they reach the kernel TCP stack. pf for BSD.
Native + fail2ban-compatible
Native fail2zig.toml for new deployments. A dedicated parser reads jail.conf,
jail.local, filter.d, action.d. The --import-config flag converts your existing fail2ban tree.
Prometheus, out of the box
Built-in HTTP metrics endpoint — bind-restricted, read-only. Ban rate, parse throughput, active bans, per-jail stats. Structured JSON logs; syslog optional.
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 · ipset · eBPF/XDP · pf | 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?
Phase 1 targets jail.conf + filter.d compatibility for the top 20 fail2ban filters (95%+ of installations). 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 strict line length limits, no regex engine in process, fixed-size arena allocator with a hard ceiling, 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.