I have helped many users speed up their sites by implementing server-side security to prevent XMLRPC and wp-login.php attacks. CloudFlare have blogged about XMLRPC.php as an attack vector. Basically, hacker bots scan for WordPress or WooCommerce sites and will try a senseless amount of password attempts over and over again. This can lead to your system being overloaded dealing with these (poorly automated) hacking attempts, slowing things down and even crashing your server (504 gateway timeout or 500 internal errors).
Although many security plugins like iThemes Security will help protect against these attacks, they still require some PHP processing so the PHP handler can still get pegged, locking up your system and slowing things down.
This tutorial will show you how to block these hacker bots server-side using iptables set by fail2ban when it finds offenders from scanning your nginx error logs. You should have root access to your VPS or dedicated server to complete this guide on Ubuntu or Debian.
Note this is only meant to mitigate against DDoS and DoS attacks on wp-login and XMLRPC. If the DDoS is powerful enough your server is likely to go down unless you have adequate protection from CloudFlare, Sucuri or some other provider.
Configure WordPress wp-login + XMLRPC DDoS Protection nginx + fail2ban
Installation overview
- Modify nginx to rate limit xmlrpc.php and wp-login
- Test DDoS protect
- Configure fail2ban filter and jail
Modify nginx to Rate Limit
Open your nginx configuration
sudo nano /etc/nginx/nginx.conf
Add the limit_req_zone
line in the http block
The zone size of 10m will store one hundred thousand addresses or so (each one takes up 64 bytes (source))
In the limit_req_zone
line we are limiting users to 1 request per second. You can consider changing this to per IP but it could cause issues for users behind NAT.
http {
#include /etc/nginx/realip.conf;
##
# Basic Settings
##
client_body_buffer_size 10K;
client_header_buffer_size 1k;
client_max_body_size 8m;
large_client_header_buffers 4 16k;
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
#reset_timedout_connection on;
fastcgi_read_timeout 60;
#DoS protection for wp-login.php, search and xml-rpc.php
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
Ctrl+X, Y and Enter to Save
Open your WordPress nginx virtual host file
sudo nano /etc/nginx/sites-available/wordpress
Paste the green section
into your existing nginx virtual host in the server block to get DoS protection for wp-login.php and xmlrpc requests.
You will need the error_log
file to test fail2ban later in the guide.
If you are using PHP7 then change your unix socket path to /run/php/php7.0-fpm.sock;
Using limit_req_status 444
we are dropping the request and not sending any response back to the user
The red items will be your domain name.
server {
server_name example.com www.example.com;
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
root /var/www/example.com/;
index index.php;
#captures wp-login and xmlrpc requests
location ~ (wp-login|xmlrpc)\.php {
limit_req zone=one burst=1 nodelay;
include fastcgi_params;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
limit_req_status 444;
}
set $skip_cache 0;
# POST requests and urls with a query string should always go to PHP
if ($request_method = POST) {
set $skip_cache 1;
}
if ($query_string != "") {
set $skip_cache 1;
}
# Don't cache uris containing the following segments
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
set $skip_cache 1;
}
# Don't use the cache for logged in users or recent commenters
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
set $skip_cache 1;
}
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(.*)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache WORDPRESS;
fastcgi_cache_valid 404 1m;
fastcgi_cache_valid 60m;
}
location ~ /purge(/.*) {
fastcgi_cache_purge WORDPRESS "$scheme$request_method$host$1";
}
location ~* ^.+\.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
access_log off; log_not_found off; expires max;
}
location = /robots.txt { access_log off; log_not_found off; }
location ~ /\.|wp-config\.php { deny all; access_log off; log_not_found off; }
}
Ctrl+X, Y and Enter to Save
Verify your virtual host configuration syntax is OK and then restart nginx
sudo nginx -t
sudo service nginx restart
Testing the nginx DDos Protection
It is time to test the nginx DDoS protection. We can use Apache benchmark utility to generate a lot of requests to a URL.
sudo apt-get install apache2-utils -y
We are going to test the web server first (Apache or nginx on port 8080) by simulating 100 requests with 1 concurrent connection
ab -n 100 -c 1 wp-bullet.com/wp-login.php
You will get a report from Apache benchmark.
Notice the number of failed requests 98
.
This is ApacheBench, Version 2.3 <$Revision: 1604373 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, https://www.zeustech.net/
Licensed to The Apache Software Foundation, https://www.apache.org/
Benchmarking wp-bullet.com (be patient).....done
Server Software: nginx/1.8.1
Server Hostname: wp-bullet.com
Server Port: 80
Document Path: /wp-login.php
Document Length: 2617 bytes
Concurrency Level: 1
Time taken for tests: 0.020 seconds
Complete requests: 100
Failed requests: 98
(Connect: 0, Receive: 0, Length: 98, Exceptions: 0)
Total transferred: 5950 bytes
HTML transferred: 5234 bytes
Requests per second: 4991.76 [#/sec] (mean)
Time per request: 0.200 [ms] (mean)
Time per request: 0.200 [ms] (mean, across all concurrent requests)
Transfer rate: 290.05 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 0 0 1.2 0 8
Waiting: 0 0 0.9 0 8
Total: 0 0 1.2 0 9
Percentage of the requests served within a certain time (ms)
50% 0
66% 0
75% 0
80% 0
90% 0
95% 0
98% 8
99% 9
100% 9 (longest request)
Now let’s test the xmlrpc.php protection
ab -n 100 -c 1 https://wp-bullet.com/xmlrpc.php
Again the majority of the attempts fail
This is ApacheBench, Version 2.3 <$Revision: 1604373 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, https://www.zeustech.net/
Licensed to The Apache Software Foundation, https://www.apache.org/
Benchmarking wp-bullet.com (be patient).....done
Server Software: nginx/1.8.1
Server Hostname: wp-bullet.com
Server Port: 80
Document Path: /xmlrpc.php
Document Length: 42 bytes
Concurrency Level: 1
Time taken for tests: 0.033 seconds
Complete requests: 100
Failed requests: 98
(Connect: 0, Receive: 0, Length: 98, Exceptions: 0)
Non-2xx responses: 2
Total transferred: 414 bytes
HTML transferred: 84 bytes
Requests per second: 3015.77 [#/sec] (mean)
Time per request: 0.332 [ms] (mean)
Time per request: 0.332 [ms] (mean, across all concurrent requests)
Transfer rate: 12.19 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 0 0 1.4 0 12
Waiting: 0 0 1.1 0 8
Total: 0 0 1.4 0 12
Percentage of the requests served within a certain time (ms)
50% 0
66% 0
75% 0
80% 0
90% 0
95% 0
98% 9
99% 12
100% 12 (longest request)
Configure fail2ban to autoban XMLRPC and wp-login Attacks
fail2ban uses filters to detect violations and jails to ban users who match the filter.
Install fail2ban
sudo apt-get update
sudo apt-get install fail2ban -y
Create fail2ban Filter for nginx XMLRPC and wp-login.php
Create the nginx filter, if you are on the latest fail2ban you may already have this file, in which case you can skip down to testing using the fail2ban-regex command.
sudo nano /etc/fail2ban/filter.d/nginx-limit-req.conf
Add this which is a regular expression match for the logs above adapted from here
[Definition]
failregex = ^ \[error\] \d+#\d+: .*limiting requests.*, client: <HOST>, server: \S+, request: "POST /xmlrpc.php.*$
^ \[error\] \d+#\d+: .*limiting requests.*, client: <HOST>, server: \S+, request: .*$
ignoreregex =
Ctrl+X, Y + Enter to Save and Exit.
Now we can test the nginx rate limiting fail2ban filter by scanning the error log specified in the nginx virtual host.
fail2ban-regex /var/log/nginx/wpbullet.error.log /etc/fail2ban/filter.d/nginx-limit-req.conf
You will see this output showing it found the login failures we generated before.
Running tests
=============
Use failregex file : /etc/fail2ban/filter.d/nginx-limit-req.conf
Use log file : log
Results
=======
Failregex: 2 total
|- #) [# of hits] regular expression
| 1) [2] ^ \[error\] \d+#\d+: .*limiting requests.*, client: , server: \S+, request: "POST /xmlrpc.php.*$
`-
Ignoreregex: 0 total
Date template hits:
|- [# of hits] date format
| [2] Year/Month/Day Hour:Minute:Second
`-
Lines: 2 lines, 0 ignored, 2 matched, 0 missed
Create fail2ban Jail for nginx Rate Limiting
Make sure the you have a fail2ban jail folder
sudo mkdir -p /etc/fail2ban/jail.d
Create the fail2ban nginx http auth jail configuration file
sudo nano /etc/fail2ban/jail.d/nginx-limit-req.conf
Paste thie configuration which uses the filter we created before, scans all nginx log files and bans users for 600 minutes who fail 3 times in a 60 second period.
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
port = http,https
logpath = /var/log/nginx/*error*.log
findtime = 60
bantime = 6000
maxretry = 3
Now that we know have made the jail, test the fail2ban syntax so make sure it’s all working
sudo fail2ban-client -d
If you didn’t see any errors (warnings are OK) then we can restart fail2ban
service fail2ban restart
Checking the nginx HTTP Auth fail2ban Status
The fail2ban client can be used to show the statistics of its jails
sudo fail2ban-client status nginx-limit-req
During a local test I managed to get the gateway IP banned.
Status for the jail: nginx-limit-req
|- filter
| |- File list: /var/log/nginx/wp-bullet.error.log /var/log/nginx/error.log
| |- Currently failed: 0
| `- Total failed: 3
`- action
|- Currently banned: 1
| `- IP list: 192.168.60.1
`- Total banned: 1
You can also list the iptables
sudo iptables -L -n
This shows the iptables chain for limiting nginx HTTP Auth requests
Chain f2b-nginx-limit-req (2 references)
target prot opt source destination
REJECT all -- 192.168.0.1 0.0.0.0/0 reject-with icmp-port-unreachable
RETURN all -- 0.0.0.0/0 0.0.0.0/0
RETURN all -- 0.0.0.0/0 0.0.0.0/0
You will see lots of bots scanning which will quickly be banned. With this WordPress security solution for xmlrpc and wp-login I do not need any heavy PHP plugins like WordFence to lock users out.
Wow! This is GOLD! Bookmarked! Thank you! 🙂
Thanks for this. I’m using fail2ban to look at the nginx logs directly. Getting nginx rate limitation in to the mix makes a lot of sense.