Changelog.
What changed, why it matters, and what to know before upgrading.
v0.4.7 is the minimum supported release; use v0.4.8 for new installs and upgrades. A v0.4.6 state upgrades through v0.4.7 first. Versions before v0.4.7 are retained here as history. Their releases, downloads and Git tags have been retired, and rollback to them is unsupported.
Latest release v0.4.8Capacity, log lifecycle and parity
Release notesBuilt on the report of a production install whose PortSentry jail stalled at 2,048 bans. All jails share one pool of 65,536 tracked addresses and 65,536 live bans, a missing log is told apart from a truncated one, every ban carries a visible origin, and a healthy stop leaves installed rules in the kernel.
Capacity and log lifecycle
- capacityOne shared tracking pool.65,536 tracked
addresses and 65,536 live bans across all enabled jails, not per jail. A live ban
holds no tracking entry. A jail that reaches a limit shows
waiting (RetryCapacity)injailsand resumes as room frees instead of pausing.#117 - file inputA missing log is not a truncation.A log
absent at start or deleted while the daemon runs shows
missing (FileNotFound)injails; the source stays healthy, protection is not degraded, and reading resumes when the file appears. Truncation and replacement remain continuity-loss events underon_truncation. Read the log file lifecycle →#118#107 - storageState file ceiling raised to 1 GiB.Was 256
MiB.
statusshows the file size and page use against the ceiling and the tracking pool against its limits. The file never shrinks; freed pages are reused inside it.
Configured bans and origins
- policyConfigured ban lists with a visible origin.
banipin[defaults]and per jail bans addresses and CIDRs while they are listed, applied at start and onreload. Every ban carries one origin,detection,manualorconfig, shown bylist,historyandfirewall show;list --originfilters on it.banandunbanon a config ban are refused.#103 - exclusionsTimed entries and expiring exclusions.
banipandignoreipentries take an optional duration ("198.51.100.0/24 1d") that runs once from first apply.ignore add --forrecords an exclusion that lapses on its own,ignore listshows EXPIRES, andignoretimesets the default life of an entry without one. - safetyLockout guard and
ignoreself.The host’s own addresses join every jail’s ignore list with sourceself.banrefuses a loopback, host or SSH-peer address before it opens the socket and names--forceas the override. The guard is a client-side refusal, not an ignore rule. - escalationEscalation decays.
bantime_increment_decay(default 30 days, measured from the ban’s end) resets escalation to the basebantime;0disables it. After a manualunban, failures count from zero while escalation memory is kept.
Operator commands
- repair
repair-sourceruns online.Through the daemon’s socket while it is running, with the same token and durable outcome as the offline form, so acknowledging a loss no longer withdraws any jail’s rules. - reloadJails are added and removed on
reload.An added jail starts in the same reload; a removed jail’s bans are lifted, its elements removed and its history kept. One invalid jail rejects the whole reload and leaves every running jail unchanged. - firewall
firewall withdraw.Removes every rule the daemon realized and holds enforcement off until the next start;statusreportsDEGRADED (FirewallWithdrawn). Run it before stopping when the table must be empty. - diagnostics
support-bundleand per-jail metrics.One redacted JSON file for a bug report, with addresses hashed unless--include-addressesis given. New seriesfail2zig_jail_bans_active,fail2zig_jail_bans_total,fail2zig_jail_failures_counted_total,fail2zig_jail_ignored_detections_total,fail2zig_jail_source_state,fail2zig_jail_pool_evictions_total,fail2zig_state_file_bytesandfail2zig_state_file_pages. - loggingAudit lines, JSON logs and help everywhere.One audit line per mutation with target, caller UID and outcome.
log_format = "json"writes one JSON object per daemon log line. Every command hashelp <command>or--help; a typo exits 2 with a suggestion. Refusals that are not outcome objects go to standard error. - securityThreat model published with the source.
docs/security/threat-model.mdrecords assets, trust boundaries and each threat with its control. See the site threat model →
Kernel-confirmed enforcement
- nftablesInstalls confirmed by the kernel, drift by events.Each install batch is confirmed by the kernel’s acknowledgement and drift is
detected from nf_tables change events, with a full audit of the owned set every 60
seconds instead of a readback after every mutation. Measured on
f2z-target(Debian 13, kernel 6.12, 8 vCPU, 16 GiB): idle at 65,536 installed bans fell from 21 % to 12 % of one core; with 20,000 bans, 3.5 %. - ingestGrouped commits and batched installs.Admitted
records commit in groups with one fsync and pending bans install in batches on all
three backends. Ingest through one file source went from 5 to 79 records per second
on the same host, with bans enforced inside the burst and no acknowledged record
lost or re-read after
kill -9. - restartInstalled rules survive a healthy stop.Every element carries its kernel timeout, so a stop at 65,536 bans takes about one second, and a restart reinstalls live bans in batches: readiness in 373 s at 65,536 bans on the same 8 vCPU host. These are measurements on that host, not guarantees.
Schema 26 upgradeOn first start, schema 25 upgrades to 26 in one transaction after
<state_file>.pre26-backup is written; the backup needs free space of
twice the database size plus 32 MiB and an existing backup refuses the start. Every
existing ban gets origin detection. v0.4.7 is the only supported upgrade
source; a v0.4.6 state goes through v0.4.7 first. v0.4.7 and older binaries refuse
schema 26 with UnsupportedSchema; rollback restores the backup with the
v0.4.7 binary. See the
installation guide
and upgrade guide.
Behavior changesThe default native_memory_ceiling_mb is now 512; a configuration that sets
256 explicitly must raise it or the daemon refuses with
NativeMemoryAdmission. A healthy stop leaves installed rules in the kernel
and no longer unbans anyone; run firewall withdraw first for an empty
table.
Known limitsstatus does not yet list a missing log; jails and
fail2zig_jail_source_state do. The per-ban footprint did not shrink (about
6 to 7 KiB of state per live ban). The full-pool eviction path is proven on the
component rig only. Event-driven drift detection is nftables-only; ipset and iptables
keep their readback cadence. Large curated lists are not a job for bans; a list loader
is under design in discussion #120. Live qualification is x86_64 only.
Stabilization at 2,000 bans
Release notesBuilt on the reports from a production install running 2,000 bans. Idle CPU at that size drops from about 56 % of one vCPU to under 3 %, a healthy stop takes one second instead of 49, file sources that lose data on a reboot resume under a per-jail policy, and per-jail exclusions are managed at runtime. v0.4.7 is the oldest supported release and the required hop for a v0.4.6 state before v0.4.8.
Steady-state cost
- idleIdle CPU at 2,000 bans under 3 %.The jail
view is published only when it changes, effect owners are confirmed by key lookup,
and the journal source waits one second after an empty poll instead of spawning
journalctlseven times a second. Measured on a one-vCPU, 4 GiB Debian 13 host with 2,000 nftables bans: mean 2.61 %, maximum 6.0 % over five minutes.#99#108 - stopA healthy stop takes one second.Realized rules are withdrawn in batches: about one second in 32 turns instead of 49 seconds in 2,000 at 2,000 bans.#109
Operator control
- file inputPer-jail
on_truncationpolicy.refuse(the default) pauses the source untilrepair-sourceacknowledges the loss while the daemon keeps running;restartresumes at the new start of the file and records a durable continuity-loss event. Both cover a log truncated in place and a log replaced after its previous file is gone, as PortSentry does at every start.statuslists paused sources and loss events.#98#112 - exclusionsRuntime exclusions.
ignore add,ignore removeandignore listmanage per-jail entries that survive restart and reload and merge with the configuredignoreip. An existing ban stays until its deadline unless--unbanis given. Each jail holds at most 128 entries, configuration and runtime combined.#103 - reload
ignoreipedits apply with a reload.Literal ignore-list edits no longer returnrestart_required, and a jail can be added or enabled on an existing state without recreating it.#110#113 - portsentryStealth and UDP detection qualified.PortSentry 2.0.7
--stealth -m pcap -i <interface>detection for TCP SYN, FIN, NULL and XMAS scans and for UDP, in addition to--connect, each through ban, blocking while banned and expiry. Legacy 1.2 and other 2.x versions stay unqualified. Read the setup guide →#75
Fixes
- storageCommitted receipts are cleaned behind held bans.Held receipts are set aside and deleted once their ban and retry window end, and everything behind them is cleaned in order. Long or permanent bans previously retained every receipt behind the oldest live ban until the state cap paused ingestion.#104
- enforcementAn expired lease is never dispatched.A ban whose lease had already ended was re-sent with a past deadline, refused by nftables and treated as backend uncertainty, which paused ingestion under short ban times. Such an owner now takes the expiry path.#111
- file inputA recreated log resumes.A log recreated while the daemon runs is read without loss, and the old, unlinked file retires instead of counting against the source limit.#114
- nftablesCoherent set readback.The set is read back as
one coherent netlink observation and an interrupted dump is retried instead of
trusted;
statsreports the retries.
Schema 25 upgradeOn first start, a state last written by v0.4.6 (schema 24) upgrades to schema 25 after
<state_file>.pre25-backup is written; the upgrade needs free space of
twice the database size plus 32 MiB and resumes if interrupted. v0.4.6 was the only
supported upgrade source for this release, and rollback restores the backup with the
v0.4.6 binary. Saved detection generations are rekeyed once at the first start.
Known limits at 0.4.7The state file was capped at 256 MiB (raised to 1 GiB in v0.4.8) and never shrinks. A
stopped daemon left its empty reserved nftables table, so a second installation on the
same host refused to start with ForeignState. At 2,000 bans one status
sample in 300 could report protection knowledge about five seconds old during idle;
stale status is not confirmation of current kernel protection. Admin refusals were
printed on standard output (moved to standard error in v0.4.8). Live qualification is
x86_64 only.
Clearer operator commands and output
Historical entry · downloads retiredSort ban lists numerically, inspect published runtime statistics on demand, and distinguish configured jail mode from observed protection. Command output and help are clearer.
Operator commands
- ban listsOptional numeric IP sorting.
fail2zig list --sortorders IPv4 before IPv6 in table, plain and JSON output. The default order is unchanged. - diagnosticsRuntime statistics on demand.
fail2zig statsandstats --detailsshow the worker's published snapshot, its age and counters accumulated since daemon start.
Output clarity
- jailsConfigured mode and observed protection are separate.Unknown ban counts remain unknown, and detailed jail information stays complete on narrow terminals.
- loggingQuieter routine logs.Periodic effect-view
diagnostics now log at debug level; use
statswhen needed.
UpgradeUpgrading from v0.4.5 keeps schema 24. Upgrade and restart the daemon to use
stats. See the
installation guide
and upgrade guide.
Storage recovery and protection continuity
Historical entry · downloads retiredRepairs load-triggered storage intervention, populated-state restart and recovery after a truncated file source. Expiry cleanup and firewall readback handling improve under load; reload and migration rollback preserve other protection owners. No new integrations.
Fixes
- storageIngestion continues through the reproduced load incident.Bounded retention and recovery work repaired the 199-ban storage intervention. The final short-incident qualification covered 210 bans and packet controls; it does not establish sustained-load latency.
- restartPopulated state restores original ban deadlines.Startup extends its systemd notification timeout while making progress, without reporting readiness early.
- file inputTruncated sources have an explicit recovery path.Offline
repair-sourceacknowledges a continuity break. It cannot recover log entries lost to truncation. - firewallLess repeated readback and settlement work.Expiry and cleanup work improve under load, and transient backend uncertainty no longer pauses otherwise healthy ingestion. nftables, iptables and ipset remain supported.
- ownershipReload and migration rollback retain other owners.Generation-changing reload preserves live bans and absent-owner history; migration rollback releases only its own decisions.
- CLIFirewall help lists
--details.The already-supported flag now appears infirewall --helpusage.
Schema 24 upgrade and rollbackOn first start, native schema 23 upgrades to 24 after a coherent pre-upgrade backup is created. Keep that backup with the matching old binary. Older binaries refuse upgraded state; restoring the backup loses decisions made since it was taken. Stop the daemon and follow the installation and upgrade guide. For truncated file input, use the documented fail2zig(1) repair-source procedure; missing logs cannot be recovered.
Known limitsA sustained workload on a small HDD-backed VM installed every subject but had p95 append-to-kernel latency of 57 seconds. A separate 400-ban restart hold can report stale protection knowledge and degraded status; observed runs retained 400 reported active bans and healthy storage, but the cause and full duration remain under investigation. Stale status does not confirm current kernel protection. Live-kernel release qualification is Debian 13 x86_64; the other four architectures have cross-build and emulated checks.
Recovery, enforcement and retention fixes
Historical entry · downloads retiredA bug-fix release for startup on populated state, firewall expiry readback, prompt ban reconciliation and bounded retry history. No new features or configuration options.
Fixes
- startupPopulated state recovers at startup.Removes a stale maintenance fence and page-at-a-time delay that could prevent the daemon from becoming ready.#84
- firewallExpected element expiry stays transient. Elements that expire between readback passes no longer put otherwise healthy enforcement into persistent intervention. Structural or foreign firewall changes remain unsafe.#83
- enforcementNew bans continue through reconciliation. Advances reconciliation after full readback and reclaims spent effect records after their live work is settled.#85
- storageOld retry detail can be reclaimed. Eligible history is cleared before accumulated rows block new decisions. Current owners, retained confirmations and unsettled actions stay protected.#86
Preserve a coherent rollback backupSchema remains 23 and the stored record format is unchanged. Older binaries cannot read checkpoint and marker data introduced in v0.4.3. Restore a matching binary and coherent state backup for rollback; swapping only the executable is insufficient. Read the current upgrade guide →
PortSentry scan detection and more resilient ingestion
Historical entry · downloads retiredConnect PortSentry’s scan events to durable fail2zig enforcement, keep processing malformed file logs, and avoid cleanup failures that could stop affected ingestion.
Features
- portsentryA qualified scan-detection recipe.PortSentry v2.0.7 history-file input supports TCP-connect scan detection with IPv4 and IPv6. The Debian 13 qualification covered nftables bans, expiry and restored connectivity. Ignored, pre-threshold and sensor-blocked records are excluded. Read the setup guide →#81
Fixes
- storageEscalation history cleans up in the correct order.Fixes SQLite foreign-key errors during escalation-history cleanup that could stop affected ingestion.#79
- file inputMalformed encoding no longer stalls file ingestion.Invalid bytes are replaced and processing continues, with source progress preserved and replacements reported. Journal input remains strict.#80
Preserve your rollback stateSchema remains 23, but older binaries cannot read the extended processor checkpoints and marked dispositions. Rollback requires a coherent pre-upgrade state backup and its matching binary. Swapping only the executable is insufficient. Read the current upgrade guide →
Firewall observability and clearer operator diagnostics
Historical entry · downloads retiredSee what fail2zig intended to install, what its latest firewall inspection verified, and where fail2zig-owned protection is attached.
Features
- firewall Read-only firewall observation.
firewall showreports the daemon’s latest retained inspection of fail2zig-owned kernel protection without refreshing, repairing or changing firewall state. - commands Expandable operator tables.
--detailsexpands table output forstatus,jails,listandfirewall showwith identifiers, thresholds, counters and verified structure or placement when available. Plain and JSON output are unchanged by the flag. - nftables Native kernel inspection.
nftables observation communicates directly with the kernel and does not require the
nftexecutable.
Diagnostics and maintenance
- health More stable protection-health reporting. Routine worker activity retains the last verified protection view, while actual storage, source, clock, expiry and firewall-readback faults remain visible.
- maintenance Focused cleanup and test fixes. Matching client and daemon versions render more clearly, cleanup no longer treats ignored or unenforceable evidence as retry work, and startup/import test coverage is tighter.
Observation boundary This is a bounded retained snapshot of fail2zig-owned protection in the daemon’s current network namespace—not a live ruleset scan or proof of packet reachability. A successful query means the request was answered, not that protection is healthy. Read the command reference →
Reliable SSH journal startup and better diagnostics
Historical entry · downloads retiredBuilt-in SSH journal jails can again discover trusted executable paths when no explicit profile is supplied, while status and startup failures provide more useful causes.
Fixes
- journald Trusted SSH executable discovery restored.
Built-in SSH jails use a bounded catalog of standard root-owned executable paths
when
journal_executablesis omitted. Explicit profiles remain exact overrides; custom journal rules still require one. - diagnostics Actionable startup and health causes. Status, jail, storage and firewall diagnostics retain more context, including source failures, journal child results and saved backend or namespace conflicts.
One executable. Durable native state.
Historical entry · downloads retiredDaemon and administration now share one executable. Native SQLite persistence keeps source progress and protection state durable, configuration accepts readable durations, and the service runs under a dedicated account.
Upgrade note There is no automatic converter for 0.3.0 binary state. Preserve the old installation for rollback and plan the protection transition. This is separate from supported fail2ban schema-4 migration. The installer never starts or restarts services; custom state paths require explicit handling. Read the current upgrade guide →
Features
- commands Daemon, administration and migration in one executable.
Use
fail2zig <command>instead offail2zig-client. The same binary includes offlinerule-testand the supported inspect/snapshot/plan/validate/cutover/status/rollback workflow. No separate client asset ships. - persistence Native runtime state moves to embedded SQLite. Durable receipts, source checkpoints, retries, protection ownership, effect intent and confirmed history share one store. Pinned SQLite C is statically embedded; no database server, SQLite CLI or shared SQLite library is required.
- configuration Readable configuration durations.
Use quoted values such as
"24h"and"1h30m"for bantime, findtime, escalation cap and jitter. Integer seconds remain valid. Only bantime accepts"permanent"; CLI duration and timeout arguments remain numeric.
Service identity
- installation A dedicated non-login service account.
The unit runs as
fail2zig, with a configurable service/monitor group. Group access is read-only; root or the daemon UID administers protection. Guarded offline ownership transitions cover verified default native SQLite paths and WAL/SHM siblings. - privileges Explicit capability and HTTP boundaries. The daemon retains NET_ADMIN, NET_RAW and DAC_READ_SEARCH. NET_RAW supports the iptables ipset extension and permits raw IP sockets. HTTP shares these privileges and defaults to loopback; disabling metrics removes HTTP/WebSocket while IPC remains available.
Qualification and limits
- release Five static architecture builds retained. Selected live Debian 13 x86_64 checks preceded the version-only update from the unpublished 0.3.1 candidate. Rebuilt 0.4.0 artifacts passed native/emulated checks. ARM64, ARM and both MIPS byte orders have cross-build, ELF and QEMU smoke evidence; non-x86 hardware enforcement and Ubuntu remain untested.
- known limits Keep default loopback ignores. A matching loopback SSH failure with ignores removed can lead to storage intervention. Firewall effects stay in the daemon’s current network namespace; custom namespace selectors and service overrides that move it between namespaces are unsupported.
More reliable enforcement and recovery
Historical entry · downloads retiredObserve-only operation becomes explicit, repeat-offender escalation works on a default install, and journald polling no longer blocks the event loop. Includes MIPS release binaries.
Upgrade noteOlder state files load unchanged; the first save writes v4. Hand-managed configurations
may need permissions corrected to 0640.
Validated on a Debian 13 host with the shipping static binary. Thanks to @mwriter for the first external bug report (#45).
Security
- config Unsafe configuration permissions are rejected.A world-writable or non-root-group-writable
config.tomlis now refused at load with its mode and the fix (chmod 0640); a non-root owner warns (SEC-012). The installer already writes 0640; hand-managed configs may need the chmod. d45a91d
Features
- firewall Explicit observe-only operation when no firewall is available.
on_no_backend = "fail-closed" | "log-only"(defaultfail-closed, ADR-007). Withlog-only, a host with no usable backend runsDEGRADEDobserve-only instead of exiting; the cause (missingCAP_NET_ADMIN, no nf_tables, transient) is instatus,/api/status,/metricsand the event stream. A config whose jails are all log-only needs no backend and runs unprivileged. Never silent, never the default. d45a91d - recidive Repeat offenders escalate without a separate log file.
recidiveis fed in-process from confirmed enforced bans in the other jails via a newinternalsource (ADR-013). No log file, nologpath; the shipped default config escalates repeat offenders for the first time. d45a91d - config Configuration errors point to the exact location.Errors carry
file:line:col, key and section (config: /etc/fail2zig/config.toml:37:1: UnknownKey (key 'logpaht' in [jails.sshd])); duplicate keys in a section are rejected;--validate-configprints one resolution line per jail.backendis accepted as a deprecated fail2ban alias ofsource(systemd→ journald) with one warning per use; the importer emitssource(#45). d45a91d - release MIPS binaries join the release targets.
mips-linux-musleabiandmipsel-linux-musleabistatic binaries (soft-float MIPS32r2), built and smoke-tested under qemu in CI (ADR-012). No real-hardware report yet;install.shdetects both byte orders. d45a91d
Performance
- journald Journald polling stays off the blocking path.journald polling no longer blocks the event loop:
journalctlruns as a non-blocking child on the loop, no threads added (ADR-011). IPC p99 under journald load 4 ms (was 21 ms). d45a91d - ipc Status requests return faster.Status round-trip p99 under 1 ms (was 3–6 ms): client and response buffers are pooled at startup instead of mapped per connection. d45a91d
Fixes
- state Observe-only entries stay unenforced after a restart.A log-only would-ban was persisted as a real ban and reinstalled in the firewall by reconcile after a restart; expiry then called the backend to unban it (BUG-012). State v4 records enforcement; reconcile and expiry act only on enforced entries. v1–v3 files load; the first save writes v4. d45a91d
- journald Journald polling retains entries and closes descriptors.One file descriptor leaked per poll in the loop-driven path (SYS-023), and entries logged between service start and the first poll were dropped as history (SYS-024). Both found on the real-box gate, both under regression tests. d45a91d
- persist Volatile state paths produce a startup warning.
state_fileunder/runor tmpfs now warns at startup that state will not survive a restart; every save logs path and bytes (SYS-021, found on the demo box after 94 days of silently wiped state). State was also saved twice on SIGTERM (DBT-008). d45a91d - firewall Firewall startup failures explain their cause.Detection names its cause (
KernelUnsupported/PermissionDenied/Transient) and the nftables probe is a read-onlyGETGEN, so a missingCAP_NET_ADMINis reported at detect time instead of as a generic init failure; non-root no longer readsNotAvailable. Known startup failures exit 1 with the cause and no error-return-trace. d45a91d - client Client output handles long values and closed pipes.Every table column sizes from its longest value;
Protection: DEGRADED (<cause>)rendered; a closed stdout pipe (| head) is a quiet exit (BUG-008..011). d45a91d
Log rotation fixes and clearer jail health
Historical entry · downloads retiredFixes a protection failure when file jails share a directory. Each jail now reports the log source it actually reads and the health of that source.
Upgrade noteDrop-in binary replacement. State v2 migrates to v3 on the first checkpoint. The log-rotation fix affects file sources; journald sources were not affected.
Security
- watcher File jails keep watching after log rotation.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 Jail status reports the actual source and its health.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 startup drops to about 20 milliseconds.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 Detached log sources affect protection status.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 Lifetime ban counts survive restarts.
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 errors identify the failure.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 Unused memory-budget scaffolding is removed.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
Consistent versions, more precise documentation
Historical entry · downloads retiredThe daemon and client report consistent versions. Documentation now describes the state-tracker entry cap and direct nftables integration accurately.
Fixes
- version Both binaries use one version source.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 The client displays the daemon version.
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 and nftables documentation matches the implementation.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
Configured filters and actions take effect
Historical entry · downloads retiredFixes false bans by applying each jail’s configured filter and honoring observe-only actions. Adds journald sources, ARMv7 binaries, and build-provenance attestations.
Upgrade noteUnknown filters now fail closed. Review each jail’s configured filter and action when upgrading.
Security
- filter Each jail uses its configured 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 Release binaries include build-provenance attestations.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 Group members can use the local client.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 SSH jails can read the systemd journal.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 Status distinguishes enforcement from observation.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 binaries join the release targets.armv7
(
arm-linux-musleabihf) release binaries now cross-compile and build in the CI + release matrices (SYS-009). 2de416e
Fixes
- action Observe-only jails leave the firewall unchanged.
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 Configuration validation works on a fresh 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 builds handle counters and time values correctly.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
Per-jail settings are applied correctly
Historical entry · downloads retiredRetry thresholds and ban durations now follow each jail’s settings. Fractional ban-escalation values are accepted by the TOML parser.
Upgrade noteDrop-in binary replacement. State v1 migrates to v2 on the first checkpoint, with no data loss.
Fixes
- state Each jail uses its own thresholds and ban durations.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 Ban escalation accepts fractional values.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
The first public release
Historical entry · downloads retiredA static musl binary with compiled log parsers, firewall integration, a local client, and built-in metrics. Released under 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) / 832 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