Changelog

Everything that shipped,
in the order it shipped.

First public release was v0.1.0 (2026-04-24). Latest is v0.2.2 (2026-06-06). Each entry lists scope, behavior change, and the commit. Benchmark deltas where the change touched a hot path.

git · github.com/ul0gic/fail2zig latest release · v0.2.2 on GitHub format · keep a changelog

0.2.2

2026-06-06 stable sha · ced6e0e

Stabilization release. Honest per-jail status — each jail now reports the log source it actually reads and its read-health — plus a real protection fix found on a live box: file jails sharing a directory could silently stop banning after a log rotation. Drop-in binary replacement; state file v2 auto-migrates to v3 on first checkpoint.

Security

  • watcher Multiple file jails watching the same directory could silently stop banning after the first log rotation — a real protection failure (BUG-007). inotify dedups the parent-directory watch, but the watch table mapped one directory to one jail, so after a rotate the other jails stopped consuming events. Affects v0.2.1 and earlier; journald sources were never affected. Disclosed via a release-notes security callout; an e2e harness on a real host now locks it in. ced6e0e

Features

  • status Honest per-jail capability and health surface (SYS-017). status, /api/status, /metrics, and the WebSocket stream now report each jail's resolved log source and read-health, populated Total bans / Jails rollups, and a DEGRADED state. The SOURCE label comes from the live source that owns the jail, so it can't disagree with runtime behavior — a journald-only sshd jail reports journald (sshd), not the auth.log it never reads, and a file jail with an absent log shows its configured path so a typo stays visible. Observability only; banning logic unchanged. 56cdd51

Performance

  • startup Cold start down to ~20 ms (was ~98 ms) via lazy state-tracker initialization (QA-002) — the ceiling-derived entry table is allocated on first use rather than at boot. ced6e0e

