Security

Fail2ban in production: beyond the SSH tutorial

Installing fail2ban takes five minutes; making it work properly in production is another story. The journald backend, progressive banning, HTTP jails behind a reverse proxy, and the pitfalls we have lived through: the real client IP, the corporate proxy you must never ban, the nftables restart that wipes everything.

Shield in front of a server rack, repeated login attempts shattering against it
Contents (6 sections)
  1. Putting fail2ban back in its place
  2. The 2026 baseline: journald, not auth.log
  3. One level up: HTTP jails behind a reverse proxy
  4. The two pitfalls that cost dearly
  5. The corporate proxy: one IP means dozens of people
  6. nftables: the restart that wipes everything

Expose an SSH port to the Internet and look at the logs: attempts arrive continuously, by the thousand, from botnets cycling through username and password pairs. Fail2ban is the classic answer: read the logs, spot repeated failures, ban the source address. Five minutes to install, and that is precisely the problem: most installs stop at the tutorial, with a default SSH jail that reassures more than it protects. Here is what a production configuration changes, pitfalls we have lived through included.

Putting fail2ban back in its place #

Let us say it clearly first: fail2ban is a noise reducer, not a primary defence. If your SSH accepts passwords, fail2ban slows attackers down; if your SSH only accepts keys (PasswordAuthentication no), brute force is already dead and fail2ban merely cleans up the logs and saves resources. The order of priorities is therefore: key-only authentication, minimal exposure (a firewall that only opens what is needed, IP allowlists for administration), and then fail2ban for the residual noise. That is the architecture of our front-end servers: the hypervisor firewall only lets public HTTP and HTTPS through, SSH uses keys, and fail2ban cleans up what is left.

The 2026 baseline: journald, not auth.log #

The first trap of older tutorials: they point fail2ban at /var/log/auth.log… which no longer exists on modern systems without rsyslog, where everything lives in the systemd journal. A jail that reads a missing or frozen file bans nobody, silently. The fix: backend = systemd. Here is our base jail.local, with the second setting that changes everything, progressive banning:

INI
# /etc/fail2ban/jail.local
[DEFAULT]
backend = systemd
banaction = nftables-multiport

# Progressive banning: each repeat offence multiplies the penalty.
bantime = 1h
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 1w

findtime = 10m
maxretry = 5

# IPs to NEVER ban: monitoring, admin. Short, justified list.
ignoreip = 127.0.0.1/8 ::1 <monitoring-ip>

[sshd]
enabled = true
mode = aggressive

Progressive banning is what makes the difference over time: an occasional scanner gets an hour, a repeat offender ends up banned for a week. And always check that a jail is actually working: fail2ban-client status sshd should show matches that keep increasing. A jail stuck at zero for weeks on an exposed server is a broken jail, not a server that has been spared.

One level up: HTTP jails behind a reverse proxy #

Brute force does not target SSH alone: web application login pages, API endpoints and admin paths get the same traffic. Fail2ban can protect them, provided it reads the reverse proxy logs (Traefik, nginx…) and matches bursts of 401 and 403 responses. Two non-negotiable conditions, both learned the hard way:

  • Ban the real client IP, never the proxy’s. If your application sits behind a reverse proxy (or worse, if Docker does NAT in front of it), the logs may only show the proxy’s internal IP. Ban it and you cut off all traffic. The chain must preserve the source address from end to end; on one of our servers, this meant switching the proxy to host networking, otherwise IPv6 visitors all showed up with the same internal Docker gateway IP.
  • Structured logs. A JSON access log can be parsed without fragile regular expressions: the fail2ban filter becomes one reliable line instead of an expression that breaks at the first change to the log format.

The two pitfalls that cost dearly #

The corporate proxy: one IP means dozens of people #

Seen in production: bursts of suspicious requests from a single IP, the reflex to ban… and it was a corporate proxy (Zscaler or similar): behind that address, dozens of legitimate employees, possibly including your own client. The same goes for external monitoring probes, whose behaviour (regular requests, varied ports) looks exactly like a scan. Before banning a “strange” IP, a whois takes ten seconds; corporate proxies and your probes go into ignoreip, documented, with the reason in a comment.

nftables: the restart that wipes everything #

Fail2ban writes its bans into the firewall (nftables nowadays). But systemctl restart nftables wipes the entire ruleset before reloading the file, taking fail2ban’s dynamic tables with it and, if Docker is running, its NAT rules: we have seen a harmless-looking restart cut off DNS for a production container. To reload an nftables configuration, use nft -f (atomic, additive), never a service restart on a machine that runs fail2ban or Docker.

Fail2ban illustrates our conviction well: security is not a product you install, it is a series of details you check. That is what our managed services cover: hardening, monitoring of jails, and immutable backups for the day everything else has failed. Any doubt about the real exposure of your servers? Let us talk.

About
01/Read nextThree articles

Related articles

Security

A stale lock file is a scan that lies

Dependency lock files are the source of truth for security scans. When a build pipeline quietly rewrites them instead of failing, the scan reads the wrong versions and returns a reassuring verdict. How we found out, and what we changed.

dependenciessoftware supply chain
Security

446 vulnerabilities, then zero: what an honest inventory changes

Each machine scanned its own images, and nobody could see the fleet. By centralising the analysis on the real inventory (111 unique images instead of 25 local databases), the counter went from 446 to 0 in one evening. The story of a measurement project, not a fixing one.

vulnerabilitiescontainers
Security

Rolling out a SIEM across 38 machines: Wazuh, from sizing to tuning

A log that stays on the compromised machine is worthless. We deployed Wazuh across the whole fleet in two days: a dedicated VM, twelve-month retention, 38 agents in three waves, and above all tuning that brought the noise down from 1,250 alerts per machine to 8 for the entire fleet. The installer pitfalls, compliance checks in containers, and the home-made detections that really matter.

wazuhsiem

What next

Spot problems before they cost you?

Centralised logs, alerts, patches applied on purpose, backups actually restored. First conversation free.

Discuss your project

Reply within 24 working hours, from one of the two founders. Or by phone: +33 7 59 96 14 99.