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 sidea.b = 1 is 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 IPC status response
  • GET /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:

ValueMechanismWhen to use
"nftables"Direct netlink to the nftables subsystemModern kernels (≥ 3.13). Preferred.
"iptables"Subprocess call to iptables / ip6tablesLegacy kernels or environments where nftables is unavailable.
"ipset"ipset + iptables match ruleHigh-volume ban lists (tens of thousands of IPs).
"log-only"No firewall action; ban decisions appear in logs onlyDry-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):

FormulaExpressionn=0n=1n=2 (example: bantime=600, multiplier=1, factor=2)
linearbantime × multiplier × (1 + factor × n)600s1800s3000s
exponentialbantime × multiplier × factor^n600s1200s2400s

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):

OffensenDuration
1st ban0600s (10 minutes)
2nd ban11200s (20 minutes)
3rd ban22400s (40 minutes)
4th ban34800s (80 minutes)
5th ban49600s (~2.7 hours)
6th ban519200s (~5.3 hours)
7th ban638400s (~10.7 hours)
8th ban776800s (~21 hours)
9th ban and beyond≥886400s (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 valid

enabled

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.

ValueBehavior
"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.toml

Success 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 ≥ 16
  • bantime > 0 in [defaults] and any per-jail override
  • findtime > 0 in [defaults] and any per-jail override
  • maxretry > 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).

Edit on GitHub →