Hardening SSH and Web Services with Fail2ban: Protect Against Brute‑Force Attacks

This guide explains how to use Fail2ban on Linux to automatically detect and block brute‑force login attempts for SSH, web authentication pages, APIs, and mail services, covering installation, configuration hierarchy, custom filters, progressive banning, performance‑optimized actions, high‑availability options, and troubleshooting steps.

Raymond Ops
Raymond Ops
Raymond Ops
Hardening SSH and Web Services with Fail2ban: Protect Against Brute‑Force Attacks

Overview

Brute‑force attacks are one of the most common threats to publicly exposed servers, targeting SSH, web login pages, and API endpoints. Fail2ban is a mature intrusion‑prevention tool that monitors log files, matches failure patterns with regular expressions, and bans offending IPs via iptables, nftables, or firewalld without manual intervention.

Core Architecture

Jail : defines the target service, log paths, filter, and ban actions.

Filter : a set of Python regular expressions that extract offending IPs from logs.

Action : the concrete operation performed when a ban is triggered (e.g., add an iptables rule, send a webhook).

The three components are orchestrated by the jail: Jail → Filter → Action.

Tool Comparison

Fail2ban is compared with DenyHosts and CrowdSec. Fail2ban supports SSH, HTTP, mail and custom services, works with iptables/nftables/firewalld, and is highly mature. CrowdSec adds distributed community threat intelligence, while DenyHosts is limited to SSH only.

Installation and Basic Configuration

# Ubuntu / Debian
sudo apt update && sudo apt install -y fail2ban

# CentOS 8+ / RHEL (EPEL)
sudo dnf install -y epel-release && sudo dnf install -y fail2ban

# Verify version
fail2ban-client version   # Expected output: Fail2Ban v1.1.x

Configuration files follow a clear priority hierarchy:

/etc/fail2ban/
├── jail.conf          # default settings (updated on upgrade)
├── jail.local        # user overrides (highest priority)
├── jail.d/           # per‑jail snippets (highest priority)
│   ├── sshd.conf
│   └── nginx.conf
├── filter.d/         # filter definitions
├── action.d/         # action definitions

Only jail.local or files under jail.d/ should be edited to avoid losing changes on package upgrades.

SSH Brute‑Force Protection

# /etc/fail2ban/jail.d/sshd.conf
[sshd]
enabled = true
port = ssh,22222          # add custom ports if SSH runs on a non‑standard port
filter = sshd
backend = systemd          # use journal when available
maxretry = 3
findtime = 300            # 5 minutes
bantime = 600             # initial ban 10 minutes (progressive ban will increase)
ignoreip = 127.0.0.1/8 ::1 10.0.0.100   # whitelist admin IPs

The built‑in sshd filter already covers most log formats. Enabling mode = aggressive adds extra patterns such as connection resets and protocol mismatches:

# /etc/fail2ban/filter.d/sshd.conf (excerpt)
[Definition]
mode = aggressive
failregex = ^<HOST> -.*"Failed password for .* from <HOST> port .* ssh2"
            ^<HOST> -.*"Invalid user .* from <HOST>"

Web Service Protection (Nginx/Apache)

Fail2ban includes a filter for HTTP Basic Auth failures. To protect custom login APIs, create a custom filter that matches POST requests returning 401 or 403:

# /etc/fail2ban/filter.d/nginx-login.conf
[Definition]
failregex = ^<HOST> -.*"POST /api/login.*" (401|403)
ignoreregex = ^<HOST> -.*"GET /health"
# /etc/fail2ban/jail.d/nginx-login.conf
[nginx-login]
enabled = true
port = http,https
filter = nginx-login
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 300
bantime = 1800   # 30 minutes
action = nftables-multiport[name=nginx-login, port="http,https", protocol=tcp] webhook-notify

Custom Jail Development

When writing a new filter, remember: <HOST> matches IPv4/IPv6 addresses.

Each line is an OR‑combined regex. ignoreregex uses the same syntax to exclude false positives.

Validate the filter with fail2ban-regex before deploying:

# Test against a real log line
fail2ban-regex "Jun 15 08:23:01 server sshd[12345]: Failed password for root from 203.0.113.5 port 22 ssh2" /etc/fail2ban/filter.d/sshd.conf

Best Practices

Progressive Ban : enable bantime.increment = true and tune bantime.factor and bantime.maxtime so repeated offenses receive exponentially longer bans.

Permanent Blacklist : write repeatedly offending IPs to a persistent blacklist file and load it with ipset for O(1) lookups.

GeoIP Filtering : block whole countries upstream of Fail2ban when the service is intended for a single region.

Performance‑Optimized Actions : prefer nftables-multiport or firewallcmd-ipset over raw iptables for large ban lists.

Whitelist Carefully : mis‑configured ignoreip can lock out administrators; always include management and monitoring IPs.

Troubleshooting & Monitoring

Common issues and their fixes:

Jail not banning: verify the filter matches the log format with fail2ban-regex.

Banned IP still reachable: ensure the banaction rule is inserted before any ACCEPT rules (check iptables -L INPUT --line-numbers or nft list ruleset).

Service crashes: look for SQLite lock errors; rotate the database or switch to MySQL if concurrency is high.

Key monitoring commands:

# Show overall status
fail2ban-client status

# Show details for a jail
fail2ban-client status sshd

# Manually ban/unban an IP
fail2ban-client set sshd banip 203.0.113.100
fail2ban-client set sshd unbanip 203.0.113.100

# Reload configuration without restarting all jails
fail2ban-client reload sshd

For production monitoring, export metrics with fail2ban_exporter and set Prometheus alerts on ban rate, total bans, and client latency.

Backup & Restore

Regularly back up /etc/fail2ban and the SQLite database. A sample backup script creates a tarball, backs up custom blacklists, and purges old archives. Restoration involves stopping the service, extracting the archive, and verifying the configuration with fail2ban-client -d before restarting.

Conclusion

Fail2ban provides an automated, low‑overhead defense against brute‑force attacks across SSH, web services, and mail servers. By combining accurate filters, progressive banning, and performance‑aware actions, operators can turn manual log inspection into real‑time protection while retaining the ability to integrate with higher‑level security platforms such as CrowdSec, WAFs, or SIEMs.

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.

linuxNginxsysadminiptablesbrute-force protectionssh securityfail2bannftables
Raymond Ops
Written by

Raymond Ops

Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.

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.