SSH password probes and noisy web login attempts are easier to handle when Debian can block repeat offenders automatically instead of waiting for a manual firewall rule. You can install Fail2Ban on Debian from the default APT repositories, keep SSH protection active, and tune ban thresholds without adding a third-party package source.
Debian 13 (Trixie), Debian 12 (Bookworm), and Debian 11 (Bullseye) all package Fail2Ban, but their defaults differ. Debian 13 sets Fail2Ban to use the systemd journal and nftables, while Debian 12 and Debian 11 keep the older iptables action and may still expect /var/log/auth.log unless you set the systemd backend yourself.
Install Fail2Ban on Debian
Refresh APT metadata, apply available package updates, then install the Debian fail2ban package:
sudo apt update
sudo apt upgrade
sudo apt install fail2ban
Review the upgrade transaction before confirming on remote servers, especially when OpenSSH, kernel, networking, or firewall packages are included.
These commands use
sudofor package and service changes. If the account cannot use sudo yet, add the account with the Debian sudoers workflow before changing security services.
Debian installs Fail2Ban and its recommended helpers from the standard main repository. The exact dependency set varies by release, but common helper packages include python3-systemd, python3-pyinotify, and whois.
| Debian release | Package version | Default log backend | Default ban action |
|---|---|---|---|
| Debian 13 (Trixie) | 1.1.0 | systemd journal | nftables |
| Debian 12 (Bookworm) | 1.0.2 | file log unless overridden | iptables-multiport |
| Debian 11 (Bullseye) | 0.11.2 | file log unless overridden | iptables-multiport |
Check the installed version with fail2ban-client:
fail2ban-client --version
Fail2Ban v1.1.0
On Debian 12, the same command reports Fail2Ban v1.0.2. On Debian 11, it reports Fail2Ban v0.11.2. Confirm the package source when you need to prove the system is using Debian’s archive package:
apt-cache policy fail2ban
fail2ban:
Installed: 1.1.0-8
Candidate: 1.1.0-8
Version table:
*** 1.1.0-8 500
500 http://deb.debian.org/debian trixie/main amd64 Packages
100 /var/lib/dpkg/status
Create a Debian Fail2Ban Baseline
Keep local settings in a separate file under /etc/fail2ban/jail.d/ instead of editing /etc/fail2ban/jail.conf or /etc/fail2ban/jail.d/defaults-debian.conf. Package updates can replace the default files, while a local drop-in keeps only the settings you intentionally changed.
The following baseline keeps the SSH jail enabled, makes the log backend explicit, adds localhost to the allowlist, and tightens SSH failures to three attempts in ten minutes. The 99- prefix keeps this file loaded after Debian’s packaged defaults. Keep the localhost entries in ignoreip, and add your real management IP addresses or CIDR networks after them before saving if you administer the server remotely.
sudo install -d -m 0755 /etc/fail2ban/jail.d
sudo tee /etc/fail2ban/jail.d/99-local-baseline.conf > /dev/null <<'EOF'
[DEFAULT]
backend = systemd
ignoreip = 127.0.0.1/8 ::1
bantime = 10m
findtime = 10m
maxretry = 5
[sshd]
enabled = true
maxretry = 3
bantime = 1h
findtime = 10m
EOF
The backend = systemd line is especially useful on Debian 12 and Debian 11 minimal systems where /var/log/auth.log may be missing or incomplete. Debian 13 already sets the SSH jail to the systemd journal, so the line makes the behavior explicit without changing the default ban action.
Validate the configuration before restarting Fail2Ban:
sudo fail2ban-client -t
OK: configuration test is successful
Restart the service, give the client socket a moment to appear, then confirm Fail2Ban is active and enabled:
sudo systemctl restart fail2ban
sleep 2
systemctl is-active fail2ban
systemctl is-enabled fail2ban
sudo fail2ban-client ping
active enabled Server replied: pong
List enabled jails and inspect the SSH jail:
sudo fail2ban-client status
sudo fail2ban-client status sshd
Status |- Number of jail: 1 `- Jail list: sshd
The detailed jail output should show either a Journal matches line or a File list line. With the systemd backend, Debian systems commonly show a journal match similar to this:
Status for the jail: sshd |- Filter | |- Currently failed: 0 | |- Total failed: 0 | `- Journal matches: _SYSTEMD_UNIT=ssh.service + _COMM=sshd `- Actions |- Currently banned: 0 |- Total banned: 0 `- Banned IP list:
Debian 12 can show sshd.service instead of ssh.service in the journal match. The important result is that the sshd jail exists, Fail2Ban is active, and the jail is reading from the expected log source.
Understand Fail2Ban Jails, Filters, and Actions
Fail2Ban has a few moving parts, and knowing the names makes troubleshooting much easier. A jail ties together the service to watch, the filter that identifies failures, and the action that blocks a source address.
| Term | Meaning | Debian example |
|---|---|---|
| Jail | Service-specific rule set that counts failures and triggers bans | [sshd] |
| Filter | Regex file that decides which log lines count as failures | /etc/fail2ban/filter.d/sshd.conf |
| Backend | Where Fail2Ban reads events from | systemd or /var/log/auth.log |
| Action | Firewall or notification action triggered after a ban | nftables, iptables-multiport, or ufw |
| Allowlist | Trusted addresses that should not be banned | ignoreip |
Do not enable every bundled jail. Enable only jails for services that are installed, running, and writing logs in the format the filter expects. A jail for Apache, Nginx, Postfix, or Dovecot will not help unless that service exists and the corresponding log source contains real failure lines.
Choose a Firewall Action on Debian
Most Debian systems should keep the packaged action unless they already standardize on another firewall manager. The log backend controls how Fail2Ban reads events; the action controls how Fail2Ban blocks addresses.
| Action | Use it when | Debian note |
|---|---|---|
nftables | You use Debian 13 defaults or direct nftables rules | Debian 13 sets this in defaults-debian.conf. |
iptables-multiport | You use Debian 12 or Debian 11 defaults | Older Fail2Ban packages use this action from jail.conf. |
ufw | You already manage host firewall policy with UFW | Install and allow SSH with UFW on Debian before switching actions. |
| firewalld actions | You intentionally use firewalld on Debian | Install and verify Firewalld on Debian first. |
If UFW is already installed, enabled, and allowing your SSH access path, set Fail2Ban’s default action to UFW in your local drop-in:
[DEFAULT]
banaction = ufw
banaction_allports = ufw
Do not enable UFW over SSH until the SSH service is allowed through the firewall and a second session is available for recovery. A firewall change can lock out the same connection you are using to administer the server.
Manage Fail2Ban Bans and Logs
The fail2ban-client command is the normal control tool for jail status, manual bans, and manual unbans.
sudo fail2ban-client status
sudo fail2ban-client status sshd
To test manual ban handling without targeting a real client, use a documentation-only address and unban it immediately after checking the jail:
sudo fail2ban-client set sshd banip 203.0.113.7
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip 203.0.113.7
The ban command returns 1 when it adds the address. The status output then lists the address under Banned IP list; after the unban command, that list should no longer include the test address.
Fail2Ban writes its own log to /var/log/fail2ban.log. Use the tail command to read recent activity or follow new entries:
sudo tail -n 30 /var/log/fail2ban.log
sudo tail -f /var/log/fail2ban.log
Use the grep command guide when you need narrower filtering. This example searches for one source address in the Fail2Ban log:
sudo grep '203.0.113.7' /var/log/fail2ban.log
For SSH authentication failures on systems using the systemd backend, compare the active jail with recent SSH journal entries:
sudo journalctl -u ssh --since "1 hour ago" --no-pager | grep -Ei 'failed|invalid'
Some Debian systems log SSH under sshd instead of ssh. If the command prints nothing but you know failed logins occurred, repeat the check with sudo journalctl -u sshd --since "1 hour ago" --no-pager.
Enable Jails for Other Debian Services
SSH protection is the normal first jail because SSH is often exposed on servers. If the OpenSSH server is not installed or enabled yet, configure SSH on Debian before depending on the sshd jail. Add service jails only after the protected service is installed and its logs are available.
For Apache, install and verify Apache on Debian first, then enable a matching jail such as apache-auth or apache-badbots when that log source exists:
[apache-auth]
enabled = true
port = http,https
logpath = %(apache_error_log)s
maxretry = 3
findtime = 10m
bantime = 1h
For Nginx, install and verify Nginx on Debian first, then enable only jails whose log paths match the site configuration. After adding any service jail, run sudo fail2ban-client -t, restart Fail2Ban, and check the named jail with sudo fail2ban-client status jail-name.
Mail and Cloudflare actions are advanced paths, not base Fail2Ban behavior. Mail actions need a sendmail-compatible mail service and often whois; Cloudflare actions need a scoped API token and a separately tested edge-blocking policy.
Test SSH Protection Safely
A real SSH ban test should come from a second machine or disposable client, not from your only current admin session. Keep the existing SSH session open, trigger failed logins against a non-existent test user from the second client, then stop before locking out a legitimate account or automation source.
ssh testuser@your-server-ip
After enough failures to cross maxretry, check the jail and log:
sudo fail2ban-client status sshd
sudo tail -n 20 /var/log/fail2ban.log
If a real client address was banned during testing, unban it before closing your recovery session:
sudo fail2ban-client set sshd unbanip YOUR_TEST_IP
Create a Custom SSH Pre-Auth Filter
Some SSH scans disconnect during key exchange before any password failure occurs. Logs for that pattern often include kex_exchange_identification followed by Connection closed by IP_ADDRESS port PORT. The standard sshd filter may count the first line as context but ignore the second line as a non-failure helper, so no ban is applied.
Create a separate filter only when you actually see repeated pre-auth connection drops from the same sources. The filter catches connection-closed events, not only malicious attempts, so use conservative thresholds.
sudo tee /etc/fail2ban/filter.d/sshd-kex.local > /dev/null <<'EOF'
[INCLUDES]
before = common.conf
[Definition]
_daemon = sshd
failregex = ^%(__prefix_line)sConnection closed by <HOST> port \d+\s*$
ignoreregex =
EOF
Test the filter against a representative sample before attaching it to a jail:
printf '%s\n' \
'Jan 12 10:00:01 debian sshd[1234]: error: kex_exchange_identification: Connection closed by remote host' \
'Jan 12 10:00:01 debian sshd[1234]: Connection closed by 192.0.2.55 port 41258' | sudo tee /tmp/sshd-kex-test.log > /dev/null
fail2ban-regex -v /tmp/sshd-kex-test.log /etc/fail2ban/filter.d/sshd-kex.local
Relevant output should show one matched line with the captured host:
Failregex: 1 total | 1) [1] ... Connection closed by <HOST> port \d+\s*$ | 192.0.2.55 Mon Jan 12 10:00:01 2026 Lines: 2 lines, 0 ignored, 1 matched, 1 missed
Remove the temporary sample after the regex test:
sudo rm -f /tmp/sshd-kex-test.log
Add a separate jail for the filter so it runs beside the standard SSH jail. Place the section in the same local drop-in used earlier, or in another file under /etc/fail2ban/jail.d/ that loads after Debian’s defaults.
[sshd-kex]
enabled = true
filter = sshd-kex
port = ssh
backend = systemd
maxretry = 5
bantime = 1h
findtime = 10m
Validate, restart, and inspect the custom jail:
sudo fail2ban-client -t
sudo systemctl restart fail2ban
sleep 2
sudo fail2ban-client ping
sudo fail2ban-client status sshd-kex
Increase maxretry if legitimate clients sometimes disconnect during key exchange. Remove the custom jail if it creates false positives for unstable networks, load balancers, or monitoring systems.
Update or Remove Fail2Ban on Debian
Fail2Ban updates through APT with the rest of the Debian package set. To update only Fail2Ban after refreshing metadata, run:
sudo apt update
sudo apt install --only-upgrade fail2ban
To remove the package while keeping local configuration files for a possible reinstall, stop the service and remove the package:
sudo systemctl disable --now fail2ban
sudo apt remove fail2ban
APT may mark helper packages installed only for Fail2Ban as autoremovable. Review the proposed transaction before confirming:
sudo apt autoremove
Use purge when you want APT to remove package-managed configuration files as well:
sudo apt purge fail2ban
Remove the local files created in this setup when you no longer need them:
sudo rm -f /etc/fail2ban/jail.d/99-local-baseline.conf
sudo rm -f /etc/fail2ban/filter.d/sshd-kex.local
The next cleanup commands delete local Fail2Ban configuration, runtime database state, and Fail2Ban logs. Skip them if you want to keep ban history or reinstall with the same settings later.
Review the candidate paths before deleting them:
sudo find /etc/fail2ban /var/lib/fail2ban -maxdepth 2 -print 2>/dev/null || true
sudo ls -1 /var/log/fail2ban.log* 2>/dev/null || true
After confirming the paths are safe to remove, delete them:
sudo rm -rf /etc/fail2ban
sudo rm -rf /var/lib/fail2ban
sudo rm -f /var/log/fail2ban.log /var/log/fail2ban.log-* /var/log/fail2ban.log.*
Confirm the package state after removal or purge:
dpkg-query -W -f='${db:Status-Abbrev} ${binary:Package}\n' fail2ban 2>/dev/null | grep -E '^(ii|rc)' || echo "fail2ban package not installed"
The message fail2ban package not installed means the package is absent. An ii status means the package is still installed, and an rc status means the package was removed but residual configuration remains. Run sudo apt purge fail2ban if you want APT to remove that package-managed configuration.
Troubleshoot Fail2Ban on Debian
Fail2Ban Fails to Start Because the SSH Log Is Missing
On Debian 12 and Debian 11 minimal systems, a fresh install can fail with this error when the enabled sshd jail expects a file log that does not exist:
ERROR Failed during configuration: Have not found any log file for sshd jail ERROR Async configuration of server failed
Add backend = systemd to the [DEFAULT] section in your local Fail2Ban drop-in, then retest and restart:
sudo fail2ban-client -t
sudo systemctl restart fail2ban
sleep 2
sudo fail2ban-client ping
systemctl is-active fail2ban
sudo fail2ban-client status sshd
fail2ban-regex Shows 0 Matched Lines
A 0 matched result usually means the filter is reading the wrong source, the log format differs from the filter, or the matched lines do not include a captured host. First confirm where SSH failures are logged:
sudo journalctl -u ssh --since "1 hour ago" --no-pager | grep -Ei 'failed|invalid'
sudo tail -n 50 /var/log/auth.log 2>/dev/null
If the failures appear in the journal but not in /var/log/auth.log, use the systemd backend. If failures appear in the file but not the journal, remove the override or set that jail’s backend and logpath to the file source.
Bans Are Logged but Firewall Rules Are Missing
Check which action the active SSH jail loaded:
sudo fail2ban-client get sshd actions
Debian 13 should normally report nftables. Debian 12 and Debian 11 normally use iptables-multiport unless you changed banaction. For nftables, inspect Fail2Ban’s table after a ban exists:
sudo nft list table inet f2b-table
For iptables-based actions, look for Fail2Ban chains after a ban exists:
sudo iptables -S | grep f2b
If the jail shows banned addresses but the firewall has no matching rule, confirm that the selected firewall package is installed, the action file exists under /etc/fail2ban/action.d/, and the action matches the firewall you actually use.
The Client Socket Is Missing
The message Failed to access socket path: /var/run/fail2ban/fail2ban.sock can appear for a few seconds immediately after a restart while Fail2Ban creates its client socket. If it persists, the service is not running or failed during startup. Start with the parser and journal instead of rerunning random ban commands:
sudo fail2ban-client -t
sudo journalctl -u fail2ban -n 50 --no-pager
Fix the reported file, option, filter, or log source, then restart Fail2Ban and recheck sudo fail2ban-client status.
Conclusion
Fail2Ban is installed from Debian’s default repositories with SSH monitoring, a tested local override, and a clear recovery path for journal-based systems. Keep the log checks close while tuning thresholds, then expand protection only to services that are installed, exposed, and writing logs Fail2Ban can parse.


