Skip to main content

5 Steps to Set Up a Fast, Secure NGINX Reverse Proxy Server

Pulblished:

Updated:

Comments: counting...

Learn how to set up a fast and secure NGINX reverse proxy server with NAXSI and PageSpeed plugins using secure TLSv1.3 and HTTP2 protocols

Update (October 6th, 2019): Added Brotli compression.

This guide assumes you are using Ubuntu 18.04, if you are not you will need to hunt down some package names.

In this tutorial, you will install NGINX with NAXSI web application firewall, and the PageSpeed and HTTP2 plugins, set up Email alerts when new versions of NGINX or NAXSI are released, and configure it as a fast and secure reverse proxy server for hosting and optimizing multiple web sites behind a single IP address.

What is NAXSI

NAXSI is a web application firewall for protecting web endpoints from malicious requests, it employs a whitelist what you need and block the rest methodology. Security should be addressed as a layered approach, NAXSI is a solid defensive layer against XSS, SQL injection, and some other nasty things bad actors can attempt, but be advised it cannot defend against every vulnerability, no single tool really can.

NAXSI means Nginx Anti XSS & SQL Injection.

Technically, it is a third party nginx module, available as a package for many UNIX-like platforms. This module, by default, reads a small subset of simple (and readable) rules containing 99% of known patterns involved in website vulnerabilities. For example, <, | or drop are not supposed to be part of a URI.

Being very simple, those patterns may match legitimate queries, it is the Naxsi’s administrator duty to add specific rules that will whitelist legitimate behaviours. The administrator can either add whitelists manually by analyzing nginx’s error log, or (recommended) start the project with an intensive auto-learning phase that will automatically generate whitelisting rules regarding a website’s behaviour.

