File format
fail2zig uses a strict TOML subset. The parser enforces:
- Unknown keys are rejected — a typo in a key name is an error, not a silent no-op. The error message includes line number and column.
- Unknown sections are rejected — only
[global],[defaults], and[jails.<name>]are valid top-level tables. - No dotted keys on the left-hand side —
a.b = 1is not supported; use the section header form[a]\nb = 1. - String arrays must be same-type — mixed arrays (strings and integers in
the same
[...]) are rejected.
Comments use # and may appear on their own line or as a trailing comment after
any value.
Validate a config file without starting the daemon:
sudo fail2zig --validate-config --config /etc/fail2zig/config.toml
# config: OK (6 jail(s) configured)[global]
Settings that apply to the daemon process as a whole.
log_level
Type: string
Default: "info"
Valid values: "debug", "info", "warn", "err"
Controls the verbosity of the daemon’s operational logs (written to stderr /
journald). "debug" includes per-line parse events and every state-tracker
decision — useful for troubleshooting but verbose on busy servers.
"err" shows only hard failures. "info" is the production default.
pid_file
Type: string (absolute path)
Default: "/run/fail2zig/fail2zig.pid"
Path where the daemon writes its PID on startup. The parent directory is created automatically (mode 0710) if it does not exist. Used by init systems and monitoring tools. Daemon mode is currently always foreground; the PID file is written but the process does not daemonize.
socket_path
Type: string (absolute path)
Default: "/run/fail2zig/fail2zig.sock"
Unix domain socket for fail2zig-client communication. The parent directory
is created at startup if missing (mode 0710, group fail2zig if that group
exists). The socket itself is created at mode 0660. Authentication uses
SO_PEERCRED — the daemon accepts commands from root (uid=0) or members of
the fail2zig group.
The fail2zig-client --socket flag must match this value if you change it.
state_file
Type: string (absolute path)
Default: "/var/lib/fail2zig/state.bin"
Binary file where ban state is persisted on clean shutdown (SIGTERM or SIGINT). On startup, if the file exists and its CRC32 checksum validates, the state tracker is seeded from it and active bans are reconciled with the firewall backend. On checksum mismatch or unrecognized format, the file is discarded and the daemon starts with empty state — a warning is logged.
Writes are atomic: the daemon writes to state.bin.tmp, fsyncs, sets permissions
to 0600, then renames to state.bin. A crash during write leaves the old state
file intact.
memory_ceiling_mb
Type: integer
Default: 64
Minimum: 16
Bounds the state tracker — the dominant memory consumer and the structure attacker traffic actually grows. The ceiling is converted to a fixed entry cap; the tracker never grows past it. When the tracker fills, it evicts the oldest un-banned record (active bans are always retained), so flood traffic cannot push the daemon past its ceiling, force a resize, or hand the OOM reaper a target. The parser hot path is zero-allocation.
This is a single, ceiling-derived entry cap — not a per-component byte budget
across allocators. Values below 16 are rejected at startup.
When to change: increase on servers with many jails or high-volume log streams so the tracker can hold more concurrent source IPs; decrease on embedded systems or containers with tight memory constraints.
metrics_bind
Type: string (IP address)
Default: "127.0.0.1"
IP address the HTTP metrics server binds to. The default binds to the loopback
interface only — the metrics endpoint is not reachable from outside the host.
Set to "0.0.0.0" to expose metrics to Prometheus scrapers on other hosts, but
only do this if the port is firewalled from public access. The endpoint is
read-only but leaks operational telemetry (ban rates, jail names, memory usage).
metrics_port
Type: integer
Default: 9100
Range: 1–65535
TCP port for the HTTP server. The server exposes:
GET /metrics— Prometheus text exposition (per-jail labels)GET /api/status— JSON status identical to the IPCstatusresponseGET /events— WebSocket upgrade for real-time ban/unban event streaming
Port 9100 is the Prometheus node_exporter convention. If you run both
node_exporter and fail2zig on the same host, change one of them.
websocket_max_clients
Type: integer
Default: 16
Range: 1–1024
Maximum number of concurrent WebSocket clients on GET /events. Once the cap is
reached, further connection attempts are refused until a slot frees. Raise it
only if you have several dashboard / event consumers; the default suits a single
dashboard.
[defaults]
Default values applied to every jail unless the jail explicitly overrides them.
The keys here mirror fail2ban’s [DEFAULT] section.
bantime
Type: integer (seconds)
Default: 600
Minimum: 1
How long an IP is banned on first offense. Subsequent offenses may result in
longer bans if bantime_increment_enabled is true.
findtime
Type: integer (seconds)
Default: 600
Minimum: 1
The sliding window in which maxretry failures must occur before a ban fires.
A findtime of 600 with maxretry of 5 means: if 5 failures from the same IP
arrive within any 600-second window, that IP is banned.
maxretry
Type: integer
Default: 5
Minimum: 1
Number of failures within findtime required to trigger a ban. Per-IP attempt
history is tracked in a 128-slot ring buffer; entries older than findtime are
ignored when evaluating the threshold.
banaction
Type: string
Default: "nftables"
Valid values: "nftables", "iptables", "ipset", "log-only"
The firewall backend used to apply bans. The daemon auto-detects the best available backend at startup, but this key overrides the selection:
| Value | Mechanism | When to use |
|---|---|---|
"nftables" | Direct netlink to the nftables subsystem | Modern kernels (≥ 3.13). Preferred. |
"iptables" | Subprocess call to iptables / ip6tables | Legacy kernels or environments where nftables is unavailable. |
"ipset" | ipset + iptables match rule | High-volume ban lists (tens of thousands of IPs). |
"log-only" | No firewall action; ban decisions appear in logs only | Dry-run mode; side-by-side verification with fail2ban. |
ignoreip
Type: string array
Default: []
IPs and CIDR ranges that are never banned, regardless of how many failures they
generate. Entries may be exact IPs ("10.0.0.1"), IPv4 CIDR ("10.0.0.0/8"),
or IPv6 CIDR ("::1/128"). IPv4-mapped IPv6 addresses (::ffff:10.0.0.1) are
canonicalized to their IPv4 form and matched against IPv4 CIDR entries.
ignoreip = ["127.0.0.1/8", "::1", "10.0.0.0/8", "192.168.0.0/16"]Bantime increment settings
These five keys control the recidive escalation policy — bans that grow longer
each time the same IP is banned. They can be set at [defaults] (applies to
every jail unless overridden) or per jail under [jails.<name>]. As of v0.1.1,
fail2zig maintains a per-jail state tracker, so different jails can have
different escalation behavior.
bantime_increment_enabled
Type: boolean
Default: false
Enables escalating bantimes. When false, every ban uses bantime regardless of
how many times the IP has been banned before.
bantime_increment_multiplier
Type: number (float)
Default: 1.0
Base multiplier applied to bantime at each ban count. For the linear formula:
bantime × multiplier × (1 + factor × n). For the exponential formula:
bantime × multiplier × factor^n.
bantime_increment_factor
Type: number (float)
Default: 1.0
The growth rate. In the linear formula, this is an additive coefficient. In
the exponential formula, this is the base of the exponent. Fractional values
such as 1.5 are accepted (a softer escalation curve).
bantime_increment_formula
Type: string
Default: "linear"
Valid values: "linear", "exponential"
Which formula computes ban duration for offense number n (zero-indexed):
| Formula | Expression | n=0 | n=1 | n=2 (example: bantime=600, multiplier=1, factor=2) |
|---|---|---|---|---|
linear | bantime × multiplier × (1 + factor × n) | 600s | 1800s | 3000s |
exponential | bantime × multiplier × factor^n | 600s | 1200s | 2400s |
The exponential formula is recommended for SSH jails — a persistent attacker hitting the same IP accumulates bans that double each time, reaching the max bantime within a handful of offenses.
bantime_increment_max_bantime
Type: integer (seconds)
Default: 604800 (7 days)
Hard cap on escalated bantimes. No matter how many times an IP has been banned, its ban duration never exceeds this value.
Example — worked escalation (exponential, multiplier=1, factor=2, max=86400):
| Offense | n | Duration |
|---|---|---|
| 1st ban | 0 | 600s (10 minutes) |
| 2nd ban | 1 | 1200s (20 minutes) |
| 3rd ban | 2 | 2400s (40 minutes) |
| 4th ban | 3 | 4800s (80 minutes) |
| 5th ban | 4 | 9600s (~2.7 hours) |
| 6th ban | 5 | 19200s (~5.3 hours) |
| 7th ban | 6 | 38400s (~10.7 hours) |
| 8th ban | 7 | 76800s (~21 hours) |
| 9th ban and beyond | ≥8 | 86400s (24 hours — capped) |
v0.1.1 note: Per-jail
bantime_increment_*keys in[jails.<name>]sections take precedence over the[defaults]values for that jail. Jails without explicit per-jail values inherit from[defaults]. The previous v0.1.0 behavior (global state tracker, per-jail values ignored) was the subject of issue #14 and was fixed in v0.1.1.
[jails.<name>]
Each jail watches one or more log files and applies bans when a filter matches
a configured number of times within a time window. The jail name (<name>) must
be a non-empty string containing ASCII alphanumeric characters, hyphens, and
underscores.
[jails.sshd] # jail named "sshd"
[jails.nginx-http-auth] # hyphen is validenabled
Type: boolean
Default: true
When false, the jail is parsed and validated but no log watcher or parser is instantiated. Useful for disabling a jail without removing its configuration.
filter
Type: string
Default: ""
Name of the filter to apply to this jail’s log lines. Must be one of the 15
built-in filter names (see filters). An unknown filter
name on an enabled jail makes the daemon fail closed — it refuses to start
rather than run a jail that silently matches nothing. This is deliberate: a typo
in filter must never leave a service quietly unprotected.
Filter names accept both hyphenated and underscore forms interchangeably:
"nginx-http-auth" and "nginx_http_auth" are identical.
logpath
Type: string array
Default: []
One or more absolute paths to log files for this jail. The daemon watches each path with inotify for new content and handles log rotation (file rename, new file creation, copytruncate truncation) automatically.
logpath = ["/var/log/auth.log", "/var/log/secure"]Missing paths at startup are logged as warnings but are not fatal — a log file that appears after the daemon starts (e.g., after a service restarts) is picked up automatically.
source
Type: string
Default: "auto"
Valid values: "auto", "file", "journald"
Where this jail reads events from.
| Value | Behavior |
|---|---|
"auto" | Resolved at startup by file existence: if a configured logpath exists, read that file; otherwise, for a journald-capable filter (currently sshd), read the systemd journal; otherwise fall back to the file tailer. Never fails closed. |
"file" | Always use the inotify file tailer on logpath. Tolerates a missing or late-appearing file. |
"journald" | Read from the systemd journal via journalctl. Fails closed at startup if journalctl is absent. In v0.2.2 only the sshd filter has a journald selector. |
On a modern systemd host where /var/log/auth.log does not exist, the default
auto makes the stock sshd jail read journald with no extra configuration.
maxretry
Type: optional integer
Default: inherits [defaults].maxretry
Minimum: 1
Overrides the global maxretry for this jail only.
findtime
Type: optional integer (seconds)
Default: inherits [defaults].findtime
Minimum: 1
Overrides the global findtime for this jail only.
bantime
Type: optional integer (seconds)
Default: inherits [defaults].bantime
Minimum: 1
Overrides the global bantime for this jail only.
banaction
Type: optional string
Default: inherits [defaults].banaction
Valid values: "nftables", "iptables", "ipset", "log-only"
Overrides the firewall backend for this jail only. Useful when one jail needs
a different banning mechanism (e.g., "log-only" for a low-confidence filter).
ignoreip
Type: optional string array
Default: inherits [defaults].ignoreip
Per-jail ignore list. When set, this entirely replaces (does not merge with)
the global ignoreip for this jail. If you want to extend the global list,
you must repeat the global entries here.
Per-jail bantime_increment_*
All five bantime_increment_* keys are accepted in jail sections. If set, the
per-jail value takes precedence over [defaults]. If absent, the jail inherits
the corresponding value from [defaults]. Per-jail escalation became active in
v0.1.1 — earlier v0.1.0 deployments routed every jail through the [defaults]
state tracker, ignoring per-jail values.
A complete worked example
A production-shape fail2zig.toml protecting SSH, nginx basic auth,
and postfix — with commentary on each block — lives on its own page:
Example jail configuration.
Use that as a starting template and adapt the four or five values
that matter for your environment (ignoreip, memory_ceiling_mb,
per-jail maxretry / bantime).
If you are migrating from an existing fail2ban install, the
field-by-field mapping between jail.conf and fail2zig.toml
lives on the migration guide.
Validation
Run this before starting or restarting the daemon:
sudo fail2zig --validate-config --config /etc/fail2zig/config.tomlSuccess output:
config: OK (3 jail(s) configured)Error output (example — unknown key):
config: line 7, col 1: unknown key 'bnatime' in section [defaults]Validation checks performed:
memory_ceiling_mb≥ 16bantime> 0 in[defaults]and any per-jail overridefindtime> 0 in[defaults]and any per-jail overridemaxretry> 0 in[defaults]and any per-jail override- Socket path parent directory exists (a warning, not a hard failure — the daemon creates it at startup)
- No duplicate jail names
- All string values are valid TOML strings
Missing log file paths are logged as warnings but do not cause validation to fail — a missing log file may appear after the daemon starts (e.g., nginx creates its error log on first request).