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".
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.
[Definition]
failregex = ^\s*WARN \[auth\] Login failed for user "<F-USER>[^"]*</F-USER>" from <HOST>(?: \(|$)
ignoreregex =
datepattern = {^LN-BEG}%%Y-%%m-%%d %%H:%%M:%%Ssudo fail2ban-regex /var/log/myapp.log /etc/fail2ban/filter.d/myapp.confPrefer 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#
[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:
- finds the timestamp with
datepattern(or the built-in detectors) and cuts it out of the line; - if
prefregexis set, matches it and keeps the<F-CONTENT>part; - tries each
failregexagainst what's left (a Pythonre.search, so anchor with^); - drops the line if an
ignoreregexmatches; - 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:
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.50The filter, with the uptime monitor's account excluded via ignoreregex:
[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:%%SWhy it's written this way:
^\s*WARNanchors 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, so203.0.113.42can'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.
sudo fail2ban-regex /var/log/myapp/auth.log /etc/fail2ban/filter.d/myapp.conffail2ban-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:
[myapp]
enabled = true
filter = myapp
logpath = /var/log/myapp/auth.log
port = http,https
maxretry = 5Worked example 2: a JSON log#
{"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"}[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 thetimekey;{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 aprefregexthat 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):
Oct 25 18:36:10 web1 myapp[1234]: authentication failure for admin from 203.0.113.42[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:
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:
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:
[Definition]
prefregex = ^\s*\[<F-MLFID>conn-\d+</F-MLFID>\] <F-CONTENT>.+</F-CONTENT>$
failregex = ^<F-NOFAIL>connect from</F-NOFAIL> <ADDR>
^authentication failedFor logs like:
2026-10-25 18:36:10 [conn-77] connect from 203.0.113.42
2026-10-25 18:36:11 [conn-77] authentication failedOlder 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:
[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.