In short, Naxsi behaves like a DROP-by-default firewall, the only task is to add required ACCEPT rules for the target website to work properly. (Source: https://github.com/nbs-system/naxsi)

Why build from source

The packaged version doesn’t include PageSpeed plugin or NAXSI WAF, if you don’t mind foregoing the little speed boost you get from the PageSpeed plugin optimizations and the security layer you get from NAXSI, there’s nothing wrong with using the packaged version. Just grab the NGINX configuration file and template files below, and remove any reference to NAXSI or PageSpeed and you will have a relatively secure reverse proxy server with HTTP2, I’m not sure if you will be able to use TLSv1.3 with the packaged version or not.

Step 1 - Build NGINX from source

First let’s install a slew of dependancies for building from source and the PageSpeed plugin, and certbot for obtaining free SSL certificates from LetsEncrypt.

apt install -y libpcre3 libpcre3-dev libssl-dev \
unzip make libgoogle-perftools-dev google-perftools \
jq gcc git zlib1g zlib1g-dev build-essential uuid-dev \
certbot git

You may need to run the command apt-add-repository universe if you get errors here, some versions of Ubuntu do not include the universe repository by default and that is where jq and certbot are sourced.

Then install the packaged version of NGINX, and tell apt it is not to install any updates, since that would be a downgrade from our source build. Installing the packaged version first will set up the service in systemd so we don’t have to worry about starting NGINX at boot time, and sets up a few other default files.

apt install -y nginx
apt-mark hold nginx-core
apt-mark hold nginx-common

Check the installed version with the command nginx -v, in my case I had version 1.14.0 installed.

We have to remove all of the configuration files since they are likely not compatible with the newer version of NGINX.

rm -rf /etc/nginx/*

Now create the build script to install NGINX from source with all the extra goodies.

touch /usr/local/bin/build-nginx-naxsi.sh
chmod +x /usr/local/bin/build-nginx-naxsi.sh

First we need to check check the OpenSSL version on your system, if you are on Ubuntu 18.04 you should have 1.1.1 already, TLSv1.3 is only available with OpenSSL version 1.1.1 or higher.

openssl version

Copy and paste the appropriate script below into the new file depending on whether or not you have OpenSSL version 1.1.1 or greater, or lower than 1.1.1. If you don’t have version 1.1.1 or greater, the second script will download the source for OpenSSL and reference that when building NGINX.

This script will check for the newest versions and re-build every time it is run, so you’ll want to keep it on the system for future updates.

/usr/local/bin/build-nginx-naxsi.sh (OpenSSL 1.1.1 or greater)

#!/usr/bin/env bash

# move previous build to backup if it exists, overwriting previous backup
if [[ -d "/usr/local/src/nginx" ]]; then
  if [[ -d "/usr/local/src/nginx-old" ]]; then
    rm -rf /usr/local/src/nginx-old
  fi
  mv /usr/local/src/nginx /usr/local/src/nginx-old
fi
# delete previous nginx config backup if it exists
if [[ -d "/usr/local/src/nginx-conf" ]]; then
  rm -rf /usr/local/src/nginx-conf
fi
# backup nginx config just in case
mkdir /usr/local/src/nginx-conf
cp -a /etc/nginx/* /usr/local/src/nginx-conf/
# create new build directory and cd to it
mkdir /usr/local/src/nginx
cd /usr/local/src/nginx
# get versions
latestNginx=$(curl -s http://hg.nginx.org/nginx/atom-tags |
grep "<title>release-" | sort --version-sort | tail -1 |
sed 's/<title>release-//g' | sed 's/<\/title>//g' | sed 's/^ *//g')
latestNaxsi=$(curl -s https://api.github.com/repos/nbs-system/naxsi/releases |
  jq -r .[].tag_name | grep -v rc | head -1)
latestPagespeed=$(curl -s https://api.github.com/repos/apache/incubator-pagespeed-ngx/tags |
  jq -r .[].name | grep stable | head -1)
# get source files for pagespeed nginx, naxsi, and brotli
wget http://nginx.org/download/nginx-`echo $latestNginx`.tar.gz
wget https://github.com/nbs-system/naxsi/archive/${latestNaxsi}.tar.gz
wget https://github.com/apache/incubator-pagespeed-ngx/archive/${latestPagespeed}.tar.gz
git clone --recursive https://github.com/google/ngx_brotli.git
tar xzf nginx-`echo $latestNginx`.tar.gz
tar xzf ${latestNaxsi}.tar.gz
tar xzf ${latestPagespeed}.tar.gz
# prepare pagespeed
nps_dir=$(find . -name "*pagespeed-ngx-*" -type d)
cd "$nps_dir"
NPS_RELEASE_NUMBER=${latestPagespeed/beta/}
NPS_RELEASE_NUMBER=${latestPagespeed/stable/}
psol_url=https://dl.google.com/dl/page-speed/psol/${NPS_RELEASE_NUMBER}.tar.gz
[ -e scripts/format_binary_url.sh ] && psol_url=$(scripts/format_binary_url.sh PSOL_BINARY_URL)
wget ${psol_url}
tar xzf $(basename ${psol_url})
# build and install
cd /usr/local/src/nginx
cd nginx-`echo $latestNginx`
./configure --conf-path=/etc/nginx/nginx.conf \
  --add-module=../naxsi-${latestNaxsi}/naxsi_src/ \
  --add-module=../$nps_dir \
  --add-module=../ngx_brotli \
  --error-log-path=/var/log/nginx/error.log \
  --http-client-body-temp-path=/var/lib/nginx/body \
  --http-fastcgi-temp-path=/var/lib/nginx/fastcgi \
  --http-log-path=/var/log/nginx/access.log \
  --http-proxy-temp-path=/var/lib/nginx/proxy \
  --lock-path=/var/lock/nginx.lock \
  --pid-path=/var/run/nginx.pid \
  --with-http_ssl_module \
  --with-http_v2_module \
  --with-stream \
  --with-stream_realip_module \
  --with-stream_ssl_module \
  --without-mail_pop3_module \
  --without-mail_smtp_module \
  --without-mail_imap_module \
  --without-http_uwsgi_module \
  --without-http_scgi_module \
  --prefix=/usr
make
make install
# backup naxsi core rules and download latest core rules
cd /etc/nginx
mv /etc/nginx/naxsi_core.rules /etc/nginx/naxsi_core.rules.bak
wget -q https://raw.githubusercontent.com/nbs-system/naxsi/master/naxsi_config/naxsi_core.rules
# do nginx config test and restart nginx if passed
check="$(/usr/sbin/nginx -t 2>&1 | grep success | sed 's/.*conf //g')"
if [[ $check == "test is successful" ]];then
  systemctl restart nginx
  sleep 5
  systemctl status nginx
else
  echo "nginx config test failed!!!"
fi
exit 0

/usr/local/bin/build-nginx-naxsi.sh (OpenSSL version lower than 1.1.1)

#!/usr/bin/env bash

# move previous build to backup if it exists, overwriting previous backup
if [[ -d "/usr/local/src/nginx" ]]; then
  if [[ -d "/usr/local/src/nginx-old" ]]; then
    rm -rf /usr/local/src/nginx-old
  fi
  mv /usr/local/src/nginx /usr/local/src/nginx-old
fi
# delete previous nginx config backup if it exists
if [[ -d "/usr/local/src/nginx-conf" ]]; then
  rm -rf /usr/local/src/nginx-conf
fi
# backup nginx config just in case
mkdir /usr/local/src/nginx-conf
cp -a /etc/nginx/* /usr/local/src/nginx-conf/
# create new build directory and cd to it
mkdir /usr/local/src/nginx
cd /usr/local/src/nginx
# # get source for openssl 1.1.1 (tls 1.3 compatibility)
git clone https://github.com/openssl/openssl.git
cd openssl
git checkout OpenSSL_1_1_1-stable
cd /usr/local/src/nginx
# get versions
latestNginx=$(curl -s http://hg.nginx.org/nginx/atom-tags |
grep "<title>release-" | sort --version-sort | tail -1 |
sed 's/<title>release-//g' | sed 's/<\/title>//g' | sed 's/^ *//g')
latestNaxsi=$(curl -s https://api.github.com/repos/nbs-system/naxsi/releases |
  jq -r .[].tag_name | grep -v rc | head -1)
latestPagespeed=$(curl -s https://api.github.com/repos/apache/incubator-pagespeed-ngx/tags |
  jq -r .[].name | grep stable | head -1)
# get source files for pagespeed nginx, naxsi, and brotli
wget http://nginx.org/download/nginx-`echo $latestNginx`.tar.gz
wget https://github.com/nbs-system/naxsi/archive/${latestNaxsi}.tar.gz
wget https://github.com/apache/incubator-pagespeed-ngx/archive/${latestPagespeed}.tar.gz
git clone --recursive https://github.com/google/ngx_brotli.git
tar xzf nginx-`echo $latestNginx`.tar.gz
tar xzf ${latestNaxsi}.tar.gz
tar xzf ${latestPagespeed}.tar.gz
# prepare pagespeed
nps_dir=$(find . -name "*pagespeed-ngx-*" -type d)
cd "$nps_dir"
NPS_RELEASE_NUMBER=${latestPagespeed/beta/}
NPS_RELEASE_NUMBER=${latestPagespeed/stable/}
psol_url=https://dl.google.com/dl/page-speed/psol/${NPS_RELEASE_NUMBER}.tar.gz
[ -e scripts/format_binary_url.sh ] && psol_url=$(scripts/format_binary_url.sh PSOL_BINARY_URL)
wget ${psol_url}
tar xzf $(basename ${psol_url})
# build and install
cd /usr/local/src/nginx
cd nginx-`echo $latestNginx`
./configure --conf-path=/etc/nginx/nginx.conf \
  --add-module=../naxsi-${latestNaxsi}/naxsi_src/ \
  --add-module=../$nps_dir \
  --add-module=../ngx_brotli \
  --error-log-path=/var/log/nginx/error.log \
  --http-client-body-temp-path=/var/lib/nginx/body \
  --http-fastcgi-temp-path=/var/lib/nginx/fastcgi \
  --http-log-path=/var/log/nginx/access.log \
  --http-proxy-temp-path=/var/lib/nginx/proxy \
  --lock-path=/var/lock/nginx.lock \
  --pid-path=/var/run/nginx.pid \
  --with-http_ssl_module \
  --with-http_v2_module \
  --with-stream \
  --with-stream_realip_module \
  --with-stream_ssl_module \
  --without-mail_pop3_module \
  --without-mail_smtp_module \
  --without-mail_imap_module \
  --without-http_uwsgi_module \
  --without-http_scgi_module \
  --prefix=/usr \
  --with-openssl=/usr/local/src/nginx/openssl
make
make install
# backup naxsi core rules and download latest core rules
cd /etc/nginx
mv /etc/nginx/naxsi_core.rules /etc/nginx/naxsi_core.rules.bak
wget -q https://raw.githubusercontent.com/nbs-system/naxsi/master/naxsi_config/naxsi_core.rules
# do nginx config test and restart nginx if passed
check="$(/usr/sbin/nginx -t 2>&1 | grep success | sed 's/.*conf //g')"
if [[ $check == "test is successful" ]];then
  systemctl restart nginx
  sleep 5
  systemctl status nginx
else
  echo "nginx config test failed!!!"
fi
exit 0

Now run the script (/usr/local/bin/build-nginx-naxsi.sh), and watch it go. You can re-check the version now and it should be newer with the same command nginx -v, in my case I’m now running 1.17.1.


Potential Build Error and Solution

If you run into errors like the following:

../naxsi-0.56/naxsi_src/naxsi_runtime.c:728:5: error: 'strncat' specified bound 1 equals source length [-Werror=stringop-overflow=]
     strncat((char*)tmp_hashname.data, "#", 1);
     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
../naxsi-0.56/naxsi_src/naxsi_runtime.c:731:3: error: 'strncat' specified bound 1 equals source length [-Werror=stringop-overflow=]
   strncat((char*)tmp_hashname.data, "#", 1);
   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
cc1: all warnings being treated as errors

This is caused by warnings being treated as errors in the GCC compiler, I beleive this behavior change was added in GCC version 8 (use the command gcc -v to check). You can disable this behavior when building NGINX by adding the line CFLAGS="-Wno-stringop-truncation -Wno-stringop-overflow -Wno-size-of-pointer-memaccess" \ right before the ./configure ... lines, it should look something like this:

...
# build and install
cd nginx-`echo $latestNginx`
CFLAGS="-Wno-stringop-truncation -Wno-stringop-overflow -Wno-size-of-pointer-memaccess" \
./configure --conf-path=/etc/nginx/nginx.conf \
  --add-module=../naxsi-${latestNaxsi}/naxsi_src \
...

Step 2 - Software update Email alerts

It is critically important that you stay on top of updates being as we are running the latest stable version, most OS packages are a few versions behind to ensure all the issues have been worked out. If a vulnerability is found, you’ll want to patch the day of! Before proceeding you should set up Postfix on your server to relay email alerts to you, and install mailutils for Email submission.

If you don’t wish to do this just skip to the next step, I do recommend you sign up for the NGINX Announce mailing list instead so you can still re-run the installer script when new versions are released.

Let’s create a script to check the latest versions of NGINX and NAXSI, and send out an Email if they are newer than what is installed.

touch /usr/local/bin/nginx-update-notifier.sh
chmod +x /usr/local/bin/nginx-update-notifier.sh

Copy and paste the script below into the file

don’t forget to change the Email address at the emailTo: line in the script.

/usr/local/bin/nginx-update-notifier.sh

#!/bin/bash

emailTo="you@yourdomain.com"

latestNginx=$(curl -s http://hg.nginx.org/nginx/atom-tags |
grep "<title>release-" | sort --version-sort | tail -1 |
sed 's/<title>release-//g' | sed 's/<\/title>//g' | sed 's/^ *//g')
latestNaxsi=$(curl -s https://api.github.com/repos/nbs-system/naxsi/releases | \
  jq -r .[].tag_name | grep -v rc | head -1)
currentNginx="$(/usr/sbin/nginx -v 2>&1 | sed 's/.*nginx\///g')"
currentNaxsi="$(/usr/sbin/nginx -V 2>&1 | grep 'naxsi' | \
  sed 's/.*naxsi-//g' | sed 's/\/.*//g')"
versionCheck="$(printf "$latestNginx\n$currentNginx" | sort -V | tail -n1)"
if [[ "$versionCheck" != "$currentNginx" ]] ||
  [[ "$latestNaxsi" != "$currentNaxsi" ]]; then
  echo -e \
  "Use build-nginx-naxsi.sh to update:
  NGINX: $currentNginx -> $latestNginx
  NAXSI: $currentNaxsi -> $latestNaxsi" | \
  mail -s "NGINX/NAXSI Update Available" $emailTo
else
  echo -e \
  "NGINX and NAXSI are up to date:
  NGINX: $currentNginx -> $latestNginx
  NAXSI: $currentNaxsi -> $latestNaxsi"
fi

exit 0

Now create the two following files to schedule this script as a systemd timer, or use cron if you prefer.

/etc/systemd/system/nginx-update-notifier.service

[Unit]
Description=Check for nginx and naxsi updates

[Service]
Type=oneshot
ExecStart=/usr/local/bin/nginx-update-notifier.sh

/etc/systemd/system/nginx-update-notifier.timer

[Unit]
Description=Check for nginx and naxsi updates

[Timer]
# Check daily at 5am
OnCalendar=*-*-* 05:00:00
Persistent=true
Unit=nginx-update-notifier.service

[Install]
WantedBy=timers.target

To enable the timer, we just need to run a few commands.

systemctl enable nginx-update-notifier.timer
systemctl start nginx-update-notifier.timer

Step 3 - Boilerplate configuration

We need a cache directory for the PageSpeed plugin to use, and some directories for site configurations and NAXSI configurations. I really think Apache had it right with the sites-available and sites-enabled format, so we will set that up.

mkdir /etc/nginx/sites-available
mkdir /etc/nginx/sites-enabled
mkdir /etc/nginx/snippets
mkdir /etc/nginx/naxsi-rules
mkdir -p /var/cache/pagespeedcache

The PageSpeed plugin works best when the cache directory is on tmpfs (stored in RAM instead of hard disk), so we will make /var/cache/pagespeedcache a tmpfs directory by declaring it as such in /etc/fstab, add the following to the bottom of the file.

/etc/fstab

Be mindful of the amount of space you give here at size=512M, I would say no more than 1/4 of your available RAM, my server has 2G of RAM so I used 512M. If you can’t allocate at least 64M to the tmpfs, just skip this step and leave it as a regular directory on the hard disk.

# tmpfs ramdisk for PageSpeed cache
tmpfs /var/cache/pagespeedcache tmpfs defaults,noatime,nosuid,nodev,noexec,mode=1777,size=512M 0 0

No we need to mount the tmpfs and set permissions.

mount -a
chown www-data. /var/cache/pagespeedcache

Let’s make a quick and easy 403 page, this is what will be shown for requests that are blocked by NAXSI, you can spruce this up however you like later.

/var/www/html/403.html

<!DOCTYPE html>
<html>
  <head>
    <title>403</title>
  </head>
  <body>
    Error 403: Access Denied
  </body>
</html>

We also need to generate Diffie-Hellman parameters, without getting into too much jargon, these are used in key exchanges and should be unique to your specific server for security, you can read more here if you are interested.

openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096

And a dummy self-signed certificate that we will temporarily use to get signed certificates from LetsEncrypt.

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365000 -nodes
mv key.pem /etc/ssl/private/ssl-cert-snakeoil.key
mv cert.pem /etc/ssl/certs/ssl-cert-snakeoil.pem

Now let’s put together some configuration files for common settings, create the following files.

/etc/nginx/snippets/limit-request-methods.conf

# Only allow common method : GET, POST and HEAD
# (HEAD is implicitly allowed with GET)
limit_except GET POST {
    deny all;
}

/etc/nginx/snippets/pagespeed.conf

# Needs to exist and be writable by nginx.
pagespeed FileCachePath /var/ngx_pagespeed_cache;

# Lazyload images
pagespeed EnableFilters lazyload_images;

# Auto convert images
pagespeed EnableFilters rewrite_images;
pagespeed EnableFilters convert_png_to_jpeg;
pagespeed EnableFilters convert_jpeg_to_webp;
pagespeed EnableFilters convert_to_webp_lossless;

# Ensure requests for pagespeed optimized resources go to the pagespeed handler
# and no extraneous headers get set.
location ~ "\.pagespeed\.([a-z]\.)?[a-z]{2}\.[^.]{10}\.[^.]+" {
  add_header "" "";
}
location ~ "^/pagespeed_static/" { }
location ~ "^/ngx_pagespeed_beacon$" { }

/etc/nginx/snippets/ssl-params.conf

Be warned that the Strict-Transport-Security header will cause you major problems if you remove SSL/TLS from any proxied site later, you should probably comment that line out for now and enable it later after you are sure everything is running smoothly.

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'TLS13-AES-256-GCM-SHA384:TLS-CHACHA20-POLY1305-SHA256:TLS-AES-256-GCM-SHA384:TLS-AES-128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_prefer_server_ciphers on;
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains" always;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
ssl_dhparam /etc/ssl/certs/dhparam.pem;

Note: For a more compatible set of SSL protocols while maintaining a decent level of security, replace the first two lines of the ssl-params.conf file with the following.

ssl_protocols SSLv3 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers 'TLS13-AES-256-GCM-SHA384:TLS-CHACHA20-POLY1305-SHA256:TLS-AES-256-GCM-SHA384:TLS-AES-128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256 EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA+RC4:EECDH:EDH+aRSA:RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!CAMELLIA';

/etc/nginx/naxsi-rules/template

LearningMode;
SecRulesEnabled;
DeniedUrl "/403.html";

## check rules
CheckRule "$SQL >= 4" BLOCK;
CheckRule "$RFI >= 4" BLOCK;
CheckRule "$TRAVERSAL >= 2" BLOCK;
CheckRule "$EVADE >= 2" BLOCK;
CheckRule "$XSS >= 4" BLOCK;

## white list
# e.g. BasicRule wl:1007 "mz:$URL:/index.html|$BODY_VAR:message|URL";

/etc/nginx/sites-available/template

# redirect http traffic to https
server {
    listen 80;
    server_name SERVERNAME;
    return 301 https://SERVERNAME$request_uri;
}

# https reverse proxy
server {
    listen 443 ssl http2;
    server_name SERVERNAME;
    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
    # ssl_certificate /etc/letsencrypt/live/SERVERNAME/fullchain.pem;
    # ssl_certificate_key /etc/letsencrypt/live/SERVERNAME/privkey.pem;
    include /etc/nginx/snippets/ssl-params.conf;
    include /etc/nginx/snippets/pagespeed.conf;
    location / {
       include /etc/nginx/snippets/limit-request-methods.conf;
       include /etc/nginx/naxsi-rules/SERVERNAME.rules;
       proxy_hide_header X-Powered-By;
       proxy_pass_header Authorization;
       proxy_pass http://SERVERIPADDRESS;
       proxy_set_header Host $host;
       proxy_set_header X-Real-IP $remote_addr;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-Forwarded-Host $host;
       proxy_set_header X-Forwarded-Proto $scheme;
       proxy_http_version 1.1;
       proxy_set_header Connection "";
       proxy_buffering off;
       client_max_body_size 0;
       proxy_read_timeout 36000s;
       proxy_redirect off;
    }

    # letsencrypt validation
    location '/.well-known/acme-challenge' {
        default_type "text/plain";
        root /var/www/html;
    }

    # 403 redirect for NAXSI rejections
    location '/403.html' {
        default_type "text/html";
        root /var/www/html;
    }
}

/etc/nginx/sites-available/default

server {
    listen 80 default_server;
    server_name _;
    return 444;
}

server {
    listen 443 ssl default_server http2;
    server_name _;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'TLS13-AES-256-GCM-SHA384:TLS-CHACHA20-POLY1305-SHA256:TLS-AES-256-GCM-SHA384:TLS-AES-128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    ssl_prefer_server_ciphers on;
    ssl_ecdh_curve secp384r1;
    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains" always;
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
    return 444;
}

Go ahead and get rid of the main configuration file with rm /etc/nginx/nginx.conf, and create it again with the following configuration.

/etc/nginx/nginx.conf

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
        worker_connections 768;
        use epoll;
        multi_accept on;
}