Fixes

  • health File-source read-health now drives DEGRADED honestly (ENH-004) — a structural was-attached→detached signal, so quiet, rotating, and not-yet-created logs never false-flag as unhealthy. ced6e0e
  • state Total bans is now a persisted per-jail lifetime counter (BUG-006). It had been a per-process count that reset on restart and could read less than Active once restored bans came back. It now persists (state v2→v3, back-compat seeded), counts new bans only, and stays ≥ Active across restarts; /metrics and the human surface read the same value. 56cdd51
  • firewall Backend-unavailable errors now carry cause-distinct messages (no kernel nf_tables / missing CAP_NET_ADMIN / transient), a batch-error fix in drainAck, and a clean fail-closed exit with no error-return-trace (SYS-014 #2). ced6e0e
  • memory Removed the dead BudgetAllocator / MemoryPool byte-budget scaffolding (DBT-004) — the state-tracker entry cap with eviction is the sole memory-ceiling mechanism, and the docs now say exactly that. ced6e0e

0.2.1

2026-05-31 stable sha · fe8a332

Patch release. Version reporting is now single-sourced and consistent across every surface, and the memory-ceiling and nftables-userspace documentation was reconciled with what the daemon actually does.

Fixes

  • version Version reporting is consistent across every surface (BUG-004). The string had been hardcoded in four places that drifted — the shipped v0.2.0 fail2zig-client reported 0.1.1. build.zig now holds a single fail2zig_version injected into both binaries via a generated build_options module; a regression test pins them to the same source. fe8a332
  • cli fail2zig-client version now shows the daemon version (BUG-005). The daemon emitted version while the client parsed daemon_version, so the daemon line was silently dropped; the field name is now contract-pinned by a test. fe8a332
  • docs Memory-ceiling documentation reconciled with reality (DBT-003): the ceiling bounds the state tracker via a ceiling-derived entry cap with eviction, the parser hot path is zero-alloc — not a per-component byte budget. Also documented that the nftables userspace package is not required; fail2zig programs nf_tables directly via netlink, verified on a box with nft absent (SYS-014). fe8a332

0.2.0

2026-05-31 stable sha · 2de416e

The enforcement-honesty release. The daemon now actually applies each jail's configured filter (a critical false-ban fix), reads sshd events from the systemd journal on modern distros that have no auth.log, honors log-only jails, and ships SLSA build provenance on every binary.

Security

  • filter SYS-020 (CRITICAL) — the runtime now applies each jail's configured filter. It had been matching every jail with a permissive default (<*><IP> — "any line containing an IP"), so benign IP-bearing lines could ban: sshd's Server listening on 0.0.0.0 on restart banned 0.0.0.0/::, and a successful login counted as an attempt. Unknown filters now fail closed; a guard never bans unspecified/loopback addresses. 2de416e
  • release SLSA build-provenance attestation on every released binary (verifiable with gh attestation verify), plus supply-chain hardening of CI: every Action SHA-pinned, the one third-party release action replaced with native gh release create, per-job least-privilege permissions (SEC-011). 2de416e
  • ipc Socket and /run/fail2zig are chowned to root:fail2zig (dir 0750, socket 0660) so fail2zig-group members use the client without root, matching the SO_PEERCRED model; degrades safely when the group is absent (SYS-018). 2de416e

Features

  • source journald log source for sshd jails (SYS-015). A jail can set source = journald (or leave the default auto) to read auth events from the systemd journal instead of a text logpath — the modern-distro path where /var/log/auth.log doesn't exist. auto resolves by file existence, so the default sshd jail protects out-of-box on a journald-only host. 2de416e
  • status Enforcement posture in status (SYS-017 v1): Protection: active | log-only | mixed, and per-jail action + enforcing in jails — computed from the same resolver the ban path uses, so it can't disagree with actual behavior. 2de416e
  • release armv7 (arm-linux-musleabihf) release binaries now cross-compile and build in the CI + release matrices (SYS-009). 2de416e

Fixes

  • action banaction is now honored at runtime (SYS-016). A log-only jail records a would-ban + metrics but no longer mutates the firewall or emits ip_banned; previously banaction was parsed and then ignored, so log-only jails created real bans. 2de416e
  • install Fresh-install onboarding (SYS-015 pre-flight): --validate-config no longer hard-fails on a missing runtime socket dir (created at startup), which broke the documented quickstart on every fresh box. Verified on a clean Debian 13 install. 2de416e
  • build 32-bit ARM cross-compile fixed (SYS-009 / #16): metrics u64 counters use a conditional Counter (lock-free on 64-bit, mutex-guarded on 32-bit), and width-dependent timeval casts corrected. mips-linux-musl remains blocked on a Zig libc limitation (SYS-013). 2de416e

0.1.1

2026-05-21 stable sha · 10a640d

Patch release. Two correctness fixes for behavior the v0.1.0 schema documented but the daemon didn't actually deliver. Drop-in binary replacement — state file v1 auto-migrates to v2 on first checkpoint, no data loss.

Fixes

  • state Per-jail maxretry · findtime · bantime · bantime_increment are now actually applied. In v0.1.0 they were parsed and accepted but silently ignored — every jail received [defaults]. Per-jail StateTracker instances now route matches through the right thresholds. State file format bumped to v2; v1 files migrate on first checkpoint. 7f4b486
  • config TOML parser now accepts fractional values for bantime_increment_multiplier and bantime_increment_factor. Both fields are f64 in the schema, but the parser was reading them as integers — factor = 1.5 for a softer escalation curve now works. 2c5ca20

0.1.0

2026-04-24 stable sha · 1340cae

First public release. A single static musl binary replaces fail2ban's Python runtime, regex engine, and shell-out action chain. Hardened across eleven Phase-7 security issues and eight real-system SYS bugs before tagging. AGPL-3.0-or-later.

Parse throughput
5.96M l/s
271× target
Ban latency p99
932 ns
p50 365 ns
Binary (musl x86_64)
877 KB
stripped
Resident memory
22 MB
64 MB ceiling

Features

  • engine Single static musl binary · zero runtime dependencies · 877 KB stripped 5a85cc2
  • core epoll / timerfd / signalfd event loop · inotify log watcher with rotation + copytruncate handling 98cd331
  • parser Comptime-specialized parsers · no regex engine in the process · zero-allocation hot path verified with FailingAllocator ec31dee
  • firewall Pure netlink for nftables (no libnftnl, no shell-out) · argv for iptables + ipset ec31dee
  • state Fixed-arena state tracker with three eviction policies · atomic CRC-checked persistence · per-IP 128-slot ring buffer a9e77dc
  • config Native TOML parser + full --import-config /etc/fail2ban pipeline · translates filter.d regexes · maps action.d backends · emits validated TOML 3af15fd
  • filters 15 built-in comptime filters: sshd, nginx-*, apache-*, postfix / dovecot / courier, named, vsftpd, proftpd, mysqld, recidive 3af15fd
  • cli fail2zig-client — status / ban / unban / list / jails / reload · table, JSON, plain output · bash / zsh / fish completions 2130f37
  • metrics Built-in HTTP/1.1 server · /metrics (Prometheus), /api/bans (JSON), /events (WebSocket, RFC 6455) 2130f37
  • demo Live internet-exposed honeypot at fail2zig.com/demo · real sshd + nginx attack surface · three-pane dashboard 1803321
  • release Static musl binaries for x86_64 + aarch64 · SHA256SUMS · hardened systemd unit · scripts/install.sh one-shot installer 1340cae

Security

  • parser SEC-001 (HIGH) — IPv4-mapped IPv6 canonicalization closes ban-evasion by peer-format flipping 8bfc74f
  • ipc SEC-002 (MEDIUM) — umask(0o117) around bind() closes the socket-permissions race window 8bfc74f
  • http SEC-005 / SEC-008 — security headers on every response · 5s per-client read deadline · 100/sec accept rate cap 8bfc74f
  • fuzz Fuzz corpora for parser · IP parser · IPC protocol · TOML config — all with FailingAllocator to surface unbounded allocation as OutOfMemory 8bfc74f
  • systemd Hardened unit: CAP_NET_ADMIN + CAP_DAC_READ_SEARCH only · ProtectSystem=strict · NoNewPrivileges · seccomp filter · systemd-analyze security 2.4 7df630f
  • license AGPL-3.0-or-later · trademark asserted on "fail2zig" name + logo · vulnerabilities reported via GitHub Private Security Advisories ef16d7c

Performance

  • bench Parse throughput 5.96M lines/sec (271× the 22K target) · ban latency p50 365 ns / p99 932 ns (1000× under the 1ms target) 8bfc74f
  • memory Hard memory ceiling held across 50K unique IPs: 21,845 entries resident, 15,606 evictions — ceiling never exceeded 8bfc74f
  • binary Static musl binary 877 KB (x86_64) / 801 KB (aarch64) — well under the 5 MB ceiling 1340cae

Fixes

  • ws SYS-010 (CRITICAL) — use-after-free in ws.dispatchFrame on close / binary frames b101619
  • filter SYS-011 (HIGH) — sshd filter matched clean disconnects, causing operator self-ban during honeypot bring-up b101619
  • state SYS-007 (HIGH) — restored bans on restart now re-installed into the firewall; previously the state tracker held them but the kernel didn't enforce dc916a9
  • state SYS-008 (HIGH) — bantime_increment now wired from [defaults] to the state tracker (was silently ignored) 3d866c7
  • ws Broadcast wiring + event schema aligned to the documented type / ts / payload envelope; dashboard counters now tick in real time 4d75186
  • ipc ISSUE-003 — daemon and client socket-path defaults unified on /run/fail2zig/fail2zig.sock 2130f37
  • build ISSUE-004 — @bitCast size mismatch (i128 → u64) resolved via @truncate 2130f37