
September 08, 2026
13 min read
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.
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
- 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.
| Stack | Access log | Error log | Typical jail |
|---|---|---|---|
| Apache + mod_php (legacy) | /var/log/apache2/access.log | /var/log/apache2/error.log | apache-auth, apache-badbots |
| Apache + PHP-FPM | /var/log/apache2/access.log | /var/log/apache2/error.log | apache-auth, custom PHP filters |
| Nginx + PHP-FPM | /var/log/nginx/access.log | /var/log/nginx/error.log | nginx-http-auth, custom PHP filters |
| PHP-FPM only | — | /var/log/php8.4-fpm.log | Custom 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. Setmaxretry = 3on public-facing VPS instances.apache-authornginx-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 longerbantime, 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 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.
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
| Scenario | maxretry | findtime | bantime | Rationale |
|---|---|---|---|---|
| SSH | 3 | 10m | 24h | Credential stuffing is always malicious |
| WordPress login | 5 | 10m | 1h | Users forget passwords legitimately |
| Laravel API login | 10 | 5m | 30m | Mobile apps retry on flaky networks |
| Scanner probes | 2 | 5m | 24h | No legitimate reason to fetch /.env |
| recidive | 3 | 1d | 1w | Repeat 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.
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 editjail.confdirectly. - Enable
sshd, web auth, bot search, andrecidivejails before adding CMS-specific filters. - Write custom filters for WordPress and Laravel login paths; test with
fail2ban-regexfirst. - Whitelist office, CI, and monitoring IPs in
ignoreipto 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
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.