When i do a test i get this result. I see every 30 seconds someone try to login with random ports on SSH. The test result looks like it ignore it and not block the IP numbers
root@mail:/var/log# sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf Running tests ============= Use failregex filter file : sshd, basedir: /etc/fail2ban Use maxlines : 1 Use datepattern : {^LN-BEG} : Default Detectors Use log file : /var/log/auth.log Use encoding : utf-8 Results ======= Prefregex: 5716 total | ^(?P<mlfid>(?:\[\])?\s*(?:<[^.]+\.[^.]+>\s+)?(?:\S+\s+)?(?:kernel:\s?\[ *\d+\.\d+\]:?\s+)?(?:@vserver_\S+\s+)?(?:(?:(?:\[\d+\])?:\s+[\[\(]?sshd(?:\(\S+\))?[\]\)]?:?|[\[\(]?sshd(?:\(\S+\))?[\]\)]?:?(?:\[\d+\])?:?)\s+)?(?:\[ID \d+ \S+\]\s+)?)(?:(?:error|fatal): (?:PAM: )?)?(?P<content>.+)$ `- Failregex: 1308 total |- #) [# of hits] regular expression | 19) [2] ^<F-NOFAIL>Received <F-MLFFORGET>disconnect</F-MLFFORGET></F-NOFAIL> from <HOST>(?: (?:port \d+|on \S+)){0,2}:\s*11: | 20) [1304] ^<F-NOFAIL><F-MLFFORGET>(Connection (?:closed|reset)|Disconnected)</F-MLFFORGET></F-NOFAIL> (?:by|from)(?: (?:invalid|authenticating) user <F-USER>\S+|.*?</F-USER>)? <HOST>(?:(?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*|\s*)$ | 21) [2] ^<F-MLFFORGET><F-MLFGAINED>Accepted \w+</F-MLFGAINED></F-MLFFORGET> for <F-USER>\S+</F-USER> from <HOST>(?:\s|$) `- Ignoreregex: 0 total Date template hits: |- [# of hits] date format | [5716] {^LN-BEG}ExYear(?P<_sep>[-/.])Month(?P=_sep)Day(?:T| ?)24hour:Minute:Second(?:[.,]Microseconds)?(?:\s*Zone offset)? `- Lines: 5716 lines, 1308 ignored, 0 matched, 4408 missed [processed in 0.36 sec] Ignored line(s): too many to print. Use --print-all-ignored to print all 1308 lines Missed line(s): too many to print. Use --print-all-missed to print all 4408 linesThanks for the detailed output, Peter. The fail2ban-regex results explain exactly what is happening. Your output shows 0 matched lines for actual failed login attempts, while the 1308 “ignored” lines are connection disconnects and successful logins (patterns marked with
<F-NOFAIL>flags that Fail2Ban tracks but does not count as failures).The most likely cause is that your system uses systemd-journald as the primary logging mechanism, and
/var/log/auth.logcontains stale or incomplete data. On Debian 12 and newer, SSH authentication events often go directly to the systemd journal rather than traditional log files.First, verify where your SSH failures are actually logged:
If you see failed login attempts there but not in
/var/log/auth.log, add the systemd backend to your local Fail2Ban drop-in, such as/etc/fail2ban/jail.d/99-local-baseline.conf:Then restart Fail2Ban:
After restarting, check that the sshd jail is detecting failures:
The “Currently failed” and “Total failed” counts should start increasing as the systemd backend reads directly from journald. The article now covers this in the baseline local drop-in setup and the troubleshooting item for missing SSH logs, so Debian 12 and Debian 11 readers can switch to the systemd backend without hunting through older section names. Thanks for reporting this, as your feedback directly improved the guide for other readers.
Here is the result: changed my domain and ops IP in this log to 192.168.121.121 my domain.com
/var/log/auth.log:
When changeing the grep:
output:
So the regex isn’t correct in my situation then?
Your logs tell the whole story, Peter. Those
kex_exchange_identification: Connection closed by remote hostmessages are pre-authentication connection failures, not login failures. The remote client disconnects during the SSH key exchange phase before any password attempt occurs.The standard sshd filter (even in aggressive mode) will not ban these IPs. Looking at your logs, each event produces two lines:
The first line triggers a failure match but contains no IP. The second line contains the IP but is marked as a non-failure helper in the default filter. You need a custom filter to catch these. Create it at
/etc/fail2ban/filter.d/sshd-kex.local:Then add a new jail in your local Fail2Ban drop-in, such as
/etc/fail2ban/jail.d/99-local-baseline.conf:Restart Fail2Ban and verify the new jail is active:
This runs alongside your existing sshd jail. The standard jail catches password failures while sshd-kex catches the pre-auth connection drops you are seeing. All settings are customizable based on your environment:
maxretrycontrols how many connection drops trigger a ban (start with 5 since legitimate clients occasionally disconnect during key exchange),bantimesets how long IPs stay blocked, andfindtimedefines the window for counting failures. The every-30-seconds pattern from a single IP is clearly automated probing, so even conservative thresholds will catch it quickly.I get a error when I add the files and update jail.local
I checked the internet and this filter
gives me this result. Looks good right?
root@mail:/etc/fail2ban/filter.d# sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd-kex.local Running tests ============= Use failregex filter file : sshd-kex, basedir: /etc/fail2ban Use datepattern : {^LN-BEG} : Default Detectors Use log file : /var/log/auth.log Use encoding : utf-8 Results ======= Failregex: 1710 total |- #) [# of hits] regular expression | 2) [1710] ^(?:\[\])?\s*(?:<[^.]+\.[^.]+>\s+)?(?:\S+\s+)?(?:kernel:\s?\[ *\d+\.\d+\]:?\s+)?(?:@vserver_\S+\s+)?(?:(?:(?:\[\d+\])?:\s+[\[\(]?sshd(?:\(\S+\))?[\]\)]?:?|[\[\(]?sshd(?:\(\S+\))?[\]\)]?:?(?:\[\d+\])?:?)\s+ )?(?:\[ID \d+ \S+\]\s+)?Connection closed by .*? `- Ignoreregex: 0 total Date template hits: |- [# of hits] date format | [7543] {^LN-BEG}ExYear(?P<_sep>[-/.])Month(?P=_sep)Day(?:T| ?)24hour:Minute:Second(?:[.,]Microseconds)?(?:\s*Zone offset)? `- Lines: 7543 lines, 0 ignored, 1710 matched, 5833 missed [processed in 0.29 sec] Missed line(s): too many to print. Use --print-all-missed to print all 5833 linesThis is fail2ban log:
The jail is working, Peter. Your log shows detections and a successful ban. The filter you found works fine.
Great tutorial, however your step to check if the fail2ban install went ok,”fail2ban --version”, throws a command not found error. “sudo fail2ban-client version” seems to be the correct form.
Checking the “systemctl status fail2ban” does work, once one has the jail.local correctly configured.
Again, great tutorial.
Thanks for catching that, RDK. You’re absolutely right, the version check command I provided was incorrect. The proper command is:
I’ve updated the install verification step to use the correct syntax. The sudo prefix isn’t required for version checks since you’re just querying the client, not modifying system configuration. Thanks for testing the steps thoroughly and reporting the issue. Feedback like this helps keep the guide accurate for everyone.
Wow! Thanks a lot! I try to find the problem a lot of time!
Thanks for the feedback.
For D12, change “backend = auto” by “backend = systemd” in “/etc/fail2ban/jail.local” to make fail2ban start 😉
That’s great advice – just what I needed to overcome “code=exited, status=255/EXCEPTION” error.
Thanks to you, Steve and Joshua James for the article. Excellent!
Thanks for sharing this, Steve. You’re correct that on Debian 12, explicitly setting the backend to systemd in a local Fail2Ban drop-in can resolve startup issues. Add this line to the [DEFAULT] section:
The systemd backend reads logs directly from the systemd journal instead of polling log files, which works more reliably on systems using journald. After adding this setting, validate the configuration with
sudo fail2ban-client -t, then restart Fail2Ban withsudo systemctl restart fail2ban. Thanks for pointing this out for Debian 12 users.