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.
0.2.2
2026-06-06 stable sha · ced6e0eStabilization 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, populatedTotal bans/Jailsrollups, and aDEGRADEDstate. The SOURCE label comes from the live source that owns the jail, so it can't disagree with runtime behavior — a journald-onlysshdjail reportsjournald (sshd), not theauth.logit 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
DEGRADEDhonestly (ENH-004) — a structural was-attached→detached signal, so quiet, rotating, and not-yet-created logs never false-flag as unhealthy. ced6e0e - state
Total bansis now a persisted per-jail lifetime counter (BUG-006). It had been a per-process count that reset on restart and could read less thanActiveonce restored bans came back. It now persists (state v2→v3, back-compat seeded), counts new bans only, and stays≥ Activeacross restarts;/metricsand 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 indrainAck, and a clean fail-closed exit with no error-return-trace (SYS-014 #2). ced6e0e - memory Removed the dead
BudgetAllocator/MemoryPoolbyte-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 · fe8a332Patch 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-clientreported0.1.1.build.zignow holds a singlefail2zig_versioninjected into both binaries via a generatedbuild_optionsmodule; a regression test pins them to the same source. fe8a332 - cli
fail2zig-client versionnow shows the daemon version (BUG-005). The daemon emittedversionwhile the client parseddaemon_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
nftabsent (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'sServer listening on 0.0.0.0on restart banned0.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 nativegh release create, per-job least-privilege permissions (SEC-011). 2de416e - ipc Socket and
/run/fail2zigare chowned toroot:fail2zig(dir 0750, socket 0660) sofail2zig-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 defaultauto) to read auth events from the systemd journal instead of a text logpath — the modern-distro path where/var/log/auth.logdoesn't exist.autoresolves 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-jailaction+enforcinginjails— 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
banactionis now honored at runtime (SYS-016). Alog-onlyjail records a would-ban + metrics but no longer mutates the firewall or emitsip_banned; previouslybanactionwas parsed and then ignored, solog-onlyjails created real bans. 2de416e - install Fresh-install onboarding (SYS-015 pre-flight):
--validate-configno 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-dependenttimevalcasts corrected.mips-linux-muslremains blocked on a Zig libc limitation (SYS-013). 2de416e
0.1.1
2026-05-21 stable sha · 10a640dPatch 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_incrementare now actually applied. In v0.1.0 they were parsed and accepted but silently ignored — every jail received[defaults]. Per-jailStateTrackerinstances 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_multiplierandbantime_increment_factor. Both fields aref64in the schema, but the parser was reading them as integers —factor = 1.5for a softer escalation curve now works. 2c5ca20
0.1.0
2026-04-24 stable sha · 1340caeFirst 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.
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
FailingAllocatorec31dee - 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/fail2banpipeline · 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.shone-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)aroundbind()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
FailingAllocatorto surface unbounded allocation asOutOfMemory8bfc74f - systemd Hardened unit:
CAP_NET_ADMIN + CAP_DAC_READ_SEARCHonly ·ProtectSystem=strict·NoNewPrivileges· seccomp filter ·systemd-analyze security2.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.dispatchFrameon 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_incrementnow wired from[defaults]to the state tracker (was silently ignored) 3d866c7 - ws Broadcast wiring + event schema aligned to the documented
type / ts / payloadenvelope; dashboard counters now tick in real time 4d75186 - ipc ISSUE-003 — daemon and client socket-path defaults unified on
/run/fail2zig/fail2zig.sock2130f37 - build ISSUE-004 —
@bitCastsize mismatch (i128 → u64) resolved via@truncate2130f37