Skip to content

Nginx

Obtaining the Nginx source

In order to install the latest version of Nginx from the mainline branch, have Nginx built against OpenSSL 1.1.1, and have extra modules built in such as GeoIP2 and LibModSecurity, we will create our own deb package using the official Nginx source repository.

First use sudo to open an interactive shell:

sudo -s

Install the prerequisites:

apt install curl gnupg2 ca-certificates lsb-release libpcre3-dev git

To set up the apt repository for mainline nginx packages, run the following command:

echo "deb http://nginx.org/packages/mainline/ubuntu `lsb_release -cs` nginx" | tee /etc/apt/sources.list.d/nginx.list
echo "deb-src http://nginx.org/packages/mainline/ubuntu `lsb_release -cs` nginx" | tee -a /etc/apt/sources.list.d/nginx.list

Next, import an official nginx signing key so apt could verify the packages authenticity:

curl -fsSL https://nginx.org/keys/nginx_signing.key | apt-key add -

Update the apt cache:

apt update

Make a directory for our source and then move into it:

mkdir /usr/local/src/nginx && cd /usr/local/src/nginx/

Then download the Nginx source:

apt install dpkg-dev -y && apt source nginx

Edit the compile rules:

nano /usr/local/src/nginx/nginx-1.xx.x/debian/rules

Find:

dh_shlibdeps -a

And change to:

dh_shlibdeps -a --dpkg-shlibdeps-params=--ignore-missing-info

Than save and exit. In order to prevent the compiler treating warnings as errors:

nano /usr/local/src/nginx/nginx-1.15.8/auto/cc/gcc

And comment out (prefix with a hash) the line:

CFLAGS="$CFLAGS -Werror"

Obtaining the module sources

Obtaining the GeoIP2 source

First we need to grab the maxminddb library. Lucky we can do this easily by adding a PPA:

add-apt-repository ppa:maxmind/ppa

Then update the apt cache and install:

apt update &&  apt install libmaxminddb0 libmaxminddb-dev mmdb-bin

Move into the /src directory and download the geoip2 module git repository:

cd /usr/local/src
git clone --recursive https://github.com/leev/ngx_http_geoip2_module

Obtaining the Nginx ModSecurity connector source

Next, we need to clone the git repository for the ModSecurity Nginx connector:

git clone https://github.com/SpiderLabs/ModSecurity-nginx

Obtaining the More Headers module source

Next, we grab the More Headers source from the OpenResty git repository:

git clone https://github.com/openresty/headers-more-nginx-module

Obtaining the Brotli module source

We get the Brotli source from the Google git repository like so:

git clone --recursive https://github.com/google/ngx_brotli

Obtining the Virtual Host Traffic Status module source

Finally, we grab the source for the Nginx Virtual Host Traffic Status module:

git clone https://github.com/vozlt/nginx-module-vts

Compiling and Installing ModSecurity 3.0

First we make sure all the depencies are met:

apt update && apt install apt-utils apache2-dev autoconf automake bison flex make gcc git build-essential libssl1.1 libmaxminddb-dev dpkg-dev libcurl4-openssl-dev libgeoip-dev liblmdb-dev libpcre++-dev libtool libxml2-dev libyajl-dev pkg-config wget zlib1g-dev doxygen curl libcurl4 libpcre3 libpcre3-dev liblua5.2-dev libfuzzy-dev libssl-dev g++ ssdeep dh-autoreconf libxml2 libxml2-dev gettext

Ignore the below for now - new instructions follow

Then we grab the source from the git repository:

git clone https://github.com/SpiderLabs/ModSecurity

and then we bring in a couple of sub-modules:

cd ModSecurity
git checkout v3/master
git submodule init
git submodule update

then we make the library:

sh build.sh

Do this instead!

The OWASP ModSecurity Core Rule Set (CRS) team has identified a Denial of Service vulnerability in the underlying ModSecurity engine. This affects all releases in the ModSecurity v3 release line. The vendor Trustwave Spiderlabs have not released an update yet. Fortunately a patch has been produced that fixes this. See https://coreruleset.org/20200914/cve-2020-15598/ for further information.

Download the 3.0.4 release as follows:

wget https://github.com/SpiderLabs/ModSecurity/releases/download/v3.0.4/modsecurity-v3.0.4.tar.gz

extract, move into the directory, download the patch, and apply:

