Merge pull request #2402 from sebres/maxentries-mem-saving

maxmatches: memory saving options
pull/2247/head
Sergey G. Brester 2019-04-19 12:51:04 +02:00 committed by GitHub
commit d67e42efa2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 189 additions and 45 deletions

View File

@ -70,6 +70,8 @@ filter = flt[logtype=short]
* `filter.d/traefik-auth.conf`: used to ban hosts, that were failed through traefik
### Enhancements
* introduced new options: `dbmaxmatches` (fail2ban.conf) and `maxmatches` (jail.conf) to contol
how many matches per ticket fail2ban can hold in memory and store in database (gh-2402, gh-2118);
* fail2ban.conf: introduced new section `[Thread]` and option `stacksize` to configure default size
of the stack for threads running in fail2ban (gh-2356), it could be set in `fail2ban.local` to
avoid runtime error "can't start new thread" (see gh-969);

View File

@ -145,6 +145,7 @@ config/filter.d/sshd.conf
config/filter.d/stunnel.conf
config/filter.d/suhosin.conf
config/filter.d/tine20.conf
config/filter.d/traefik-auth.conf
config/filter.d/uwimap-auth.conf
config/filter.d/vsftpd.conf
config/filter.d/webmin-auth.conf
@ -333,9 +334,11 @@ fail2ban/tests/files/logs/solid-pop3d
fail2ban/tests/files/logs/squid
fail2ban/tests/files/logs/squirrelmail
fail2ban/tests/files/logs/sshd
fail2ban/tests/files/logs/sshd-journal
fail2ban/tests/files/logs/stunnel
fail2ban/tests/files/logs/suhosin
fail2ban/tests/files/logs/tine20
fail2ban/tests/files/logs/traefik-auth
fail2ban/tests/files/logs/uwimap-auth
fail2ban/tests/files/logs/vsftpd
fail2ban/tests/files/logs/webmin-auth

View File

@ -5,11 +5,11 @@
# Changes: in most of the cases you should not modify this
# file, but provide customizations in fail2ban.local file, e.g.:
#
# [Definition]
# [DEFAULT]
# loglevel = DEBUG
#
[Definition]
[DEFAULT]
# Option: loglevel
# Notes.: Set the log level output.
@ -68,6 +68,15 @@ dbfile = /var/lib/fail2ban/fail2ban.sqlite3
# Values: [ SECONDS ] Default: 86400 (24hours)
dbpurgeage = 1d
# Options: dbmaxmatches
# Notes.: Number of matches stored in database per ticket (resolvable via
# tags <ipmatches>/<ipjailmatches> in actions)
# Values: [ INT ] Default: 10
dbmaxmatches = 10
[Definition]
[Thread]
# Options: stacksize

View File

@ -69,6 +69,9 @@ findtime = 10m
# "maxretry" is the number of failures before a host get banned.
maxretry = 5
# "maxmatches" is the number of matches stored in ticket (resolvable via tag <matches> in actions).
maxmatches = %(maxretry)s
# "backend" specifies the backend used to get files modification.
# Available options are "pyinotify", "gamin", "polling", "systemd" and "auto".
# This option can be overridden in each jail as well.

View File

@ -54,6 +54,7 @@ class Fail2banReader(ConfigReader):
["string", "logtarget", "STDERR"],
["string", "syslogsocket", "auto"],
["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"],
["int", "dbmaxmatches", None],
["string", "dbpurgeage", "1d"]]
self.__opts = ConfigReader.getOptions(self, "Definition", opts)
if updateMainOpt:
@ -73,7 +74,7 @@ class Fail2banReader(ConfigReader):
# Also dbfile should be set before all other database options.
# So adding order indices into items, to be stripped after sorting, upon return
order = {"thread":0, "syslogsocket":11, "loglevel":12, "logtarget":13,
"dbfile":50, "dbpurgeage":51}
"dbfile":50, "dbmaxmatches":51, "dbpurgeage":51}
stream = list()
for opt in self.__opts:
if opt in order:

View File

@ -93,6 +93,7 @@ class JailReader(ConfigReader):
opts = [["bool", "enabled", False],
["string", "backend", "auto"],
["int", "maxretry", None],
["int", "maxmatches", None],
["string", "findtime", None],
["string", "bantime", None],
["string", "usedns", None], # be sure usedns is before all regex(s) in stream

View File

@ -72,6 +72,8 @@ protocol = [
['', "DATABASE", ""],
["set dbfile <FILE>", "set the location of fail2ban persistent datastore. Set to \"None\" to disable"],
["get dbfile", "get the location of fail2ban persistent datastore"],
["set dbmaxmatches <INT>", "sets the max number of matches stored in database per ticket"],
["get dbmaxmatches", "gets the max number of matches stored in database per ticket"],
["set dbpurgeage <SECONDS>", "sets the max age in <SECONDS> that history of bans will be kept"],
["get dbpurgeage", "gets the max age in seconds that history of bans will be kept"],
['', "JAIL CONTROL", ""],
@ -103,6 +105,7 @@ protocol = [
["set <JAIL> banip <IP> ... <IP>", "manually Ban <IP> for <JAIL>"],
["set <JAIL> unbanip [--report-absent] <IP> ... <IP>", "manually Unban <IP> in <JAIL>"],
["set <JAIL> maxretry <RETRY>", "sets the number of failures <RETRY> before banning the host for <JAIL>"],
["set <JAIL> maxmatches <INT>", "sets the max number of matches stored in memory per ticket in <JAIL>"],
["set <JAIL> maxlines <LINES>", "sets the number of <LINES> to buffer for regex search for <JAIL>"],
["set <JAIL> addaction <ACT>[ <PYTHONFILE> <JSONKWARGS>]", "adds a new action named <ACT> for <JAIL>. Optionally for a Python based action, a <PYTHONFILE> and <JSONKWARGS> can be specified, else will be a Command Action"],
["set <JAIL> delaction <ACT>", "removes the action <ACT> from <JAIL>"],
@ -130,6 +133,7 @@ protocol = [
["get <JAIL> datepattern", "gets the patern used to match date/times for <JAIL>"],
["get <JAIL> usedns", "gets the usedns setting for <JAIL>"],
["get <JAIL> maxretry", "gets the number of failures allowed for <JAIL>"],
["get <JAIL> maxmatches", "gets the max number of matches stored in memory per ticket in <JAIL>"],
["get <JAIL> maxlines", "gets the number of lines to buffer for <JAIL>"],
["get <JAIL> actions", "gets a list of actions for <JAIL>"],
["", "COMMAND ACTION INFORMATION",""],

View File

@ -177,7 +177,7 @@ class Fail2BanDb(object):
def __init__(self, filename, purgeAge=24*60*60):
self.maxEntries = 50
self.maxMatches = 10
self._lock = RLock()
self._dbFilename = filename
self._purgeAge = purgeAge
@ -541,8 +541,13 @@ class Fail2BanDb(object):
#TODO: Implement data parts once arbitrary match keys completed
data = ticket.getData()
matches = data.get('matches')
if matches and len(matches) > self.maxEntries:
data['matches'] = matches[-self.maxEntries:]
if self.maxMatches:
if matches and len(matches) > self.maxMatches:
data = data.copy()
data['matches'] = matches[-self.maxMatches:]
elif matches:
data = data.copy()
del data['matches']
cur.execute(
"INSERT INTO bans(jail, ip, timeofban, data) VALUES(?, ?, ?, ?)",
(jail.name, ip, int(round(ticket.getTime())), data))
@ -667,7 +672,7 @@ class Fail2BanDb(object):
tickdata = {}
m = data.get('matches', [])
# pre-insert "maxadd" enries (because tickets are ordered desc by time)
maxadd = self.maxEntries - len(matches)
maxadd = self.maxMatches - len(matches)
if maxadd > 0:
if len(m) <= maxadd:
matches = m + matches
@ -702,10 +707,12 @@ class Fail2BanDb(object):
queryArgs.append(fromtime - forbantime)
if ip is None:
query += " GROUP BY ip ORDER BY ip, timeofban DESC"
else:
query += " ORDER BY timeofban DESC LIMIT 1"
cur = self._db.cursor()
return cur.execute(query, queryArgs)
def getCurrentBans(self, jail = None, ip = None, forbantime=None, fromtime=None):
def getCurrentBans(self, jail = None, ip = None, forbantime=None, fromtime=None, maxmatches=None):
tickets = []
ticket = None
@ -717,10 +724,20 @@ class Fail2BanDb(object):
for banip, timeofban, data in results:
# logSys.debug('restore ticket %r, %r, %r', banip, timeofban, data)
ticket = FailTicket(banip, timeofban, data=data)
# filter matches if expected (current count > as maxmatches specified):
if maxmatches is None:
maxmatches = self.maxMatches
if maxmatches:
matches = ticket.getMatches()
if matches and len(matches) > maxmatches:
ticket.setMatches(matches[-maxmatches:])
else:
ticket.setMatches(None)
# logSys.debug('restored ticket: %r', ticket)
if ip is not None: return ticket
tickets.append(ticket)
return tickets if ip is None else ticket
return tickets
@commitandrollback
def purge(self, cur):

View File

@ -43,7 +43,7 @@ class FailManager:
self.__maxRetry = 3
self.__maxTime = 600
self.__failTotal = 0
self.maxEntries = 50
self.maxMatches = 50
self.__bgSvc = BgService()
def setFailTotal(self, value):
@ -87,7 +87,7 @@ class FailManager:
attempt = 1
else:
# will be incremented / extended (be sure we have at least +1 attempt):
matches = ticket.getMatches()
matches = ticket.getMatches() if self.maxMatches else None
attempt = ticket.getAttempt()
if attempt <= 0:
attempt += 1
@ -97,10 +97,13 @@ class FailManager:
fData.setLastReset(unixTime)
fData.setRetry(0)
fData.inc(matches, attempt, count)
# truncate to maxEntries:
matches = fData.getMatches()
if len(matches) > self.maxEntries:
fData.setMatches(matches[-self.maxEntries:])
# truncate to maxMatches:
if self.maxMatches:
matches = fData.getMatches()
if len(matches) > self.maxMatches:
fData.setMatches(matches[-self.maxMatches:])
else:
fData.setMatches(None)
except KeyError:
# if already FailTicket - add it direct, otherwise create (using copy all ticket data):
if isinstance(ticket, FailTicket):

View File

@ -213,7 +213,8 @@ class Jail(object):
try:
if self.database is not None:
forbantime = self.actions.getBanTime()
for ticket in self.database.getCurrentBans(jail=self, forbantime=forbantime):
for ticket in self.database.getCurrentBans(jail=self,
forbantime=forbantime, maxmatches=self.filter.failManager.maxMatches):
#logSys.debug('restored ticket: %s', ticket)
if not self.filter.inIgnoreIPList(ticket.getIP(), log_ignore=True):
# mark ticked was restored from database - does not put it again into db:

View File

@ -443,6 +443,12 @@ class Server:
def getUseDns(self, name):
return self.__jails[name].filter.getUseDns()
def setMaxMatches(self, name, value):
self.__jails[name].filter.failManager.maxMatches = value
def getMaxMatches(self, name):
return self.__jails[name].filter.failManager.maxMatches
def setMaxRetry(self, name, value):
self.__jails[name].filter.setMaxRetry(value)

View File

@ -135,7 +135,13 @@ class Ticket(object):
return self._data['failures']
def setMatches(self, matches):
self._data['matches'] = matches or []
if matches:
self._data['matches'] = matches
else:
try:
del self._data['matches']
except KeyError:
pass
def getMatches(self):
return [(line if not isinstance(line, (list, tuple)) else "".join(line)) \

View File

@ -183,10 +183,19 @@ class Transmitter:
else:
if self.__quiet: return
return db.filename
elif name == "dbmaxmatches":
db = self.__server.getDatabase()
if db is None:
logSys.log(logging.MSG, "dbmaxmatches setting was not in effect since no db yet")
return None
else:
db.maxMatches = int(command[1])
if self.__quiet: return
return db.maxMatches
elif name == "dbpurgeage":
db = self.__server.getDatabase()
if db is None:
logSys.warning("dbpurgeage setting was not in effect since no db yet")
logSys.log(logging.MSG, "dbpurgeage setting was not in effect since no db yet")
return None
else:
db.purgeage = command[1]
@ -310,6 +319,11 @@ class Transmitter:
self.__server.setLogTimeZone(name, value)
if self.__quiet: return
return self.__server.getLogTimeZone(name)
elif command[1] == "maxmatches":
value = command[2]
self.__server.setMaxMatches(name, int(value))
if self.__quiet: return
return self.__server.getMaxMatches(name)
elif command[1] == "maxretry":
value = command[2]
self.__server.setMaxRetry(name, int(value))
@ -398,6 +412,12 @@ class Transmitter:
return None
else:
return db.filename
elif name == "dbmaxmatches":
db = self.__server.getDatabase()
if db is None:
return None
else:
return db.maxMatches
elif name == "dbpurgeage":
db = self.__server.getDatabase()
if db is None:
@ -433,6 +453,8 @@ class Transmitter:
return self.__server.getDatePattern(name)
elif command[1] == "logtimezone":
return self.__server.getLogTimeZone(name)
elif command[1] == "maxmatches":
return self.__server.getMaxMatches(name)
elif command[1] == "maxretry":
return self.__server.getMaxRetry(name)
elif command[1] == "maxlines":

View File

@ -881,18 +881,20 @@ class JailsReaderTest(LogCaptureTestCase):
self.assertTrue(
find_set('syslogsocket') < find_set('loglevel') < find_set('logtarget')
)
# then dbfile should be before dbpurgeage
# then dbfile should be before dbmaxmatches and dbpurgeage
self.assertTrue(find_set('dbpurgeage') > find_set('dbfile'))
self.assertTrue(find_set('dbmaxmatches') > find_set('dbfile'))
# and there is logging information left to be passed into the
# server
self.assertSortedEqual(commands,
[['set', 'dbfile',
'/var/lib/fail2ban/fail2ban.sqlite3'],
['set', 'dbpurgeage', '1d'],
['set', 'loglevel', "INFO"],
['set', 'logtarget', '/var/log/fail2ban.log'],
['set', 'syslogsocket', 'auto']])
self.assertSortedEqual(commands,[
['set', 'syslogsocket', 'auto'],
['set', 'loglevel', "INFO"],
['set', 'logtarget', '/var/log/fail2ban.log'],
['set', 'dbfile', '/var/lib/fail2ban/fail2ban.sqlite3'],
['set', 'dbmaxmatches', 10],
['set', 'dbpurgeage', '1d'],
])
# and if we force change configurator's fail2ban's baseDir
# there should be an error message (test visually ;) --

View File

@ -331,9 +331,9 @@ class DatabaseTest(LogCaptureTestCase):
# be returned
self.assertEqual(len(self.db.getBans(jail=self.jail,bantime=-1)), 2)
def testGetBansMerged_MaxEntries(self):
def testGetBansMerged_MaxMatches(self):
self.testAddJail()
maxEntries = 2
maxMatches = 2
failures = [
{"matches": ["abc\n"], "user": set(['test'])},
{"matches": ["123\n"], "user": set(['test'])},
@ -349,29 +349,44 @@ class DatabaseTest(LogCaptureTestCase):
ticket.setAttempt(1)
self.db.addBan(self.jail, ticket)
# should retrieve 2 matches only, but count of all attempts:
self.db.maxEntries = maxEntries;
self.db.maxMatches = maxMatches;
ticket = self.db.getBansMerged("127.0.0.1")
self.assertEqual(ticket.getIP(), "127.0.0.1")
self.assertEqual(ticket.getAttempt(), len(failures))
self.assertEqual(len(ticket.getMatches()), maxEntries)
self.assertEqual(ticket.getMatches(), matches2find[-maxEntries:])
self.assertEqual(len(ticket.getMatches()), maxMatches)
self.assertEqual(ticket.getMatches(), matches2find[-maxMatches:])
# add more failures at once:
ticket = FailTicket("127.0.0.1", MyTime.time() - 10, matches2find,
data={"user": set(['test', 'root'])})
ticket.setAttempt(len(failures))
self.db.addBan(self.jail, ticket)
# should retrieve 2 matches only, but count of all attempts:
self.db.maxEntries = maxEntries;
ticket = self.db.getBansMerged("127.0.0.1")
self.assertEqual(ticket.getAttempt(), 2 * len(failures))
self.assertEqual(len(ticket.getMatches()), maxEntries)
self.assertEqual(ticket.getMatches(), matches2find[-maxEntries:])
self.assertEqual(len(ticket.getMatches()), maxMatches)
self.assertEqual(ticket.getMatches(), matches2find[-maxMatches:])
# also using getCurrentBans:
ticket = self.db.getCurrentBans(self.jail, "127.0.0.1", fromtime=MyTime.time()-100)
self.assertTrue(ticket is not None)
self.assertEqual(ticket.getAttempt(), len(failures))
self.assertEqual(len(ticket.getMatches()), maxEntries)
self.assertEqual(ticket.getMatches(), matches2find[-maxEntries:])
self.assertEqual(len(ticket.getMatches()), maxMatches)
self.assertEqual(ticket.getMatches(), matches2find[-maxMatches:])
# maxmatches of jail < dbmaxmatches (so read 1 match and 0 matches):
ticket = self.db.getCurrentBans(self.jail, "127.0.0.1", fromtime=MyTime.time()-100,
maxmatches=1)
self.assertEqual(len(ticket.getMatches()), 1)
self.assertEqual(ticket.getMatches(), failures[3]['matches'])
ticket = self.db.getCurrentBans(self.jail, "127.0.0.1", fromtime=MyTime.time()-100,
maxmatches=0)
self.assertEqual(len(ticket.getMatches()), 0)
# dbmaxmatches = 0, should retrieve 0 matches by last ban:
ticket.setMatches(["1","2","3"])
self.db.maxMatches = 0;
self.db.addBan(self.jail, ticket)
ticket = self.db.getCurrentBans(self.jail, "127.0.0.1", fromtime=MyTime.time()-100)
self.assertTrue(ticket is not None)
self.assertEqual(ticket.getAttempt(), len(failures))
self.assertEqual(len(ticket.getMatches()), 0)
def testGetBansMerged(self):
self.testAddJail()

View File

@ -179,6 +179,7 @@ def _start_params(tmp, use_stock=False, use_stock_cfg=None,
"pidfile = " + pjoin(tmp, "f2b.pid"),
"backend = polling",
"dbfile = " + db,
"dbmaxmatches = 100",
"dbpurgeage = 1d",
"",
)

View File

@ -69,9 +69,9 @@ class AddFailure(unittest.TestCase):
self.assertEqual(self.__failManager.getFailTotal(), 0)
self.__failManager.setFailTotal(13)
def testFailManagerAdd_MaxEntries(self):
maxEntries = 2
self.__failManager.maxEntries = maxEntries
def testFailManagerAdd_MaxMatches(self):
maxMatches = 2
self.__failManager.maxMatches = maxMatches
failures = ["abc\n", "123\n", "ABC\n", "1234\n"]
# add failures sequential:
i = 80
@ -86,8 +86,8 @@ class AddFailure(unittest.TestCase):
ticket = manFailList["127.0.0.1"]
# should retrieve 2 matches only, but count of all attempts (4):
self.assertEqual(ticket.getAttempt(), len(failures))
self.assertEqual(len(ticket.getMatches()), maxEntries)
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:])
self.assertEqual(len(ticket.getMatches()), maxMatches)
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxMatches:])
# add more failures at once:
ticket = FailTicket("127.0.0.1", 1000002000 - 10, failures)
ticket.setAttempt(len(failures))
@ -98,8 +98,8 @@ class AddFailure(unittest.TestCase):
ticket = manFailList["127.0.0.1"]
# should retrieve 2 matches only, but count of all attempts (8):
self.assertEqual(ticket.getAttempt(), 2 * len(failures))
self.assertEqual(len(ticket.getMatches()), maxEntries)
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:])
self.assertEqual(len(ticket.getMatches()), maxMatches)
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxMatches:])
# add self ticket again:
self.__failManager.addFailure(ticket)
#
@ -108,8 +108,16 @@ class AddFailure(unittest.TestCase):
ticket = manFailList["127.0.0.1"]
# same matches, but +1 attempt (9)
self.assertEqual(ticket.getAttempt(), 2 * len(failures) + 1)
self.assertEqual(len(ticket.getMatches()), maxEntries)
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:])
self.assertEqual(len(ticket.getMatches()), maxMatches)
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxMatches:])
# no matches by maxMatches == 0 :
self.__failManager.maxMatches = 0
self.__failManager.addFailure(ticket)
manFailList = self.__failManager._FailManager__failList
ticket = manFailList["127.0.0.1"]
self.assertEqual(len(ticket.getMatches()), 0)
# test set matches None to None:
ticket.setMatches(None)
def testFailManagerMaxTime(self):
self._addDefItems()

View File

@ -197,6 +197,8 @@ class Transmitter(TransmitterBase):
self.setGetTest("dbfile", tmpFilename)
# the same file name (again no jails / not changed):
self.setGetTest("dbfile", tmpFilename)
self.setGetTest("dbmaxmatches", "100", 100)
self.setGetTestNOK("dbmaxmatches", "LIZARD")
self.setGetTest("dbpurgeage", "600", 600)
self.setGetTestNOK("dbpurgeage", "LIZARD")
# the same file name (again with jails / not changed):
@ -211,6 +213,12 @@ class Transmitter(TransmitterBase):
self.assertEqual(self.transm.proceed(
["get", "dbfile"]),
(0, None))
self.assertEqual(self.transm.proceed(
["set", "dbmaxmatches", "100"]),
(0, None))
self.assertEqual(self.transm.proceed(
["get", "dbmaxmatches"]),
(0, None))
self.assertEqual(self.transm.proceed(
["set", "dbpurgeage", "500"]),
(0, None))
@ -374,6 +382,12 @@ class Transmitter(TransmitterBase):
self.assertLogged("Ban 192.0.2.2", wait=True)
self.assertNotLogged("Ban 192.0.2.1")
def testJailMaxMatches(self):
self.setGetTest("maxmatches", "5", 5, jail=self.jailName)
self.setGetTest("maxmatches", "2", 2, jail=self.jailName)
self.setGetTest("maxmatches", "-2", -2, jail=self.jailName)
self.setGetTestNOK("maxmatches", "Duck", jail=self.jailName)
def testJailMaxRetry(self):
self.setGetTest("maxretry", "5", 5, jail=self.jailName)
self.setGetTest("maxretry", "2", 2, jail=self.jailName)

