Guides
fail2ban for MySQL, MariaDB, PostgreSQL and MongoDB
A database port open to the internet gets password-sprayed like SSH. fail2ban can stop that, but only if the database logs failed logins with the client address. Most don't by default. This guide shows the logging setting and the jail for each one.
To protect MySQL or MariaDB with fail2ban, make the server log failed logins (log_error_verbosity = 3 on MySQL 8, log_warnings = 2 on MariaDB, the default there). Then enable the stock [mysqld-auth] jail with logpath set to your error log. The mysqld-auth filter matches Access denied for user '...'@'<IP>' and bans port 3306.
[mysqld-auth]
enabled = true
backend = auto
logpath = /var/log/mysql/error.log
maxretry = 5
bantime = 1hBefore any of this: a database that only your app server uses shouldn't listen on the internet. bind-address = 127.0.0.1 (or a private address) plus a firewall rule beats any jail. Use fail2ban when remote access is a real requirement, as one layer among the fail2ban best practices.
Step 1: make MySQL or MariaDB log failed logins#
The filter needs a line like one of these in the log:
2026-10-25T18:36:10.123456Z 31 [Note] [MY-010926] [Server] Access denied for user 'root'@'203.0.113.42' (using password: YES)
2026-10-25 18:36:10 31 [Warning] Access denied for user 'root'@'203.0.113.42' (using password: YES)The first is MySQL 8.x, the second MariaDB. Whether those lines appear depends on the log level.
MySQL 8 logs Access denied as a Note, and the default log_error_verbosity = 2 (errors and warnings) drops notes. Raise it:
[mysqld]
log_error_verbosity = 3
log_error = /var/log/mysql/error.logRestart MySQL: sudo systemctl restart mysql. Verbosity 3 also logs other informational notes, so the error log grows faster. Make sure logrotate handles it (the Ubuntu package does).
MariaDB logs access-denied errors at log_warnings = 2, which has been the default since 10.2.4. Check it with SELECT @@log_warnings;. What usually differs is where it logs. On Debian and Ubuntu the packaged config leaves log_error commented out, so under systemd the errors go to the journal, not a file. Either read the journal (see below) or enable a file:
[mysqld]
log_warnings = 2
log_error = /var/log/mysql/error.logCreate the directory if it doesn't exist (sudo install -d -o mysql -g adm -m 2750 /var/log/mysql), then sudo systemctl restart mariadb.
Use log_warnings = 2 (log-warnings = 2 in older configs) in the [mysqld] section, and check log_error points at a file fail2ban can read.
Step 2: find the log path#
| System | Default error log |
|---|---|
| Ubuntu / Debian, MySQL 8 | /var/log/mysql/error.log |
| Debian / Ubuntu, MariaDB | systemd journal (journalctl -u mariadb) unless log_error is set |
| RHEL / Rocky / Alma, MariaDB | /var/log/mariadb/mariadb.log |
| RHEL / Rocky / Alma, MySQL 8 | /var/log/mysql/mysqld.log |
| Fedora / Arch, MariaDB | journal on most installs, or /var/log/mariadb/mariadb.log |
| Official Docker images | container stdout (docker logs) |
The jail's default logpath changed in fail2ban 1.1.1. It now tries /var/log/mariadb/mariadb.log, /var/log/mariadb/error.log, /var/log/mysql/mysqld.log, /var/log/mysql/error.log and /var/log/mysqld.log. Older versions point at the syslog daemon log (/var/log/daemon.log on Debian), which usually doesn't contain MySQL's messages at all. Set logpath yourself and you're independent of the version.
Ask the server where it logs: SELECT @@log_error;. A value of stderr means the journal under systemd.
Step 3: the jail#
File-based (MySQL 8, or MariaDB with log_error set):
[mysqld-auth]
enabled = true
backend = auto
port = 3306
logpath = /var/log/mysql/error.log
maxretry = 5backend = auto matters on Ubuntu 24.04, whose defaults-debian.conf sets backend = systemd for every jail. Without it, the jail would ignore your logpath (how backends work).
Journal-based (MariaDB on Debian/Ubuntu without log_error):
[mysqld-auth]
enabled = true
backend = systemd
journalmatch = _SYSTEMD_UNIT=mariadb.service
filter = mysqld-auth[_daemon="(?:mysqld|mariadbd)"]
port = 3306
maxretry = 5The filter has no journalmatch, so the jail sets one. In the journal MariaDB 10.5+ lines are tagged mariadbd, not mysqld, and the stock filter only accepts mysqld as the daemon name. The _daemon parameter fixes that. Without it, journal lines such as db1 mariadbd[812]: ... Access denied for user ... don't match.
Step 4: test#
# file
sudo fail2ban-regex /var/log/mysql/error.log mysqld-auth
# journal
sudo fail2ban-regex -m "_SYSTEMD_UNIT=mariadb.service" systemd-journal 'mysqld-auth[_daemon="(?:mysqld|mariadbd)"]'Generate a failure first with a wrong password from another host: mysql -h db.example.com -u test -pwrong. Then reload and check:
sudo fail2ban-client -t
sudo systemctl restart fail2ban
sudo fail2ban-client status mysqld-authConnections from localhost over the Unix socket are logged as 'user'@'localhost'. fail2ban can't ban a hostname, and you wouldn't want it to ban your own apps anyway. More on reading the regex output on fail2ban-regex.
PostgreSQL#
fail2ban has no stock PostgreSQL filter, and by default PostgreSQL doesn't put the client address on the failure line:
2026-10-25 18:36:10.123 UTC [4321] postgres@postgres FATAL: password authentication failed for user "postgres"Add the client host (%h) to log_line_prefix. On Debian/Ubuntu the packaged prefix is '%m [%p] %q%u@%d ', so extend it:
log_line_prefix = '%m [%p] %q%u@%d %h '
log_hostname = offlog_hostname must stay off so %h is an IP address. Reload PostgreSQL (sudo systemctl reload postgresql). Failures now look like this:
2026-10-25 18:36:10.123 UTC [4321] postgres@postgres 203.0.113.42 FATAL: password authentication failed for user "postgres"
2026-10-25 18:36:11.456 UTC [4322] postgres@postgres 198.51.100.7 FATAL: no pg_hba.conf entry for host "198.51.100.7", user "postgres", database "postgres", no encryptionlog_connections = on isn't needed for this. It logs successful connection attempts on separate lines, which don't help the filter. Create a custom filter that matches the prefix above:
[Definition]
failregex = ^\s*(?:\S+ )?\[\d+\] \S*@\S* <HOST> FATAL: password authentication failed for user "<F-USER>[^"]*</F-USER>"
^\s*(?:\S+ )?\[\d+\] (?:\S*@\S* )?(?:\S+ )?FATAL: no pg_hba\.conf entry for host "<HOST>"
ignoreregex =Both lines above match it, for IPv4 and IPv6 clients. Local socket connections ([local]) don't. Note the two spaces after FATAL:, which is how PostgreSQL writes it.
[postgresql]
enabled = true
filter = postgresql
backend = auto
port = 5432
logpath = /var/log/postgresql/postgresql-*-main.log
maxretry = 5On RHEL-family systems the log is under the data directory, for example logpath = /var/lib/pgsql/data/log/postgresql-*.log. Test with sudo fail2ban-regex /var/log/postgresql/postgresql-16-main.log /etc/fail2ban/filter.d/postgresql.local.
If you change log_line_prefix later, the filter must change with it. The regex is tied to the prefix format.
MongoDB#
The stock mongodb-auth filter and [mongodb-auth] jail (port 27017, logpath = /var/log/mongodb/mongodb.log) were written for MongoDB's old plain-text log format. MongoDB 4.4 and newer log structured JSON, which the stock filter doesn't match. A failed login in a current version is a JSON object with "c":"ACCESS", "id":20249, "msg":"Authentication failed" and the client address in attr.remote (some versions use client).
A local filter for the JSON format:
[Definition]
failregex = ^.*"c":"ACCESS",\s*"id":20249,.*"msg":"Authentication failed".*"(?:remote|client)":"<ADDR>:\d+"
datepattern = {^LN-BEG}\{"t":\{"\$date":"%%Y-%%m-%%dT%%H:%%M:%%S\.%%f%%z"\}
ignoreregex =It matches lines of this shape:
{"t":{"$date":"2026-10-25T18:36:10.123+00:00"},"s":"I", "c":"ACCESS", "id":20249, "ctx":"conn12","msg":"Authentication failed","attr":{"mechanism":"SCRAM-SHA-256","speculative":false,"principalName":"admin","authenticationDatabase":"admin","remote":"203.0.113.42:51234","extraInfo":{},"error":"AuthenticationFailed: SCRAM authentication failed, storedKey mismatch"}}[mongodb-json]
enabled = true
filter = mongodb-json
backend = auto
port = 27017
logpath = /var/log/mongodb/mongod.log
maxretry = 5Check one of your own failure lines in the regex tester before enabling it, because the field name holding the address has varied between releases. Drivers that try speculative authentication can also log a failed attempt during a normal handshake, so keep maxretry above 1 and whitelist your app servers. Packaged MongoDB logs to /var/log/mongodb/mongod.log, not the mongodb.log the stock jail expects.
Built-in filter reference: mysqld-auth, mongodb-auth, mssql-auth and more#
The built-in filters this guide uses, exactly as fail2ban 1.1.1 ships them — with the log lines they match and a link to test them. Looking for something else? See filters for web apps and admin panels, or the full filter library.
mysqld-auth MySQL / MariaDB
The mysqld-auth filter bans hosts with repeated 'Access denied' errors in the MySQL/MariaDB error log. File: /etc/fail2ban/filter.d/mysqld-auth.conf; jail [mysqld-auth]; log on Debian/Ubuntu: /var/log/mariadb/mariadb.log.
Log lines it bans (from fail2ban's test suite):
130324 0:04:00 [Warning] Access denied for user 'root'@'192.168.1.35' (using password: NO)130324 8:24:09 [Warning] Access denied for user 'root'@'220.95.238.171' (using password: NO)130324 17:56:13 [Warning] Access denied for user 'root'@'61.160.223.112' (using password: NO)
Show the failregex fail2ban compiles (1 line)
^(?:\[\])?\s*(?:<[^.]+\.[^.]+>\s+)?(?:\S+\s+)?(?:kernel:\s?\[ *\d+\.\d+\]:?\s+)?(?:@vserver_\S+\s+)?(?:(?:(?:\[\d+\])?:\s+[\[\(]?mysqld(?:\(\S+\))?[\]\)]?:?|[\[\(]?mysqld(?:\(\S+\))?[\]\)]?:?(?:\[\d+\])?:?)\s+)?(?:\[ID \d+ \S+\]\s+)?(?:(?:\d{6}|\d{4}-\d{2}-\d{2})[ T]\s?\d{1,2}:\d{2}:\d{2} )?(?:\d+ )?\[\w+\] (?:\[[^\]]+\] )*Access denied for user '<F-USER>[^']+</F-USER>'@'<HOST>'(?:\s+(?:to database '[^']*'|\(using password: (?:YES|NO)\)){1,2})?\s*$Test mysqld-auth in the browser or on the server: sudo fail2ban-regex /var/log/mariadb/mariadb.log mysqld-auth · source
mongodb-auth MongoDB
The mongodb-auth filter bans failed MongoDB authentication attempts. File: /etc/fail2ban/filter.d/mongodb-auth.conf; jail [mongodb-auth]; log on Debian/Ubuntu: /var/log/mongodb/mongodb.log.
Log lines it bans (from fail2ban's test suite):
2016-11-20T00:04:00.111+0100 [conn1] end connection 192.0.2.35:53276 (0 connections now open)2016-11-20T00:24:00.111+0100 [conn5] end connection 192.0.2.171:53276 (0 connections now open)2016-11-20T00:24:00.111+0100 [conn334] end connection 192.0.2.176:53276 (0 connections now open)
Show the failregex fail2ban compiles (1 line)
^\s+\[conn(?P<__connid>\d+)\] Failed to authenticate [^\n]+<SKIPLINES>\s+\[conn(?P=__connid)\] end connection <HOST>Test mongodb-auth in the browser or on the server: sudo fail2ban-regex /var/log/mongodb/mongodb.log mongodb-auth · source
mssql-auth Microsoft SQL Server on Linux
The mssql-auth filter bans failed SQL Server logins. File: /etc/fail2ban/filter.d/mssql-auth.conf; jail [mssql-auth]; log on Debian/Ubuntu: /var/opt/mssql/log/errorlog.
Log lines it bans (from fail2ban's test suite):
2020-02-24 16:05:21.00 Logon Login failed for user 'Backend'. Reason: Could not find a login matching the name provided. [CLIENT: 192.0.2.1]2020-02-24 16:30:25.88 Logon Login failed for user '===)jf02hüas9ä##22f'. Reason: Could not find a login matching the name provided. [CLIENT: 192.0.2.2]2020-02-24 16:31:12.20 Logon Login failed for user ''. Reason: An attempt to login using SQL authentication failed. Server is configured for Integrated authentication only. [CLIENT: 192.0.2.3]
Show the failregex fail2ban compiles (1 line)
^\s*Logon\s+Login failed for user '<F-USER>(?:[^']*|.*)</F-USER>'\. [^'\[]+\[CLIENT: <ADDR>\]$Test mssql-auth in the browser or on the server: sudo fail2ban-regex /var/opt/mssql/log/errorlog mssql-auth · source
phpmyadmin-syslog phpMyAdmin
The phpmyadmin-syslog filter bans failed phpMyAdmin logins (phpMyAdmin logging to syslog). File: /etc/fail2ban/filter.d/phpmyadmin-syslog.conf; jail [phpmyadmin-syslog]; log on Debian/Ubuntu: /var/log/auth.log.
Log lines it bans (from fail2ban's test suite):
Aug 22 14:50:22 eurostream phpMyAdmin[16358]: user denied: root (mysql-denied) from 192.0.2.1
Show the failregex fail2ban compiles (1 line)
^(?:\[\])?\s*(?:<[^.]+\.[^.]+>\s+)?(?:\S+\s+)?(?:kernel:\s?\[ *\d+\.\d+\]:?\s+)?(?:@vserver_\S+\s+)?(?:(?:(?:\[\d+\])?:\s+[\[\(]?phpMyAdmin(?:\(\S+\))?[\]\)]?:?|[\[\(]?phpMyAdmin(?:\(\S+\))?[\]\)]?:?(?:\[\d+\])?:?)\s+)?(?:\[ID \d+ \S+\]\s+)?user denied: (?:\S+|.*?) \(mysql-denied\) from <HOST>\s*$Test phpmyadmin-syslog in the browser or on the server: sudo fail2ban-regex /var/log/auth.log phpmyadmin-syslog · source
FAQ#
How do I use fail2ban with MySQL?#
Set log_error_verbosity = 3 in MySQL 8's [mysqld] section so Access denied notes are logged, then enable the [mysqld-auth] jail with logpath = /var/log/mysql/error.log and backend = auto. Test with fail2ban-regex /var/log/mysql/error.log mysqld-auth after a failed remote login.
Why does the fail2ban mysqld-auth jail not ban anything?#
Usually the failures aren't logged or aren't where the jail looks. MySQL 8 needs log_error_verbosity = 3. MariaDB on Debian logs to the journal unless log_error is set. Older fail2ban versions point the jail at /var/log/daemon.log. Run SELECT @@log_error; and set logpath to match.
Does fail2ban work with MariaDB?#
Yes. MariaDB logs [Warning] Access denied for user ... at the default log_warnings = 2. With a log file, use the stock mysqld-auth jail. With the journal, set backend = systemd, journalmatch = _SYSTEMD_UNIT=mariadb.service and filter = mysqld-auth[_daemon="(?:mysqld|mariadbd)"].
Is there a fail2ban filter for PostgreSQL?#
Not in fail2ban itself. Add %h to PostgreSQL's log_line_prefix (for example '%m [%p] %q%u@%d %h ') so failed logins include the client IP, then match FATAL: password authentication failed for user with a short custom filter and ban port 5432.
Does the fail2ban mongodb-auth filter work with MongoDB 6 or 7?#
No. The stock filter expects the plain-text log format used before MongoDB 4.4. Current versions log JSON, where a failed login is event id 20249 with "msg":"Authentication failed". Use a JSON-aware local filter that captures the address from the remote field.
Spotted something wrong or out of date? Tell us — every page is checked against the current fail2ban release.