tar -xf modsecurity-v3.0.4.tar.gz
cd modsecurity-v3.0.4/
wget https://gist.githubusercontent.com/crsgists/0e1f6f7f1bd1f239ded64cecee46a11d/raw/181bc852065e9782367f1dc67c96d4d250e73a46/cve-2020-15598.patch
patch -p1 < cve-2020-15598.patch

End of new instructions, proceed as normal

Before the next step, we'll install auto-apt. This is no longer maintained and not in the Ubuntu 20.04 repositories so we'll need to install it manually:

cd /opt
wget https://mirrors.edge.kernel.org/ubuntu/pool/universe/a/auto-apt/auto-apt_0.3.24_amd64.deb
dpkg -i auto-apt_0.3.24_amd64.deb
apt install -f

Auto-apt helps to install missing dependences when running ./configure

Before we use it, we should run:

auto-apt update
auto-apt updatedb && sudo auto-apt update-local

Use it like this:

cd /usr/local/src/ModSecurity
auto-apt run ./configure

Now make with the following simple command:

make

Instead of running make install, we'll instead utilise a script called checkinstall. This allows us to create and install via a deb package, making it easier to remove or upgrade in future. More information here: https://help.ubuntu.com/community/CheckInstall

Install checkinstall:

apt install checkinstall

Then make the deb package and install:

checkinstall

It may prompt for a package summary and a version. Fill this in as appropriate.

If you get an error relating to permissions in the tmp directory, open the checkinstall config file and change the setting for BASE_TMP_DIR:

nano /etc/checkinstallrc

Change the BASE_TMP_DIR line as follows:

BASE_TMP_DIR=/var/checkinstall/tmp

Save the file then create the directory:

mkdir -p /var/checkinstall/tmp

The libModSecurity library now is installed at /usr/local/modsecurity/lib/libmodsecurity.so

It can be removed at any time by running:

dpkg -r modsecurity

You can check that the package is installed with:

dpkg -l modsecurity

The package will also have saved in the /usr/local/src/ModSecurity directory.

Installing Nginx

Move into the nginx source directory

cd /usr/local/src/nginx/nginx-x.xx.x

Edit the rules file:

nano debian/rules

Add the configuration options for the extra modules:

config.status.nginx: config.env.nginx
    cd $(BUILDDIR_nginx) && \
    CFLAGS="" ./configure ...stuff... --add-module=/usr/local/src/ModSecurity-nginx --add-module=/usr/local/src/headers-more-nginx-module --add-module=/usr/local/src/ngx_http_geoip2_module --add-module=/usr/local/src/ngx_brotli --add-module=/usr/local/src/nginx-module-vts

config.status.nginx_debug: config.env.nginx_debug
    cd $(BUILDDIR_nginx_debug) && \
    CFLAGS="" ./configure ...stuff... --add-module=/usr/local/src/headers-more-nginx-module --add-module=/usr/local/src/ModSecurity-nginx --add-module=/usr/local/src/ngx_http_geoip2_module --add-module=/usr/local/src/ngx_brotli --add-module=/usr/local/src/nginx-module-vts --with-debug   

And finally create the package:

apt build-dep nginx -y && dpkg-buildpackage -us -uc -b

Once finished, you should have a deb file in the parent directory (/usr/local/src/nginx). Install it with:

dpkg -i nginx_x.xx.x-x~focal_amd64.deb

Note! If you already had Nginx installed for whatever reason, remove it first before installing our new one. Remove with:

apt remove nginx nginx-common nginx-full -y --allow-change-held-packages

Start the Nginx service:

systemctl start nginx

If you get the below error:

Failed to start nginx.service: Unit nginx.service is masked.

Then run the following command:

systemctl unmask nginx

Enable the Nginx service so that it starts automatically on boot:

systemctl enable nginx

Now prevent Nginx from being automatically updated with another version from the Ubuntu repositories:

apt-mark hold nginx

Exit the sudo shell:

exit

Further Reading:

https://nginx.org/en/docs/beginners_guide.html
https://www.linode.com/docs/web-servers/nginx/how-to-configure-nginx/
https://www.nginx.com/resources/wiki/start/

Configuring and Securing Nginx

Virtual hosts are created in /etc/nginx/sites-available/

These files are symlinked in /etc/nginx/sites-enabled/ to enable the sites.

Assuming that the DNS for example.com has already been configured in your DNS management interface (eg at Cloudflare), we can create a secure certificate for the domain example.com using acme.sh as below:

acme.sh must be ran as root so ensure you're still in the root shell (sudo -s)

Issue an RSA certificate and install to a custom location:

~/.acme.sh/acme.sh --issue --dns dns_cloudns -d example.com -d www.example.com --keylength 4096 --key-file /etc/letsencrypt/rsa-certs/example.com/privkey.pem --ca-file /etc/letsencrypt/rsa-certs/example.com/chain.pem --cert-file /etc/letsencrypt/rsa-certs/example.com/cert.pem --fullchain-file /etc/letsencrypt/rsa-certs/example.com/fullchain.pem --pre-hook "mkdir -p /etc/letsencrypt/rsa-certs/example.com" --post-hook "find /etc/letsencrypt/rsa-certs/example.com/ -name '*.pem' -type f -exec chmod 600 {} \;" --renew-hook "find /etc/letsencrypt/rsa-certs/example.com/ -name '*.pem' -type f -exec chmod 600 {} \; -exec service nginx reload \;"

and issue an ECC certificate:

~/.acme.sh/acme.sh --issue --dns dns_cloudns -d example.com -d www.example.com --keylength ec-384 --key-file /etc/letsencrypt/ecc-certs/example.com/privkey.pem --ca-file /etc/letsencrypt/ecc-certs/example.com/chain.pem --cert-file /etc/letsencrypt/ecc-certs/example.com/cert.pem --fullchain-file /etc/letsencrypt/ecc-certs/example.com/fullchain.pem --pre-hook "mkdir -p /etc/letsencrypt/ecc-certs/example.com" --post-hook "find /etc/letsencrypt/ecc-certs/example.com/ -name '*.pem' -type f -exec chmod 600 {} \;" --renew-hook "find /etc/letsencrypt/ecc-certs/example.com/ -name '*.pem' -type f -exec chmod 600 {} \; -exec service nginx reload \;"

The above commands also take of creating the custom directory, setting the permissions, and reloading Nginx

To generate DH parameters for secure key exchange:

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

Word of warning - the above step may take some time to complete!

Create a header.conf file as below:

mkdir -p /etc/nginx/custom-config
nano /etc/nginx/custom-config/header.conf

and enter the following:

add_header Front-End-Https on;
add_header Content-Security-Policy upgrade-insecure-requests;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload;";
add_header X-Frame-Options SAMEORIGIN;
add_header X-Download-Options noopen;
add_header X-Permitted-Cross-Domain-Policies none;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Feature-Policy "geolocation 'none'; camera 'none'; speaker 'none';";

Create a proxy.conf file as below:

nano /etc/nginx/custom-config/proxy.conf

and enter the following:

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header Early-Data $ssl_early_data;
proxy_connect_timeout 3600;
proxy_send_timeout 3600;
proxy_read_timeout 3600;
proxy_redirect off;
proxy_buffering off;
proxy_max_temp_file_size 0;
proxy_store off;
proxy_http_version 1.1;
proxy_headers_hash_max_size 512;
proxy_headers_hash_bucket_size 64;

Create a ssl.conf file and enter the following:

nano /etc/nginx/custom-config/ssl.conf

and enter the following:

ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_session_timeout 4h;
ssl_session_cache shared:SSL:30m;
ssl_session_tickets off;
ssl_protocols TLSv1.3 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_early_data on;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_stapling on;
ssl_stapling_verify on;
resolver 127.0.0.1 1.1.1.1 valid=300s;
resolver_timeout 10s;

To create a server block, we first make sure the directories are created:

mkdir /etc/nginx/sites-available
mkdir /etc/nginx/sites-enabled

and then create a site config file:

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

The config file should look something like below:

server {

    listen 80 default_server;
    listen [::]:80 default_server;

    return 301 https://$host$request_uri;
}

server {

    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name www.example.com;

    ssl_certificate /etc/letsencrypt/rsa-certs/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/rsa-certs/example.com/privkey.pem;
    ssl_certificate /etc/letsencrypt/ecc-certs/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/ecc-certs/example.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload;";

    return 301 https://example.com$request_uri;
}

server {

    listen 443 ssl http2 default_server;
    listen [::]:443 ssl http2 ipv6only=on;

    server_name example.com;

    ssl_certificate /etc/letsencrypt/rsa-certs/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/rsa-certs/example.com/privkey.pem;
    ssl_certificate /etc/letsencrypt/ecc-certs/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/ecc-certs/example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/ecc-certs/example.com/chain.pem;

    include /etc/nginx/custom-config/ssl.conf;
    include /etc/nginx/custom-config/header.conf;

    location / {
        include /etc/nginx/custom-config/proxy.conf;
        proxy_pass http://127.0.0.1:8080;
    }
}

Quick Explanation:

The config starts with a server block that redirects traffic from HTTP to HTTPS for the domains example.com and www.example.com. There are also two server blocks listening on secure port 443, both for IPv4 and IPv6. The first block redirects all requests for https://www.example.com to https://example.com. Note - You will also need to have an entry in your DNS for 'www' for this to work. The next section configures the SSL certificate locations. Then we include the 3 configuration files we created earlier, and finally we tell Nginx to pass all requests through to the Apache backend at 127.0.0.1:8080

Now we need to make sure the necessary settings are set in the main nginx config file:

nano /etc/nginx/nginx.conf
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;
    error_log /var/log/nginx/error.log;

    server_tokens off;
    sendfile on;
    tcp_nodelay on;
    keepalive_timeout  65;
    types_hash_max_size 2048;

    #gzip  on;

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

In particular, make sure the include file for sites-enabled in within the http block.

To enable a site, we create a symbolic link in the sites-enabled folder:

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

Check the Nginx config with:

nginx -t

Reload to apply all the changes:

service nginx reload

Exit root

exit

Further Reading:

https://mozilla.github.io/server-side-tls/ssl-config-generator/
https://cipherli.st/
https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
https://wiki.mozilla.org/Security/Server_Side_TLS
https://geekflare.com/http-header-implementation/
https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/#
http://nginx.org/en/docs/http/ngx_http_proxy_module.html
https://haydenjames.io/nginx-tuning-tips-tls-ssl-https-ttfb-latency/ https://gist.github.com/plentz/6737338

Module Configuration

GeoIP2

We have the GeoIP2 module compiled into Nginx, but we now need the database in order to make use of it.

Update the apt cache and install GeoIPUpdate

apt update && apt install geoipupdate

Create an account with MaxMind at https://www.maxmind.com/en/geolite2/signup

Make a note of the AccountID and LicenseKey once the account is set up.

Create a configuration file (if it doesn't already exist):

nano /etc/GeoIP.conf

And paste the following into it, replacing the placeholders with the correct information:

# The following AccountID and LicenseKey are required placeholders.
# For geoipupdate versions earlier than 2.5.0, use UserId here instead of AccountID.
AccountID <insert accountid here>
LicenseKey <insert licensekey here>

# Include one or more of the following edition IDs:
# * GeoLite2-City - GeoLite 2 City
# * GeoLite2-Country - GeoLite2 Country
# For geoipupdate versions earlier than 2.5.0, use ProductIds here instead of EditionIDs.
EditionIDs GeoLite2-City GeoLite2-Country

Run geoipupdate in order to download the current database:

geoipupdate

Then create a cron job:

crontab -e
8 7 * * 4 /usr/local/bin/geoipupdate > /dev/null 2>&1

The above cron job will run geoipupdate at At 07:08 every 4th day of the week (Thursday).

Example Usage:

We'll now use this information to GeoIP based rule.

Open your nginx.conf file for editing:

nano /etc/nginx/nginx.conf

and add the following inside the http section:

...
    geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
            $geoip2_data_country_code country iso_code;
      }

    map $geoip2_data_country_code $allowed_country {
            default no;
            US yes;
      }
...

The first section tells Nginx where to find the GeoIP database.

The second section creates a variable called $allowed_country and sets the default value to 'no' and a value of 'yes' if the country code is 'US'.

Now open your server block in your site config file - for example:

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

and add the following section:

server {
...
    if ($allowed_country = no) {
              return 403;
        }
...
}

So what we are saying here is that if a client device tries to access the site and the IP is not classified as being from the US, then access is forbidden (403 is the HTTP error code for 'forbidden').

Obviously you can amend this to your own requirements.

GeoIP2 uses the ISO 3166-2 standard and country codes can be found at https://en.wikipedia.org/wiki/ISO_3166-2

Finally, reload or restart Nginx:

service nginx reload

Headers More

This module allows you to add, set, or clear any output or input header that you specify.

This is an enhanced version of the standard headers module because it provides more utilities like resetting or clearing "builtin headers" like Content-Type, Content-Length, and Server.

It also allows you to specify an optional HTTP status code criteria using the -s option and an optional content type criteria using the -t option while modifying the output headers with the more_set_headers and more_clear_headers directives.

Here we'll use it to hide the server header:

nano /etc/nginx/nginx.conf

Add the following directive within the http block:

http {

    ...

    more_clear_headers Server;

    ...
}

Reload Nginx

service nginx reload

Further Reading

https://github.com/openresty/headers-more-nginx-module#readme

Brotli

This module allows you to compress data using Brotli instead of Gzip. Simply add the following lines to the optimisation.conf file created earlier:

brotli on;
brotli_comp_level 6;
brotli_static on;
brotli_types text/plain text/css application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript image/x-icon image/vnd.microsoft.icon image/bmp image/svg+xml;

Reload Nginx

nginx reload

You can test that it's working at the following website:
https://nixcp.com/tools/brotli-test/

Further Reading

https://github.com/google/ngx_brotli/blob/master/README.md

Virtual Host Traffic Status

This is an Nginx module that provides access to virtual host status information. It contains the current status such as servers, upstreams, caches. This is similar to the live activity monitoring of nginx plus. The built-in html is also taken from the demo page of old version.

It's configured as follows:

http {
    vhost_traffic_status_zone;

    ...

    server {

    ...

        location /vts_status {
            vhost_traffic_status_display;
            vhost_traffic_status_display_format html;
        }

The vhost_traffic_status_display_format can be set to html or, if you prefer, json.

The page can be restricted by IP address as described in the next section

Further Reading

https://github.com/laurrentt/nginx-module-vts

Restrict Sensitive Pages to IP Addresses

Create a file called allowed-ip.conf

nano /etc/nginx/conf.d/allowed-ip.conf

and to this file, add your static IP addresses - one per line - as follows:

allow 192.168.1.11;  
allow 192.168.1.22;

Obviously substitute those addresses with your own static IP addresses.

Once done, you can simply add this to the relevant location block within your server block. for example, to restrict the Wordpress login page to your static IP addresses only:

location ~ \.php$ {
    location ~ \wp-login.php$ {
        include /etc/nginx/conf.d/allowed-ip.conf;
        deny all;
    }
}

If you want to whitelist a DynDNS domain rather than an IP address, follow the following procedure:

Create the following file:

nano /usr/local/sbin/get-ddns-nginx.sh

and add the following, substituting 'example.dyndns.com' with your own DDNS address:

#!/bin/bash
host example.dyndns.com | grep "has address" | sed 's/.*has address //' | awk '{print "allow\t\t" $1 ";\t\t# DDNS IP" }' > /etc/nginx/conf.d/allowed-ddns.conf
service nginx reload

Make the file executable:

chmod 700 /usr/local/sbin/get-ddns-nginx.sh

And add a line to cron:

crontab -e
*/30 * * * * /usr/local/sbin/get-ddns-nginx.sh > /dev/null 2>&1

This will update the IP address associated with your DynDNS domain every 30 minutes, and place the correct 'allow' command into a file at /etc/nginx/conf.d/allowed-ddns.conf

The only thing left to do is add the include into the Nginx location block along with your other whitelisted addresses eg:

location ~ \.php$ {
    location ~ \wp-login.php$ {
        include /etc/nginx/conf.d/allowed-ip.conf;
        include /etc/nginx/conf.d/allowed-ddns.conf;
        deny all;
    }
}

Don't forget to reload Nginx after any changes!

Restrict Sensitive Pages with a Password

Instead of restriting a page to an IP Address, we could instead password protect the location. In order to do this, we first create out password hash using the htpasswd command

htpasswd -c /etc/nginx/htpasswd yourStrongPassword

Make sure the file is secure:

chown root:nginx /etc/nginx/htpasswd
chmod 640 /etc/nginx/htpasswd

You would then restrict your sensitive page as follows:

...

    location /sensitivepage {
        include /etc/nginx/conf.d/allowed-ip.conf;
        deny all;
    }
}

Don't forget to reload Nginx after any changes!