Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Fail2ban Configuration for PHP Sites

By Kokil Thapa | Last reviewed: September 2026

Brute-force attacks against /wp-login.php, Laravel login routes, and phpMyAdmin endpoints are routine background noise on any public PHP server. Fail2ban configuration for PHP sites turns your web server and PHP-FPM logs into an automated defence layer. It reads failed auth attempts, then adds offending IPs to your firewall for a timed ban. I've deployed this stack on dozens of Ubuntu servers running Linux system administration workloads for legal-tech portals, WooCommerce stores, and custom Laravel apps. This guide covers install steps, the jails that matter for PHP, custom filters, and the tuning mistakes that block paying customers.

What is Fail2ban and why does it matter for PHP sites?

Fail2ban is a log-parsing daemon written in Python. It tails log files, applies regular expressions called filters, and triggers ban actions when a single IP crosses a retry threshold within a time window. It does not sit in the HTTP request path. Your PHP application keeps serving pages normally until the firewall drops packets from a banned address.

PHP sites attract predictable attack surfaces. WordPress xmlrpc.php floods, WooCommerce checkout probing, Laravel /login POST storms, and scanner bots hunting for .env files all leave fingerprints in logs. Manual IP blocking does not scale when your server sees hundreds of probes daily. Fail2ban handles the repetitive work while you sleep.

It complements—not replaces—application security. Validate inputs server-side, keep PHP 8.3 or 8.5 current, and patch frameworks promptly. Fail2ban catches what slips through at the network edge. Pair it with Symfony security firewall configuration or Laravel's built-in throttling for defence in depth.

Fail2ban Log Monitoring PipelineApache / Nginxaccess + error logsPHP-FPMslow + error logsAuth LogsSSH + mailFail2ban Daemonfilter + jail + actionFirewall BanUFW / nftablesPHP App Stays OnlineBanned IPs dropped before PHP-FPM
Fail2ban configuration for PHP sites: logs feed the daemon, which updates firewall rules without touching PHP code.

How do you install and enable Fail2ban on Ubuntu for PHP hosting?

Most PHP production servers I maintain run Ubuntu 22.04 or 24.04 with Apache or Nginx fronting PHP-FPM 8.3 or 8.4. Fail2ban installs cleanly from the distribution package on both releases.

Install Fail2ban and verify the service

  1. Update packages and install Fail2ban:
sudo apt update
sudo apt install fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client status

You should see a list of active jails. A fresh install often enables sshd only. That is a good start, but PHP sites need web-server jails too.

Create a local override file

Never edit /etc/fail2ban/jail.conf directly. Package upgrades overwrite it. Put your Fail2ban configuration for PHP sites in /etc/fail2ban/jail.local instead:

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local

Set global defaults that suit PHP hosting traffic:

[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
backend  = auto
banaction = ufw
ignoreip = 127.0.0.1/8 ::1 YOUR_OFFICE_IP

The ignoreip line is critical. Add your office IP, VPN endpoint, and any monitoring service that legitimately hits login pages. I've seen a misconfigured ignoreip lock out a client mid-deploy on a Notary Kathmandu production server.

Point jails at the correct log paths

Log locations differ between Apache and Nginx. Match your stack to the paths below. If you run both—as some migration setups do—enable jails for each active daemon only.

StackAccess logError logTypical jail
Apache + mod_php (legacy)/var/log/apache2/access.log/var/log/apache2/error.logapache-auth, apache-badbots
Apache + PHP-FPM/var/log/apache2/access.log/var/log/apache2/error.logapache-auth, custom PHP filters
Nginx + PHP-FPM/var/log/nginx/access.log/var/log/nginx/error.lognginx-http-auth, custom PHP filters
PHP-FPM only/var/log/php8.4-fpm.logCustom slowlog filter

Enable the web-server jails in jail.local:

[apache-auth]
enabled  = true
port     = http,https
logpath  = /var/log/apache2/error.log
maxretry = 3

[nginx-http-auth]
enabled  = true
port     = http,https
logpath  = /var/log/nginx/error.log
maxretry = 3

[nginx-botsearch]
enabled  = true
port     = http,https
logpath  = /var/log/nginx/access.log
maxretry = 2

Reload after every change:

sudo fail2ban-client reload
sudo fail2ban-client status nginx-http-auth

For deeper PHP-FPM tuning on the same server, read the guide on PHP-FPM configuration for high-traffic sites. Fail2ban and pool sizing solve different problems, but both reduce outage risk.

Which Fail2ban jails protect PHP applications from brute-force attacks?

Stock jails catch generic auth failures and bad bots. PHP CMS and framework login endpoints often need custom jails because default filters miss their log formats.

Essential jails for every PHP server

  • sshd — Blocks SSH brute force. Always enable it. Set maxretry = 3 on public-facing VPS instances.
  • apache-auth or nginx-http-auth — Catches HTTP basic auth failures and some 401 patterns.
  • apache-badbots / nginx-botsearch — Stops scanner bots probing for admin paths and exploit scripts.
  • recidive — Bans IPs that get banned repeatedly across multiple jails. Set a longer bantime, such as 7 days.

CMS-specific jails worth enabling

WordPress 7.1 and WooCommerce 11.1 expose high-value targets. Magento 2.4.x admin URLs and Laravel 13.x /login routes see similar traffic. Community filter packages exist, but I prefer writing project-specific filters because log formats vary with custom themes and reverse proxies.

Enable recidive for repeat offenders:

[recidive]
enabled  = true
logpath  = /var/log/fail2ban.log
banaction = ufw
bantime  = 1w
findtime = 1d
maxretry = 3
Fail2ban Ban SequenceAttacker IPPOST /loginNginx Log 401Fail2ban MatchRetry Counter5 in 10 minUFW DROPBan Expires After bantimeIP auto-unbanned unless recidive jail triggers
Each failed PHP login attempt increments a counter; Fail2ban fires a firewall ban when maxretry is exceeded within findtime.

How do you write custom Fail2ban filters for Laravel and WordPress?

Stock filters rarely match Laravel's log output or WordPress login failures logged in combined access logs. Custom filters live in /etc/fail2ban/filter.d/. Jails referencing them go in jail.local.

WordPress wp-login.php filter

WordPress logs failed logins as HTTP 200 with a redirect, not a 401. Match POST requests to wp-login.php paired with error indicators in the response size or a custom log format. The simplest approach uses Nginx access logs with a dedicated log format:

log_format wp_login '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

access_log /var/log/nginx/wp-login.access.log wp_login;

Create /etc/fail2ban/filter.d/wordpress-login.conf:

[Definition]
failregex = ^<HOST> - .* "POST /wp-login\.php
ignoreregex =

Test before deploying. Fail2ban ships a built-in regex tester:

sudo fail2ban-regex /var/log/nginx/wp-login.access.log \
  /etc/fail2ban/filter.d/wordpress-login.conf

Cross-check complex patterns with the online regex tester before pasting them into production filters. One bad escape sequence silently matches nothing.

Enable the jail:

[wordpress-login]
enabled  = true
filter   = wordpress-login
port     = http,https
logpath  = /var/log/nginx/wp-login.access.log
maxretry = 5
findtime = 600
bantime  = 3600

Laravel authentication filter

Laravel 12 and 13.x typically log HTTP requests through Nginx or Apache, not through Monolog by default. Point Fail2ban at your web server access log and match POST requests to your login route:

[Definition]
failregex = ^<HOST> - .* "POST /login HTTP.*" (401|422|429)
            ^<HOST> - .* "POST /api/login HTTP.*" (401|422|429)
ignoreregex =

If you log auth failures to a dedicated Laravel channel, point logpath at storage/logs/auth.log instead. On production Laravel applications I maintain, I add a log line in the failed login handler:

Log::warning('Failed login attempt', [
    'ip' => $request->ip(),
    'email' => $request->input('email'),
]);

Then the filter becomes precise:

failregex = ^\[.*\] .*\.WARNING: Failed login attempt .* "ip":"<HOST>"

This pattern avoids banning users who load the login page without posting credentials. That distinction matters on high-traffic Court Marriage in Nepal lead-capture forms behind admin panels.

Block PHP vulnerability scanners

Scanners hunt for /vendor/phpunit, /.env, and /config.php.bak. A single 404 does not warrant a ban. Two or three probe hits within minutes do:

[Definition]
failregex = ^<HOST> -.*"(GET|POST).*(\.env|phpunit|wp-config\.php|\.git)
ignoreregex =

Save as /etc/fail2ban/filter.d/php-scanner.conf and set maxretry = 2. Aggressive thresholds here are appropriate. Legitimate users never request /.env.

PHP Site Jail PriorityCritical — Enable FirstsshdrecidiveHigh — Web Stacknginx-http-authnginx-botsearchMedium — CMS / Framework Customwordpress-loginlaravel-loginphp-scannerOptional — phpMyAdmin / XML-RPCOnly if those endpoints exist on the server
Prioritise SSH and recidive jails first, then web auth, then CMS-specific custom filters for PHP sites.

How do you tune Fail2ban without blocking legitimate PHP traffic?

Overly aggressive bans are the most common production complaint I hear after enabling Fail2ban. Shared office IPs, mobile carrier NAT, and CDN edge nodes all look like single sources of many requests.

Handle reverse proxies and Cloudflare

If Cloudflare or another CDN sits in front of your PHP site, Fail2ban sees Cloudflare edge IPs in logs—not the attacker's real address. You have two options. Log the real IP from X-Forwarded-For in Nginx, or use Cloudflare's own firewall rules for edge blocking. I've compared both approaches in the article on Fail2ban vs Cloudflare for DDoS protection. For many Nepal-hosted sites without a CDN, direct Fail2ban on the origin server is simpler and costs nothing.

Configure Nginx to log real IPs when behind a proxy:

set_real_ip_from 103.21.244.0/22;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

Without this, you may ban Cloudflare itself and take the site offline for everyone.

Whitelist trusted sources

Expand ignoreip beyond localhost:

ignoreip = 127.0.0.1/8
           YOUR_HOME_IP
           MONITORING_SAAS_IP
           CI_RUNNER_IP

GitLab CI runners that hit staging login pages during automated tests trigger bans quickly if whitelisted incorrectly. Add runner IPs before enabling strict login jails on staging. This mirrors lessons from Ansible playbooks for PHP server provisioning, where Fail2ban config belongs in your idempotent server setup.

Choose sane retry thresholds

ScenariomaxretryfindtimebantimeRationale
SSH310m24hCredential stuffing is always malicious
WordPress login510m1hUsers forget passwords legitimately
Laravel API login105m30mMobile apps retry on flaky networks
Scanner probes25m24hNo legitimate reason to fetch /.env
recidive31d1wRepeat offenders get longer blocks

Monitor and unban quickly

Check banned IPs at any time:

sudo fail2ban-client status wordpress-login
sudo fail2ban-client get wordpress-login banip --with-time

Manually unban a locked-out client:

sudo fail2ban-client set wordpress-login unbanip 203.0.113.45

Watch /var/log/fail2ban.log after deploys. A filter that matches every 404 will ban search engine crawlers within hours. Run fail2ban-regex against a full day's log sample before enabling a new jail in production.

Common Fail2ban GotchasBanning CDN Edge IPSite down for all usersFix: real_ip_header configMissing ignoreipOffice locked out mid-deployFix: whitelist office + CIWrong Log PathJail runs but bans nothingFix: fail2ban-regex testOverbroad RegexGooglebot bannedFix: ignoreregex + tighter ruleTest every filter before enabling in production
Four Fail2ban misconfigurations that cause outages on PHP sites—and the fix for each.

Integrate with your deployment workflow

Fail2ban config should live in version control alongside your server provisioning. I store filter files and jail.local snippets in a private Git repo and deploy them with Ansible or Deployer hooks. After symlink swaps on zero-downtime releases, reload Fail2ban so it reopens log file handles:

sudo fail2ban-client reload

Log rotation can also break tailing. Fail2ban handles copytruncate rotations, but verify your logpath entries after switching from Apache to Nginx during a website migration.

For Apache versus Nginx trade-offs on the same stack, see Nginx vs Apache for PHP sites in 2026. Your choice affects log format and jail selection, not whether Fail2ban belongs on the server.

Key Takeaways

  • Install Fail2ban on Ubuntu, configure via jail.local, and never edit jail.conf directly.
  • Enable sshd, web auth, bot search, and recidive jails before adding CMS-specific filters.
  • Write custom filters for WordPress and Laravel login paths; test with fail2ban-regex first.
  • Whitelist office, CI, and monitoring IPs in ignoreip to prevent self-inflicted lockouts.
  • Configure real IP logging when Cloudflare or another CDN sits in front of your PHP application.
  • Reload Fail2ban after deploys and log rotation changes so jails keep tailing the correct files.

People Also Ask

Does Fail2ban work with PHP-FPM on Nginx?

Yes. Fail2ban reads Nginx access and error logs, not PHP-FPM directly. Point jails at /var/log/nginx/access.log and /var/log/nginx/error.log. Add PHP-FPM log paths only if you write custom filters for slowlog or error patterns. PHP-FPM 8.3 and 8.4 log to /var/log/php8.x-fpm.log on Ubuntu defaults.

Can Fail2ban block SQL injection attempts on PHP sites?

Fail2ban blocks IPs, not individual requests. It can ban an attacker after repeated suspicious query strings appear in access logs if your filter matches them. It does not replace prepared statements or ORM parameter binding. Treat it as a network-layer supplement to secure PHP code.

How is Fail2ban different from ModSecurity for PHP?

ModSecurity inspects request payloads inline and can reject a single malicious request immediately. Fail2ban reacts after the fact by counting log entries and banning IPs. ModSecurity uses more CPU per request. Fail2ban is lighter and easier to maintain on small VPS instances common for Nepal business sites at Rs 1,500–3,000/month (~USD 11–22).

Should I use iptables or UFW with Fail2ban on PHP servers?

On Ubuntu, set banaction = ufw in jail.local if UFW is your active firewall. Fail2ban inserts rules into UFW's chain automatically. Use iptables-multiport only when UFW is not managing the host. Check with sudo ufw status before choosing.

Ship Fail2ban before the first brute-force wave hits

Fail2ban configuration for PHP sites takes an afternoon to set up correctly. The payoff runs for years. Every unprotected WordPress install, Laravel admin panel, and phpMyAdmin instance on a public IP will attract login probes within days of going live. Start with SSH and web auth jails, add CMS filters tested against real logs, and whitelist the IPs that matter.

If you want Fail2ban baked into your next deployment alongside PHP-FPM tuning, firewall hardening, and monitored backups, review support and maintenance services or browse the portfolio of production sites already running this stack. For a broader server hardening review, contact us with your current distro, web server, and PHP version.

Frequently Asked Questions

Fail2ban watches Apache or Nginx access and error logs plus PHP-FPM logs, matches failed login patterns with filter rules, and bans repeat offender IPs via iptables, nftables, or UFW.

On Ubuntu 22.04 or 24.04, run sudo apt update, sudo apt install fail2ban, then sudo systemctl enable --now fail2ban. Verify with sudo fail2ban-client status. A fresh install often enables sshd only. Copy jail.conf to jail.local rather than editing jail.conf directly, because package upgrades overwrite the original. Set global defaults in jail.local such as bantime, findtime, maxretry, banaction, and ignoreip, then enable web-server jails matching your Apache or Nginx stack and reload with sudo fail2ban-client reload.

Enable sshd with maxretry 3 on public VPS instances, plus apache-auth or nginx-http-auth for HTTP auth failures, apache-badbots or nginx-botsearch for scanner bots, and recidive for repeat offenders with a longer bantime such as seven days. Stock jails catch generic patterns, but WordPress 7.1, WooCommerce 11.1, Magento 2.4.x admin URLs, and Laravel 13.x login routes often need custom filters because log formats vary with themes and reverse proxies. Each failed login increments a counter; Fail2ban bans the IP when maxretry is exceeded within findtime.

Fail2ban itself is free open-source software. You only pay for the VPS hosting it runs on, commonly Rs 1,500 to 3,000 per month (~USD 11 to 22) for small Nepal business sites.

Yes. Fail2ban reads Nginx access and error logs, not PHP-FPM directly. Point jails at /var/log/nginx/access.log and /var/log/nginx/error.log. Add PHP-FPM log paths only for custom slowlog or error filters.

Custom filters live in /etc/fail2ban/filter.d/ and jails reference them in jail.local. WordPress failed logins often return HTTP 200 with a redirect, so match POST requests to wp-login.php in a dedicated Nginx access log. Laravel 12 and 13.x filters should match POST /login or POST /api/login with 401, 422, or 429 status codes. For precision, log failed attempts from Laravel's auth handler to storage/logs/auth.log and match the IP in that file. Always test filters with sudo fail2ban-regex against real log samples before enabling production jails.

ModSecurity inspects request payloads inline and can reject a single malicious request immediately. Fail2ban reacts after the fact by counting log entries and banning IPs at the firewall. ModSecurity uses more CPU per request. Fail2ban is lighter and easier to maintain on small VPS instances common for Nepal business sites. They complement each other rather than replace each other. Fail2ban also does not sit in the HTTP request path; your PHP application keeps serving pages until the firewall drops packets from a banned address.

On Ubuntu, set banaction = ufw in jail.local if UFW is your active firewall. Fail2ban inserts rules into UFW's chain automatically. Use iptables-multiport only when UFW is not managing the host. Check with sudo ufw status before choosing.

Expand ignoreip beyond localhost to include your office IP, VPN endpoint, monitoring services, and GitLab CI runner IPs that hit staging login pages. If Cloudflare sits in front of your site, configure Nginx real IP logging from X-Forwarded-For or use Cloudflare firewall rules instead, because banning Cloudflare edge IPs takes the site offline. Choose sane thresholds: SSH maxretry 3, WordPress login maxretry 5, Laravel API login maxretry 10 for flaky mobile networks, and scanner probes maxretry 2. Monitor with sudo fail2ban-client status and unban locked-out clients with sudo fail2ban-client set jailname unbanip.

Fail2ban blocks IPs, not individual requests. It can ban an attacker after repeated suspicious query strings appear in access logs if your filter matches them. It does not replace prepared statements or ORM parameter binding in PHP. Treat it as a network-layer supplement to secure application code, not a substitute for input validation and framework security features like Laravel throttling or Symfony security firewall configuration.

For Apache with mod_php, point jails at /var/log/apache2/access.log and /var/log/apache2/error.log. Apache with PHP-FPM uses the same paths plus custom PHP filters. Nginx with PHP-FPM uses /var/log/nginx/access.log and /var/log/nginx/error.log for nginx-http-auth and nginx-botsearch jails. PHP-FPM 8.3 and 8.4 on Ubuntu default to /var/log/php8.x-fpm.log for custom slowlog filters. Match logpath entries to your active daemon only; do not enable jails for a web server you are not running.

Package upgrades overwrite /etc/fail2ban/jail.conf, wiping any direct edits. Copy it to /etc/fail2ban/jail.local and put all Fail2ban configuration for PHP sites there. This local override survives apt upgrades and keeps your bantime, findtime, maxretry, ignoreip, and custom jail definitions intact. Store filter files and jail.local snippets in version control alongside server provisioning, deploying them with Ansible or Deployer hooks the same way you manage other PHP server configuration.

Without real IP logging, Fail2ban sees Cloudflare edge IPs in logs, not the attacker's address, and may ban Cloudflare itself. Configure Nginx with set_real_ip_from for Cloudflare IP ranges, real_ip_header X-Forwarded-For, and real_ip_recursive on. Alternatively, use Cloudflare's own firewall rules for edge blocking. For many Nepal-hosted sites without a CDN, direct Fail2ban on the origin server is simpler and costs nothing extra beyond the VPS you already run.

Create /etc/fail2ban/filter.d/php-scanner.conf matching GET or POST requests for .env, phpunit, wp-config.php, or .git paths in access logs. A single 404 does not warrant a ban, but two or three probe hits within minutes do. Set maxretry to 2 with an aggressive bantime such as 24 hours. Legitimate users never request /.env. Enable this jail after SSH, recidive, and web auth jails are working. Run fail2ban-regex against a full day's log sample first, because a filter matching every 404 can ban search engine crawlers within hours.

Reload after every jail.local or filter change with sudo fail2ban-client reload, then verify individual jails with sudo fail2ban-client status jailname. After zero-downtime Deployer symlink swaps, reload Fail2ban so it reopens log file handles. Log rotation can also break tailing; Fail2ban handles copytruncate rotations, but verify logpath entries after switching from Apache to Nginx during a migration. Watch /var/log/fail2ban.log after deploys to catch filters that match too broadly and ban legitimate traffic.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: