Fail2ban Brute-Force Defense: Automated IP Banning & SSH Hardening Guide
This comprehensive guide covers Fail2ban deployment for SSH brute-force protection, including architecture, configuration, testing, safe rollout, firewall action verification, parameter tuning, whitelist management, custom web filters, multi-node considerations, incident response, and essential SSH hardening beyond Fail2ban.
Introduction
Public-facing SSH servers quickly attract automated login attempts against accounts like root, admin, and test. Fail2ban acts as a host-based second line of defense by parsing trusted logs for repeated failures and invoking the local firewall to temporarily ban offending source addresses. It does not replace SSH key authentication, MFA, source-network restrictions, vulnerability patching, or perimeter defenses. Attackers can rotate IPs, logs may be missing or rewritten by proxies, and if the login interface has bypasses or credentials are already leaked, banning failure counts alone cannot solve the problem.
This article references Fail2ban 0.11/1.x packages. Different distributions, packaging, and firewall backends may provide different action files; always verify with fail2ban-client -V, configuration tests, and the contents of /etc/fail2ban/action.d/ on the target host.
Fail2ban Architecture: Five Processing Stages
Log source : text logs, systemd journal, or other supported backends.
Filter : uses failregex to extract source addresses from failure logs, with <HOST> as the IP/host match marker.
Jail : combines filter, log source, time window, retry count, and action.
Action : executes ban/unban via nftables, iptables, firewalld, or other mechanisms.
State database : records ban information so that service restarts can handle existing state per configuration.
Three key time parameters: findtime: window for counting failures. maxretry: number of matches within the window that triggers a ban. bantime: ban duration.
Example: findtime = 10m, maxretry = 5, bantime = 1h means five matches from the same source in ten minutes triggers a one-hour ban. These "failures" are filter matches against log lines, not active probes. If log format changes, source IPs are missing, or regex is too broad, banning decisions become ineffective or cause false positives.
Core deployment proof points: logs exist, filter matches only real failures, action is compatible with the host firewall, and admin access is not accidentally banned.
Pre-Deployment Evidence Gathering
1. Confirm SSH Exposure
Check actual sshd listening addresses and ports:
sudo ss -lntp | grep -E 'sshd|:22([[:space:]]|$)'
sudo sshd -T | grep -E '^(port|listenaddress|passwordauthentication|permitrootlogin|pubkeyauthentication) ' sshd -Tshows the expanded effective configuration, more reliable than grepping /etc/ssh/sshd_config because of Include and Match blocks. Example output:
port 22
passwordauthentication no
permitrootlogin prohibit-password
pubkeyauthentication yesFocus on: whether password auth is allowed, whether root can log in directly, whether listening on all interfaces, and whether cloud security groups, border firewalls, or NAT expose the port to the internet. Even with PasswordAuthentication no, logs may show username enumeration, key attempts, or handshake abuse. Fail2ban reduces repeat sources, but restricting source networks and disabling unnecessary auth methods remain higher priority.
2. Locate Real Authentication Logs
Debian/Ubuntu typically write SSH auth to /var/log/auth.log; RHEL-family uses /var/log/secure; with journald enabled, read the service unit directly. Do not assume paths by distribution name; verify on the spot:
sudo journalctl -u <SSH服务单元> --since '30 minutes ago' --no-pager
sudo test -r /var/log/auth.log && sudo tail -n 50 /var/log/auth.log
sudo test -r /var/log/secure && sudo tail -n 50 /var/log/secureOutput may contain usernames, source IPs, and host info; redact before sharing. Distinguish genuine auth failures from normal noise (pre-auth disconnects, health checks, admin typos). Do not write custom regex without log evidence.
3. Inspect Current Firewall Stack
Fail2ban actions must match the host's actual firewall:
sudo nft list ruleset
sudo iptables-save
sudo firewall-cmd --stateCommand failures indicate missing components. Do not run multiple unclear firewall management layers simultaneously (e.g., firewalld managing nftables while Fail2ban also manipulates chains). Save current rules as a read-only baseline:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="<备份目录>"
STAMP="$(date +%Y%m%d-%H%M%S)"
sudo install -d -m 0700 "${BACKUP_DIR}"
if command -v nft >/dev/null 2>&1; then
sudo nft list ruleset | sudo tee "${BACKUP_DIR}/nft-rules-${STAMP}.txt" >/dev/null
fi
if command -v iptables-save >/dev/null 2>&1; then
sudo iptables-save | sudo tee "${BACKUP_DIR}/iptables-rules-${STAMP}.txt" >/dev/null
fiThese files are for diff comparison, not blind full restores. Full restores may overwrite concurrent legitimate changes; a firewall owner must review before any restore.
Installation
Debian/Ubuntu
sudo apt-get update
sudo apt-get install fail2banRHEL-family
Fail2ban often comes from EPEL. Enable the approved repo for the OS version first, then install: sudo dnf install fail2ban Do not temporarily add unvetted public repos for a single package. After install, verify version and files before enabling the service:
fail2ban-client -V
rpm -ql fail2ban 2>/dev/null | head -n 30 || true
dpkg -L fail2ban 2>/dev/null | head -n 30 || trueCheck for existing configurations:
sudo find /etc/fail2ban -maxdepth 2 -type f -print | sort
sudo systemctl status fail2ban --no-pagerIf the service was already running, the task is a policy change, not a fresh install. Export effective config and current ban state first; do not overwrite original files.
Configuration Principle: Only Write Local Overrides
jail.confand filter.d/*.conf may change on package upgrades. Production config belongs in jail.local, jail.d/*.local, or local filter files, tracked in version control.
Backup existing config first:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="<备份目录>"
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" >/dev/nullBackups must be verified readable, checksummed, and copied to an independent location per policy. Keeping backups on the same potentially compromised host is not a complete DR strategy.
Minimal SSH Jail Configuration
On systemd systems, prefer the journal backend to avoid log-path variance across distributions. Create /etc/fail2ban/jail.d/sshd.local:
[sshd]
enabled = true
backend = systemd
port = <SSH端口>
filter = sshd
findtime = 10m
maxretry = 5
bantime = 1h
ignoreip = 127.0.0.1/8 ::1 <管理网段>Key points:
With backend = systemd, logpath is usually omitted; log matching is driven by the filter's journal conditions and system logs. port must match sshd's actual listening port. Service name ssh works, but custom ports should use the numeric value. ignoreip should only include confirmed static admin egress, bastion hosts, and localhost. Do not whitelist entire office networks, cloud VPCs, or all RFC1918 ranges. maxretry too low causes false positives from shared NAT, automation with stale keys, or concurrent admin tools; too high weakens protection. Tune based on MFA, password policy, user count, and historical log baselines. bantime is not "longer is better". Dynamic public IPs get reassigned quickly; permanent bans accumulate collateral damage.
If using text auth logs, switch to:
[sshd]
enabled = true
backend = auto
port = <SSH端口>
filter = sshd
logpath = <日志文件>
findtime = 10m
maxretry = 5
bantime = 1h
ignoreip = 127.0.0.1/8 ::1 <管理网段> <日志文件>must be a confirmed, root-controlled path (e.g., /var/log/auth.log). Do not allow unprivileged users to write logs that Fail2ban trusts, or attackers could forge entries to ban arbitrary IPs.
Test Configuration and Regex Before Starting
1. Parse All Config
sudo fail2ban-client -t
sudo fail2ban-client -d > /tmp/fail2ban-effective-config.txt -ttests config; -d dumps parsed config for verifying final backend, port, logpath, and action. Search for the target jail:
grep -nE "\['(add|set)', '<Jail名称>'" /tmp/fail2ban-effective-config.txtIf the version lacks an option, adjust per fail2ban-client --help and package examples; do not force-restart with syntax errors.
2. Validate Filter Against Historical Logs
For text logs:
sudo fail2ban-regex \
<日志文件> \
/etc/fail2ban/filter.d/sshd.conf \
--print-all-matchedFor systemd journal, export a controlled time window and test via stdin:
sudo journalctl -u <SSH服务单元> \
--since '24 hours ago' \
--no-pager \
| sudo fail2ban-regex - /etc/fail2ban/filter.d/sshd.confExample output:
Failregex: 87 total
Ignoreregex: 0 total
Lines: 1250 lines, 0 ignored, 87 matched, 1163 missedDo not judge by matched count alone; spot-check matched lines:
Are they all auth failures, not successful logins or service start messages?
Does <HOST> extract the actual source address?
Are sshd listen addresses, proxy addresses, or other log IPs mistakenly treated as attackers?
Are multi-line logs, IPv6 addresses, and different auth methods covered?
Do not test by live brute-forcing production SSH. Historical logs plus fail2ban-regex suffice for filter validation; action is verified separately with an isolated test IP.
Gradual Enablement: Avoid Locking Yourself Out
Starting Fail2ban creates/modifies firewall rules and may affect remote access. Prerequisites:
A second management channel (cloud console, out-of-band, serial, or another bastion path).
Current admin egress verified and added to precise ignoreip.
Fail2ban config and firewall baseline backed up. fail2ban-client -t passes.
Jail and action validated on a same-version test host.
Maintenance window and rollback owner confirmed.
Start service without dropping current SSH session:
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban --no-pager
sudo journalctl -u fail2ban --since '10 minutes ago' --no-pagerFrom a second verified management terminal, re-login to confirm auth works, then check jail:
sudo fail2ban-client status
sudo fail2ban-client status sshdExample output:
Status for the jail: sshd
|- Filter
| |- Currently failed: 1
| `- Total failed: 12
`- Actions
|- Currently banned: 2
|- Total banned: 3
`- Banned IP list: 198.51.100.20 2001:db8::20Example addresses are from documentation-reserved ranges. Confirm jail is running, failure counters update, action has no errors, and admin sources are not in the banned list.
Verify Firewall Action, Not Just Client Status
Fail2ban showing "banned" does not guarantee packets are dropped. Inspect the backend rules:
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 || trueChain and set names depend on the action; do not hardcode names in cleanup scripts.
With an isolated test source that won't affect management connections, an authorized person can perform a manual ban/unban:
sudo fail2ban-client set sshd banip <测试IP>
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip <测试IP> <测试IP>must not be the current SSH client, bastion, LB health check, or shared NAT egress. Record test IP owner and verification time; confirm unban afterward.
Handling False Positives and Rollback
1. Unban a Single IP
If a legitimate source is banned via console, do minimal remediation:
sudo fail2ban-client set <Jail名称> unbanip <被误封IP>
sudo fail2ban-client status <Jail名称>Then investigate why it hit the threshold; adjust auth client, thresholds, or trusted networks. Do not disable the entire jail permanently over one false positive.
2. Pause a Single Jail
Emergency stop for a specific jail:
sudo fail2ban-client stop <Jail名称>
sudo fail2ban-client statusThe jail stops detecting/banning; its action usually cleans its own rules, but verify the firewall. Before restarting, fix config and test:
sudo fail2ban-client -t
sudo fail2ban-client start <Jail名称>
sudo fail2ban-client status <Jail名称>3. Full Fail2ban Rollback
If config or action causes widespread failure, preserve evidence via out-of-band, then:
sudo systemctl stop fail2ban
sudo systemctl status fail2ban --no-pager
sudo nft list ruleset | grep -i f2b || true
sudo iptables-save | grep -i f2b || trueService stop normally runs actionstop to clean rules, but do not assume success. After confirming network recovery, restore /etc/fail2ban from pre-change backup, then test config. Save the faulty version for postmortem first:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_ARCHIVE="<备份目录>/fail2ban-<时间戳>.tar.gz"
FAILED_COPY="<备份目录>/fail2ban-failed-$(date +%Y%m%d-%H%M%S).tar.gz"
sudo tar -C /etc -czf "${FAILED_COPY}" fail2ban
sudo tar -C /etc -xzf "${BACKUP_ARCHIVE}"
sudo fail2ban-client -tAfter test passes, restart service and verify from second management path. Do not run nft flush ruleset or wipe all iptables rules to regain access; that removes other security policies and widens the incident scope.
Parameter Tuning from Log Baselines
1. maxretry
maxretry = 3seems stricter but shared NAT, automation with old keys, or concurrent admin tools easily ban entire egress points. First, analyze historical failure distribution per source within the window.
Extract recent SSH failures for manual analysis (regex must match local log format):
sudo journalctl -u <SSH服务单元> --since '7 days ago' --no-pager \
| grep -E 'Failed password|Invalid user|authentication failure' \
> /tmp/sshd-failures-sample.log
sudo fail2ban-regex \
/tmp/sshd-failures-sample.log \
/etc/fail2ban/filter.d/sshd.conf \
--print-all-matchedDo not use unverified awk field positions to count IPs; OpenSSH log format varies by auth method and message type. Let Fail2ban's own filter validate extraction, or use structured log parsers.
2. findtime
Too short catches only high-speed brute force, missing low-and-slow attempts; too long accumulates scattered legitimate failures. Public SSH entry points can start with 10-minute to 1-hour windows in small canary deployments, but final values must be driven by business login patterns and security policy.
3. bantime
Shorter bans reduce shared-address collateral; longer bans reduce repeat-source overhead. Some Fail2ban versions support incremental bans:
[DEFAULT]
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 1dThese options must be tested on the local version. Monitor long-term ban counts and false positives; incremental bans are not equivalent to permanent blocklists.
Whitelist (ignoreip) Lifecycle Management
ignoreipprotects bastions, VPN egress, and automation nodes from bans, but it also bypasses that jail's detection. Overly broad whitelists let attackers inside trusted networks evade Fail2ban; stale entries (old office egress, recycled cloud IPs) may be inherited by new tenants.
View current effective ignore list:
sudo fail2ban-client get sshd ignoreip
sudo fail2ban-client status sshdAudit each entry: address ownership, purpose, owner, approval ticket, expiry. Best practices:
Use minimal single IPs or precise CIDRs; never whitelist entire VPCs, office nets, or all RFC1918 space.
Evaluate IPv4 and IPv6; only listing IPv4 egress doesn't secure IPv6 management paths.
Prefer stable bastion/VPN egress over admins' changing home IPs.
Do not rely on uncontrolled hostnames as long-term trust boundaries; DNS changes, boot-time resolution, and version differences make scope unpredictable.
Temporary whitelists must auto-expire or require ticket review; never add without removal process. SSH_CONNECTION 's first field is usually the client address seen by sshd; can be a cross-check clue:
printf '%s
' "${SSH_CONNECTION:-not-an-ssh-session}"This value may be a bastion, NAT, or proxy address; do not auto-append script output to ignoreip. Cross-verify with network topology and auth logs.
Whitelist changes reduce protection scope and are security policy changes. Before editing: backup config, confirm second management channel, save effective config. After edit, test:
sudo fail2ban-client -t
sudo fail2ban-client -d > /tmp/fail2ban-effective-config.after-ignoreipReview diff, then reload:
sudo fail2ban-client reload
sudo fail2ban-client get sshd ignoreip
sudo fail2ban-client status sshdValidate from both whitelisted and non-whitelisted controlled test sources. Rollback by restoring previous local config, running fail2ban-client -t, then reload, confirming effective list returns to pre-change state.
Do not whitelist an entire ISP netblock because one legitimate user was banned. Correct handling: unban the single IP, determine cause; if client cached old password or automation retries auth, fix the client. Whitelists are for explicit network trust relationships, not quick patches for false positives.
Custom Filter for Web Login Endpoints
Web jails are more error-prone because client IPs may traverse reverse proxies, and log fields are application-defined. Prerequisite: access logs must contain trustworthy source IPs; proxies must overwrite (not blindly trust) client-supplied X-Forwarded-For, and app/web server must only accept forwarded headers from trusted proxies.
Assume verified app log format:
2026-07-16T10:30:01Z login_failed remote_addr=192.0.2.10 user=alice reason=bad_passwordCreate /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 =Create jail:
[app-login]
enabled = true
filter = app-login
backend = auto
logpath = <日志文件>
port = http,https
findtime = 10m
maxretry = 8
bantime = 30m
ignoreip = 127.0.0.1/8 ::1 <管理网段>Before production, prepare matching and non-matching samples:
sudo fail2ban-regex \
<日志样本文件> \
/etc/fail2ban/filter.d/app-login.local \
--print-all-matched \
--print-all-missedPrevent three mis-match classes:
Successful login lines containing similar fields.
Application writing user input verbatim to logs, allowing injection of newlines or forged remote_addr.
Reverse proxy logs showing proxy IP as source, causing ban of all users sharing that entry point.
If Fail2ban runs on an app node where all requests come from a load balancer, the host L3/L4 firewall typically sees only the LB address. Even if logs contain real client IPs, local firewall rules banning those IPs may not intercept traffic because packet source is already the proxy. In such cases, enforce rate-limiting and bans at the WAF, reverse proxy, or centralized perimeter layer, not on the app node pretending rules work.
Action Selection: nftables, iptables, firewalld
Fail2ban packages ship multiple actions. List locally available ones:
sudo find /etc/fail2ban/action.d -maxdepth 1 -type f \
\( -name '*nft*' -o -name '*iptables*' -o -name '*firewall*' \) \
-printf '%f
' | sortDo not copy a banaction name from another version's docs if the file doesn't exist locally. If overriding default action, validate on a test host and confirm final commands via fail2ban-client -d. Example:
[DEFAULT]
banaction = nftables-multiportThis name only works if the corresponding action file exists and nftables is the approved backend. With firewalld, choose an action compatible with its lifecycle and verify rules persist or recover correctly after firewall-cmd --reload.
IPv6 must also be validated. If public SSH listens on both 0.0.0.0 and [::], banning only IPv4 leaves a direct bypass. Check that jail extracts IPv6 from logs, action creates corresponding rules, and a controlled IPv6 test source passes canary validation.
Log Rotation, Journal, and State Database
1. Text Log Rotation
With backend = auto, polling, or other text log backends, verify Fail2ban continues tracking new files after logrotate. Check rotation config:
sudo grep -R "<日志文件>" /etc/logrotate.conf /etc/logrotate.d 2>/dev/null
sudo fail2ban-client get <Jail名称> logpathDo not force-rotate production logs for testing unless service reopen behavior, disk space, and rollback are assessed. In a test environment with identical config, rotate and observe if jail failure counters continue updating.
2. systemd journal
Journal backend avoids path/inode rotation issues but depends on journald retaining logs long enough. Check current usage and persistence:
sudo journalctl --disk-usage
sudo systemd-analyze cat-config systemd/journald.confIf logs are dropped early due to space policy or rate limiting, Fail2ban cannot judge attacks from missing events. Changing journald parameters affects whole-system logging; evaluate disk and compliance separately. This article does not recommend blindly increasing retention for Fail2ban.
3. Fail2ban Database
Common packages use /var/lib/fail2ban/fail2ban.sqlite3 for persistent state. Path and retention per effective config:
sudo fail2ban-client -d | grep -E 'dbfile|dbpurgeage'
sudo ls -lh /var/lib/fail2ban 2>/dev/nullDo not delete or manually edit SQLite while service runs. On corruption: stop service, backup file, check logs, follow current version maintenance procedure. Deleting the database loses ban history and is an impactful operation.
Monitoring and Routine Patrol
1. Local Status 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 'Metrics beyond "how many IPs banned" should include:
Log ingestion continuity.
Filter failure counter staying at zero or sudden spikes.
Action command failures.
Abnormal growth in current ban count.
False-positive ticket volume.
SSH success/failure login trends.
Whether border layer still pushes high connection volumes to the host.
If using a third-party Fail2ban exporter, Prometheus metric names vary by implementation; use actual exposed metrics, not copied non-existent ones. Restrict exporter listen address and access to avoid leaking ban lists.
2. Audit Script
Read-only status and log collection script:
#!/usr/bin/env bash
set -euo pipefail
JAIL_NAME="<Jail名称>"
command -v fail2ban-client >/dev/null 2>&1 || {
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 2>&1; then
sudo nft list ruleset | grep -i -A 10 -B 3 f2b || true
fi
if command -v iptables-save >/dev/null 2>&1; then
sudo iptables-save | grep -i f2b || true
fiThis script does not judge IP maliciousness. Ban lists involve security disposition; correlate with auth logs, asset ownership, threat intel, and change records.
Long-Term Repeat Offenders and recidive Jail
Standard jails only handle failures for their service in the current window. A source may repeatedly get short bans from different jails every few hours, then resume after unban. Fail2ban's bundled recidive filter reads Fail2ban's own ban logs to impose longer bans on repeat triggers.
Before enabling, confirm Fail2ban writes file logs:
sudo fail2ban-client -d | grep -E 'logtarget|dbfile'
sudo test -r /var/log/fail2ban.log && sudo tail -n 30 /var/log/fail2ban.log
sudo test -r /etc/fail2ban/filter.d/recidive.conf && sudo sed -n '1,160p' /etc/fail2ban/filter.d/recidive.confIf logtarget points only to systemd journal and /var/log/fail2ban.log doesn't exist, file-based recidive config cannot be copied verbatim. Determine supported log sources for the local version; do not change global log paths just for one jail.
If file logging condition met, evaluate in test:
[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 <管理网段>These times illustrate config relationships, not universal defaults. Recidive false positives have larger impact: mobile, residential, and cloud proxy public IPs get reassigned; long bans may hit innocent users. Shared NAT means one user's failures affect the whole egress.
Test with existing Fail2ban logs before production:
sudo fail2ban-regex \
/var/log/fail2ban.log \
/etc/fail2ban/filter.d/recidive.conf \
--print-all-matchedSpot-check matches come from real Ban records; ensure recidive's own logs don't create recursive errors. After config test, canary on a single non-critical node, observe long-term ban list and collateral damage.
Recidive is not a threat intel platform. Cross-host, cross-business long-term blocking should be decided by a central security platform combining asset, ASN, proxy, geo, and attack behavior. A single host jail must not directly sync addresses to an enterprise-wide permanent blocklist.
Multi-Host and Container Boundaries
Fail2ban is single-node by default: failures seen on node A don't auto-ban on node B. If the same service sits behind a load balancer, attack requests spread across nodes may never hit per-node thresholds. Options: rate-limit at unified ingress, centralized log detection calling border firewall/WAF/security platform policies.
Auto-syncing ban IPs from one host to the entire fleet is high-impact security automation. Must handle NAT shared addresses, IPv6, expiry, conflict recovery, audit, and rollback; cannot use an unauthenticated message queue script to broadcast.
In container/Kubernetes environments, verify network path:
Which network namespace runs Fail2ban?
Do logs contain real client IPs?
Do ban rules apply to host, Pod, or ingress gateway?
Do CNI, Service, and kube-proxy/eBPF data paths traverse those rules?
Is state lost on Pod recreation?
For protecting Ingress login endpoints, prefer Ingress Controller native rate-limiting, WAF, identity platform lockout policies, and centralized security controls. Fail2ban suits nodes that see both trusted logs and real packet sources.
After Mass Failures: Determine If Already Compromised
Fail2ban only reacts to observed log failures. Attackers with valid keys, stolen sessions, or vulnerability exploits may produce only a single successful login, never triggering maxretry. Discovering brute force does not mean "banned attack IPs" equals incident closure.
1. Freeze Timeline and Preserve Raw Evidence
Record alert time, host timezone, jail, source address, total failures, current ban state. Export target window raw logs to a protected forensics directory:
#!/usr/bin/env bash
set -euo pipefail
EVIDENCE_DIR="<取证目录>"
START_TIME="<开始时间>"
END_TIME="<结束时间>"
sudo install -d -m 0700 "${EVIDENCE_DIR}"
sudo journalctl -u <SSH服务单元> \
--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/nullUse systemd-parseable times with explicit timezone. Logs contain accounts and IPs; restrict access and preserve per forensics process. Do not truncate or clean original logs to save space.
2. Check for Anomalous Successful Authentications
sudo journalctl -u <SSH服务单元> \
--since '<开始时间>' \
--until '<结束时间>' \
--no-pager \
| grep -E 'Accepted (password|publickey)|session opened'
last -Fai | head -n 50
sudo lastb -Fai | head -n 50 lastand lastb rely on wtmp/btmp, which may be rotated, corrupted, or disabled; they cannot alone prove no intrusion. Correlate successful login sources, accounts, key fingerprints, sudo records, bastion audit, and cloud platform login records.
If auditd enabled, supplement:
sudo ausearch -m USER_LOGIN,USER_AUTH \
-ts '<开始时间>' \
-te '<结束时间>' \
-iDifferent audit versions accept limited time formats; check ausearch --help. Absence of audit records does not prove no logins.
3. Read-Only Check of Accounts, Keys, and Persistence
getent passwd
sudo stat /etc/passwd /etc/shadow /etc/group /etc/sudoers
sudo find /root /home \
-xdev \
-type f \
\( -name authorized_keys -o -name authorized_keys2 \) \
-printf '%TY-%Tm-%Td %TH:%TM:%TS %u:%g %m %p
'
sudo systemctl list-timers --all --no-pager
sudo find /etc/cron.d /etc/cron.daily /var/spool/cron \
-xdev -type f -printf '%TY-%Tm-%Td %TH:%TM:%TS %u:%g %m %p
' \
2>/dev/nullCompare against asset baseline and config management records. File mtime anomalies are clues, not proof of compromise; normal patches, account provisioning, and automation also change these files.
Collect process and network snapshots:
ps -eo user,pid,ppid,lstart,etime,cmd --forest
sudo ss -plant
sudo lsof -nP -i 2>/dev/nullOutput may contain sensitive parameters; redact before posting to public tickets.
4. On Suspected Compromise: Don't "Investigate and Fix" on the Same Host
If unknown successful logins, foreign keys, privilege escalation, suspicious outbound connections, or persistence artifacts are found, escalate to a security incident. Network isolation, disk snapshots, key revocation, and host rebuild are high-impact actions:
Blast radius : isolation breaks business; credential revocation may affect automation and other hosts.
Pre-execution checks : confirm asset, dependencies, current sessions, evidence preservation, security owner.
Snapshots/backups : preserve disk, memory, or cloud audit per forensics requirements; avoid ordinary backups overwriting evidence.
Canary : drain affected node from LB, confirm redundancy, then isolate network.
Post-execution verification : ensure no production traffic enters, credentials rotated, lateral assets scoped.
Rollback : security isolation usually shouldn't roll back before root cause found; restore service via trusted image rebuild and re-onboarding.
Do not just change passwords, install Fail2ban, and continue on a suspected-compromised host. Attackers may hold root or persistent backdoors; the original host is no longer a trusted execution environment. Credential rotation must be done from a clean terminal.
5. Distinguish Attack Noise from Escalation-Worthy Events
Evidence pointing to common internet scanning: many non-existent usernames, dispersed sources, no successful auth, no baseline changes in services/files. Evidence requiring immediate escalation: same failing source later succeeds, unexpected account success, post-login sudo/key/cron changes, anomalous outbound connections, log gaps or wiping traces.
Conclusion must be supported by logs, audit, config diffs, and process/network evidence together. Fail2ban's Total banned is a disposition counter, not a host safety indicator.
Essential SSH Hardening Beyond Fail2ban
1. Disable Unnecessary Password Authentication
Modifying sshd is high-risk; misconfiguration causes remote lockout. Keep current session, prepare out-of-band access, backup config, and syntax-check with sshd -t. Example flow:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="<备份目录>"
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}"
sudo sshd -t
sudo sshd -T | grep -E '^(passwordauthentication|permitrootlogin|pubkeyauthentication) 'Actual changes should go into a controlled sshd_config.d file, but first confirm main config includes the corresponding Include and that options aren't overridden by later Match blocks. After edit:
sudo sshd -t
sudo systemctl reload <SSH服务单元>
sudo systemctl status <SSH服务单元> --no-pager reloadis still impactful. Keep original session; verify key login from second terminal before exiting. If fails, immediately restore backup via original session, run sshd -t, then reload. Never close the only connection before validating a new one.
2. Network-Level Access Restriction
When security groups, ACLs, VPN, or bastion can restrict SSH to admin networks, prioritize reducing exposure. Firewall changes must verify current source IPs, redundant management paths, rule priority, and cloud rollback procedures before precise canary. Do not change default policy to DROP in an SSH session without first allowing the current management path.
3. Account and Key Governance
Prohibit shared accounts; use auditable individual identities.
Rotate keys regularly; revoke departed/leaked keys.
Use MFA, short-lived certs, or centralized identity for privileged access.
Restrict sudo; log privilege escalation.
Patch OpenSSH and OS vulnerabilities promptly.
Alert on anomalous successful logins, off-hours logins, and new source logins.
Fail2ban mainly handles failed behavior; the real danger is often a single anomalous successful login, so successful auth monitoring is equally important.
Config Management and Bulk Changes
Multiple servers should not rely on manual jail.local edits. Place local overrides in config management, generating configs per OS, Fail2ban version, firewall backend, and service role. Template variables at minimum: admin network, SSH port, log backend, thresholds; sensitive notification credentials injected from secrets store.
Before each release, on same-version test node:
sudo fail2ban-client -t
sudo fail2ban-client -d > /tmp/fail2ban-effective-config.new
sudo diff -u \
<变更前有效配置文件> \
/tmp/fail2ban-effective-config.newEffective config output may differ in ordering or runtime environment; manually review jail, filter, backend, ignoreip, and action. Rollout pattern: test node → single production → single AZ small batch → full fleet. Each batch validates remote login, jail status, firewall rules, and false-positive alerts.
Fail2ban supports config reload via client:
sudo fail2ban-client -t
sudo fail2ban-client reload
sudo fail2ban-client statusReload can still change existing jails and firewall rules; not a no-impact operation. Supported reload options per fail2ban-client --help; do not copy version-specific flags like --restart, --unban. Bulk changes must retain a pause switch and previous config; any batch showing login anomalies stops further rollout.
Alert Triage and Disposition Evidence
Fail2ban ban logs should not all escalate at same priority. Layer by asset exposure and correlated evidence:
Single source, few failures, no success: low-level stats, watch for recurrence.
Same source across multiple accounts or jails: raise priority, correlate border logs and threat intel.
Many distributed sources low-and-slow: host thresholds may miss; enforce at ingress layer or centralized detection.
Failure followed by success, sudo, key changes, or anomalous outbound: escalate immediately as suspected compromise.
Fail2ban action error, log ingestion stop, or ban count anomalously zero: alert as protection failure, not "no attack".
Every high-priority alert must retain: host, jail, source IP, first/last time, failure count, ban duration, raw log location, and subsequent success auth check. Source IP is a clue, not attacker identity; VPN, proxy, Tor, cloud hosts, and shared NAT affect attribution.
After disposition, record unban, whitelist, threshold change, or border block with operator and ticket number. If filter adjusted, include before/after match samples and fail2ban-regex results proving no success events or proxy addresses were included. If temporary unban, document when the client causing legitimate failures will be fixed to avoid repeated triggers.
Monitoring must also watch for data absence: Fail2ban process alive but log source disconnected, journal retention insufficient, filter broken by service upgrade — all can cause ban count to drop to zero. Treating "zero bans" as environment safety creates blind spots.
Production Go-Live Checklist
Confirm each item before enabling in production:
Log source is real, continuous, and cannot be forged by low-privilege users. fail2ban-regex validated on real historical samples for matches and misses.
Trusted management sources precisely added to ignoreip.
Both IPv4 and IPv6 paths evaluated.
Action matches actual nftables/iptables/firewalld management.
Config test passes; second management channel available.
Fail2ban config and firewall baseline backed up.
Validated on test host, then canary on single non-critical node.
After start, verify login from second terminal and inspect real firewall rules.
Documented steps for unban single IP, stop single jail, full rollback.
Alerts for false-positive rate, action errors, and anomalous logins.
SSH keys, MFA, network restrictions, and account governance implemented in parallel.
Conclusion
Fail2ban's value is not maximizing "Ban" counts in logs, but turning repeated malicious attempts into verifiable, rollback-capable host-level dispositions. Reliable deployment starts from real logs, uses fail2ban-regex to prove the filter, uses firewall rules to prove the action, and uses a second management path to prove admin access isn't locked out.
As systems scale to multi-node, reverse proxy, or Kubernetes, the client IP in logs may diverge from the source address the firewall actually sees. At that point, move disposition upstream to the layer that sees real traffic. Regardless of architecture changes, Fail2ban remains one layer of defense-in-depth: reducing attack surface, strengthening identity, continuous patching, and monitoring anomalous successful logins are the primary measures against brute force.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Raymond Ops
Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
