Using Cloudflare and Fail2ban with WordPress – Building a WordPress Blog Part 4

Now that you’ve probably followed my last few posts, you should now have a WordPress site that’s ready to be launched. However, the last thing that you need to happen to your freshly built website is some kid with a bit of skills and a Kali VM or another kid’s automated script to take it down. I’ve seen a surprising number of WordPress sites that don’t even have /wp-admin or /wp-login.php firewalled off, let alone how poorly the rest of the website is locked down. Unfortunately you can’t completely stop the master hacker teenagers, but I’ll to you how tell mitigate against some common WordPress attacks. I’m not a security expert, so the majority of what you’ll see in here is based off of my own findings and what I’ve heard online.

Prerequisites

There really isn’t much you need, but make sure that you have the following:

  • Full admin access to your WordPress site and full root access to your server. There’s a few steps that assume that you’ve already followed my previous posts. All steps assume that you’re running Debian, but most of them should be applicable to most popular distros.
  • If you’re using Cloudflare, then some knowledge of how to use it is required. Also, make sure that the Proxy status is set to Proxied for your DNS records.

Locking Down /wp-admin and /wp-login.php (And other endpoints)

Despite it being pretty trivial and there being various methods of doing so, it seems to be pretty common for people to leave /wp-admin and /wp-login.php open to the internet. Not only does this give someone the ability to brute force your login, but it’s also another way for a potential zero-day to be exploited. I’ll be showing two ways of locking down /wp-admin and /wp-login.php to a specific IP address, but there’s many different methods. Regardless of what method you use, you’ll want to whitelist /wp-admin/admin-ajax.php since it’s used for WordPress’s AJAX API, which might cause some issues if it’s blocked.

There’s also a few different endpoints that you might want to block depending on what you’re using your website for.

The Cloudflare Way

This is the method that I personally use, and it’s also easier than the Nginx way if you’re using Cloudflare. Simply navigate to Cloudflare, click on your website, Security, WAF, and then finally click Create firewall rule. Your rule should look something like this:

You can also click the blue Edit expression link, copy and paste everything in the below code block, and replace the IP addresses with yours:

(http.request.uri contains "wp-admin" and ip.src ne 12.34.56.78 and not http.request.uri contains "/wp-admin/admin-ajax.php") or (http.request.uri contains "wp-login.php" and ip.src ne 12.34.56.78 and not http.request.uri contains "/wp-admin/admin-ajax.php")

Since Cloudflare firewall rules seem to take a bit to update, I would recommend waiting a minute after clicking Save before testing it. To test that your firewall rule works, navigate to /wp-admin and /wp-login.php. Once you’ve verified that you can still access it, do the same thing on a device with a different IP address. If everything is correct, you’ll see a screen that looks something like this:

The Nginx Way

Even though using Nginx is a little bit harder, you don’t need to be using Cloudflare for this step to apply to you. Locate your Nginx server block for your WordPress site (If you’ve followed my previous posts, then it should be in /etc/nginx/conf.d/wordpress.conf), then add the following lines above the very last } (Replace the IP addresses with yours):

  # wp-admin access
  location /wp-admin/admin-ajax.php {
      allow all;
  } 
  location /wp-admin {
      allow 12.34.56.78;
      deny all;
  }
  location = /wp-login.php {
      allow 12.34.56.78;
      deny all;
  }

Test your server block to ensure that it’s correct:

sudo nginx -t

And finally, restart Nginx:

sudo systemctl restart nginx

Like with the Cloudflare method, you’ll want to verify that /wp-admin and /wp-login.php is still accessible for only you, but blocked for everyone else. Try to navigate to /wp-admin and /wp-login.php, and you should still be able to access it. If you can still access it, try to navigate to /wp-admin and /wp-login.php with a device that has a different IP address. That device should be presented with a screen that looks like this:

Unable to Block /wp-login.php

If you were (un)lucky like me, then you’ll quickly notice that instead of your whitelisted IP address getting brought to a WordPress login screen, this will happen instead:

What appears to be happening is that Nginx is unable to execute the PHP code for whatever reason, so it’s just downloading it instead (Not sure if I described the issue correctly, please let me know in the comments if I was wrong).

The first thing that I tried was to remove the = sign since I noticed that removing it gave me different results when troubleshooting a different problem:

location /wp-login.php {
      allow 12.34.56.78;
      deny all;
  }

The good thing was that my login screen was working again, but the bad thing was that it wasn’t blocking anything.

I also gave up on trying to navigate Nginx’s documentation, but after searching online and trying many different things, I found this random article that actually worked. Armed with the knowledge from that random article, I looked for the following code in my server block:

 }
  location ~ \.php$ {
    fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
    include snippets/fastcgi-php.conf;

    # Add headers to serve security related headers
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Permitted-Cross-Domain-Policies none;
    add_header X-Frame-Options "SAMEORIGIN";
  }

And made it look like this:

 }
  location ~ \.php$ {
    location ~ \wp-login.php$ {
        allow 12.34.56.78;
        deny all;
        fastcgi_pass unix:/run/php/php7.4-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        include snippets/fastcgi-php.conf;
    }
    fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
    include snippets/fastcgi-php.conf;

    # Add headers to serve security related headers
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Permitted-Cross-Domain-Policies none;
    add_header X-Frame-Options "SAMEORIGIN";
  }

And to my surprise and relief, that actually worked. I don’t know how it works, so please don’t ask me to explain it. In the unlikely chance that you came here from a search engine looking to solve the same seemingly strange and obscure problem and I helped you, then you’re welcome internet stranger ;). If I didn’t, well I guess good luck on your journey.

Blocking xmlrpc.php

xmlrpc.php is used for an old API used to interact with WordPress. Since many API’s and features have taken its place and it’s commonly used for exploits, it’s best to disable it. You can either use a plugin, or firewall it off.

Cloudflare

To block this endpoint using Cloudflare, create a new firewall rule that looks something like this:

Like earlier, you’ll want to wait a minute to let the firewall rule update after clicking Save. After you’ve waited, try to navigate to /xmlrpc.php, and you should see an Access Denied screen like I showed you earlier.

Nginx

Like with blocking /wp-admin and /wp-login.php, all you need to do is locate your Nginx server block, and add the following lines:

# Block xmlrpc.php
  location = /xmlrpc.php {
      deny all;
  }

Test the server block:

sudo nginx -t

Then restart Nginx:

sudo systemctl restart nginx

Try to navigate to /xmlrpc.php, and you should get a 403 Forbidden screen like I showed you earlier.

Other Endpoints to Block

There’s a few other endpoints that I would recommend blocking. However, blocking certain ones might cause WordPress to break or not work properly. I’ll tell you what ones might cause WordPress to break by blocking them.

  • /feed: This is a list of all of your posts in RSS format. It’s useful if you’re making a blog because users can easily add it to an RSS feed reader. It also reveals some information about your website, so if you don’t need it, then disable it.
  • /wp-json: This appears to be some sort of WordPress JSON API. From what I can tell by looking at logs, it can reveal quite a bit of information about your website. Since there’s legitimate uses for it, I whitelist it based on IP address like with /wp-admin and /wp-login.php. However, even though I haven’t had any problems, it doesn’t mean that doing what I did won’t cause problems for you. Experiment with firewall rules before doing this on a production website and come to your own conclusions.

Besides blocking endpoints, you should also disable features that you don’t need, like comments for example. Usually you’ll be able to find a plugin that disables what you don’t need.

Correcting IP Addresses in Logs when Behind Cloudflare

If you’re using Cloudflare, you’ve probably discovered that the source IP addresses in your Nginx logs are Cloudflare’s IP address, not your visitors IP address. This can be a problem when running security measures on your server like Fail2ban since they won’t be able to get the actual IP address. Luckily, this is an easy problem to fix.

Open /etc/nginx/nginx.conf in a text editor, and add the following lines below line 37. This will create a new log file called wordpress.log, which will be identical to the default access.log, but it will include your visitors actual IP address:

log_format  wordpress  '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" '
                '"$http_user_agent" "$http_x_forwarded_for" "$http_cf_connecting_ip"';
    access_log /var/log/nginx/wordpress.log wordpress;

Next, add a new line under line 66, and paste the following lines below. These are the lines that will get the actual IP address from Cloudflare (By the time you’re reading this, Cloudflare might have changed their IP address slightly, so check here):

# Get actual IP address from Cloudflare
    set_real_ip_from 103.21.244.0/22;
    set_real_ip_from 103.22.200.0/22;
    set_real_ip_from 103.31.4.0/22;
    set_real_ip_from 104.16.0.0/12;
    set_real_ip_from 104.24.0.0/14;
    set_real_ip_from 108.162.192.0/18;
    set_real_ip_from 131.0.72.0/22;
    set_real_ip_from 141.101.64.0/18;
    set_real_ip_from 162.158.0.0/15;
    set_real_ip_from 172.64.0.0/13;
    set_real_ip_from 173.245.48.0/20;
    set_real_ip_from 188.114.96.0/20;
    set_real_ip_from 190.93.240.0/20;
    set_real_ip_from 197.234.240.0/22;
    set_real_ip_from 198.41.128.0/17;
    set_real_ip_from 2400:cb00::/32;
    set_real_ip_from 2606:4700::/32;
    set_real_ip_from 2803:f800::/32;
    set_real_ip_from 2405:b500::/32;
    set_real_ip_from 2405:8100::/32;
    set_real_ip_from 2c0f:f248::/32;
    set_real_ip_from 2a06:98c0::/29;
    set_real_ip_from 2c0f:f248::/32;
    real_ip_header X-Forwarded-For;

When you’re done, make sure to test the configuration file and restart Nginx. After that, you should start seeing your visitors IP address in wordpress.log.

Sucuri Security

Sucuri Security is a WordPress security plugin that can do stuff like audit logins, scan your website for malicious code, and many other things. I’ll be showing you how to configure some of the features. I recommend that you look through all of the settings yourself and configure everything to your specific needs.

Enable The WordPress Integrity Diff Utility

This feature will show the differences between WordPress files stored on your web server and ones provided by WordPress. After you install the plugin, select Sucuri Security, then Settings:

Select the Scanner tab:

And finally, scroll down to WordPress Integrity Diff Utility, and click Enable:

Apply Hardening Options

There’s a few options under here that will make your WordPress site a little bit more secure by doing stuff like removing unnecessary files. Assuming that you’re still at the same screen as the previous step, click the Hardening tab:

Then under hardening options, select the options that you see fit (I still want the theme and plugin editor):

If you want to change how often the secret keys are updated (Determines how often you’ll need to log in again), select the Post-Hack tab:

Then from the dropdown, choose an option that you see fit:

Enable Email Alerts

To enable email alerts, select the Alerts tab:

Enter an email address, then click submit (Remove [email protected] first):

Then finally, scroll through all of the options on that page and configure them as you see fit.

Fail2ban

Fail2ban is a neat little program that runs on your server that will check your logs, and if a certain IP address keeps matching a specific configured pattern within a certain amount of time, then it will block it. This only scratches the surface of what it can do. I’ll be showing you how to use Fail2ban to block IP addresses that are causing too many 4xx errors. There’s many automated scripts scanning websites on the internet, and since they’re trying to probe your website for vulnerabilities, naturally they’ll cause a lot of 4xx and other similar errors.

Installation

First, install Fail2ban, and select yes on all of the prompts:

sudo apt install fail2ban iptables iptables-persistent

Start Fail2ban:

sudo systemctl start fail2ban

And verify it’s running by using one of the two commands:

sudo systemctl status fail2ban

Or:

sudo fail2ban-client status

Once you’ve verified that Fail2ban is running correctly, allow it to run at startup:

sudo systemctl enable fail2ban

After you install Fail2ban, open /etc/fail2ban/fail2ban.conf in a text editor, and look for line 69 (Nice). Chances are you won’t want it to wipe the database after a single day, so change it to something longer like 99 years (Or shorter if you want it to wipe it at some point):

dbpurgeage = 99y

Save the file, then restart Fail2ban:

sudo systemctl restart fail2ban

Jail Configuration

By default, Fail2ban doesn’t have a filter for matching Nginx 4xx errors, so we’ll have to create one ourselves. Create a new file in /etc/fail2ban/filter.d/, and name it nginx-4xx.conf, then add the following lines (Regex I used, I slightly modified it):

[Definition]
failregex = ^<HOST>.*"(GET|POST).*" (401|403|404|444) .*$
ignoreregex = 

Next, open /etc/fail2ban/jail.conf in a text editor, and go to line 47. Uncomment bantime.rndtime, and add a value that you see fit:

# "bantime.rndtime" is the max number of seconds using for mixing with random time 
# to prevent "clever" botnets calculate exact time IP can be unbanned again:
bantime.rndtime = 24h

Paste the following code block starting at line 274. Change the highlighted values as you see fit, and also experiment with these settings first so you don’t ban legitimate users. If you you didn’t follow my steps for correcting IP addresses when behind Cloudflare, logpath should be /var/log/nginx/access.log:

[nginx-4xx]
enabled = true
port = http,https
logpath = /var/log/nginx/wordpress.log
maxretry = 5
findtime = 10m
bantime = 30m
action = iptables-allports

After you’ve saved the file and restarted Fail2ban, test it out. It might not seem like that it’s working at first since Fail2ban checks the log every few seconds (At least from what I can tell, not sure how often it actually gets checked). If you’re using Cloudflare, skip to the Cloudflare section since there’s additional steps that you need to do.

After you’ve verified that your IP was banned, unban it:

sudo fail2ban-client unban 12.34.56.78

And so you don’t accidentally ban your own IP like I’ve down multiple times, look for line 92 in /etc/fail2ban/jail.conf, uncomment it, and add your IP address:

ignoreip = 127.0.0.1/8 ::1 12.34.56.78

Cloudflare

If you tried to have Fail2ban ban your IP address, you’ll probably notice that your IP address wasn’t banned (Or in my case, IPv6 IP’s weren’t getting banned, but IPv4 IP’s were) despite sudo iptables -L or sudo ip6tables -L showing your IP address getting blocked. Luckily the solution is easy.

First, log into your Cloudflare dashboard, click the person icon in the top right corner, then click My Profile:

Select API Tokens from the sidebar:

Scroll down to API Keys, then click the View button by Global API Key:

After you’ve entered your password, copy this API key and save it somewhere secure. You’ll need it later:

It’s a fake API key, don’t bother trying it

Go back to your server, open /etc/fail2ban/action.d/cloudflare.conf, and scroll down to line 81. You’ll paste your API key next to cftoken, and enter the email address associated with your Cloudflare account next to cfuser. It should look something like the example below:

cftoken = 4655434b594f554e49434b554b4e4f5757484f55415245

cfuser = [email protected]

Open up jail.conf again, scroll down to where you’ve added the jail, and add cloudflare to the action line:

action = iptables-allports cloudflare

Before testing, just be aware that banning an IP through Cloudflare might take a little bit. This is because not only do you have to wait for Fail2ban to check your Nginx logs, but you also have to wait for Cloudflare to ban the IP as well, which seem to take at least 10 seconds.

Once your IP address has been banned, you can view the ban in the WAF settings under the Tools tab and IP Access Rules, and it should look something like this:

You can unban your IP by either clicking the X button, or use the fail2ban-client command I showed you earlier. Don’t forget to add your IP address under ignoreip in jail.conf so you don’t ban yourself by accident.

Making the Fail2ban Service Apart of the Nginx Service

If the Fail2ban service happens to crash, then you’ll loose the benefits of Fail2ban. However, if you wanted to, you can make it so if Fail2ban crashes, then it takes Nginx down with it. This also means that your website will be down until Fail2ban and Nginx is restarted, so only do this if you don’t mind potentially loosing uptime.

All you need to do is edit /lib/systemd/system/nginx.service, and add the highlighted line:

[Unit]
Description=A high performance web server and a reverse proxy server
Documentation=man:nginx(8)
BindsTo=fail2ban.service
After=network.target nss-lookup.target

Run the following command to make systemd pick up the changes you made:

sudo systemctl daemon-reload

Then to test this, kill Fail2ban:

sudo pkill fail2ban

Check the status of Fail2ban to ensure that it’s not running:

sudo systemctl status fail2ban

Then check to see if Nginx is also stopped:

sudo systemctl status fail2ban

Both services should be stopped, and you should be able to restart both services.

Cloudflare Settings

If you’re using Cloudflare, there’s some settings that you might want to look at or change. I’ll be showing what settings I personally use on both of my websites.

SSL/TLS Settings

To access these settings, simply click the SSL/TLS button after clicking your website when you log in:

Under Overview, there really isn’t a whole lot to change. I recommend selecting Full (Strict) for the SSL/TLS mode. If you can’t choose Full (Strict), you can also enable the SSL/TLS Recommender if you want Cloudflare to check if your website can use a more secure SSL/TLS mode.

Next, select Edge Certificates in the sidebar:

I have all of the options under here enabled, and I keep the Minimum TLS Version at the default. I also have HSTS enabled, but make sure to read the warning first before enabling it.

Security Settings

Accessing these settings as just as easy as accessing the SSL/TLS settings. All you need to do is just click the Security then WAF button:

Create a rate limiting rule to help mitigate against DDoS attacks. If you’re using a crappy server like me, then this is especially important. Simply click on the Rate limiting rules tab, then Create rule:

If you want it to match everything, then set it to if URI path contains /. Then for the rest of the settings, choose them as you see fit. I originally had it set to 100 requests within 10 seconds, but I had problems with it being too strict.

After you’ve configured rate limiting, select Settings in the sidebar:

I have everything turned on here. I have Security Level set to High, and I have Challenge Passage set to 1 day. The default security level isn’t strict enough, and if a visitor had completed a Cloudflare challenge, then they won’t have to do it again for 1 day.

Caching Settings

To access these settings, simply click Caching in the sidebar, then Configuration:

I only changed three settings in this menu. I have both Always Online and Crawler hints enabled, and I have Browser Cache TTL set to 1 month. Always Online is an especially neat feature because if your server goes down, then it will display a copy from the Wayback Machine instead. If you’re not changing stuff all that often on your website, then a longer browser cache TTL can help speed up your website and reduce bandwidth for repeat visitors.

Click Tiered Cache in the sidebar, and then enable Argo Tiered Cache. According to Cloudflare, this does some fancy magic routing to help improve performance and reduce bandwidth on your server.

Page Rules

Since some of Cloudflare’s features like caching can cause issues on /wp-admin, it’s best to disable them. This is easily possible thanks to Page Rules. From the sidebar, select Rules:

Click Create Page Rule to create a new rule:

Your page rule should look something like this, and replace example.com with your domain name:

After clicking Save Page Rule, this will disable Cloudflare’s features just on /wp-admin while leaving them enabled for everywhere else on your website.

Conclusion

I hope you enjoyed reading this post and this series in general, and maybe learned something new in the process. I definitely learned a lot about writing blog posts, which is why I decided to include a dedicated heading section. If you’ve read through all of my posts in their entirety, I bet that there’s probably some minor differences that I haven’t even noticed. If you’re getting tired of this series, don’t worry, since I have plans to write actually interesting stuff. Hell even I’m getting tired of writing posts for a series that people probably don’t want to read, so I’m glad that I stuck to writing only four parts.

Like usual, if you have a suggestion, problem, or complaint, then leave a comment, and I’ll try my best to address it.

References

A lot of my information is from looking at configuration files since I’ve forgotten how I did some stuff and what tutorials I followed.

These random tutorials did a better job of telling me how to block paths in Nginx than Nginx’s own documentation: https://www.linuxshelltips.com/block-access-wp-admin-and-wp-login/, https://www.linuxshelltips.com/block-xml-rpc-in-wordpress/

Using Fail2ban with Cloudflare: https://gridpane.com/kb/using-fail2ban-with-cloudflare/

Subscribe
Notify of

0 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments