Fail2Ban, the Swiss army knife of quick rules to block aggressive intruders. As part of a layered defense, fail2ban is quick and easy to deploy to stop aggressive attackers from compromising your site with automated and brute force approaches.
In this blog we are going to give you a quick recipe for making a defense on WordPress sites you can easily deploy whether you are behind a load balancer or directly connected to the internet.
In our setup, we have a mix of deployment strategies. However, the stack on the machine is generally always the same. This is Varnish → NGINX → PHP-FPM.
For staging and testing websites, we have a separate server that has HAProxy sitting infront and the server is NAT’d. This allows us to spawn new servers and move them around quite quickly for testing needs. We also do this for production websites that require HA.
So when writing a Fail2Ban rule, we need to
- Block the real IP of the attacker
- Not block ourselves (obviously, but we’ve all done it)
- Make sure they are blocked even when we are NAT’d
The following is a quick way to see if a ban will work at IPtables or you need our NGINX rules.
sudo ss -tnp | grep ':443' | head -n 20
FIN-WAIT-2 0 0 <machine>:53758 <external-ip>:443
If the above is only your gateway, then your IPTables rule likely won’t help. We do it anyway though. If you are getting a collection of public IP’s in here, then you are directly connected to the internet and IPTables is the best choice.
sudo apt install fail2ban
As easy as that.
This is our custom action for banning in an NGINX
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = echo "deny <ip>;" >> /etc/nginx/fail2ban/wordpress.conf && nginx -t && nginx -s reload
actionunban = sed -i '\#deny <ip>;#d' /etc/nginx/fail2ban/wordpress.conf && nginx -t && nginx -s reload
It requires the following in your /etc/nginx/nginx.conf inside the http { block.
...
http {
include /etc/nginx/fail2ban/*.conf;
...
This will be where Fail2Ban adds/removes rules.
You will need to make sure the fail2ban folder exists.
Once done you can reload with
nginx -s reload
Fail2Ban Filters
Filters are the rules that tell Fail2Ban how to detect a threat from a log file or some other form.
Our filters use the GELF format, but you can adapt them to standard NGINX log rules like below. We also need the real IP address, so we use the following rules to get a JSON log and set the real IP recursively. You should only do ones that are applicable to your network!
set_real_ip_from 127.0.0.1;
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
log_format gelf_json escape=json '{ "timestamp": "$time_iso8601", '
'"real_ip":"$real_client_ip", '
'"remote_addr": "$remote_addr", '
'"connection": "$connection", '
'"connection_requests": $connection_requests, '
'"pipe": "$pipe", '
'"body_bytes_sent": $body_bytes_sent, '
'"request_length": $request_length, '
'"request_time": $request_time, '
'"response_status": $status, '
'"request": "$request", '
'"request_method": "$request_method", '
'"host": "$host", '
'"upstream_cache_status": "$upstream_cache_status", '
'"upstream_addr": "$upstream_addr", '
'"http_x_forwarded_for": "$http_x_forwarded_for", '
'"http_referrer": "$http_referer", '
'"http_user_agent": "$http_user_agent", '
'"http_version": "$server_protocol", '
'"remote_user": "$remote_user", '
'"http_x_forwarded_proto": "$http_x_forwarded_proto", '
'"upstream_response_time": "$upstream_response_time", '
'"nginx_access": true,'
'"environment": "transient" }';
/etc/fail2ban/filter.d/wordpress-xmlrpc.conf
Standard log
[Definition]
# Match POST requests to xmlrpc.php in standard nginx access logs
failregex =
^<HOST> .*"POST\s+/xmlrpc\.php\s+HTTP/.*"
ignoreregex =
or with GELF JSON
[Definition]
# Match POST requests to xmlrpc.php in nginx JSON logs
failregex =
^.*"remote_addr"\s*:\s*"<HOST>".*"request"\s*:\s*"POST\s+//xmlrpc\.php(?:\s|\\).*$
^.*"remote_addr"\s*:\s*"<HOST>".*"request"\s*:\s*"POST\s+/xmlrpc\.php(?:\s|\\).*$
ignoreregex =
/etc/fail2ban/filter.d/wordpress.conf
Standard log
[Definition]
# Match POST requests to wp-login.php in standard nginx access logs
failregex =
^<HOST> .*"POST\s+/wp-login\.php\s+HTTP/.*"
ignoreregex =
Or with GELF JSON
[Definition]
# Match POST requests to wp-login.php in nginx JSON logs
failregex = ^.*"remote_addr"\s*:\s*"<HOST>".*"request"\s*:\s*"POST\s+//wp-login\.php(?:\s|\\).*$
ignoreregex =
Fail2Ban Jail
This is where the actual banning happens.
You will need to adjust the log location depending how you log. We use independent site logs, but you may have one large one.
In these Jail’s we use both IPTables for banning and NGINX. Iptables is still useful for direct connections or future topology changes, but NGINX is the authoritative enforcement point when NAT’d.
[wordpress-xmlrpc-site]
enabled = true
filter = wordpress-xmlrpc
action = nginx-deny
iptables-multiport[name=Wordpress, port="http,https"]
logpath = /var/log/nginx/site_name.access.log
# XML-RPC abuse is usually brute-force or amplification
maxretry = 3
findtime = 300
bantime = 86400
backend = auto
# Never ban LB or localhost
ignoreip = 127.0.0.1/8 ::1 10.245.0.0/16
[wordpress-nginx-site]
enabled = true
filter = wordpress
action = nginx-deny
iptables-multiport[name=Wordpress, port="http,https"]
logpath = /var/log/nginx/site_name.access.log
maxretry = 15
findtime = 600
bantime = 3600
backend = auto
# Never ban LB or localhost
ignoreip = 127.0.0.1/8 ::1 10.245.0.0/16
Conlusion
Now you will see anyone brute forcing those endpoints banned from your site. Well done!