http {

        ##
        # Basic Settings
        ##

        include /etc/nginx/naxsi_core.rules;

        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        keepalive_requests 100000;
        types_hash_max_size 2048;
        server_tokens off;
        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        ##
        # Logging Settings
        ##

        # Log to file
        #access_log /var/log/nginx/access.log;
        #error_log /var/log/nginx/error.log;
        # Log to syslog
        error_log syslog:server=unix:/dev/log,facility=local7,tag=nginx,severity=error;
        access_log syslog:server=unix:/dev/log,facility=local7,tag=nginx,severity=info;

        ##
        # Brotli Settings
        ##

        brotli on;
        brotli_comp_level 6;
        brotli_static on;
        brotli_types text/xml image/svg+xml application/x-font-ttf image/vnd.microsoft.icon application/x-font-opentype application/json font/eot application/vnd.ms-fontobject application/javascript font/otf application/xml application/xhtml+xml text/javascript  application/x-javascript text/plain application/x-font-truetype application/xml+rss image/x-icon font/opentype text/css image/x-win-bitmap;

        ##
        # Gzip Settings
        ##

        gzip on;
        gzip_disable "msie6";
        gzip_vary on;
        gzip_proxied any;
        gzip_comp_level 6;
        gzip_buffers 16 8k;
        gzip_http_version 1.1;
        gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;

        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;

        ##
        # Caching
        ##

        open_file_cache max=1000 inactive=20s;
        open_file_cache_valid 30s;
        open_file_cache_min_uses 5;
        open_file_cache_errors off;
}

Lastly, let’s enable the default server, this will ignore all requests to the NGINX server by it’s IP address, so if you just type the public IP address into your browser it will not respond, it will only respond to requests with a valid hostname. We will enable it by making a symlink from the file in sites-available to the sites-enabled directory.

ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/

Step 4 - Setting up backend sites

Now for the easy part! Any time you wish to host a new site, just run through this checklist (remember to restart NGINX after each one):

  1. Copy the template in the template in naxsi-rules to nameofyoursite.rules and the template in sites-available to nameofyoursite.conf and edit it accordingly, and symlink it to the sites-enabled directory
  2. Get a free SSL certificate from LetsEncrypt and update the site configuration file to use it
  3. Configure NAXSI rules

Let’s set up the site example.com with a backend at IP Address 10.5.5.5.

cd /etc/nginx/naxsi-rules
cp template example.com.rules
cd /etc/nginx/sites-available
cp template example.com.conf

Below are the lines I edited, I’ve omitted the lines that weren’t changed, note that the SSL certificate paths are still commented out, we need to use the snakeoil (self-signed) certificate for the moment.

The default settings are very restrictive, you may need to open them up a bit, you could for example modify the limit-request-methods.conf file if all sites need changing, or make a new snippet file and modify that for example limit-request-methods-wordpress-sites.conf and import that snippet in the main site configuration file, or just copy it’s contents to the main site configuration file instead of including the snippet file and modify them there if it’s just one site.

/etc/nginx/sites-available/example.com.conf

# redirect http traffic to https
server {
    ...
    server_name example.com www.example.com;
    return 301 https://www.example.com$request_uri;
}

# https reverse proxy
server {
    ...
    server_name example.com www.example.com;
    ...
    # ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    # ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ...
    location / {
       ...
       proxy_pass http://10.5.5.5;
       ...
    }

    ...
}

Now symlink the configuration file to the sites-enabled directory.

ln -s /etc/nginx/sites-available/example.com.conf /etc/nginx/sites-enabled/

If you ever need to take the site down, you can just delete the symlink and leave the configuration file in sites-available for later use or reference (e.g. rm /etc/nginx/sites-enabled/example.com.conf).

Great, now test the configuration with the command nginx -t and then restart NGINX with the command service nginx restart if the test passed. The NGINX server needs to be accessible from the outside internet, and have the domain name example.com resolve to it’s public IP address before proceeding. LetsEncrypt needs to verify we own the domain before it will hand out a certificate, so when we run the next command, the certbot program will create a file with a unique identifier in it, and the remote LetsEncrypt server will request that file from example.com/.well-known/acme-challenge, if the file matches it will issue the certificate. certbot should automatically set up a service timer in systemd when it is installed to renew certificates before they expire, so you shouldn’t need to worry about that.

certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com

You will be asked to provide your Email address for expiration alerts, and to accept the terms of service. The renewal alerts are handy because if something ever goes wrong and renewal fails, you’ll be informed before there is a real problem.

You should now have a valid SSL certificate for example.com, we just need to enable it in the sites configuration file by commenting out the snakeoil certificate paths and uncommenting the LetsEncrypt certificate paths.

/etc/nginx/sites-available/example.com.conf (Again)

    # ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    # ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

After testing and restarting NGINX again, your site should be live. Right now the NAXSI web application firewall is in learning mode, so it will not block any requests. You should visit your site, click all the buttons, fill out all the forms, and generally do all of the things you would expect a normal user to do while NAXSI collects logs about which requests it would normally block.

All that’s left is to add exclusions for the NAXSI rules that triggered in learning mode, and disable learning mode. Look in your NGINX error log for lines with the text NAXSI_FMT, if you used the NGINX configuration file above the logs will be in your syslog file. There will likely be a lot, but I’ll work through one example with you, I’ve chosen a pretty complex one for your benefit.

This will look very intimidating the first time you see it, but I promise once you work through a few it will become second nature pretty quickly.

cat /var/log/syslog | grep 'NAXSI_FMT' | less

nginx: [error] 12267#0: *127 NAXSI_FMT: ip=23.42.23.13&server=example.com&uri=/api/v1&learning=1&vers=0.56&total_processed=25&total_blocked=1&block=1&cscore0=$SQL&score0=48&cscore1=$XSS&score1=40&cscore2=$TRAVERSAL&score2=24&zone0=BODY&id0=1010&var_name0=query&zone1=BODY&id1=1011&var_name1=query&zone2=BODY&id2=1015&var_name2=query&zone3=BODY&id3=1205&var_name3=query&zone4=BODY&id4=1310&var_name4=query&zone5=BODY&id5=1311&var_name5=query, client: 23.42.23.13, server: example.com, request: "POST /api/v1 HTTP/2.0", host: "example.com", referrer: "https://example.com/"

There is an example of the rule syntax in the naxsi-rules/template file that will be in all of the site files for easy reference, it looks like this:

BasicRule wl:1007 "mz:$URL:/index.html|$BODY_VAR:message|URL";

To break that down some we have:

  • BasicRule - Each rule starts with this
  • wl:1007 - Whitelist rule ID 1007
  • "mz:..."; - Match zone, or what zones to target for this whitelist
  • | - Separate multiple targets, think of it as the word “and”
  • $URL:/index.html - Target specific URL
  • $BODY_VAR:message - Target the variable called ‘message’ in the request body
  • URL - Target the URL path

All of this information is in the error log, since this is a complex event there are multiple zones, so I’m only looking at zone 0 right now, we have:

  • Rule ID: id0=1010
  • URL: uri=/api/v1
  • Match Zone: zone0=BODY
  • Variable: var_name0=query

We can build our rule with that information, and it looks like this:

BasicRule wl:1010 "mz:$URL:/api/v1|$BODY_VAR:query";

If var_name0 in the error log was blank, we wouldn’t specify the variable name query, and it would look like this:

BasicRule wl:1010 "mz:$URL:/api/v1|BODY";

If you were seeing the same match for all URLs, you could omit the URL in the rule, and it would whitelist the variable for any URL, like this:

BasicRule wl:1010 "mz:$BODY_VAR:query";

If the variable name was constantly changing, for example query_83jd82w and query_g8dj37s and so on, you could use a regular expression like this:

BasicRule wl:1010 "mz:$URL:/api/v1|$BODY_VAR_X:^query_.*$";

You can also use regular expressions for the URL, and finally, if the match zone is a user input that you know for sure is sanitzed on the backend site, you could whitelist all rule IDs for that zone by specifying an ID of 0, like this:

BasicRule wl:0 "mz:$URL:/api/v1|$BODY_VAR:query";

The goal is to be specific to let through as little exceptions as possible, so keep that in mind. Now to finish up, since all of these zones are matching the same variable, URL, and portion, only the rule ID is different between them, I can whitelist them all in one rule, like this:

BasicRule wl:1010,1011,1015,1205,1310,1311 "mz:$URL:/api/v1|$BODY_VAR:query";

A more detailed explaination of the whitelisting options and syntax can be found on the NAXSI GitHub page , I recommend skimming through the whole wiki, as there are many features and options available that I haven’t covered here.

I usually write out my rules in a text editor for this part, each rule on a new line. Once that is done, copy and paste all the lines into the bottom of the file /etc/nginx/naxsi-rules/example.com.conf, while you are there comment out the line LearningMode to enable NAXSI rule enforcement.

/etc/nginx/naxsi-rules/example.com.conf

#LearningMode;
SecRulesEnabled;
DeniedUrl "/403.html";

## check rules
CheckRule "$SQL >= 4" BLOCK;
CheckRule "$RFI >= 4" BLOCK;
CheckRule "$TRAVERSAL >= 2" BLOCK;
CheckRule "$EVADE >= 2" BLOCK;
CheckRule "$XSS >= 4" BLOCK;

## white list
# e.g. BasicRule wl:1007 "mz:$URL:/index.html|$BODY_VAR:message|URL";
BasicRule wl:1010,1011,1015,1205,1310,1311 "mz:$URL:/api/v1|$BODY_VAR:query";
BasicRule wl:111 "mz:$URL:/somepath/|$ARGS_VAR:shoes";
BasicRule wl:1050 "mz:$URL:/someotherpath/|$ARGS_VAR:boots";
...

Nice, you should visit your website and do all the things again to make sure you didn’t miss any rules. You can visit https://example.com/delete in your browser to test NAXSI, if you haven’t whitelisted /delete, you should get the 403 page we made earlier.

Step 5 - Extra security

I highly recommended setting up Mitchell Krogza’s “Ultimate Bad Bot and Referrer Blocker” for NGINX. It’s free and open source, and works extremely well to add another layer of security by denying known bad actors across the internet. You can get it and the setup instructions on this GitHub page .