Single binary.
Zero dependencies.
Real intrusion prevention.

Predictable behavior under hostile input.

5.96M
lines/sec parsed
932ns
ban decision p99
877KB
static binary
0
runtime deps
01 · Why it’s different

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.

§ 01

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.

~12K LOC · no interpreter · no VM
§ 02

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.

hard ceiling · configurable · eviction policy
§ 03

Higher throughput

Comptime-generated parsers with SIMD acceleration. Measured at 5.96M lines/sec — keeping pace with busy SSH, postfix, and nginx log streams.

5.96M l/s measured · SIMD · zero-alloc hot path
§ 04

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.

no import cost · no rule-compile cost
§ 05

Runs anywhere

Static musl binary. Works on distroless containers, scratch images, hardened minimal servers, embedded Linux, OpenWrt routers — anywhere a Python runtime is unacceptable.

musl · 4 architectures · no runtime
§ 06

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.

validated · documented · import path
02 · Benchmarks

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

fail2ban
~420 l/s
SSHGuard
~3.1k l/s
CrowdSec
~4.9k l/s
fail2zig
5.96M l/s
Measured with a ReleaseSafe build, tests/benchmark/parse_throughput.zig. Bars scale linearly. Reproduction guide in tests/benchmark/README.md.

Memory under attack · 50K unique IPs · sustained

fail2ban
~740 MB
CrowdSec
~210 MB
SSHGuard
~58 MB
fail2zig
≤64 MB
Hard ceiling, configurable. Under a 50K unique-IP attack: 21,845 resident entries + 15,606 evicted. The daemon never exceeded the cap. Eviction policy is operator-defined.
03 · Architecture

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.

01 / LOG WATCHER

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.

linux · bsd · journald · stdin
02 / PARSER ENGINE

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.

no pcre · no allocations/line · simd
03 / STATE TRACKER

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.

evict-oldest · ban-all · drop-unbanned
04 / BAN EXECUTOR

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.

nftables · ipset · xdp · pf
05 / CONFIG

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.

validated · documented · hot-reload (ph.2)
06 / METRICS

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.

/metrics · structured · signed-audit
04 · Landscape

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
05 · Install

Drop-in migration, two commands.

Fetch the binary, import the existing config, swap the service. That is the migration path.

terminal · linux/x86_64
# 1. fetch the static binary (877 KB, musl-linked)
$ 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
targets · stripped · static via github releases
x86_64-linux-musl
877 KB
aarch64-linux-musl
stripped
armv7-linux-musleabihf
stripped
mips-linux-musl
stripped
x86_64-freebsd
planned
aarch64-openbsd
planned
reproducible builds · checksums signed with cosign · minisign key in-repo
06 · FAQ

Reasonable skepticism, addressed.

Q.01

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.

Q.02

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.

Q.03

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 →

Q.04

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.

Q.05

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.

Star on GitHub  → Read the docs