← Start of tutorial

WIUNIX ACADEMY

Fail2ban for Nginx: trustworthy logs, scoped bans and recovery

Page 3 of 6. Review the earlier steps and prerequisites before continuing.

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
08

LET’S BUILD

Observe one authorized request without a ban

From a separate authorized lab client, open the existing protected HTTPS URL once with no credentials, using normal certificate validation. Do not brute-force it or use an account belonging to anyone else. Fail2ban is still masked. Read only the final records locally: expect ISO timestamp, f2b, the actual socket IP and 401. An already authenticated request should record its normal success status. Do not replace the hostname with a raw IP or bypass TLS verification to make this pass.

Bash

Dedicated lab host; Bash shell

sudo tail -n 8 "/var/log/nginx/academy-auth.log"
How do I know it worked?

Observed address matches the direct client, with no proxy or trusted-header rewrite.

Create a dedicated filter

Use a new project-specific name instead of modifying nginx-http-auth.conf. The packaged nginx-http-auth filter targets Nginx error-log messages and is not a parser for this custom access log. Record the format/filter as one maintained contract.

Bash

Dedicated lab host; Bash shell

sudo test ! -e /etc/fail2ban/filter.d/academy-nginx-401.conf && sudo nano /etc/fail2ban/filter.d/academy-nginx-401.conf
09

LET’S BUILD

Anchor the address and status

Save the complete filter. Doubled percent signs are required by Fail2ban’s configuration interpolation; they are not doubled in the log. Date removal leaves the f2b marker. <HOST> parses IPv4/IPv6 and usedns=no in the jail prevents DNS lookups; no arbitrary hostname should enter this numeric log. Full-line anchors reject appended junk and non-401 statuses. Regex success alone does not prove firewall enforcement or resistance to log loss.

INI · /etc/fail2ban/filter.d/academy-nginx-401.conf

File contents; edit on the dedicated lab host

[Definition]
datepattern = ^%%Y-%%m-%%dT%%H:%%M:%%S%%z
failregex = ^\s*f2b <HOST> 401\s*$
ignoreregex =
How do I know it worked?

A new filter exists with explicit timestamp parsing and no user-controlled header/request capture.

10

LET’S BUILD

Check positive, negative and IPv6 fixtures

These are synthetic documentation addresses, not traffic. Expect five matches, three misses and eight recognized timestamps. The negative trailing fake IP must not match. The fixed historical timestamps are suitable for regex parsing, not a live findtime-window test. Keep fixture data out of the real monitored log.

Bash

Dedicated lab host; Bash shell

set -e
sudo tee "/srv/academy-fail2ban-lab/fixture.log" >/dev/null <<'LOG'
2026-10-20T12:00:00+00:00 f2b 192.0.2.60 401
2026-10-20T12:00:01+00:00 f2b 192.0.2.60 401
2026-10-20T12:00:02+00:00 f2b 192.0.2.60 401
2026-10-20T12:00:03+00:00 f2b 2001:db8::60 401
2026-10-20T12:00:04+00:00 f2b 2001:db8::60 401
2026-10-20T12:00:05+00:00 f2b 192.0.2.61 200
2026-10-20T12:00:06+00:00 f2b 192.0.2.61 403
2026-10-20T12:00:07+00:00 f2b 192.0.2.61 401 injected 192.0.2.60
LOG
sudo fail2ban-regex "/srv/academy-fail2ban-lab/fixture.log" /etc/fail2ban/filter.d/academy-nginx-401.conf --usedns=no --print-all-matched --print-all-missed
EXAMPLE OUTPUT
Lines: 8 lines, 0 ignored, 5 matched, 3 missed
Date template hits: 8
How do I know it worked?

5 matched / 3 missed; correct IPv4 and IPv6 identities; 8 date hits.

File backend and journald are separate designs

This jail deliberately uses polling against an unbuffered dedicated file. It works without python-systemd or pyinotify and handles ordinary rename/reopen log rotation, but still needs an actual rotation check on your host. backend=systemd reads journal records using journalmatch and must not use logpath. Nginx access logs are not automatically in journald just because Nginx runs under systemd. Review journal permissions, retention, rate limiting and the actual MESSAGE format before designing a journal filter. Never silently switch backends to hide an empty file.

Build a configuration with no firewall action

The isolated daemon uses only new scratch configuration, socket, PID and log files. It does not copy other jails or credentials. The audit action records validated addresses in a local text file; it invokes no nft, iptables, UFW, network request or service control. This separation matters more than merely giving a production action a test name.

Bash

Dedicated lab host; Bash shell

set -e
sudo install -d -m 0700 "/srv/academy-fail2ban-lab/conf/filter.d" "/srv/academy-fail2ban-lab/conf/action.d"
sudo cp /etc/fail2ban/filter.d/academy-nginx-401.conf "/srv/academy-fail2ban-lab/conf/filter.d/"
sudo touch "/srv/academy-fail2ban-lab/events.log"
sudo nano "/srv/academy-fail2ban-lab/conf/fail2ban.conf"
Explore all tutorials ↗