How to Use Fail2ban to Automatically Block Malicious SSH Login Attempts

This guide explains how to configure Fail2ban on Linux servers to detect repeated SSH authentication failures, extract source IPs from logs, and automatically apply firewall rules via nftables, iptables, or firewalld, while covering verification, safe deployment, parameter tuning, whitelist management, and integration with broader security practices.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
How to Use Fail2ban to Automatically Block Malicious SSH Login Attempts

What Fail2ban Actually Does

Fail2ban processes authentication logs in five stages:

Log source : plain‑text file, systemd journal, or other supported back‑ends.

Filter : a failregex extracts the source address using the <HOST> placeholder.

Jail : combines a filter, log source, time window ( findtime), retry limit ( maxretry) and an action.

Action : runs firewall commands (nftables, iptables, firewalld, etc.) to ban or unban the address.

State database : stores ban state so it survives service restarts.

The three most common time parameters are: findtime: sliding window for counting failures. maxretry: number of matches within findtime that triggers a ban. bantime: how long the ban lasts.

Example: findtime = 10m, maxretry = 5, bantime = 1h means five failures from the same source within ten minutes result in a one‑hour block. Only log lines matched by the filter count; Fail2ban does not generate login attempts.

Pre‑Installation Checks

1. Verify SSH Exposure

Check which addresses and ports the SSH daemon is listening on:

sudo ss -lntp | grep -E 'sshd|:22([[:space:]]|$)'
sudo sshd -T | grep -E '^(port|listenaddress|passwordauthentication|permitrootlogin|pubkeyauthentication) '

Use sshd -T because it expands Include and Match directives that may be hidden in /etc/ssh/sshd_config.

2. Locate Real Authentication Logs

Typical log files are:

Debian/Ubuntu: /var/log/auth.log RHEL/CentOS: /var/log/secure If the system uses systemd, the journal can be queried directly. Always verify the actual path on the host.

3. Inspect the Current Firewall Stack

Identify the active firewall back‑end:

sudo nft list ruleset
sudo iptables-save
sudo firewall-cmd --state

Avoid configuring multiple back‑ends simultaneously because rule ordering and persistence become unpredictable.

4. Backup Existing Configuration

Save /etc/fail2ban and create a checksum before making changes:

#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/path/to/backup"
STAMP=$(date +%Y%m%d-%H%M%S)
sudo install -d -m 0700 "$BACKUP_DIR"
sudo tar -C /etc -czf "$BACKUP_DIR/fail2ban-$STAMP.tar.gz" fail2ban
sudo sha256sum "$BACKUP_DIR/fail2ban-$STAMP.tar.gz" | sudo tee "$BACKUP_DIR/fail2ban-$STAMP.tar.gz.sha256"

Installation

Debian/Ubuntu:

sudo apt-get update
sudo apt-get install fail2ban

RHEL‑based systems (EPEL may be required): sudo dnf install fail2ban After installation verify the version:

fail2ban-client -V

Configuration Principle: Use Local Overrides Only

