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!