Filters & actions

How to write a custom fail2ban filter

A filter is a small INI file with one or more regular expressions. This guide builds working filters for a plain-text app log, a JSON log and a syslog line, tests them with fail2ban-regex, and covers the mistakes behind "No failure-id group".

Updated 10 min readChecked against fail2ban 1.1.1

To create a custom fail2ban filter, save a file such as /etc/fail2ban/filter.d/myapp.conf with a [Definition] section and a failregex that matches the failure line and marks the attacker's address with <HOST>, for example failregex = ^\s*WARN \[auth\] Login failed for user "\S+" from <HOST>. Test it with fail2ban-regex /var/log/myapp.log /etc/fail2ban/filter.d/myapp.conf, then reference it from a jail with filter = myapp.

/etc/fail2ban/filter.d/myapp.conf
[Definition]
failregex = ^\s*WARN \[auth\] Login failed for user "<F-USER>[^"]*</F-USER>" from <HOST>(?: \(|$)
ignoreregex =
datepattern = {^LN-BEG}%%Y-%%m-%%d %%H:%%M:%%S
bash
sudo fail2ban-regex /var/log/myapp.log /etc/fail2ban/filter.d/myapp.conf

Prefer to build it in the browser? The regex tester runs your failregex against pasted log lines with fail2ban's tag and date handling, and the filter generator drafts a filter from a sample line.

Anatomy of a filter file#

/etc/fail2ban/filter.d/example.conf
[INCLUDES]
# read other files first (shared regex fragments such as __prefix_line)
before = common.conf
# after = example.local    (a same-named .local is read automatically)

[Definition]
_daemon     = myapp                      ; private helper variables start with _
prefregex   =                            ; optional common prefix every line must match
failregex   = <regex 1>
              <regex 2>
ignoreregex =
datepattern = {^LN-BEG}
journalmatch = _SYSTEMD_UNIT=myapp.service
maxlines    = 1

[Init]
# parameters a jail can override: filter = example[mode=strict]
mode = normal
Key Purpose
[INCLUDES] before / after Files read before/after this one. common.conf provides %(__prefix_line)s for syslog-style lines.
failregex One or more regexes, one per line (indented). A line matching any of them is a failure. Required.
ignoreregex Lines matching these are never failures, even if a failregex matches.
prefregex Regex every line must match first; <F-CONTENT>…</F-CONTENT> cuts out the part failregex then sees. Speeds up filters with many regexes.
datepattern How to find the timestamp. Default: fail2ban's built-in date detectors.
journalmatch Journal filter used when the jail runs with backend = systemd.
maxlines Lines buffered for multi-line regexes (default 1).
[Init] Parameters (like mode) a jail can override in brackets: filter = myapp[mode=aggressive].

Because of config interpolation, a literal % must be written %% in filter files. That's why date patterns look like %%Y-%%m-%%d.

How fail2ban applies a filter to a line#

For each new log line, fail2ban:

  1. finds the timestamp with datepattern (or the built-in detectors) and cuts it out of the line;
  2. if prefregex is set, matches it and keeps the <F-CONTENT> part;
  3. tries each failregex against what's left (a Python re.search, so anchor with ^);
  4. drops the line if an ignoreregex matches;
  5. records a failure for the captured host (or <F-ID>) with the line's timestamp.

Step 1 matters most: your failregex must not include the timestamp. In the example above the date is removed, which leaves a leading space, hence ^\s*WARN.

Tags you can use in failregex#

Tag Matches Notes
<HOST> IPv4, IPv6 or hostname Hostnames are resolved according to usedns; with usedns = no only IPs match
<ADDR> IPv4 or IPv6 address only Best choice when the log always has an IP
<IP4> / <IP6> Only IPv4 / only IPv6
<DNS> Hostname
<CIDR> A prefix length like 24 Use as <ADDR>/<CIDR>
<SUBNET> IP with optional /prefix Bans the whole subnet when one is logged
<F-ID>…</F-ID> Any text you capture Ban by an ID instead of an IP (with suitable actions)
<F-USER>…</F-USER> The user name Available as <F-USER> in actions; also used to group multi-user attempts
<F-NOFAIL>…</F-NOFAIL> Marks a match that is not a failure Used to collect data (like the IP) from one line for a later failure line
<F-MLFID>…</F-MLFID> A session/connection ID Ties several lines together (multi-line)
<F-MLFFORGET> / <F-MLFGAINED> Session ended / login succeeded Forget the session; a success clears pending failures
<F-CONTENT>…</F-CONTENT> (in prefregex) the part passed on to failregex
<F-ALT_USER>, <F-ALT_USER1>… Alternative capture for <F-USER> First non-empty one wins
<F-ANYTHING>…</F-ANYTHING> Your own named capture (upper case) Usable as <F-ANYTHING> in actions
<SKIPLINES> Any lines in between (with maxlines > 1) Old-style multi-line; prefer <F-MLFID>

