# How to write a custom fail2ban filter

> Write a fail2ban custom filter step by step: filter.d anatomy, failregex tags like <HOST> and <F-USER>, datepattern, ignoreregex, JSON logs and testing.
>
> Source: https://fail2ban.dev/filters/custom/ · Updated 2026-10-25 · Checked against fail2ban 1.1.1
> fail2ban.dev is an independent guide, not the official fail2ban project.

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`.

```ini
[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
```
sudo fail2ban-regex /var/log/myapp.log /etc/fail2ban/filter.d/myapp.conf
```

## Anatomy of a filter file

```ini
[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
```
|---|---|
| `[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:

```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
```

```ini
[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
```

- `^\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
```

The jail:

```ini
[myapp]
enabled  = true
filter   = myapp
logpath  = /var/log/myapp/auth.log
port     = http,https
maxretry = 5
```

```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}"
```
- `.*?` (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
```
[INCLUDES]
before = common.conf

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

## Multiple failregex lines

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

```ini
            ^\s*WARN \[auth\] Invalid API token from <HOST>
            ^\s*WARN \[auth\] Too many 2FA attempts from <HOST>
```

## 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
              from 192\.0\.2\.50$
```

## 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](/guides/minecraft/) |
| 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
prefregex = ^\s*\[<F-MLFID>conn-\d+</F-MLFID>\] <F-CONTENT>.+</F-CONTENT>$
failregex = ^<F-NOFAIL>connect from</F-NOFAIL> <ADDR>
            ^authentication failed
```

```log
2026-10-25 18:36:11 [conn-77] authentication failed
```

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

```ini
[Definition]
failregex = %(known/failregex)s
            ^%(__prefix_line)sdirectory index of "[^"]+" is forbidden, client: <HOST>
```

## 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](/troubleshooting/config-errors/) covers start-up messages, and the [filter library](/filters/) 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.
