Restrict sensitive URLs with Nginx
The public_html directory is where your WordPress site lives, along with all
its plugins, themes, configuration files, uploads and more. Many of these files
are meant to be accessed directly through the browser, like wp-login.php, or
the JavaScript library bundled in your theme.
Others, however, are meant to remain private, accessible only via application
code, for example wp-config.php, or not accessible via the web at all, such as
.git directories, backups and log files. In this lesson we'll update our Nginx
configuration to deny access to these sensitive files.
Blocking access with Nginx
There are several ways you can block access to a particular URL or pattern in
Nginx. You can use location blocks, which are quite efficient:
location = /wp-config.php {
deny all;
}
These support regular expressions too:
location ~* \.sql$ {
deny all;
}
There is one problem with location blocks however, and that is execution
order, which
also depends on location type and the length of the match. There is some insight
into how Nginx handles this in this Server Fault
question.
Given this complex behavior, it's very easy to make a mistake, so if you do use
location blocks, make sure you test each block, especially when it comes to
deny rules.
Alternatively, you can use an Nginx map with a simple if statement, which is
processed before any location blocks are evaluated. This is very similar to the
blacklist map we did in an earlier lesson with
Fail2Ban. We define a new map in our http
{} context:
map $uri $uri_blacklisted {
default 0;
/wp-config.php 1;
~\.sql$ 1;
}
Then use that map in our server {} context to return a 403 error code if the
URL was blocked.
if ($uri_blacklisted) {
return 403;
}
I personally prefer this map approach to the location blocks approach
because I don't have to worry about the order or length, and it's slightly
easier to maintain. A map is evaluated top-down and the first match wins, so you
have full control over the order.
I typically have a global map of blacklisted URLs for all sites on the server, as well as some additional individual per-site block maps.
What should be blocked
There is no one comprehensive list of things to block and it's worth revisiting your list at least a couple times per year, to make sure your blocks are still relevant.
I'll share the list of patterns I typically block. You can use this as a starting point to build your own list.
wp-config.phpin an unlikely scenario where a misconfiguration leads to PHP files being served as downloads, this file has plenty of secrets.wp-admin/install.php,wp-admin/setup-config.phpin an event where WordPress can't find its tables (wrong prefix, wrong db name, etc), we don't want strangers to be able to run the installer.*.sql,*.tar.gzand other archives and database files. Many popular backup plugins like to leave these around in a publicly accessible directory. Some poor admin habits leave backup.tar.gz and database.sql, sometimes with less "guessable" names.- Any
*.logfiles may contain potentially sensitive information. All the logs we need are in the privatelogsdirectory outside our document root, however certain configurations may still result in publicly accessible log files, such aswp-content/debug.log. - Anything that starts with a
.such as.gitdirectories,.htaccessfiles (from previous migrations perhaps),.envfiles, as well as swap files. Files that end with a~may also be backup or swap files. Some programs generate.bakfiles too. phpinfo.phpandinfo.phpare very common names for temporary quick checks that often last many years in a public directory.- Users should never be able to upload PHP files to the media library, but in
case somebody manages to, we can deny execution of
wp-content/uploads/*.php.
Here's what these look like in my $uri_blacklisted map in the http {}
context defined in my /config/nginx/nginx.conf file:
map $uri $uri_blacklisted {
default 0;
# Config and installer
/wp-config.php 1;
/wp-admin/install.php 1;
/wp-admin/setup-config.php 1;
# Various database and file backups
~\.sql$ 1;
~\.sql\.gz$ 1;
~\.tar$ 1;
~\.tar\.gz$ 1;
~\.zip 1;
~\.bak 1;
~\.db 1;
# Logs
~\.log 1;
~/error_log$ 1;
# Dot and swap files except ACME challenge
~^/\.well-known/acme-challenge/ 0; # allowed
~/\. 1;
~~$ 1;
~\.swo$ 1;
~\.swp$ 1;
# phpinfo
~/phpinfo\.php$ 1;
~/info\.php$ 1;
}
It's also a good idea to test every entry in the list, to make sure it blocks what you're expecting it to. Also note that Cloudflare might already be blocking some of these or similar patterns at the edge.
Fail2ban
This is entirely optional but it's a good hardening practice. If somebody is hitting these restricted URL patterns, then it's a good idea to ban their IP address for a few hours. As you may have guessed, this can easily be done with Fail2ban.
This article is for premium members only. One-time payment of $96 unlocks lifetime access to all existing and future content on wpshell.com, and many other perks.