View File

@ -168,6 +168,14 @@ persistent datastore. Set to
get the location of fail2ban
persistent datastore
.TP
\fBset dbmaxmatches <INT>\fR
sets the max number of matches
stored in database per ticket
.TP
\fBget dbmaxmatches\fR
gets the max number of matches
stored in database per ticket
.TP
\fBset dbpurgeage <SECONDS>\fR
sets the max age in <SECONDS> that
history of bans will be kept
@ -286,6 +294,11 @@ sets the number of failures
<RETRY> before banning the host
for <JAIL>
.TP
\fBset <JAIL> maxmatches <INT>\fR
sets the max number of matches
stored in memory per ticket in
<JAIL>
.TP
\fBset <JAIL> maxlines <LINES>\fR
sets the number of <LINES> to
buffer for regex search for <JAIL>
@ -393,6 +406,11 @@ gets the usedns setting for <JAIL>
gets the number of failures
allowed for <JAIL>
.TP
\fBget <JAIL> maxmatches\fR
gets the max number of matches
stored in memory per ticket in
<JAIL>
.TP
\fBget <JAIL> maxlines\fR
gets the number of lines to buffer
for <JAIL>

View File

@ -156,6 +156,11 @@ Database filename. Default: /var/lib/fail2ban/fail2ban.sqlite3
.br
This defines where the persistent data for fail2ban is stored. This persistent data allows bans to be reinstated and continue reading log files from the last read position when fail2ban is restarted. A value of \fINone\fR disables this feature.
.TP
.B dbmaxmatches
Max number of matches stored in database per ticket. Default: 10
.br
This option sets the max number of matched log-lines could be stored per ticket in the database. This also affects values resolvable via tags \fB<ipmatches>\fR and \fB<ipjailmatches>\fR in actions.
.TP
.B dbpurgeage
Database purge age in seconds. Default: 86400 (24hours)
.br
@ -276,6 +281,9 @@ regex (Python \fBreg\fRular \fBex\fRpression) to be added to the filter's failre
.TP
.B ignoreregex
regex which, if the log line matches, would cause Fail2Ban not consider that line. This line will be ignored even if it matches a failregex of the jail or any of its filters.
.TP
.B maxmatches
max number of matched log-lines the jail would hold in memory per ticket. By default it is the same value as \fBmaxretry\fR of jail (or default). This option also affects values resolvable via tag \fB<matches>\fR in actions.
.SS Backends
Available options are listed below.