Never edit jail.conf or filter.d/*.conf directly because package upgrades may overwrite them. Place custom settings in jail.local, jail.d/*.local or custom filter files.

Example minimal SSH jail ( /etc/fail2ban/jail.d/sshd.local):

[sshd]
enabled = true
backend = systemd
port = 22
filter = sshd
findtime = 10m
maxretry = 5
bantime = 1h
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24

When backend = systemd, omit logpath because the filter reads from the journal.

The port must match the actual sshd listening port; numeric ports are safer for custom ports. ignoreip should contain only verified management CIDRs. maxretry that is too low can lock out legitimate automation; too high reduces protection. bantime that is too long can cause collateral damage on shared NAT addresses; consider incremental bans.

Testing Configuration and Regex Before Enabling

Validate the configuration syntax: sudo fail2ban-client -t Dump the effective configuration for inspection:

sudo fail2ban-client -d > /tmp/fail2ban-effective-config.txt

Search for a specific jail to confirm parameters:

grep -nE "\['(add|set)', '<JailName>'" /tmp/fail2ban-effective-config.txt

Test the filter against real log samples without affecting the firewall:

sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf --print-all-matched

When using the journal, export a time‑bounded slice and pipe it to fail2ban-regex:

sudo journalctl -u ssh.service --since '24 hours ago' --no-pager \
  | sudo fail2ban-regex - /etc/fail2ban/filter.d/sshd.conf

Inspect the output to ensure only genuine failure lines are matched and no successful login lines are captured.

Gray‑Scale Deployment: Avoid Locking Yourself Out

Before starting the service ensure:

A second management channel (cloud console, out‑of‑band access, or bastion host).

The management CIDR is present in ignoreip.

A backup of the current configuration and firewall baseline.

Successful fail2ban-client -t test results.

Enable and start the service without terminating the current SSH session:

sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban --no-pager
sudo journalctl -u fail2ban --since '10 minutes ago' --no-pager

From a second terminal verify that SSH login still works and inspect the jail status:

sudo fail2ban-client status
sudo fail2ban-client status sshd

Typical output shows the number of currently failed attempts, total failures, currently banned IPs and the list of banned addresses.

Validate the Firewall Action

Fail2ban may report a ban, but you must confirm that the firewall actually drops packets:

sudo nft list ruleset | grep -i -A 20 -B 5 f2b
sudo iptables-save | grep -i f2b
sudo firewall-cmd --direct --get-all-rules 2>/dev/null | grep -i f2b || true

Do not rely on hard‑coded chain names; inspect the generated rules to understand the exact objects created.

Handling False Positives and Rollback

Unban a Single IP

sudo fail2ban-client sshd unbanip 203.0.113.45
sudo fail2ban-client status sshd

After unbanning, review the original log lines that caused the ban to decide whether to adjust maxretry or ignoreip.

Pause an Entire Jail

sudo fail2ban-client stop sshd
sudo fail2ban-client status

Stopping a jail removes its monitoring and usually cleans up its firewall rules; verify cleanup before resuming.

Rollback All Fail2ban Changes

sudo systemctl stop fail2ban
# Verify no fail2ban rules remain
sudo nft list ruleset | grep -i f2b || true
sudo iptables-save | grep -i f2b || true
# Restore configuration backup
sudo tar -C /etc -xzf /path/to/backup/fail2ban-backup.tar.gz
sudo fail2ban-client -t
sudo systemctl start fail2ban

Avoid flushing the entire rule set (e.g., nft flush ruleset) because it would also remove unrelated security policies.

Parameter Tuning Based on Log Baselines

maxretry

Setting maxretry = 3 looks stricter but can easily lock out shared NAT or automated tools. Analyze historical failure distributions per source before choosing a value.

findtime

A short window catches fast brute‑force attacks; a long window may aggregate unrelated failures. Typical SSH deployments start with 10 minutes to 1 hour and adjust based on observed login behavior.

bantime

Short bans reduce collateral damage; longer bans reduce repeat attempts. Some Fail2ban versions support incremental bans:

[DEFAULT]
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 1d

Test incremental bans carefully; they are not permanent blacklists.

Lifecycle Management of ignoreip (Whitelist)

Whitelist entries prevent the jail from banning trusted management IPs, bastion hosts, or VPN exit points. Best practices:

Use the smallest possible CIDR (single IP or precise subnet).

Include both IPv4 and IPv6 if the service listens on both.

Prefer stable bastion/VPN exit addresses over individual user home IPs.

Avoid hostnames because they can resolve to unexpected addresses.

Temporary entries must have an automatic expiry or be reviewed manually.

Check the current whitelist:

sudo fail2ban-client get sshd ignoreip
sudo fail2ban-client status sshd

After changes, test the configuration again with fail2ban-client -t and reload.

Custom Filter for Web Login Interfaces

Assume the application logs lines such as:

2026-07-16T10:30:01Z login_failed remote_addr=192.0.2.10 user=alice reason=bad_password

Create a filter file /etc/fail2ban/filter.d/app-login.local:

[Definition]
failregex = ^\S+\s+login_failed\s+remote_addr=<HOST>\s+user=\S+\s+reason=bad_password\s*$
ignoreregex =

Define a jail:

[app-login]
enabled = true
filter = app-login
backend = auto
logpath = /var/log/app.log
port = http,https
findtime = 10m
maxretry = 8
bantime = 30m
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24

Test the filter with fail2ban-regex against matching and non‑matching samples to avoid false positives (e.g., successful logins, injected newlines, proxy IPs).

Choosing the Correct Action Backend

List the available action files on the host:

sudo find /etc/fail2ban/action.d -maxdepth 1 -type f \( -name '*nft*' -o -name '*iptables*' -o -name '*firewall*' \) -printf '%f
' | sort

Do not copy a banaction name that does not exist on the target system. If a custom action is needed, test it on a non‑production host and verify the final command with fail2ban-client -d. Example to use the nftables multi‑port action:

[DEFAULT]
banaction = nftables-multiport

When firewalld is the manager, select a compatible action and verify that rules survive a firewall-cmd --reload.

Log Rotation, Systemd Journal, and State Database

Text Log Rotation

With backend = auto or polling, ensure that logrotate does not break Fail2ban monitoring. Verify the current logpath against the system's logrotate configuration.

Systemd Journal

The journal avoids inode problems but requires sufficient retention. Check usage and persistence:

sudo journalctl --disk-usage
sudo systemd-analyze cat-config systemd/journald.conf

If the journal discards entries too early, Fail2ban will miss attacks.

Fail2ban SQLite Database

Most packages store state in /var/lib/fail2ban/fail2ban.sqlite3. Query the location and purge age:

sudo fail2ban-client -d | grep -E 'dbfile|dbpurgeage'
sudo ls -lh /var/lib/fail2ban

Never edit the SQLite file while the service is running; stop the service, back up the file, and follow the package’s upgrade procedure.

Monitoring and Daily Inspection

Host‑Level Health Checks

sudo systemctl is-active fail2ban
sudo fail2ban-client ping
sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo journalctl -u fail2ban --since '1 hour ago' --no-pager | grep -E 'ERROR|WARNING|Ban |Unban '

Key metrics include log ingestion health, filter failure counters, action command errors, abnormal ban growth, false‑positive tickets, SSH success/failure trends, and firewall load.

Read‑Only Auditing Script

#!/usr/bin/env bash
set -euo pipefail
JAIL_NAME="sshd"
command -v fail2ban-client >/dev/null || { echo "fail2ban-client not found" >&2; exit 1; }
sudo systemctl status fail2ban --no-pager
sudo fail2ban-client ping
sudo fail2ban-client status
sudo fail2ban-client status "$JAIL_NAME"

echo "Recent Fail2ban warnings and errors:"
sudo journalctl -u fail2ban --since '24 hours ago' --no-pager | grep -E 'ERROR|WARNING' || true

echo "Fail2ban‑managed firewall objects:"
if command -v nft >/dev/null; then
  sudo nft list ruleset | grep -i -A 10 -B 3 f2b || true
fi
if command -v iptables-save >/dev/null; then
  sudo iptables-save | grep -i f2b || true
fi

The script only reads status and logs; it never modifies firewall rules.

Recidive Jail for Repeated Offenders

The built‑in recidive filter reads Fail2ban’s own ban log to apply longer bans to sources that repeatedly trigger other jails. Ensure Fail2ban writes to a file (e.g., /var/log/fail2ban.log) before enabling:

sudo fail2ban-client -d | grep -E 'logtarget|dbfile'
sudo test -r /var/log/fail2ban.log && sudo tail -n 30 /var/log/fail2ban.log

Example recidive jail configuration:

[recidive]
enabled = true
filter = recidive
backend = auto
logpath = /var/log/fail2ban.log
findtime = 1d
maxretry = 5
bantime = 1w
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24

Test the filter against the existing Fail2ban log before production use. Long bans on shared NAT or dynamic IP ranges can affect innocent users.

Multi‑Host and Container Environments

Fail2ban is host‑local; a ban on node A does not automatically propagate to node B. For services behind load balancers, consider edge rate‑limiting, centralized log analysis, or a security platform that can push firewall rules to the perimeter.

In Kubernetes or container deployments, verify:

Which network namespace Fail2ban runs in.

Whether logs contain the true client IP or only the load‑balancer IP.

Whether firewall rules affect the host, the pod, or the ingress gateway.

CNI, Service, and kube‑proxy data paths.

Pod recreation will lose Fail2ban state unless persisted.

If protecting an Ingress endpoint, native rate‑limiting, WAF, or identity‑platform lockout is usually preferable to host‑local Fail2ban.

Large Number of Failures – Verify Compromise First

Preserve Evidence

Record alert time, host timezone, jail name, source IP, total failures and current ban status. Export the relevant log window to a protected forensic directory:

#!/usr/bin/env bash
set -euo pipefail
EVIDENCE_DIR="/path/to/evidence"
START_TIME="2026-07-16 00:00:00"
END_TIME="2026-07-16 23:59:59"
sudo install -d -m 0700 "$EVIDENCE_DIR"
sudo journalctl -u ssh.service --since "$START_TIME" --until "$END_TIME" --output=short-iso-precise --no-pager \
  | sudo tee "$EVIDENCE_DIR/ssh-journal.log" >/dev/null
sudo journalctl -u fail2ban --since "$START_TIME" --until "$END_TIME" --output=short-iso-precise --no-pager \
  | sudo tee "$EVIDENCE_DIR/fail2ban-journal.log" >/dev/null
sudo sha256sum "$EVIDENCE_DIR/ssh-journal.log" "$EVIDENCE_DIR/fail2ban-journal.log" | sudo tee "$EVIDENCE_DIR/SHA256SUMS" >/dev/null

Look for Unexpected Successful Logins

sudo journalctl -u ssh.service --since "$START_TIME" --until "$END_TIME" | grep -E 'Accepted (password|publickey)|session opened'
last -Fai | head -n 50
sudo lastb -Fai | head -n 50
# If auditd is enabled
sudo ausearch -m USER_LOGIN,USER_AUTH -ts "$START_TIME" -te "$END_TIME" -i

Correlate with asset inventory and configuration management records.

Do Not Remediate on a Potentially Compromised Host

If a breach is suspected, treat the incident as a security event: isolate the host, take disk and memory snapshots, revoke credentials, and rebuild from a trusted image. Simple password changes or installing Fail2ban on a possibly compromised host is insufficient.

Additional SSH Hardening Beyond Fail2ban

Disable Unnecessary Password Authentication

Modify sshd_config carefully; keep the current session and a backup before reloading:

#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/path/to/backup"
STAMP=$(date +%Y%m%d-%H%M%S)
sudo install -d -m 0700 "$BACKUP_DIR"
sudo cp -a /etc/ssh/sshd_config "$BACKUP_DIR/sshd_config.$STAMP"
# Verify syntax
sudo sshd -t
# Reload
sudo systemctl reload sshd
sudo systemctl status sshd --no-pager

Restrict Network Access

Prefer security‑group, ACL, VPN, or bastion host restrictions that limit SSH to trusted management CIDRs. Verify source IP, redundancy, rule priority and cloud rollback mechanisms before applying a DROP default.

Account and Key Governance

Disallow shared accounts; use individual, auditable identities.

Rotate keys regularly and revoke keys for departed personnel.

Enforce MFA, short‑lived certificates, or centralized identity for privileged access.

Limit sudo privileges and log all privilege escalations.

Patch OpenSSH and the underlying OS promptly.

Alert on anomalous successful logins, out‑of‑hours access, or new source IPs.

Fail2ban mitigates repeated failures, but a single successful unauthorized login is far more dangerous and must be monitored separately.

Configuration Management and Batch Changes

Never edit jail.local manually on many servers. Store local overrides in a configuration‑management system, generate them per OS, Fail2ban version, firewall back‑end and service role, and inject secrets from a vault.

Before any rollout, test on a matching version node:

sudo fail2ban-client -t
sudo fail2ban-client -d > /tmp/fail2ban-effective-config.new
sudo diff -u /path/to/previous-effective-config.txt /tmp/fail2ban-effective-config.new

Batch deployment should follow a safe pipeline: test node → single non‑critical production host → small zone → full fleet. Use fail2ban-client reload only after confirming the new configuration works; avoid --restart or --unban unless explicitly supported.

Alert Grading and Evidence Collection

Not all bans are equal. Suggested grading:

Low: few failures from a single source, no successful logins.

Medium: same source hits multiple accounts or jails.

High: distributed low‑rate attempts that bypass host thresholds.

Critical: a ban followed by a successful login, sudo usage, key change, or abnormal outbound traffic.

Failure alert: action errors, log ingestion stops, or zero bans when traffic is present.

Each high‑priority alert must retain host, jail, source IP, first/last timestamps, failure count, ban duration, original log locations and any subsequent successful login checks. Remember that an IP is a clue, not proof of attacker identity.

Release Checklist

Verify the log source exists, is continuous and cannot be forged by unprivileged users.

Confirm fail2ban-regex matches real failure samples and does not match success lines.

Ensure trusted management IPs are listed in ignoreip with precise CIDRs.

Validate IPv4 and IPv6 paths.

Confirm the selected action matches the actual firewall manager.

Backup current configuration and firewall baseline.

Test on a non‑critical node, then gray‑deploy with a second management channel.

After enabling, verify login from a second terminal and inspect real firewall rules.

Document procedures for unbanning a single IP, stopping a jail, and full rollback.

Set up alerts for false‑positive rates, action errors, and abnormal login patterns.

Apply SSH key hygiene, MFA, network restrictions and account governance.

Conclusion

Fail2ban’s value is not to increase the number of “Ban” entries but to turn repeated malicious attempts into a verifiable, reversible host‑side mitigation. A reliable deployment starts from trustworthy logs, proves the filter with fail2ban-regex, proves the action with actual firewall rules, and finally confirms via a second management path that you have not locked out legitimate operators.

When the environment scales to multiple nodes, reverse proxies, or Kubernetes, the IP seen in host logs may differ from the real client address. In such cases, move the mitigation to the edge (load balancer, WAF, or centralized security platform). Regardless of architecture, Fail2ban remains only one layer of defense; comprehensive SSH hardening, key management, MFA and continuous monitoring are the core measures against brute‑force attacks.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

firewalliptablesLinux securitySSHbrute-force protectionFail2bannftables
MaGe Linux Operations
Written by

MaGe Linux Operations

Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.