This tutorial goes over how to set up basic security rules for hosting a WordPress website behind an Nginx reverse proxy that also serves cached and static content.

One of the biggest advantages of Apache as a web server is its flexibility to implement custom-level access and security rules through its .htaccess file. For example, many security plugins make heavy use of .htaccess to implement their policies.

If you’re using Nginx to serve some content dynamically, or even to serve most content, it makes sense to have these rules implemented at the Nginx level, since Apache will not get to see a large proportion of requests.

We can create security rules in external files, which we can then call from within the Nginx server configuration files. This way we only need to update rules once, and the new rules apply to all sites you host. Let’s create a set of basic rules.

root@system:~# nano /etc/nginx/snippets/secrules.conf

This is just a set I’ve put together from other people have shared. I have modified it to include particular nuisance requests that hit some of my servers:

# secrules.conf, a snippet containing security rules for Nginx hosts

# Block request methods that are unnecessary for serving your content
if ($request_method !~ ^(GET|POST|HEAD)$ ) {
	return 444;
	}

# Block scripts from being executed from your uploads folder. They will be served as text.
# If you don't serve scripts at all, you could block them altogether instead.
location ~* ^/wp-content/uploads/.*.(php|pl|py|jsp|asp|htm|html|shtml|sh|cgi)$ {
	types { }
	default_type text/plain;
	}

# Block attempts to access PHPMyAdmin. If you actually use it, don't include this rule!
location ~* .(administrator|[pP]hp[mM]y[aA]dmin) {
	deny all;
	}

# Disallow common hacks
location ~* .(display_errors|set_time_limit|allow_url_include.*disable_functions.*open_basedir
	|set_magic_quotes_runtime|webconfig.txt.php|file_put_contentssever_root
	|wlwmanifest) {
		deny all;
		}

location ~* .(globals|encode|localhost|loopback|xmlrpc|revslider) {
	deny all;
	}

# Disallow access to sensitive files
## WARNING - The first rule \. interferes with access to the .well-known directory,
## and will disallow LetsEncrypt using webroot. In this case you may want to change
## it to \.ht
location ~ /(\.|wp-config.php|readme.html|license.txt|nginx.conf|wp-config-sample.php
    |readme.txt|dbconfig.php) {
		deny all;
		}

# Disallow scripts
location ~* \.(pl|cgi|py|sh|lua)$ { return 444; }

# Help guard against SQL injection
location ~* .(\;|'|\"|%22).*(request|insert|union|declare|drop)$ {
	deny all;
	}

# Disallow access to parts of wp-includes
# Many sites recommend blocking wp-includes altogether but in my experience this breaks WordPress
location ~* wp-admin/includes { deny all; }
location ~* wp-includes/theme-compat/ { deny all; }
location ~* wp-includes/js/tinymce/langs/.*.php { deny all; }

Full disclosure: This file is modified from content in lamosty.com and geekytuts.net, changing rules that haven’t worked for me in the past (or adding warnings), and including some others.

Now that we have our basic secrules, we can call it from within the Nginx server block. An example configuration following on from our previous tutorial could be like this:

server {
	listen 80;
	server_name example.com www.example.com;
	root /var/www/example.com/html;
	index index.php;

	include snippets/supercache.conf
	include snippets/secrules.conf

	location / {
		try_files $cachefile $uri $uri/ /index.php;
	}

	location ~ \.php$ {
        proxy_pass http://localhost:8080$request_uri;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

	location ~ /\. {
		deny all;
	}

}

Before doing anything else, test the new Nginx configurations:

root@system:~# nginx -t

If all goes well, reload Nginx and enjoy the warm and fuzzy feeling that your server is a little better protected from basic attacks. We can’t really be immune to geting hacked, just try not to be the low-hanging fruit.