Illustrative WIUNIX Academy studio with seated operator and DEMO Nginx log-to-Fail2ban diagram.

WIUNIX ACADEMY

Fail2ban for Nginx: trustworthy logs, scoped bans and recovery

Trace repeated HTTP authentication failures from a minimal Nginx log to a dedicated jail, rehearse safely, then activate a narrowly scoped host firewall action.

Intermediate55 min readUpdated
Make this guide yours

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Ubuntu 24.04 LTSReviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment11 editable values

THE OUTCOME

What will you have at the end?

A dedicated jail whose input, threshold, enforcement boundary and recovery can be explained and checked.

01Direct client → Nginxsecure.example.net · TCP 443
02Nginx → polling/var/log/nginx/academy-auth.log · 401
03Jail → nftables input5 / 600s → 600s
Supported operating systems and versions

Five documentation-reviewed OS paths. Isolated checks cover parsing and harmless-action decisions; no real-address ban, internet protection, full OS installation or CDN enforcement is claimed. Custom values are not execution tests.

Before you begin

  • Disposable dedicated direct-host Nginx lab with valid HTTPS and an existing Basic-auth location.
  • Independent console, authorized key-based SSH and sudo; no existing Fail2ban deployment or competing rule ownership.
  • New standard-directory Nginx log and new private scratch path /srv/academy-fail2ban-lab.
  • Official OS repository access and an existing protected credential-management process.

What this guide protects

For Linux administrators with an existing direct-host Nginx HTTPS virtual host and a working HTTP Basic authentication location. You will count repeated 401 responses only in that location, prove the parser and a harmless action, then configure a TCP-port-only nftables action. This detects repeated unauthorized requests, including missing credentials; it does not distinguish a mistyped password from an attack. It is not an SSH hardening or TLS setup guide.

The packet and identity boundary

The client must connect directly to host Nginx on TCP 443. The socket peer becomes $remote_addr, the first and only IP in the dedicated record. Nginx → file → polling backend → threshold → nftables input hook is the complete path. HTTP/3 uses UDP and is not blocked by this TCP action; disable QUIC for this lab or design and validate a separate UDP action.

Do not apply this action to Docker-published Nginx, a reverse proxy/CDN, Cloudflare Tunnel, PROXY protocol or a real_ip-enabled virtual host. With a proxy, the packet source is the proxy. Banning the restored visitor IP in the origin input chain will not block that visitor; banning the proxy can disconnect everyone. Docker bridge publishing normally traverses forwarding/NAT, not this host input hook.

Choose public settings before copying

Use your existing server 203.0.113.20, authorized SSH account admin/port 22, direct certificate name secure.example.net and actual HTTPS listener 443. 198.51.100.10 is the exact IPv4 egress seen by this server, not your laptop LAN IP. Add any genuine IPv6 management source manually in the protected jail editor; never allow an entire ISP or 0.0.0.0/0. Choose a new absolute scratch directory /srv/academy-fail2ban-lab and dedicated log /var/log/nginx/academy-auth.log under the standard Nginx log directory so package rotation covers it. Values and custom progress remain in sessionStorage in this tab/language/article/environment; default progress uses localStorage. They never enter URLs, server writes or shared links. Never enter secrets. Applying a value does not test a deployment.

01

LET’S BUILD

Inspect the lab and keep a console

Use a disposable dedicated host with independent provider console access and an already working key-based SSH session. Keep that session open. Inspect listeners, rules and effective Nginx configuration locally; its dump may contain private configuration, so do not paste it into public tickets. Stop if Nginx is proxied, real_ip rewrites addresses, paths collide, Fail2ban already owns jails or a firewall manager controls the rules. Do not transplant this fresh-host procedure into an existing security policy.

The native fields validate syntax/relationships; the extra preflight rejects nonstandard log directories and collisions with synthetic test identities. Inspect parent symlinks and actual path ownership separately before continuing.

Bash

Dedicated lab host; Bash shell

set -e
cat /etc/os-release
dpkg-query -W nginx fail2ban nftables 2>/dev/null || true
sudo ss -lntup
if command -v nft >/dev/null; then sudo nft list ruleset; else printf '%s\n' 'nft not installed: inspect current firewall ownership before continuing'; fi
sudo nginx -v
sudo nginx -T
sudo systemctl status fail2ban --no-pager || true
test ! -e "/srv/academy-fail2ban-lab"
test ! -L "/srv/academy-fail2ban-lab"
test ! -e "/var/log/nginx/academy-auth.log"
test ! -L "/var/log/nginx/academy-auth.log"
python3 - <<'PY'
from pathlib import PurePosixPath
from ipaddress import ip_address
log = PurePosixPath('/var/log/nginx/academy-auth.log')
lab = PurePosixPath('/srv/academy-fail2ban-lab')
assert log.parent == PurePosixPath('/var/log/nginx') and log.suffix == '.log'
assert lab.is_absolute() and len(lab.parts) >= 3
assert not any(str(lab).startswith(p) for p in ('/etc/', '/var/log/', '/proc/', '/sys/', '/dev/'))
assert '198.51.100.10' not in ('192.0.2.60', '192.0.2.61')
assert not ip_address('198.51.100.10').is_loopback
print('Public path and fixture-separation checks passed')
PY
How do I know it worked?

A direct-host listener on 443, unused dedicated paths and no pre-existing Fail2ban deployment; independent console works.

02

LET’S BUILD

Confirm a second management connection

Run this panel on your workstation, not inside the server. Confirm the host key through your established trust record. It uses existing access only and changes no SSH or firewall settings. Keep the console route ready even though the proposed ban covers the web port only.

Bash

Administrator workstation; existing SSH identity

ssh -p 22 admin@203.0.113.20
How do I know it worked?

A separate authorized session opens; retain the original session.

Upstream release and OS package are different

Upstream stable was 1.1.1 on review day. This guide installs the selected distribution’s maintained candidate: Ubuntu 26.10 lists 1.1.0-11, Ubuntu 24.04 base lists 1.0.2-3, Debian 13 lists 1.1.0-8 and Debian 12 lists 1.0.2-2. Updates/security pockets may differ; record the actual candidate. Ubuntu 26.04 Launchpad lists 1.1.0-9 in Release; its package page and official publication record agree after retry. Never substitute another Ubuntu codename or install the upstream DEB over distribution ownership. Older upstream 1.0.2 alone is not evidence of Python 3.12 compatibility; distribution patches matter. Nginx package versions also differ; preserve your patched distro version and inspect its actual build.

Explore all tutorials ↗