Configure WordPress wp-login + XMLRPC DDoS Protection nginx + fail2ban

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 CloudFlareSucuri 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.

Sources

Limit nginx requests based on URL