Guides 11794 Published by

Fail2Ban is an open-source intrusion prevention daemon that scans service logs for attack patterns and automatically blocks offending IPs through firewall rules. Debian 13 Trixie modernizes the package by defaulting to the systemd journal backend, switching to nftables for ban actions, and requiring Python 3.12. Administrators configure it through a layered override system of *.conf, *.local, and jail.d/ drop-in files, replacing legacy log paths with journalmatch directives. Production deployments combine targeted SSH, web, and mail jails with a long-term recidive jail, all validated through fail2ban-regex and managed via the daemon's client/server architecture.





How to Install and Configure Fail2Ban on Debian 13 Trixie

A hardened intrusion-prevention setup using the latest Fail2Ban package, the systemd journal backend, and nftables.

Fail2Ban is the de facto standard for automated intrusion prevention on Linux servers, and the Trixie release brings it firmly into the systemd journal era with native nftables support. The current Debian package (fail2ban 1.1.0-8) ships with a backend that reads directly from journald, defaults to nftables for banning instead of legacy iptables, and drops support for Python 2 in favor of Python 3.12. If you are running a machine on the internet, even for a few hours, you will see brute-force attempts before the package finishes installing. Fail2Ban turns those attempts into firewall rules without you having to watch logs manually.

This tutorial walks through the full installation, configuration architecture, filter and action setup, and production-hardening strategies for Debian 13. The material assumes you are running Trixie as a fresh install or upgrading from Bookworm with custom configurations you want to verify.

Fail2ban

What Fail2Ban Actually Does

Fail2Ban runs as a long-lived daemon managed by systemd. It watches service logs, counts how many times a source IP triggers a failure pattern within a rolling time window, and when the threshold is exceeded, it runs firewall commands to block that IP. That is it. The rest is just configuration granularity.

The original codebase came from Cyril Jaquier in 2004 as a Python script protecting against SSH brute-force attacks. Two decades of community maintenance have expanded it into a general-purpose log-monitoring engine. It supports SSH, Nginx, Apache, Postfix, Dovecot, FTP, DNS, VoIP, VPNs, databases, and dozens of web applications. The Debian package is maintained by the Debian Python Team with upstream coordination.

Fail2Ban is released under the GNU GPL v2. The source lives at https://github.com/fail2ban/fail2ban, and the documentation is hosted at https://fail2ban.readthedocs.io/. Python 3.5 or newer is required, though Trixie ships with Python 3.12.

Architecture Overview

Fail2Ban uses a client/server architecture rather than running as a monolithic process. This design choice has real consequences for how you manage it.

The server (fail2ban-server) is the long-running daemon. It manages jails, coordinates filters and actions, and stores ban state in a SQLite database. The client (fail2ban-client or f2b-client on newer versions) is the CLI tool you use to interact with the server over a Unix-domain socket at /run/fail2ban/fail2ban.sock. Filters live in /etc/fail2ban/filter.d/ and define regular expressions that identify failure patterns. Actions live in /etc/fail2ban/action.d/ and define what happens on ban, unban, start, and stop.

Each jail runs in its own thread with its own filter, fail manager, and ban manager. A crash in one filter does not take down another. The server keeps all ban state in memory and optionally in SQLite, so configuration reloads do not wipe out existing bans.

Data flows from the service writing a log entry, through the backend (pyinotify, polling, or systemd), into the filter where it is matched against failregex patterns, into the FailManager where failure tickets accumulate, and finally into the BanManager which executes the configured action when the threshold is exceeded.

Installation

Install the package and verify it started:

sudo apt update
sudo apt install fail2ban

Check the version and service status:

fail2ban-client --version
sudo systemctl status fail2ban

You should see an active running service with the package version displayed. The package installs a specific file layout under /etc/fail2ban/. The shipped configuration files include fail2ban.conf, jail.conf, paths-debian.conf, and drop-in directories for filters, actions, and jail fragments.

The file tree looks like this:

/etc/fail2ban/
├── fail2ban.conf
├── fail2ban.local
├── action.d/
├── filter.d/
├── jail.conf
├── jail.local
├── jail.d/
│   └── 00-main.conf
├── paths-common.conf
├── paths-debian.conf
└── paths-overrides.local

A rule of thumb that will save you headaches later: never edit *.conf files shipped by the package. Always create *.local files or drop-in .conf files in jail.d/. The package manager overwrites *.conf on upgrades but leaves your *.local and jail.d/ directories untouched.

Configuration Layers

Fail2Ban resolves configuration from multiple layers, with later sources overriding earlier ones. The order matters because settings stack:

