Protect SSH and web authentication with Fail2ban
Use local jail overrides, verify filters against real logs, test bans safely, and retain an unban path.
Reduce repeated authentication abuse without locking administrators out.
- Ubuntu Server 22.04 LTS, 24.04 LTS
- Fail2ban 1.x
- Correct logs Confirm failures appear in journald or selected files.
- Trusted sources Inventory management and monitoring addresses.
- Recovery Keep console or a second source available.
OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.
Full guide
What you will build
- A distribution-managed Fail2ban installation whose local jail overrides survive package updates and separate journald-backed SSH from file-backed web authentication logs.
- Measured SSH and Nginx filters, explicit trusted-address exceptions, conservative retry windows, and firewall actions validated before activation.
- A controlled ban and unban drill from a disposable source, with console recovery, false-positive observation, and rollback.
- fail2ban-regex demonstrates that real failure lines match while successful logins and unrelated requests do not.
- fail2ban-client --test accepts the merged configuration and only the intended sshd and nginx-http-auth jails load.
- A disposable test address is banned after the measured threshold, current administration remains reachable, and explicit unban removes the address.
- Operators can identify backend, filter, action, chain, evidence, owner, allowlist, duration, and emergency recovery for every enabled jail.
Architecture
How the parts fit together
Fail2ban reads authentication events through jail-specific backends, applies a filter to extract source addresses, counts failures inside findtime, and invokes a ban action after maxretry. The action inserts temporary firewall state for bantime. ignoreip excludes trusted sources, while fail2ban-client provides status and explicit recovery control.
- sshd writes authentication failure events to journald; Nginx writes HTTP authentication failures to its error log.
- Each jail backend feeds events to its selected filter.
- Fail2ban counts matches for each source during findtime and triggers the configured action after maxretry.
- The firewall blocks that source for bantime while status and journal record the decision.
- Expiry or an explicit unban removes action state; operators review false positives and tune from evidence.
Assumptions
- The host already uses a reviewed firewall framework compatible with the selected Fail2ban banaction.
- SSH client source addresses are visible directly rather than hidden behind an untrusted proxy or shared NAT that would make a ban overly broad.
- Nginx error logs contain genuine HTTP authentication failures at the configured path and are rotated without losing backend access.
- Trusted management addresses are stable, minimal, and reviewed; the example documentation range is replaced before use.
- Console or a second independent management path remains available throughout testing.
Key concepts
- Jail
- The operational policy joining one filter, log backend, thresholds, and ban action.
- Filter
- Regular expressions that match failure evidence and identify the address to ban.
- findtime
- The rolling time window in which retries are counted.
- maxretry
- Number of matching failures allowed in findtime before the jail bans the source.
- bantime
- How long the action blocks a source unless explicitly changed or removed.
- ignoreip
- Explicit addresses and networks that the jail never bans; it is a safety control and potential blind spot.
Before you copy
Values used in this guide
{{managementCidr}}Smallest trusted management address or CIDR excluded from bans.
Example: 203.0.113.10/32{{testIp}}Disposable external source used for the controlled ban/unban drill.
Example: 198.51.100.27Security and production boundaries
- Fail2ban is rate-based reaction, not authentication hardening. Keep SSH keys, MFA where applicable, patched services, network policy, and strong web authentication.
- A broad ignoreip range can exempt attackers. Prefer exact management addresses and audit every exception.
- A shared NAT address may represent many legitimate users; banning it can create a denial of service.
- Never run the controlled failure test from the only active administration source.
- Treat custom filters as security code: an incorrect address capture can ban a proxy, load balancer, or innocent client.
Stop before continuing if
- Stop if the real client address is not trustworthy or is hidden behind shared infrastructure.
- Stop if fail2ban-regex matches successful or unrelated events, misses representative failures, or captures the wrong address.
- Stop if configuration test fails or the selected action conflicts with the host firewall.
- Stop if current management access is in the possible test source range without console recovery.
- Do not widen ignoreip or disable the firewall globally to resolve one failed test.
command
Install Fail2ban
Install the distribution package and keep vendor jail.conf unchanged.
Why this step matters
The Ubuntu package integrates service files, default filters, actions, and Python dependencies with host updates while leaving local policy for a separate override.
What to understand
Install from the configured signed distribution repositories and record candidate/version. Do not edit jail.conf because package upgrades can replace vendor defaults.
Installation may start or enable a service depending on packaging; inspect state before any jail can affect access.
System changes
- Installs Fail2ban packages, unit, filters, actions, and default configuration; package scripts may create runtime state.
Syntax explained
apt update- Refreshes signed repository metadata.
apt install --yes fail2ban- Installs the distribution package non-interactively after repository review.
sudo apt update && sudo apt install --yes fail2banSetting up fail2ban ...
Checkpoint: Record package and initial state
apt-cache policy fail2ban && systemctl is-enabled fail2ban; systemctl is-active fail2banContinue whenThe expected Ubuntu package is installed and any automatic service state is understood before local jails are enabled.
Stop whenPackage origin is unexpected or an existing active configuration has not been inventoried.
If this step fails
Fail2ban starts immediately with unknown jails.
Likely causePrevious local overrides remain on disk or another package installed configuration.
sudo fail2ban-client statussudo find /etc/fail2ban -maxdepth 2 -type f -name '*.local' -o -name '*.conf' | sort
ResolutionStop, inventory, and preserve existing policy before merging this guide.
Security notes
- Package installation does not make default thresholds appropriate for the host.
Alternatives
- Use the vendor-supported package for another distribution and validate its paths/backends.
Stop conditions
- Do not overwrite an existing active Fail2ban deployment without migration review.
config
Create local overrides
List trusted sources precisely and start with measured thresholds.
Why this step matters
A local override makes trust, timing, and jail-specific backends explicit while preserving vendor filters and preventing the file-backed Nginx jail from inheriting the sshd journal backend.
What to understand
DEFAULT sets the smallest trusted management CIDR, one-hour ban, ten-minute window, and five retries. Replace the documentation example before activation.
sshd uses backend=systemd because Ubuntu authentication events are in journald. nginx-http-auth uses backend=auto and its exact error log path.
Thresholds begin conservatively and must be based on measured legitimate behavior, shared NAT, automation, and attack volume.
System changes
- Creates /etc/fail2ban/jail.d/oneliners.local; no new ban occurs until validated configuration is loaded.
Syntax explained
ignoreip- Exempts loopback and the smallest reviewed management source.
bantime/findtime/maxretry- Defines one-hour bans after five failures inside ten minutes.
backend=systemd- Reads sshd evidence from the system journal.
backend=auto + logpath- Selects a file-capable backend for the exact Nginx error log.
/etc/fail2ban/jail.d/oneliners.localValues stay on this page and are never sent or saved.
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 {{managementCidr}}
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
backend = systemd
[nginx-http-auth]
enabled = true
backend = auto
logpath = /var/log/nginx/error.logLocal jail override saved
Checkpoint: Inspect merged jail policy
sudo fail2ban-client -d | grep -E "\['(add|set)', '(sshd|nginx-http-auth)'" | head -n 40Continue whenOnly intended jails, exact management CIDR, systemd ssh backend, and file-backed Nginx policy appear.
Stop whenThe example CIDR remains, an unexpected jail loads, or Nginx inherits systemd backend.
If this step fails
Configuration interpolation reports an unknown option.
Likely causeA directive is unsupported by the packaged Fail2ban version or placed in the wrong section.
fail2ban-client --versionsudo fail2ban-client --test
ResolutionUse the packaged manual and valid jail scope, then retest the full merge.
Security notes
- Broad trusted networks create permanent blind spots; document every address owner.
Alternatives
- Use per-jail ignoreip when web and SSH administration sources differ.
Stop conditions
- No activation with placeholder trust ranges.
verification
Test filters against real logs
Inspect matched and missed lines before enabling a jail.
Why this step matters
Testing against real positive and negative logs demonstrates what the jail will count and whom it will ban before a regex gains firewall effects.
What to understand
fail2ban-regex applies the packaged sshd filter to the system journal and reports matched, ignored, and missed lines. Preserve sanitized fixtures for future upgrades.
Review captured addresses, not just total counts. Successful authentication, health checks, unrelated errors, proxy addresses, and IPv6 examples must remain unmatched.
Run a separate test for Nginx using its exact log file and nginx-http-auth filter.
System changes
- No firewall or jail change; reads authentication evidence and computes filter results.
Syntax explained
systemd-journal- Uses the Fail2ban systemd journal input source.
sshd- Selects the packaged sshd filter definition.
--print-all-matched- Prints every matching line for manual address and context review.
sudo fail2ban-regex systemd-journal sshd --print-all-matchedFailregex: 18 total Lines: 122, 18 matched
Checkpoint: Approve positive and negative evidence
sudo fail2ban-regex systemd-journal sshd --print-all-matchedContinue whenRepresentative failures match with correct source addresses; successes and unrelated events do not.
Stop whenA legitimate line matches, a real failure is missed, or source extraction names a proxy/shared address incorrectly.
If this step fails
No lines match despite recent failed SSH logins.
Likely causeJournal selection, sshd unit naming, filter version, or event format differs.
journalctl -u ssh --since '-10 minutes' --no-pagerjournalctl _COMM=sshd --since '-10 minutes' --no-pagersudo fail2ban-client get sshd journalmatch
ResolutionAlign the journal match with actual Ubuntu events and rerun fixtures; do not broaden regex blindly.
Security notes
- Sanitize usernames and addresses before sharing test fixtures.
Alternatives
- Use a curated fixture file in CI plus a target-host journal smoke test.
Stop conditions
- False positives block jail enablement.
verification
Validate configuration
Stop on any parse or interpolation error.
Why this step matters
The configuration test resolves includes, interpolation, jail backends, filters, log paths, and actions before the daemon changes firewall state.
What to understand
fail2ban-client --test parses the complete installed configuration. A success line and zero exit code are required.
Also inspect fail2ban-client -d output for the exact action and backend; syntactically valid configuration can still select the wrong firewall family.
Repeat after every package, filter, action, log, proxy, or firewall change.
System changes
- No intended persistent change; validates the merged Fail2ban configuration.
Syntax explained
fail2ban-client --test- Parses the complete server configuration without starting the service.
sudo fail2ban-client --testOK: configuration test is successful
Checkpoint: Require a clean configuration
sudo fail2ban-client --test; echo $?Continue whenConfiguration test successful and exit status 0.
Stop whenAny parse, interpolation, backend, logpath, filter, or action error appears.
If this step fails
Test reports no accessible log file for nginx-http-auth.
Likely causeThe log path is wrong, rotation has not created it, or backend inherited systemd.
sudo namei -l /var/log/nginx/error.logsudo fail2ban-client -d | grep -A10 nginx-http-auth
ResolutionCorrect backend=auto and the real Nginx error path, then repeat regex and config tests.
Security notes
- Do not start with `--test` findings merely because sshd jail alone appears valid.
Alternatives
- Disable the unready web jail explicitly and deploy sshd alone with documented scope.
Stop conditions
- Any validation error blocks start/restart.
command
Start and inspect jails
Enable Fail2ban and confirm only intended jails are loaded.
Why this step matters
Starting and inspecting exact loaded jails proves the daemon accepted policy and lets operators catch unexpected firewall actions before controlled abuse testing.
What to understand
enable --now adds boot persistence and starts the service. status must list only sshd and nginx-http-auth in the intended rollout.
Inspect each jail's filter, action, retry values, ignore list, and current bans, then inspect nftables/iptables ownership.
Keep console access until permitted management traffic and ordinary service authentication remain healthy.
System changes
- Enables and starts Fail2ban; active jails begin monitoring and may add temporary firewall rules when thresholds are met.
Syntax explained
systemctl enable --now- Enables at boot and starts Fail2ban immediately.
fail2ban-client status- Lists active jails and server state.
sudo systemctl enable --now fail2ban && sudo fail2ban-client statusJail list: nginx-http-auth, sshd
Checkpoint: Confirm intended live policy
sudo fail2ban-client status && sudo fail2ban-client status sshd && sudo fail2ban-client status nginx-http-authContinue whenExactly two intended jails are active with zero unexplained bans and correct log/journal sources.
Stop whenUnexpected jails or bans appear, management fails, or action errors reach the journal.
If this step fails
Fail2ban starts then exits.
Likely causeA runtime backend, database, socket, filter, log permission, or action error passed static parsing.
systemctl status fail2ban --no-pagerjournalctl -u fail2ban -n 100 --no-pagersudo fail2ban-client --test
ResolutionDisable only the faulty jail if necessary, correct its concrete error, validate, and restart.
Security notes
- Watch existing firewall rules before and after start to detect backend conflicts.
Alternatives
- Enable one jail at a time and observe before adding the next.
Stop conditions
- Do not continue when current administration is degraded.
warning
Trigger a controlled ban
Generate failures only from a disposable source, never the current management path.
Why this step matters
A disposable-source drill proves event generation, filter counting, threshold, firewall action, status, and continued recovery access without risking the operator's only session.
What to understand
Use a host whose address is testIp, not included in ignoreip, and safe to lose temporarily. Keep console and a separate approved SSH session open.
Generate exactly the measured number of failed authentications without sending real credentials. Watch the jail log and status from the safe session.
After ban, confirm the test source is blocked while the management source still works. Do not test against a shared NAT or production client address.
System changes
- Creates one temporary Fail2ban-managed firewall ban for the explicit disposable test address.
Syntax explained
maxretry- Determines how many controlled failure matches trigger the test ban.
findtime- Requires failures to occur within the configured counting window.
bantime- Determines automatic expiry if explicit unban is not used.
Test address appears in Banned IP list; administration remains connected
Checkpoint: Prove ban and safe administration
sudo fail2ban-client status sshdContinue when198.51.100.27 appears in Banned IP list, its new SSH connection fails, and independent administration remains available.
Stop whenA different address is banned, no console exists, shared users are affected, or firewall action is unclear.
If this step fails
Failures increment but the test address is not blocked.
Likely causeAction failed, wrong address family/path is active, or test did not cross maxretry inside findtime.
sudo fail2ban-client status sshdjournalctl -u fail2ban --since '-10 minutes' --no-pagersudo nft -a list ruleset
ResolutionInspect action and timing; do not increase failure volume or broaden the ban until exact behavior is understood.
Security notes
- Never generate the test from the production management source or with a valid account password.
Alternatives
- Use a disposable network namespace/VM with its own source address.
Stop conditions
- Abort immediately if any legitimate management path is affected.
command
Verify recovery
Unban the test address and confirm the firewall rule disappears.
Why this step matters
Explicit unban proves the documented recovery control, daemon socket, jail selection, and firewall cleanup before operators depend on the policy.
What to understand
Target one jail and exact address. Return value 1 means a ban was removed; 0 often means it was not present.
Confirm the address leaves jail status and the Fail2ban-owned firewall set/chain, then verify connectivity from the disposable source.
Unban does not add the address to ignoreip; future qualifying failures can ban it again.
System changes
- Removes the exact test address from the sshd jail and its active firewall ban.
Syntax explained
set sshd- Targets only the SSH jail.
unbanip {{testIp}}- Removes one exact source address from that jail.
Values stay on this page and are never sent or saved.
sudo fail2ban-client set sshd unbanip {{testIp}}1
Checkpoint: Verify complete recovery
sudo fail2ban-client status sshd && sudo nft -a list ruleset | grep -i f2bContinue whenThe test address is absent from jail and firewall state, and its connection path recovers.
Stop whenA different address is removed, firewall state remains, or connectivity still fails for another reason.
If this step fails
unbanip returns 0 while the client remains blocked.
Likely causeAnother jail, firewall layer, address family, or upstream control owns the block.
sudo fail2ban-client statussudo nft -a list rulesetsudo iptables-save | grep -i 198.51.100.27
ResolutionIdentify the actual owner of the block; do not flush all firewall rules.
Security notes
- Log emergency unbans and investigate the events that caused them.
Alternatives
- Wait for short test bantime expiry while observing removal, if access is not urgent.
Stop conditions
- Never flush an entire Fail2ban chain to recover one address.
verification
Review false positives
Observe jail status and logs before tuning thresholds.
Why this step matters
A post-rollout observation window distinguishes useful abuse reduction from false positives, action errors, log gaps, and noisy thresholds.
What to understand
Review currently/total failed and banned counts per jail, recent journal actions, service restarts, and legitimate authentication support reports.
Correlate spikes with proxy changes, scanners, deployments, shared NAT, monitoring, and credential rotation before tuning.
Change one parameter at a time through a reviewed local override, validate, reload/restart, and repeat filter fixtures.
System changes
- No intended state change; reads jail counters and service journal for operational review.
Syntax explained
status sshd- Shows current filter and action counters plus banned addresses for the SSH jail.
journalctl -u fail2ban --since today- Reads today's daemon decisions and action errors.
--no-pager- Produces bounded noninteractive output for review.
sudo fail2ban-client status sshd && journalctl -u fail2ban --since today --no-pagerCurrently banned: 0
Checkpoint: Close the observation window
sudo fail2ban-client status sshd && sudo fail2ban-client status nginx-http-auth && journalctl -u fail2ban --since today --no-pagerContinue whenNo legitimate address is banned, action errors are absent, counters are plausible, and both log sources remain current.
Stop whenFalse positives, missing events, repeated daemon restarts, or action failures appear.
If this step fails
Ban counts surge after a reverse-proxy change.
Likely causeLogs now record the proxy address or trust headers incorrectly, collapsing clients into one identity.
sudo fail2ban-regex /var/log/nginx/error.log nginx-http-auth --print-all-matchedsudo tail -n 100 /var/log/nginx/error.log
ResolutionRestore trustworthy client-address logging and validate negative/positive fixtures before re-enabling the affected jail.
Security notes
- Restrict access to ban telemetry that contains client addresses and account names.
Alternatives
- Export aggregate jail metrics to the protected monitoring system.
Stop conditions
- Disable only the faulty jail if its false positives cannot be corrected promptly.
Finish line
Verification checklist
sudo fail2ban-client --testReports success.sudo fail2ban-client statusLists intended jails.sudo fail2ban-client set sshd unbanip TEST_IPReturns 1 for the banned test address.Recovery guidance
Common problems and safe checks
The sshd jail is active but never counts known failures.
Likely causeThe systemd journal match, backend, service unit name, timestamp window, or distribution filter differs.
journalctl -u ssh --since '-10 minutes' --no-pagersudo fail2ban-client get sshd journalmatchsudo fail2ban-regex systemd-journal sshd --print-all-missed
ResolutionAlign the jail with the actual journal unit and validated distribution filter; do not invent a broad match against arbitrary log text.
The Nginx jail reports no accessible log file.
Likely causeA global systemd backend was incorrectly applied to a file jail, logpath differs, or permissions/rotation are wrong.
sudo fail2ban-client -d | grep -A8 nginx-http-authsudo namei -l /var/log/nginx/error.logsudo tail -n 50 /var/log/nginx/error.log
ResolutionUse backend=auto for the file-backed jail, correct the exact path, and rerun filter plus configuration tests.
An administrator is banned unexpectedly.
Likely causeThe management source changed, shared NAT accumulated retries, or ignoreip is incomplete.
sudo fail2ban-client status sshdsudo fail2ban-client get sshd ignoreipjournalctl -u fail2ban --since '-1 hour' --no-pager
ResolutionUse console or an independent source to unban the exact address, verify logs and identity, then make the narrowest evidence-based policy change.
Fail2ban shows a ban but the source can still connect.
Likely causeThe wrong banaction/address family is selected, traffic takes another firewall path, or a container/cloud rule bypasses the action.
sudo fail2ban-client get sshd banactionsudo nft -a list rulesetsudo iptables-save | grep -i f2b
ResolutionSelect and test the action that integrates with the active firewall and both required address families.
Reference
Frequently asked questions
Should backend=systemd be placed in DEFAULT?
Not when file-backed jails are also enabled. Set systemd for sshd and auto for the Nginx log jail so each reads the evidence it actually produces.
Can Fail2ban replace key-only SSH?
No. It reduces repeated attempts after they appear in logs; strong authentication and least exposure prevent compromise more directly.
Why test both matched and missed lines?
Counting positive matches alone can hide a filter that also bans successful users or captures the wrong address. Negative fixtures measure false positives.
Recovery
Rollback
Disable the problematic jail and remove its rules.
- Unban affected administrators from console.
- Disable the jail in the local override.
- Validate and restart Fail2ban.
- Confirm Fail2ban-owned firewall chains match the remaining jails.
Evidence