An Nginx Application Firewall that will Work for Most People

Nginx firewall Nginx is a powerful web server that’s lean on resources. An application firewall can be built with just two of the modules included in a standard build. The ngx_http_geo_module, the ngx_http_map_module and two text files can make it all seem seamless. If you don’t care about controlling the error messages, you can use the ngx http access module instead of the “geo” module.

The Firewall Files

I used to break up everything into separate files but I found out using two files was a lot easier to manage. At the HTTP context level, I have the first part of the firewall as:

include /etc/nginx/conf.d/forbidden.conf;

Even though most of the file is blocking something, there’s a section devoted to allowing IP addresses and ranges.

I have the second part of the firewall at the SERVER context level as:

include /etc/nginx/conf.d/restrictions.conf;

This file will produce errors if the first file isn’t already loaded with Nginx (restart or reload).

I’ll start by showing you the “forbidden.conf” file, one section at a time.

The Firewall “forbidden.conf” File

It doesn’t matter what order you place each section. There are no overlapping variables.

This section is for single IP addresses or address ranges. If you don’t mind a “403 Forbidden” response for blocked IP addresses, you can use the “deny” directive. Otherwise, you should use the “geo” directive. Using the “geo” directive means you can return a 444 (no response) if you wish.

If you use “deny”:

##############################
# Forbidden IP addresses
#
deny 192.168.1.1;
deny 192.168.1.0/24;

If you use the “geo” module with CIDRs:

##############################
# Forbidden IP addresses
#
geo $forbidden_ip_cidr {
default 0;
192.168.1.1 1;
192.168.1.0/24 1;
}

If you use the “geo” module with ranges:

##############################
# Forbidden IP addresses
#
geo $forbidden_ip_range {
ranges;
default 0;
192.168.1.1 1;
192.168.1.0-196.168.1.255 1;
}

You can do it with or without ranges, but you can’t use both CIDRs and ranges within the same variable.

This section is for unwanted referrers:

##############################
# Forbidden Referrers
#
map $http_referer $forbidden_ref {
default 0;
~*buttons-for-your-website.com 1;
~*buttons-for-website.com 1;
~*semalt.com 1;
}

This section is for unwanted user agents. If they’re well-known crawlers or spiders, you’ll want to add them to your robots.txt file as well:

##############################
# Forbidden User Agents
#
map $http_user_agent $forbidden_ua {
default 0;
~*AhrefsBot 1;
~*BaiduSpider 1;
}

This section is for URI strings. These are URIs that would normally generate 404 errors as well as URIs you want to block access to.

##############################
# Forbidden URIs
#
map $request_uri $forbidden_uri {
default 0;
~*^/phpmyadmin 1;
~*^/wp-config 1;
}

This section is for IP addresses allowed to connect, regardless of other restrictions:

##############################
# Allowed IP Addresses
#
geo $allowed {
default 0;
123.123.123.123 1;
123.123.123.0/24 1;
123.123.0.0/16 1;
}

These should be the IP address ranges assigned to your connection at any given time. If you use SSH tunneling, you should also include your server’s IP address.

The Firewall “restictions.conf” File

Again, it doesn’t matter what order you place each section. There are no overlapping variables.

I won’t get into it here, but you can prevent logging of certain requests (I’ll have to write another article for it). I have it shown here so you can see how it works. Don’t include “set $loggable 0;” if you don’t have Nginx set up that way.

Any instance of 403 can be replaced with 444 if you don’t want to return any response at all.

Let’s start with some generic stuff:

##############################
# Generic
#
if ($request_method !~ ^(GET|HEAD|POST)$) { set $loggable 0; return 403; }
if ($request_uri ~* /(apple.touch.icon|browserconfig.xml|favicon|mstile)) { set $loggable 0; }

This section is for the things that don’t need nested conditions. You can only use the first condition if you use the “geo” module to block IP addresses (if you use the “http access” module, you don’t need it anyway):

##############################
# Drop Forbidden IPs/Referrers
#
if ($forbidden_ip)  { set $loggable 0; return 403; }
if ($forbidden_ref) { set $loggable 0; return 403; }

This section is for restricting logins with WordPress. It can only be used if you’re the only one that logs in. This example will work with similar platforms:

##############################
# Restrict Logins
#
if ($request_uri ~* ^/(wp-login)) { set $drop_login 1; }
if ($allowed) { set $drop_login 0; }
if ($drop_login) { set $loggable 0;return 403; }

This section is for restricting the administrative back-end for WordPress to logged in users. This example will work for similar platforms:

##############################
# Restrict Admin
#
if ($request_uri ~* ^/wp-admin) { set $drop_admin 1; }
if ($http_cookie ~* wordpress_logged_in) { set $drop_admin 0; }
if ($drop_admin) { set $loggable 0;return 403; }

This section is for dropping user agents. If they want access to robots.txt, let them have it. You have to “whitelist” yourself if you use a user agent (other than a web browser) to reach your website and you block it for everyone else (like wget or cURL):

##############################
# Drop Forbidden User Agents
#
if ($forbidden_ua) { set $drop_ua 1; }
if ($request_uri ~* ^/robots.txt) { set $drop_ua 0; }
if ($allowed) { set $drop_ua 0; }
if ($http_cookie ~* wordpress_logged_in) { set $drop_ua 0; }
if ($drop_ua) { set $loggable 0;return 403; }

This section is for returning a 404 error for specific URIs. If your URI list includes things that often generate 404 errors (and should), this will allow you to prevent the logging of them. Again, you have to “whitelist” yourself:

##############################
# Drop Forbidden URIs
#
if ($forbidden_uri) { set $drop_uri 1; }
if ($allowed) { set $drop_uri 0; }
if ($http_cookie ~* wordpress_logged_in) { set $drop_uri 0; }
if ($drop_uri) { set $loggable 0;return 404; }

Other Notes about this Application Firewall

This firewall may seem complex but it really isn’t. I didn’t include some of the things I use because they’re not required by most people. I’ll probably refer back to this article when (and if) I explain some of the things I’m doing.

I designed this application firewall for WordPress but it can be used as a basis for other platforms.

Share this: