Intrusion Prevention
Topic 67

Intrusion Prevention

SecurityHardening

Intrusion prevention is the layer that watches what is already happening on a running server and reacts in real time — reading logs, matching patterns, and inserting firewall blocks before an attacker gets further. It is distinct from the static hardening you did at install time: SSH key-only auth, a closed firewall, and minimal packages reduce the attack surface, but they do nothing about the host that is pounding your SSH port with 4,000 credential guesses an hour, or the bot crawling your web app for .env files. Intrusion prevention turns that ongoing noise into automatic, time-boxed bans.

The operational consequence is that prevention is reactive and only as good as the signal it reads. A tool like fail2ban cannot block what it never sees, so a service that logs nothing — or logs to a journal the tool is not watching — is invisible to it. And every automated ban is a potential self-inflicted outage: misjudge the threshold and you lock out your own monitoring, your CI runner, or yourself. The discipline is tuning the trade-off between catching attackers fast and never banning legitimate traffic.

Detection Versus Prevention

An intrusion detection system (IDS) tells you something happened; an intrusion prevention system (IPS) acts on it. The line matters because acting automatically on a noisy signal is how you take yourself offline. Host-based IDS tools like AIDE or the host engine in Wazuh/OSSEC compute a baseline of file checksums and alert when /etc/passwd, a sudoers file, or a web root changes — they detect tampering after the fact but do not stop it. Prevention tools sit on a live event stream and close the door while the attack is still in progress.

On a Linux server the dominant prevention pattern is log-driven banning: parse authentication and application logs, count failures per source IP inside a sliding window, and when a host crosses a threshold, push a kernel firewall rule that drops its packets for a set duration. The block is the enforcement; the log is the only sensor. This is cheap, effective against brute-force and scanning, and useless against an attacker who succeeds on the first try — single-packet exploits and valid stolen credentials produce no failed-login signal to count.

fail2ban on Debian and Ubuntu

fail2ban is the default tool in the Debian/Ubuntu world. It runs as a systemd service, tails log files (or reads the systemd journal), and matches lines against per-service filters — regular expressions that extract the offending IP. A jail ties a filter to a log source, a threshold (maxretry), a counting window (findtime), and a ban duration (bantime), then calls an action that installs the firewall rule. The shipped defaults are conservative and the SSH jail is the one almost everyone enables first.

Never edit jail.conf directly — a package upgrade overwrites it. Put your overrides in jail.local (or drop-ins under /etc/fail2ban/jail.d/), where they survive upgrades and take precedence. On a modern Ubuntu box, point the SSH jail at the journal rather than a file, because rsyslog may not be installed and /var/log/auth.log may not exist.

# /etc/fail2ban/jail.local
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
# never lock yourself out of your jump host / monitoring
ignoreip = 127.0.0.1/8 ::1 203.0.113.10
# use the systemd journal, not /var/log/auth.log
backend  = systemd

[sshd]
enabled  = true
# escalating bans for repeat offenders
bantime.increment = true
bantime.factor    = 2

After editing, validate and reload without dropping existing bans using fail2ban-client reload, and confirm the jail is live with fail2ban-client status sshd, which prints the currently banned IPs and the filter's hit count. On Red Hat and Fedora the package and configuration are identical, but the SSH log identity differs and the default backend assumptions change — RHEL ships fail2ban from EPEL and the journal backend is the reliable choice there too.

The Enforcement Backend

A ban is only as solid as the firewall layer it writes to. The historical default action shells out to iptables and adds one rule per banned IP, which is fine for dozens of hosts but degrades when a jail holds thousands — every packet is checked against a linear list of rules. The modern action, nftables with named sets, stores banned addresses in a single hashed set so lookup stays constant-time no matter how many entries it holds. On current Debian and Ubuntu, prefer the nftables-multiport / nftables-allports actions.

# jail.local — use nftables sets instead of one iptables rule per IP
[DEFAULT]
banaction      = nftables-multiport
banaction_allports = nftables-allports

One caveat that catches people: if ufw manages the firewall and fail2ban writes raw nftables rules, the two layers can fight over chain ordering, and a ban inserted after a broad ufw allow rule never matches. Either let fail2ban use the ufw action so bans land in ufw's own ordering, or keep both on the same backend and verify with nft list ruleset that the ban chain is evaluated before the allow rules.

Tuning the Ban Trade-off

Every parameter trades a false negative against a false positive. A low maxretry (3) and a long bantime (24h) catch brute-force fast but will eventually ban a real user fumbling a new SSH key, a NAT gateway behind which a hundred legitimate users sit, or your own CI fleet on a bad deploy. A high maxretry and short bantime almost never block anyone real but let a slow, distributed brute-force walk through under the radar. There is no universally correct setting — there is only the setting that fits this service's traffic.

For SSH on the open internet, escalating bans (bantime.increment) are the right shape: a first offence costs an hour, a tenth offence costs days, so persistent attackers are pushed out while a one-off fumble recovers quickly. Always populate ignoreip with your management network, monitoring probes, and any source you cannot afford to lock out — a banned Nagios or Prometheus blackbox probe will page you about an outage that is entirely self-made.

fail2ban vs Firewall Allowlisting vs Bastion

fail2ban — reactive, log-driven banning that reacts to observed abuse after it starts. Choose it when the service must stay publicly reachable but you want the automated brute-force noise dropped without manual intervention.

Firewall allowlisting — a static ufw or nftables rule that permits only known source CIDRs and drops everything else. Choose it when the legitimate client set is small and stable (a fixed office or VPN range), giving far stronger protection than any log parser because attackers never reach the service at all.

VPN or bastion — remove the public exposure entirely: SSH only over a VPN or through a single hardened jump host. Choose it for administrative access, where there is no reason for the open internet to touch the port. These are layers, not alternatives — a hardened fleet runs all three.

Common Mistakes
  • Editing jail.conf instead of jail.local — the next apt upgrade overwrites the file and silently reverts every threshold and jail you set.
  • Pointing the SSH jail at /var/log/auth.log on a journal-only Ubuntu box where that file does not exist — fail2ban starts, finds nothing to read, and bans no one while looking healthy in systemctl status.
  • Leaving ignoreip empty, then banning your own jump host, monitoring probe, or office NAT IP — and locking yourself out of the very box you were protecting.
  • Running the legacy iptables banaction with jails that accumulate thousands of IPs — per-packet matching against a linear rule list adds measurable latency under attack.
  • Letting ufw and fail2ban write to the firewall independently, so a ban rule lands after a broad allow rule and never matches — the IP shows as banned in fail2ban-client status but its packets still pass.
  • Treating fail2ban as a substitute for real authentication hardening — it slows brute-force but does nothing against valid stolen credentials, so disabling password auth and using keys still comes first.
  • Setting bantime to -1 (permanent) on a public service — the ban table grows without bound and you eventually block recycled cloud or carrier IPs that now belong to legitimate users.
Best Practices
  • Put every override in /etc/fail2ban/jail.local or a drop-in under jail.d/, never in jail.conf, so upgrades cannot revert your config.
  • Set backend = systemd on Debian/Ubuntu and RHEL so jails read the journal directly and do not depend on rsyslog writing a log file.
  • Populate ignoreip with your management CIDR, monitoring probes, and CI egress IPs before enabling any jail — verify with fail2ban-client get sshd ignoreip.
  • Switch banaction to nftables-multiport with named sets so ban lookups stay constant-time as the jail fills with thousands of IPs.
  • Enable bantime.increment on internet-facing SSH so repeat offenders escalate to multi-day bans while a one-off fumble clears in an hour.
  • Reload with fail2ban-client reload after edits and confirm hits with fail2ban-client status sshd — a jail that reports zero matched lines is parsing nothing.
  • Pair prevention with a host IDS like AIDE or Wazuh that baselines /etc and your web roots, so an attacker who slips past the ban layer still trips a file-integrity alert.
Comparable toolsCrowdSec — a modern fail2ban alternative with a shared reputation feed; bans are informed by IPs other operators have already seen attackingWazuh / OSSEC — host IDS with active-response that pairs file-integrity and log analysis with automatic blockingWindows — Account Lockout Policy plus Windows Defender Firewall connection rules cover the same brute-force ground without a log-parsing daemon

Knowledge Check

What is the core limitation of log-driven prevention like fail2ban?

  • It can only act on attacks that produce a log signal it is watching — a single-packet exploit or valid stolen credentials generate no failures to count
  • It can only ever ban individual IPv4 addresses one at a time and silently ignores every single inbound connection that happens to arrive at the host over IPv6 instead
  • It blocks attackers in the firewall but keeps no record at all of which IPs were banned or when, leaving nothing for later forensic review
  • It only ever acts against already-authenticated users inside a session and is completely blind to anonymous scanners probing the open port

Why put your changes in jail.local instead of jail.conf?

  • jail.conf is replaced on package upgrade, while jail.local overrides it and survives upgrades
  • jail.conf is shipped with read-only permissions by the package and cannot be edited even by the root account
  • jail.local is parsed measurably faster at startup because it is preloaded into kernel memory at boot
  • jail.conf only configures failure detection, while the actual bans must be defined separately in jail.local

Why prefer the nftables banaction with named sets over the legacy per-IP iptables action?

  • Banned IPs live in one hashed set with constant-time lookup, instead of one rule per IP checked linearly on every packet
  • nftables bans are permanent by default and never expire on their own over time, unlike the strictly time-limited iptables rules
  • iptables can only log offending traffic and never actually drop the packets, so any bans written there end up having no real effect
  • nftables automatically replicates and shares its named ban set with every other server on the same network

What is the trade-off of setting maxretry = 3 with a 24-hour bantime on public SSH?

  • Brute-force is caught fast, but a real user fumbling a key or a whole NAT'd office can be banned for a day on three mistakes
  • It disables key-based authentication entirely and silently forces every user account on the host back to interactive password logins
  • It hard-caps the SSH daemon at exactly three simultaneous concurrent sessions regardless of which source address they happen to come from
  • It actively prevents fail2ban from ever escalating persistent repeat offenders to longer ban durations

You got correct