I have divided my home network into three separate areas: a datacenter, a DMZ (demilitarized zone), and an internal client network. To prevent my clients from having direct access to the datacenter, I have set up an NGINX reverse proxy in the DMZ. This allows both clients within the network and those from the WAN (wide area network) to connect. It is great to be able to secure all sites with a Let’s Encrypt certificate.
In the coming days, I plan to publish an article on configuring Apache2 as a reverse proxy with ModSecurity and the OWASP ruleset.
Table of Contents
To install NGINX and Certbot from the default repository on a debian-based operating system, you can use the package manager apt:
apt update apt install nginx certbot python3-certbot-nginx
To request a wildcard certificate for your domain using a DNS challenge and Certbot, you will need to follow these steps:
certbot certonly --manual --preferred-challenges=dns --server https://acme-v02.api.letsencrypt.org/directory -d *.stangneth.com
Follow the instructions provided by Certbot for creating a TXT record in your DNS settings. This will typically involve logging into your domain provider’s control panel and adding a new TXT record with the value provided by Certbot.
Once the TXT record has been added, return to the terminal and press Enter to continue the certificate request process.
Certbot will verify that the TXT record has been added correctly and, if successful, will issue a wildcard certificate for your domain.
Please note that this process may vary depending on your specific domain provider and the DNS management tools they offer. You may need to consult your provider’s documentation or support resources for additional guidance.
It will be stored under:
/etc/letsencrypt/live/stangneth.com/
Start and enable NGINX:
systemctl start nginx systemctl enable nginx
Check if NGINX is running without failures:
systemctl status nginx
To unlink the default site in NGINX, you will need to remove the symbolic link to the default site configuration file from the sites-enabled
directory. This directory is used by NGINX to determine which site configurations should be enabled and used when the server starts. Disable the default site:
unlink /etc/nginx/sites-enabled/default
Switch to the sites-available
directory and create an own configuration file:
cd /etc/nginx/sites-enabled/ vi 001-example.stangneth.com.conf
To catch all default requests on port 80 and port 443 (HTTP and HTTPS) and return a 404 error in NGINX, you can use the following configuration:
server { listen 80 default_server; server_name _; return 404; } server { listen 443 ssl default_server; server_name _; ssl_certificate /etc/letsencrypt/live/stangneth.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/stangneth.com/privkey.pem; return 404; }
This configuration sets up two server blocks, one for port 80 and one for port 443. The default_server
directive specifies that these blocks should catch all default requests that do not match any other server block. The return
directive is used to return a 404 error to the client.
To rewrite all HTTP requests to HTTPS in NGINX, you can use the following configuration:
server { listen 80; server_name example.stangneth.com; rewrite ^ https://$host$request_uri permanent; }
Finally you can add the part for the first website on port 443:
server { listen 443 ssl; server_name example.stangneth.com; ssl_certificate /etc/letsencrypt/live/stangneth.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/stangneth.com/privkey.pem; ssl_session_cache builtin:1000 shared:SSL:10m; ssl_protocols TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; ssl_prefer_server_ciphers on; access_log /var/log/nginx/access.log; location / { allow 172.16.0.0/24; proxy_pass https://172.16.0.234; proxy_read_timeout 90; } }
I added an allow for a specific IP range, so not everyone from the Internet can reach the page. That can be good for ressources which should have an offical certificate from letsencrypt but not reachable from the Internet. In my case a mediawiki.
Activate the web site:
ln -s /etc/nginx/sites-avaialable/001-example.stangneth.com.conf /etc/nginx/sites-enabled/001-example.stangneth.com.conf
Restart now NGINX and check if the site is reachable (be sure DNS is ok!):
service nginx configtest systemctl restart nginx systemctl status nginx
Install dependecies:
apt install -y apt-utils autoconf automake build-essential git libcurl4-openssl-dev libgeoip-dev liblmdb-dev libpcre++-dev libtool libxml2-dev libyajl-dev pkgconf wget zlib1g-dev
Switch to /usr/local/src
and clone the git repo:
cd /usr/local/src git clone --depth 1 -b v3/master --single-branch https://github.com/SpiderLabs/ModSecurity
Switch to the directory and install ModSecurity:
cd ModSecurity/ git submodule init git submodule update ./build.sh ./configure make make install
Next clone the NGINX modsecurity connector from git:
cd .. git clone --depth 1 https://github.com/SpiderLabs/ModSecurity-nginx.git
Check which version of NGINX is installed:
nginx -v
In this case it’s 1.18.0. So I downloaded the source code:
wget http://nginx.org/download/nginx-1.18.0.tar.gz tar zxvf nginx-1.18.0.tar.gz
Now compile the modsecurity-nginx module and copy it to the module directory:
cd nginx-1.18.0/ ./configure --with-compat --add-dynamic-module=../ModSecurity-nginx make modules cp /usr/local/src/nginx-1.18.0/objs/ngx_http_modsecurity_module.so /etc/nginx/modules/
Within the nginx.conf file load the module:
vi /etc/nginx/nginx.conf
user www-data; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; load_module /etc/nginx/modules/ngx_http_modsecurity_module.so;
Restart NGINX and check the status:
systemctl restart nginx systemctl status nginx
Create a new directory for the ModSecurity configuration files:
mkdir /etc/nginx/modsec
Download the default modsecurity.conf-recommed from the SpiderLabs git repo:
wget -P /etc/nginx/modsec/ https://raw.githubusercontent.com/SpiderLabs/ModSecurity/v3/master/modsecurity.conf-recommended mv /etc/nginx/modsec/modsecurity.conf-recommended /etc/nginx/modsec/modsecurity.conf
To be sure that ModSecurity can find the unicode.mapping file copy it to /etc/nginx/modsec
cp /usr/local/src/ModSecurity/unicode.mapping /etc/nginx/modsec/
In the modsecurity.conf enable the SecRuleEngine from “DetectionOnly” to “On”:
vi /etc/nginx/modsec/modsecurity.conf
Create a .conf file where it’s possible to load necessary configurations for specific websites:
vi /etc/nginx/modsec/main.conf
Include "/etc/nginx/modsec/modsecurity.conf"
Download the latest OWASP coreruleset from git repo:
cd /usr/local/src wget https://github.com/SpiderLabs/owasp-modsecurity-crs/archive/v3.0.2.tar.gz tar -xzvf v3.0.2.tar.gz cd owasp-modsecurity-crs-3.0.2/
Copy the example configuration file:
cp crs-setup.conf.example crs-setup.conf
Open the main.conf file and load the ruleset:
# OWASP CRS v3 rules Include /usr/local/src/owasp-modsecurity-crs-3.0.2/crs-setup.conf Include /usr/local/src/owasp-modsecurity-crs-3.0.2/rules/*.conf
To enable ModSecurity for a specific site edit the .conf file.
vi /etc/nginx/sites-available/001-example.stangneth.com.conf
server { listen 443 ssl; server_name example.stangneth.com; modsecurity on; modsecurity_rules_file /etc/nginx/modsec/main.conf; modsecurity_rules ' SecRuleRemoveById 949110 ';
The SecRuleRemoveById section is an example. It will often happen that a website dont need the complete ruleset or false/positive alerts will be triggerd.
It is possible to check which rule was taken:
tail -f /var/log/nginx/error.log
An output can look like this example:
2022/12/27 14:38:39 [error] 1155#1155: *18 [client 172.16.0.123] ModSecurity: Access denied with code 403 (phase 2). Matched "Operator `Ge' with parameter `5' against variable `TX:ANOMALY_SCORE' (Value: `5' ) [file "/usr/local/src/owasp-modsecurity-crs-3.0.2/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "44"] [id "949110"] [rev ""] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [data ""] [severity "2"] [ver ""] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "172.16.0.233"] [uri "/api/v1/tickets/14"] [unique_id "167188911882.664343"] [ref ""], client: 172.16.0.123, server: example.stangneth.com, request: "PUT /api/v1/tickets/14?all=true HTTP/1.1", host: "example.stangneth.com", referrer: "https://example.stangneth.com/"
So it was neccessary to remove the [id “949110”] from the configuration for THIS website.
Great guide.
Thanks
Thank you so much for your kind words! I’m delighted to hear that the guide was helpful for you. If you have any further questions or need any assistance, please don’t hesitate to reach out to me. I’m here to help. Thanks again and all the best!