<HOST>, <ADDR> and <SUBNET> also match an address in square brackets, such as [2001:db8::25]. Every failregex needs at least one failure-id tag (<HOST>, <ADDR>, <IP4>, <IP6>, <DNS>, <F-ID> or <F-MLFID>), either in itself or in prefregex.

Worked example 1: a plain-text application log#

The log:

/var/log/myapp/auth.log
2026-10-25 18:36:10 WARN [auth] Login failed for user "admin" from 203.0.113.42
2026-10-25 18:36:12 WARN [auth] Login failed for user "bob" from 198.51.100.7 (invalid password)
2026-10-25 18:36:20 INFO [auth] Login successful for user "alice" from 192.0.2.10
2026-10-25 18:36:25 WARN [auth] Login failed for user "monitor" from 192.0.2.50

The filter, with the uptime monitor's account excluded via ignoreregex:

/etc/fail2ban/filter.d/myapp.conf
[Definition]
failregex = ^\s*WARN \[auth\] Login failed for user "<F-USER>[^"]*</F-USER>" from <HOST>(?: \(|$)
ignoreregex = for user "monitor"
datepattern = {^LN-BEG}%%Y-%%m-%%d %%H:%%M:%%S

Why it's written this way:

  • ^\s*WARN anchors the regex right after the removed timestamp, so a user name containing "from 1.2.3.4" can't inject a fake IP.
  • [^"]* stops at the closing quote. .* would happily run past it.
  • (?: \(|$) requires the IP to be followed by ( or the end of the line, so 203.0.113.42 can't match as a prefix of something longer.
  • datepattern = {^LN-BEG}… says the date is at the beginning of the line, which is faster and avoids matching a date inside the message.
bash
sudo fail2ban-regex /var/log/myapp/auth.log /etc/fail2ban/filter.d/myapp.conf

fail2ban-regex lists hits per regex and per date template, then a summary. For this log it reports Lines: 4 lines, 1 ignored, 2 matched, 1 missed: two failures, the monitor line ignored, the successful login missed, as intended. The fail2ban-regex page explains every part of the output and options like --print-all-missed.

The jail:

/etc/fail2ban/jail.local
[myapp]
enabled  = true
filter   = myapp
logpath  = /var/log/myapp/auth.log
port     = http,https
maxretry = 5

Worked example 2: a JSON log#

/var/log/myapp/events.json
{"time":"2026-10-25T18:36:10Z","level":"warn","event":"login_failed","user":"admin","ip":"203.0.113.42"}
{"time":"2026-10-25T18:36:11Z","level":"info","event":"login_ok","user":"alice","ip":"192.0.2.10"}
{"time":"2026-10-25T18:36:12Z","level":"warn","event":"login_failed","user":"root","ip":"2001:db8::25"}
/etc/fail2ban/filter.d/myapp-json.conf
[Definition]
failregex = ^\{.*?"event":"login_failed",.*?"user":"<F-USER>[^"]*</F-USER>",.*?"ip":"<ADDR>"
datepattern = "time":"{DATE}"
  • datepattern = "time":"{DATE}" tells fail2ban the timestamp is the value of the time key; {DATE} stands for the built-in date formats, which include ISO 8601.
  • .*? (non-greedy) between keys and <ADDR> instead of <HOST>, because the field always holds an IP. Both the IPv4 and IPv6 lines match.
  • The regex depends on key order. If your logger doesn't guarantee it, write one failregex per order or match on a single key ("ip":"<ADDR>") together with a prefregex that requires "event":"login_failed".

Worked example 3: a syslog line with %(__prefix_line)s#

Apps that log through syslog get a prefix such as web1 myapp[1234]:. Don't write it by hand; include common.conf and use %(__prefix_line)s, which matches the hostname, daemon name and PID (and works for journal lines too):

log
Oct 25 18:36:10 web1 myapp[1234]: authentication failure for admin from 203.0.113.42
/etc/fail2ban/filter.d/myapp-syslog.conf
[INCLUDES]
before = common.conf

[Definition]
_daemon = myapp
failregex = ^%(__prefix_line)sauthentication failure for <F-USER>\S+</F-USER> from <HOST>$

_daemon = myapp restricts the prefix to lines from myapp; the same message from another program doesn't match.

Multiple failregex lines#

List several regexes, one per line, indented. A line counts as a failure if any of them matches:

ini
failregex = ^\s*WARN \[auth\] Login failed for user "<F-USER>[^"]*</F-USER>" from <HOST>
            ^\s*WARN \[auth\] Invalid API token from <HOST>
            ^\s*WARN \[auth\] Too many 2FA attempts from <HOST>

There's no limit, but each extra regex costs CPU on every line. With many regexes sharing a prefix, move the prefix into prefregex.

ignoreregex#

ignoreregex excludes lines that a failregex matched: a monitoring account, a health check, your own office. It's checked only for matching lines, so it can be simple:

ini
ignoreregex = for user "(?:monitor|healthcheck)"
              from 192\.0\.2\.50$

To never ban an address at all, use ignoreip in the jail instead; it's clearer and faster.

datepattern#

You need a custom datepattern only if fail2ban doesn't detect the timestamp (the Date template hits section of fail2ban-regex shows what it found) or detects the wrong one.

Log timestamp datepattern
2026-10-25 18:36:10 at line start {^LN-BEG}%%Y-%%m-%%d %%H:%%M:%%S
[25/Oct/2026:18:36:10 +0000] (web access log) ^[^\[]*\[({DATE})
"time":"2026-10-25T18:36:10Z" "time":"{DATE}"
1761417370 (Unix epoch) {^LN-BEG}EPOCH
[18:36:10] time of day only (Minecraft latest.log) ^\[%%H:%%M:%%S — see the Minecraft filter
No timestamp at all {NONE} (uses the time the line is read)

{DATE} inserts the built-in patterns, {^LN-BEG} anchors at the line start. When fail2ban matches a line but can't find a date, it logs Found a match but no valid date/time found and, inside a jail, can't apply findtime correctly. Fix the datepattern, or set logtimezone if the times are off by hours.

Multi-line logs#

When the IP and the failure are on different lines, tie them with an ID. Mark the connect line as a non-failure that carries the IP, and the failure line with the same session ID:

ini
[Definition]
prefregex = ^\s*\[<F-MLFID>conn-\d+</F-MLFID>\] <F-CONTENT>.+</F-CONTENT>$
failregex = ^<F-NOFAIL>connect from</F-NOFAIL> <ADDR>
            ^authentication failed

For logs like:

log
2026-10-25 18:36:10 [conn-77] connect from 203.0.113.42
2026-10-25 18:36:11 [conn-77] authentication failed

Older filters use maxlines = 10 with <SKIPLINES> instead; that still works but is slower. The stock sshd filter is the most complete real-world example of <F-MLFID> handling.

Extend a stock filter instead of copying it#

To add a regex to a shipped filter, create a .local next to it and reference the original with %(known/…)s, so upgrades still improve the rest:

/etc/fail2ban/filter.d/nginx-forbidden.local
[Definition]
failregex = %(known/failregex)s
            ^%(__prefix_line)sdirectory index of "[^"]+" is forbidden, client: <HOST>

If the stock filter has a prefregex (sshd, nginx-http-auth, postfix, dovecot…), your added regex is matched against the <F-CONTENT> part only, not the whole line; look at the filter's prefregex first. You can also extend a single option from the jail: ignoreregex = %(known/ignoreregex)s <regex>.

Common mistakes#

"No failure-id group in '…'". The regex has no <HOST>, <ADDR>, <F-ID> or similar tag. Another cause: fail2ban-regex treats your second argument as a regex string, not a file, because the path is relative or misspelled. Pass a full path (/etc/fail2ban/filter.d/myapp.conf) or a filter name from filter.d (myapp).

Greedy .* before <HOST>. ^.*from <HOST> matches the last "from" on the line; if the user name or URL is attacker-controlled, they can make you ban any IP. Use specific text, [^"]*, \S+ or .*?, and anchor with ^.

Including the timestamp in failregex. The date is removed before matching, so a regex that starts with ^\d{4}-\d{2}-\d{2} never matches.

Unanchored regexes. Without ^, a regex can match in the middle of a line, including inside user-supplied text. Anchor at the start, and at the end where the format allows.

Single %. In filter files % must be %%, or you'll get an interpolation error when the config loads.

Escaping. Characters with regex meaning need a backslash: \[, \], \., \(, \), \?, \+.

Testing against old lines inside a jail. fail2ban-regex matches all lines; a jail ignores lines older than findtime. A filter that tests fine may still not ban for an old log. Generate fresh failures to test the jail.

Still stuck? Config and regex errors covers start-up messages, and the filter library has the source and sample lines of every stock filter to copy from.

FAQ#

How do I create a custom filter in fail2ban?#

Create /etc/fail2ban/filter.d/<name>.conf with a [Definition] section and a failregex containing <HOST> (or <ADDR>) where the attacker's IP appears. Test it with sudo fail2ban-regex /path/to/log /etc/fail2ban/filter.d/<name>.conf, then add a jail with filter = <name>, logpath and enabled = true, and reload.

Can a fail2ban filter have multiple failregex lines?#

Yes. Put each regex on its own line under failregex =, indented with spaces. A log line is a failure if any of them matches. Use prefregex for a common prefix when you have many, and test all of them at once with fail2ban-regex.

What is the difference between failregex and ignoreregex?#

failregex defines what counts as a failure. ignoreregex is checked only on lines a failregex matched and discards them, for example failed logins of a monitoring account. To never ban certain IPs, use ignoreip in the jail instead of an ignoreregex.

How do I set a custom datepattern in a fail2ban filter?#

Add datepattern = to [Definition] using strptime codes with doubled percent signs, e.g. {^LN-BEG}%%Y-%%m-%%d %%H:%%M:%%S, or {DATE} to reuse the built-in formats at a position, as in "time":"{DATE}". Check the "Date template hits" section of fail2ban-regex to confirm it's used.

Spotted something wrong or out of date? Tell us — every page is checked against the current fail2ban release.