2026年11月12日
2026年11月12日
WordPress DDoS Protection: Cloudflare, Nginx Rate Limits and Fail2ban
This article is a localized edition of the Japanese original. Read the Japanese original
A DDoS (distributed denial-of-service) attack sends a flood of requests from many sources so that your server can no longer respond. On WordPress sites, a common pattern is automated traffic piling up on the login page wp-login.php or on xmlrpc.php, until PHP and the database can't keep up.
Think of defense in three layers: stop traffic before it reaches your server (a CDN/WAF such as Cloudflare), throttle the requests that do arrive at the web server (Nginx or Apache), and block sources that keep coming back (Fail2ban). Attacks large enough to saturate your network link can't be stopped by your own server settings alone. For those you need an upstream service like Cloudflare, or protection from your hosting provider.
The Cloudflare details in this article were checked against Cloudflare's official documentation (as of November 2026).
Check what is happening first
When the site is slow or down, first find out whether it really is an attack and what is being targeted. Adjust the log path to your environment (on Ubuntu, Apache logs to /var/log/apache2/access.log and Nginx to /var/log/nginx/access.log).
# Top 20 source IPs by number of requests
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# Top 20 URLs by number of requests (query strings removed)
awk '{print $7}' /var/log/nginx/access.log | cut -d'?' -f1 | sort | uniq -c | sort -rn | head -20
# Top 20 source IPs hitting wp-login.php and xmlrpc.php
grep -E '"(GET|POST) /(wp-login|xmlrpc)\.php' /var/log/nginx/access.log \
| awk '{print $1}' | sort | uniq -c | sort -rn | head -20
# Connection summary, and the number of established connections on port 443
ss -s
ss -tn state established '( sport = :443 )' | wc -l
How to read the results:
- A lot of traffic from a few IPs: per-IP limits and Fail2ban, described below, work well.
- A little traffic each from many IPs: per-IP limits are less effective, and upstream protection such as Cloudflare becomes the main defense.
- Traffic concentrated on
wp-login.phporxmlrpc.php: apply strict limits to just those paths.
If you use Cloudflare, the IPs in your server logs will be Cloudflare's. Set up the configuration in "Logging visitor IPs when you use Cloudflare" below before you investigate.
Stop traffic upstream with Cloudflare
When you add your domain to Cloudflare, point its name servers to Cloudflare, and set your DNS records to proxied (the orange cloud), traffic passes through Cloudflare before it reaches your server. For setup steps, see "Using Cloudflare DDoS protection with WordPress".
HTTP DDoS attack protection
Cloudflare's HTTP DDoS attack protection (a managed ruleset that detects and stops HTTP DDoS attacks) is always on for every plan, including Free. It works without any configuration.
You can change its sensitivity and action from the "DDoS protection" tab of the "Security rules" page in the dashboard, using "Create override". On the Free plan you can create one override, and it applies to all requests. Unless legitimate visitors are being blocked or attacks are getting through, the defaults are fine.
Under Attack mode (only while you are under attack)
With I'm Under Attack mode on, visitors see a check page, and only those who pass a browser check (a few seconds) continue to the site. To turn it on for the whole zone, switch on "Under Attack Mode" in "Quick Actions" on the zone's overview page.
Cloudflare itself describes this feature as a last resort: every visitor sees the check page, and clients that can't run JavaScript and third-party analytics tools are affected. Turn it off once the attack subsides. To enable it only on specific paths such as the admin area, you can also create a Configuration Rule that matches those paths and turn on "I'm Under Attack".
Rate limiting rules (one on the Free plan)
A rate limiting rule blocks an IP for a while when it sends more than a set number of requests within a set time. On the Free plan, the limits are:
| Item | Free plan |
|---|---|
| Number of rules | 1 |
| Fields you can match on | Path (URI Path) and Verified Bot |
| Counted per | IP |
| Counting period | 10 seconds |
| Mitigation timeout | 10 seconds |
Because you only get one rule on the Free plan, the practical choice is the frequently targeted /wp-login.php. The counting period is 10 seconds, so start with something like "block when more than 5 requests in 10 seconds", then check that people who use the login page aren't affected and adjust. For an example configuration, see "Limiting access to wp-login.php with Cloudflare".
WAF managed rules
The WAF managed ruleset available on the Free plan is the "Cloudflare Free Managed Ruleset". It blocks attacks on high-impact, widely exploited vulnerabilities and is available on every plan. The "Cloudflare Managed Ruleset", which includes rules for WordPress, requires the Pro plan or higher. For WAF configuration, see "Configuring the Cloudflare WAF".
Logging visitor IPs when you use Cloudflare
Behind Cloudflare, the connecting address your server sees is a Cloudflare IP. The visitor's IP is in the CF-Connecting-IP header.
However, anyone can send this header. If you copy $_SERVER['HTTP_CF_CONNECTING_IP'] into REMOTE_ADDR unconditionally in PHP, an attacker who connects to your server directly, bypassing Cloudflare, can pretend to be any IP they like. Configure the web server so that it trusts the header only on connections coming from Cloudflare's IPs.
In Nginx, use ngx_http_realip_module.
# Put this in the http {} block or a server {} block
# List each Cloudflare IP range (the two lines below show the format)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 2400:cb00::/32;
# ...list every published range
real_ip_header CF-Connecting-IP;
In Apache 2.4, use mod_remoteip (Ubuntu/Debian example).
sudo a2enmod remoteip
Create /etc/apache2/conf-available/remoteip.conf with the following.
RemoteIPHeader CF-Connecting-IP
# List each Cloudflare IP range (the two lines below show the format)
RemoteIPTrustedProxy 173.245.48.0/20
RemoteIPTrustedProxy 2400:cb00::/32
sudo a2enconf remoteip
sudo apache2ctl configtest
sudo systemctl restart apache2
In Apache, change %h to %a in your log format so the visitor IP is logged. Cloudflare publishes its IP ranges at https://www.cloudflare.com/ips/, and they can change, so use the current list from that page rather than one copied from an article.
Throttle requests in Nginx
Use Nginx's limit_req to limit how often each IP can send requests. limit_req_zone goes in the http {} block (on Ubuntu, inside http {} in /etc/nginx/nginx.conf, or in a file under /etc/nginx/conf.d/).
# Put this in the http {} block
# Per IP, up to 5 requests per minute to the login page
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=5r/m;
In your site's server {} block, add settings for wp-login.php and xmlrpc.php. Adjust the PHP-FPM socket path to your environment.
server {
# ...existing settings...
# wp-login.php: up to 5 per minute per IP (allow a temporary burst of 2)
location = /wp-login.php {
limit_req zone=wplogin burst=2 nodelay;
limit_req_status 429;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
# xmlrpc.php: deny it if you don't use it
location = /xmlrpc.php {
deny all;
access_log off;
}
}
location = /wp-login.phptakes priority over the usuallocation ~ \.php$. If you don't include the PHP handling settings inside it,wp-login.phpstops working.burstis how many requests may temporarily exceed the rate, andnodelayprocesses those extra requests immediately instead of queuing them.- Requests over the limit get a 429 (Too Many Requests) response.
xmlrpc.php is used by Jetpack and some mobile apps. If you use them, don't deny it; rate-limit it the same way as wp-login.php. For details, see "Disabling XML-RPC for security".
sudo nginx -t
sudo systemctl reload nginx
You can also rate-limit the whole site (location /), but a single page view generates many requests for images, CSS and JavaScript, so a low value will block ordinary visitors. If you do, check your normal request rate in the access log before choosing a value. For more on limit_req, see "Rate limiting with Nginx limit_req".
Protect the login page in Apache
Apache has no built-in equivalent of limit_req. If your administrators use fixed IPs, the simplest approach is to allow wp-login.php and xmlrpc.php only from those IPs. Add this to the .htaccess in your WordPress directory (Apache 2.4 syntax).
<FilesMatch "^(wp-login\.php|xmlrpc\.php)$">
Require ip 203.0.113.10
Require ip 198.51.100.0/24
</FilesMatch>
Order Deny,Allow and Allow from are Apache 2.2 syntax. In 2.4, use Require. Replace the IPs with your own.
If administrators' IPs change, for example when working from home or on the road, this method won't work. Use Cloudflare rate limiting or Fail2ban, described next. mod_evasive, which adds rate-based blocking to Apache, is covered in "Protecting Apache from DDoS with mod_evasive".
Block repeat offenders with Fail2ban
Fail2ban watches log files and, when an IP repeats a defined pattern, blocks it in the firewall for a set time. On Ubuntu/Debian, install it like this.
sudo apt install -y fail2ban
Create /etc/fail2ban/filter.d/wordpress-login.conf with a filter that counts POST requests to the login page and XML-RPC.
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php
^<HOST> .* "POST /xmlrpc\.php
ignoreregex =
Add a jail that uses this filter to /etc/fail2ban/jail.local.
[wordpress-login]
enabled = true
filter = wordpress-login
backend = auto
logpath = /var/log/nginx/access.log
port = http,https
maxretry = 10
findtime = 60
bantime = 3600
An IP that sends 10 POSTs (maxretry) within 60 seconds (findtime) is blocked for 3600 seconds (bantime). Successful logins are POSTs too, so set maxretry to a value that normal logins never reach.
Confirm that the filter matches your log before you enable it.
# Check how many log lines the filter matches
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-login.conf
sudo systemctl restart fail2ban
# Show the IPs currently banned
sudo fail2ban-client status wordpress-login
# Unban an IP that was banned by mistake
sudo fail2ban-client set wordpress-login unbanip 203.0.113.10
Be careful if you are behind Cloudflare. The connections reaching your server come from Cloudflare's IPs, so even if you log visitor IPs, blocking a visitor IP in the server's firewall has no effect. Worse, if Cloudflare IPs are what's in your log, Fail2ban will block Cloudflare's IPs and lock out ordinary visitors too. With Cloudflare, the basic approach is to block at Cloudflare, using its rate limiting and WAF.
Fail2ban settings for WordPress are also covered in "Preventing brute-force attacks on WordPress with Fail2ban".
Reduce WordPress’s load in advance
When the attack targets ordinary pages and every request runs PHP and the database, even a modest number of requests can bog the server down. A page cache (a caching plugin, Nginx FastCGI cache, or Cloudflare's cache) lets you serve more pages without touching PHP, so the same volume of traffic is easier to withstand.
Returning a 403 based on the User-Agent inside PHP (functions.php and the like) does little to reduce load, because the check runs after WordPress has already loaded, and User-Agents are trivial to fake. If you block, block before PHP: at the web server or at Cloudflare.
Cautions
- Before changing settings, keep a copy of the configuration file, and test with
sudo nginx -torsudo apache2ctl configtestbefore applying. - Limits that are too low block ordinary visitors and administrators. Check your normal request rates in the access log first, and start with generous values.
- In case your own IP gets blocked, make sure you know how to get in through the server console (for example, your hosting provider's control panel).
Summary
- Use the access log to see how many IPs the attack comes from and which URLs it targets
- Cloudflare's HTTP DDoS protection is always on, even on the Free plan. Use Under Attack mode only during an attack, and spend the Free plan's one rate limiting rule on
/wp-login.php - Behind Cloudflare, configure the web server to trust
CF-Connecting-IPonly on connections from Cloudflare's IPs - In Nginx, limit the login page with
limit_reqand denyxmlrpc.phpif you don't use it. In Apache 2.4, useRequire ip - Use Fail2ban on servers that are not behind Cloudflare, and test the filter with
fail2ban-regexbefore enabling it
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)