Skip to main content

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.php in 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.php in 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.gz and 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 *.log files may contain potentially sensitive information. All the logs we need are in the private logs directory outside our document root, however certain configurations may still result in publicly accessible log files, such as wp-content/debug.log.
  • Anything that starts with a . such as .git directories, .htaccess files (from previous migrations perhaps), .env files, as well as swap files. Files that end with a ~ may also be backup or swap files. Some programs generate .bak files too.
  • phpinfo.php and info.php are 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.