1. fail2ban.conf          (daemon-level settings)
2. fail2ban.local         (admin daemon overrides)
3. jail.conf              (package-supplied templates, read-only reference)
4. jail.d/*.conf          (ordered drop-in fragments, sorted lexicographically)
5. jail.local             (your primary customization file)

Each layer can define global defaults in [DEFAULT], individual jail sections like [sshd] or [nginx-http-auth], and filter or action definitions. Python ConfigParser interpolation syntax (%(variable)s) lets one setting reference another. For example, you can set bantime = 1h and then define recidive_bantime = %(bantime)s * 7 to get a seven-hour ban for repeat offenders.

Core Configuration

/etc/fail2ban/fail2ban.conf controls the daemon itself. The shipped file is well-commented. The key directives you will likely want to override live in fail2ban.local:

[Definition]
# Increase log retention for forensic analysis
dbpurgeage = 604800   # 7 days in seconds
# Use syslog for Fail2Ban's own logging
logtarget = SYSLOG
syslog_socket = /dev/log

The dbpurgeage setting controls how long ban records persist in the SQLite database at /var/lib/fail2ban/fail2ban.sqlite3. The default is 86,400 seconds (24 hours). You can inspect the database directly with sqlite3 if you need to audit ban history.

Global Defaults in jail.local

The [DEFAULT] section in jail.local sets behavior that applies to every jail unless a specific jail overrides it. A minimal working configuration looks like this:

[DEFAULT]
# Ignore your own management IPs
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24 10.0.0.0/8
# Ban offending IPs for 1 hour
bantime = 1h

# Look back over the last 30 minutes
findtime = 30m

# Allow up to 5 failures before banning
maxretry = 5
# Use systemd journal backend
backend = systemd

# Warn on hostname lookups without blocking
usedns = warn

# Email destination for ban notifications
destemail = admin@example.com
sender = fail2ban@example.com
mta = sendmail

# Default action: ban + send whois report
action = %(action_mw)s

# Use nftables (Debian Trixie default)
banaction = nftables[type=multiport]

Fail2Ban defines reusable action shortcuts. %(action_)s bans only. %(action_mw)s bans and sends an email with a whois lookup. %(action_mwl)s includes relevant log lines in the email. %(action_xarf)s sends an XARF abuse report to the upstream provider.

Debian-Specific Defaults in Trixie

Debian 13 Trixie introduced several changes that differ from Bookworm. The most impactful shift for administrators is the default ban action moving from iptables-multiport to nftables. SSH and Postfix backends switched from file-based polling to systemd journal reading. python3-systemd moved from a Recommends to a hard Depends.

Upgrading from Bookworm requires no manual config changes. Fail2Ban's post-install script detects existing /etc/fail2ban/jail.local and preserves it. You should verify that customizations still work with nftables and the systemd backend, though.

The Debian-specific paths and defaults live in /usr/share/fail2ban/debian.conf. If your services use non-standard log paths, override them in /etc/fail2ban/paths-overrides.local:

[DEFAULT]
sshd_log         = /var/log/custom-ssh.log
apache_access_log  = /opt/apache/logs/access.log
nginx_error_log    = /opt/nginx/logs/error.log

The systemd Backend

Debian has moved away from traditional file-based syslog to systemd-journald as the primary log collector. Services like sshd, postfix, and others write their logs directly to the journal. Fail2Ban can no longer rely on /var/log/auth.log existing for SSH monitoring on a stock Trixie install.

The systemd backend queries the journal via libsystemd instead of polling files. This has two implications for your configuration. First, you set journalmatch in the filter rather than logpath in the jail. Second, the filter must include a journal match rule:

# /etc/fail2ban/filter.d/sshd.conf
[Definition]
journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd + _COMM=sshd-session

The + is a logical AND. Both conditions must be true for an entry to be included. You can use or for logical OR, wildcards for prefix matching, and ! for exclusion.

If you still want /var/log/auth.log to receive sshd messages alongside the journal, check your rsyslog configuration. The line auth,authpriv.* /var/log/auth.log should be uncommented in /etc/rsyslog.d/50-default.conf. Restart rsyslog after changing it.

Query the journal manually to verify your logs are being captured:

sudo journalctl -u sshd.service --since today | head -20
sudo journalctl -u sshd.service _COMM=sshd-session | grep -i "failed" | tail -20

Jail Configuration Examples

Here are production-ready jail configurations for common services. Each lives in its own drop-in file under /etc/fail2ban/jail.d/.

SSH with Aggressive Settings

# /etc/fail2ban/jail.d/01-sshd.conf
[sshd]
enabled     = true
port        = ssh
filter      = sshd
backend     = systemd
bantime     = 24h
findtime    = 15m
maxretry    = 3
mode        = extra
action      = %(action_mw)s

The mode = extra setting catches additional attack patterns beyond standard authentication failures. Mode values include normal, ddos (connection resets and protocol errors), extra (negotiation failures), and aggressive (all of the above combined).

Nginx HTTP Auth and Bot Protection

# /etc/fail2ban/jail.d/02-nginx.conf
[nginx-http-auth]
enabled  = true
port     = http,https
filter   = nginx-http-auth
logpath  = /var/log/nginx/error.log
bantime  = 1h
findtime = 10m
maxretry = 5
action   = %(action_mwl)s

[nginx-limit-req]
enabled  = true
port     = http,https
filter   = nginx-limit-req
logpath  = /var/log/nginx/error.log
bantime  = 30m
findtime = 10m
maxretry = 2
[nginx-botsearch] enabled = true port = http,https filter = nginx-botsearch logpath = /var/log/nginx/error.log bantime = 48h maxretry = 1

Postfix SMTP Authentication

# /etc/fail2ban/jail.d/03-postfix.conf
[postfix]
enabled   = true
port      = smtp,465,submission
filter    = postfix
logpath   = /var/log/mail.log
backend   = systemd
bantime   = 6h
findtime  = 30m
maxretry  = 3
action    = %(action_mw)s

[postfix-sasl]
enabled   = true
port      = smtp,465,submission,imap,imaps,pop3,pop3s
filter    = postfix[sasl]
logpath   = /var/log/mail.log
backend   = systemd
bantime   = 12h
findtime  = 30m
maxretry  = 4
[postfix-rbl] enabled = true port = smtp,465,submission filter = postfix[mode=rbl] logpath = /var/log/mail.log backend = systemd bantime = 24h maxretry = 1

The rbl mode hits against the Realtime Blackhole List are almost always malicious. A single strike is appropriate.

Recidive Jail for Repeat Offenders

The built-in [recidive] jail monitors Fail2Ban's own logs for IPs that have already been banned. It provides escalating penalties:

# /etc/fail2ban/jail.d/recidive.conf
[recidive]
enabled  = true
filter   = recidive
logpath  = /var/log/fail2ban.log
action   = %(action_mwl)s
bantime  = 30d
findtime = 90d
maxretry = 3

Ban repeat offenders for a month, look back three months, and trigger after three bans within that window. This catches sustained attackers who keep trying after their initial ban expires.

Filter Testing and Debugging

The fail2ban-regex tool validates your filter configurations against actual log data:

# Test against a log file
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

# Test against journal entries
sudo fail2ban-regex --journalmatch '_SYSTEMD_UNIT=sshd.service' \
    /dev/stdin /etc/fail2ban/filter.d/sshd.conf <<< "Aug 12 ... Failed password ..."
# Verbose output showing match details
fail2ban-regex -v /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

When debugging a jail that refuses to activate, follow this sequence: check that Fail2Ban can parse your config with fail2ban-client ping, test the filter with fail2ban-regex, enable debug logging temporarily with fail2ban-set-logging-level debug, watch real-time logs with tail -f /var/log/fail2ban.log, and restore info level when finished.

Common pitfalls on Trixie include setting logpath when backend = systemd is active (remove logpath and rely on journalmatch in the filter), missing /var/log/auth.log (rsyslog may not be writing it), and banned IPs reappearing after expiration (enable the recidive jail).

Management Commands

# Service control
sudo systemctl start fail2ban
sudo systemctl stop fail2ban
sudo systemctl restart fail2ban
sudo systemctl enable --now fail2ban

# Check status
sudo systemctl status fail2ban
sudo tail -f /var/log/fail2ban.log
# Interactive client
sudo fail2ban-client -i
> status
> status sshd
> set sshd banip 10.20.30.40
> set sshd unbanip 10.20.30.40
> get sshd banip

# One-shot commands
fail2ban-client status
fail2ban-client status sshd
fail2ban-client set sshd banip 10.20.30.40

# Modern CLI (Fail2Ban 1.x)
sudo f2b-client status
sudo f2b-client jail list
sudo f2b-client jail info sshd
sudo f2b-client ban add sshd 10.20.30.40
sudo f2b-client ban list sshd

The f2b-client command is a newer addition to Fail2Ban 1.x. It provides a more modern interface for the same operations. Use whichever you prefer.

Advanced Configuration

Fail2Ban supports escalating ban times through exponential backoff. Set bantime.increment = true in your jail configuration. The ban duration doubles with each successive offense until it hits bantime.maxtime. With bantime = 1h and bantime.maxtime = 86400, the progression would be 1 hour, 2 hours, 4 hours, 8 hours, 16 hours, 32 hours, then capped at 24 hours.

You can also define custom multipliers instead of exponential growth:

bantime.increment = true
bantime.multipliers = 1 5 30 60 300 720 1440 2880

With bantime = 60s, bans progress from 1 minute through 5 minutes, 30 minutes, 1 hour, 5 hours, 12 hours, 1 day, then 2 days.

Add randomness to prevent attackers from timing their ban expiration:

bantime.rndtime = 1800

This adds up to 30 minutes of random time to each ban duration.

The Verdict

Fail2Ban remains a solid choice for automated intrusion prevention on Debian servers. The Trixie package brings it into the modern logging stack with systemd journal support and nftables as the default ban action. The client/server architecture makes it easy to manage, and the filter/action system is flexible enough to protect almost any service.

The learning curve is moderate. Writing custom filters requires understanding regular expressions and Fail2Ban's interpolation syntax. The systemd backend shift may require adjustments if you have custom log paths. But the out-of-the-box jail configurations cover the services most people run.

One criticism is that the default bantime of 10 minutes is too short for production use. Override it. Ten minutes is enough time for an automated scanner to try a few thousand credentials before moving on. An hour is better. A day is reasonable for SSH. A month for recidive catches sustained attackers.

Production Configuration Example

Here is a complete hardened configuration for a typical Debian 13 Trixie VPS. Copy these files into your system:

/etc/fail2ban/jail.local:

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24 10.0.0.0/8 203.0.113.0/24
bantime     = 1h
findtime    = 30m
maxretry    = 5
bantime.increment = true
bantime.maxtime   = 86400
bantime.rndtime   = 900
backend = systemd
usedns  = warn
logencoding = auto

destemail = root@localhost
sender    = fail2ban@%(__fq_hostname)s
mta       = sendmail
action = %(action_mw)s

[recidive]
enabled  = true
filter   = recidive
logpath  = /var/log/fail2ban.log
bantime  = 30d
findtime = 90d
maxretry = 3
action   = %(action_mwl)s

/etc/fail2ban/jail.d/01-sshd.conf:

[sshd]
enabled     = true
port        = ssh
filter      = sshd
backend     = systemd
bantime     = 24h
findtime    = 15m
maxretry    = 3
mode        = extra
action      = %(action_mw)s

/etc/fail2ban/jail.d/02-services.conf:

[nginx-http-auth]
enabled   = true
port      = http,https
filter    = nginx-http-auth
logpath   = /var/log/nginx/error.log
bantime   = 1h
findtime  = 10m
maxretry  = 5
[nginx-limit-req] enabled = true port = http,https filter = nginx-limit-req logpath = /var/log/nginx/error.log bantime = 30m findtime = 10m maxretry = 2
[postfix] enabled = true port = smtp,465,submission filter = postfix logpath = /var/log/mail.log backend = systemd bantime = 6h findtime = 30m maxretry = 3
[postfix-sasl] enabled = true port = smtp,465,submission,imap,imaps,pop3,pop3s filter = postfix[sasl] logpath = /var/log/mail.log backend = systemd bantime = 12h findtime = 30m maxretry = 4
[dovecot] enabled = true port = pop3,pop3s,imap,imaps,submission,465,sieve filter = dovecot logpath = /var/log/mail.log backend = systemd bantime = 2h findtime = 10m maxretry = 5

/etc/fail2ban/fail2ban.local:

[Definition]
logtarget = SYSLOG
dbpurgeage = 2592000

Verify and reload:

sudo fail2ban-client reload && echo "Config OK" || echo "Config error — check logs"
fail2ban-client status
fail2ban-client status sshd
sudo tail -f /var/log/fail2ban.log

Watch the logs for a day or two. Adjust maxretry and bantime values based on your actual traffic patterns. Too aggressive, and legitimate users get locked out. Too lenient, and attackers keep trying. The recidive jail handles sustained attackers, so err on the side of slightly aggressive for the initial jails.

Quick Reference

ActionCommand
Installapt install fail2ban
Enable and startsystemctl enable --now fail2ban
View jail statusfail2ban-client status or f2b-client jail list
Ban IP manuallyfail2ban-client set <jail> banip <ip>
Unban IP manuallyfail2ban-client set <jail> unbanip <ip>
Test filter regexfail2ban-regex <logfile> <filter.conf>
Reload configsystemctl reload fail2ban
Restart servicesystemctl restart fail2ban
Check Fail2Ban logstail -f /var/log/fail2ban.log
Enable debug loggingfail2ban-set-logging-level debug