Update September 15, 2020: Hi! If you reached this page chances are that you are a blogger :) I would like to introduce DynaBlogger, a new blogging platform I just launched as a simple alternative to Wordpress and Ghost for people who find these platforms overkill for their needs. This very website is now hosted on DynaBlogger! Trying it is easy: there is a free 14 days trial - no credit card required - and with the coupon "EARLYBIRD" there's a 50% discount for the first three months. Also, if you choose a yearly plan 2 months are free. Check it out! :)

If you also have a blog powered by WordPress, and care at least a bit about keeping it secure, you may have thought about preventing others from accessing its administration pages; in this post I will show a simple way to restrict access to wp-admin by IP address using Nginx.

The reason, of course, is that about every WordPress user is aware of the default location of both its administration area (the famous wp-admin folder), and of the login page, therefore it is likely that sooner or later someone will try to gain unauthorised access to your blog, in a way or another, and have some fun. WordPress, historically, has never been too solid from a security point of view, plus -out of the box- it does not lock down a user or IP address after a reasonable number of failed login attempts, as it should; so unless you have a very strong password, it may not take too long for a malicious, patient visitor to figure out your password (especially since Amazon announced the availability of reasonably priced Cluster GPU instances on their EC2 platform…)

So, unless you really have to let your visitors be able to register and login on your blog, it is advisable that you also keep anyone else away from the login page altogether. This may be possible or not depending on the reason why your site needs your users to login locally to perform some actions, but -luckily- there are almost always workarounds. In most cases, in fact, this has only to do with enabling your users to leave comments on the site. If this is true for you, too, I recommend you think about “outsourcing” the management of your comments to third-party services, the most popular ones being IntenseDebate (by WordPress.com parent company Automattic) and, in particular, Disqus – which also happens to be my favourite one these days, and that therefore I recommend.

(Note: if just the though of having your blog’s comments stored elsewhere, by someone else, makes you shiver, trust me: there is nothing to worry about, but only to gain. First, most of these services are free -including the aforementioned two most popular ones- so it won’t cost you a penny. Second, they integrate pretty well with WordPress in such a way that your comments are actually synchronised between your blog and the remote service, so you always have a local copy of all your blog’s comments. Third, by outsourcing your comments you will be able to relieve the load on your own servers quite a bit, and improve the cacheability of your site: if your blog caches the dynamic pages to static HTML files that can be served more quickly, normally -depending on the caching solution you use- each time a user leaves a comment the cached copy of the same page must be rebuilt as the changes required to display the new comment invalidate the cache; by using Disqus -or similar- instead, a remote JavaScript call to the third-party servers fetches the comments to render on the page, therefore the durability of your cached pages is significantly higher. Fourth, since these services already integrate anti spam solutions, you may also disable Akismet and other plugins on your blog, and you know that the less plugins you install, the lighter and faster your WordPress blog will be. Fifth, these services just work better than WordPress’ built in comments, have interesting social features, and even help improve the SEO performance of your site in a number of ways. What more could you ask for?)

Back in topic, assuming you have by now removed any need for your users to login locally on your site, there are several ways of protecting your site’s login page and and administration area. To begin with, you could customise your WordPress copy and change the location and names of the files and folders involved in the login and administration, or change altogether the way WordPress handles authentication and authorisation on your site. But this could be pretty time consuming, and would make it a lot more difficult to upgrade your WordPress copy each time a new version is released.

A lot easier and quicker is to use plugins which add this sort of functionality to WordPress, without your having to fiddle with the core files, and without compromising upgrades. But as said, the more plugins you install, the slower your blog will be. Also, many of the plugins available at the moment to secure your blog, either don’t work as expected with the latest release of WordPress, or do not work at all. Among the few ones that seem to work, I have seen some that operate by basically hiding in a way or another the wp-admin folder as well as the login page. Unfortunately, while these seem to work, it is often possible to break their functionality and bypass the protection they offer.

My advice is not to waste too much time with these plugins, either. If you want a way of securing the administration of your blog that is simple, effective, and doesn’t slow down your blog or require plugins or any heavy customisation, my recommendation is that you simply restrict access to the reserved area(s) by IP address at web server level. This just works, and because the restriction happens without even involving WordPress at all, it’s very efficient in terms of resources as well, and will perform better in case of brute force attack that floods your site with login attemps perhaps leading to a sort of DoS. With the tecnique I suggest here, in fact, visitors trying to access reserved folders and pages from unauthorised IP addresses are instead shown a 404 page that can also be cached statically as well as the rest of the site. Therefore, with this sort of configuration a light and fast web server such as Nginx (which I strongly recommend over heavier ones like Apache) is already able to serve thousands of requests per second with significant levels of concurrency, even on a cheap VPS. In case of smaller DoS or brute force attempts, this configuration may help.

Restricting access to an application by IP is easy with any web server; here is an example with Nginx, but it shouldn’t take long to adapt these rules to another web server (make sure you add the following lines within the server section of your virtual host configuration, and not within a location block):

server { ... set $noadmin 1; if ($remote_addr = "xxx.xxx.xxx.xxx") { set $noadmin 0; } if ($remote_addr = "yyy.yyy.yyy.yyy") { set $noadmin 0; } if ($noadmin = 1) { rewrite ^/wp-admin/(.*)$ /index.php?q=$1 last; rewrite ^/wp-([^/]*?).php(.*)$ /index.php?q=$1 last; } ... }

First, we set a flag (a custom variable in nginx) to true, meaning we’ll restrict access to the admin pages by default. Then, for each of the IP addresses we want to grant access to, we set that flag to false (nginx doesn’t support nested or combined conditions AFAIK, therefore I am using multiple lines here). Lastly, if the flag remains set to true -meaning the request comes from an unauthorised IP address- nginx rewrites the routes for the reserved folders and pages (you can customise these as you wish) in such a way that while the URL in the browser won’t change, WordPress’ 404 page is returned instead, as if those folders or pages did not exist. We do this by simulating a WordPress search, using the name of the folder or page requested as query. In the second rewrite rule, we basically block any PHP page starting with wp (thus also wp-login.php), for improved security: all the WordPress PHP files matching this naming convention, in fact, are not supposed to be requested directly from a browser.

Update: reader Dave Ross pointed out in the comments that admin-ajax.php should be excluded by the rules above since it is required by some plugins for AJAX functionality – Thanks Dave, I usually avoid anyway using AJAX based plugins unless I am totally sure they do not compromise in any way the security of a site, but yours was actually a very good point.

It’s easy to exclude admin-ajax.php by adding a negative lookahead to the regular expression in the first rule:

if ($noadmin = 1) { rewrite ^/wp-admin/((?!admin-ajax\.php).*)$ /index.php?q=$1 last; rewrite ^/wp-([^/]*?).php(.*)$ /index.php?q=$1 last; }

This way the only requests that will be allowed within /wp-admin regardless of the IP address are the requests made for that particular file.

I like this trick because it’s simple, light on WordPress and.. it just works. At this point, though, you’ve surely realised that this means you will only be able to access the administration of your blog (thus to write posts) while connected from IP addresses you know. So what about Internet cafes, a workplace, and so on? My advice here is to overcome this limitation by using a VPN or, even easier, an SSH tunnel and route all the traffic through it. First, all your traffic will be encrypted -which is something you should always care about when you connect from other Internet connections than yours- and therefore even an SSL certificate is no longer strictly required to access the reserved areas of your site; second, because you will be routing all your Internet traffic through your VPN/SSH gateway, the IP address your site will see is the IP address of your gateway, and not that of whichever Internet connection you are using at the moment. And you do know the IP address of your gateway, therefore you can configure the rules shown above to authorise that IP address.

I usually set the rules above to allow my home IP address as well as those of my servers and, by default, I always route all the traffic through one of these servers with an SSH tunnel when I am not at home. To do this easily, if you are on Mac I recommend the combination of the utilities NetworkLocation and Meerkat, so you won’t have to manually change SSH tunnel depending on where you are.