Secure your website or web app with nginx and modsecurity (WAF - web app firewall)

A POS near a plant

Have you ever

Have you ever deployed your backend application so that it faces the public internet directly, without any protection layer?
Direct access to backend examples
If so, this isn’t considered a good security practice and leaves your backend vulnerable to external attackers.
Let’s secure it with an affordable solution.

What we gonna do?

We’ll install nginx and compile ModSecurity into it. Nginx will act as a reverse proxy, and with ModSecurity’s support, it will help filter out malicious requests before they reach your backend.

PART 1: COMPILING AND INSTALLING

1. Install dependencies

We need to install the following dependencies in order to build nginx and ModSecurity from source code

dnf groupinstall -y 'Development tools'
dnf install -y libmodsecurity libmodsecurity-devel
dnf install -y pcre pcre-devel
dnf install -y openssl openssl-devel
dnf install -y libxml2 libxml2-devel
dnf install -y libxslt libxslt-devel
dnf install -y gd gd-devel
dnf install -y perl-ExtUtils-Embed

2. Download source code

Go to https://nginx.org/en/download.html and find the latest stable release version and download it

# Go to temporary directory
cd /tmp
# Download nginx source code from nginx download page
curl https://nginx.org/download/nginx-1.28.0.tar.gz -O
# Decompress it
tar -xzf nginx-1.28.0.tar.gz

You’ll also need to clone ModSecurity from GitHub:

git clone https://github.com/owasp-modsecurity/ModSecurity-nginx.git

3. Compiling nginx with ModSecurity

# Go into nginx source code directory
cd /tmp/nginx-1.28.0

# Run configuration tool with lots of parametters
./configure --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules \
--conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body \
--http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi \
--http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid \
--lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-compat --with-debug --with-file-aio \
--with-http_addition_module --with-http_auth_request_module --with-http_dav_module \
--with-http_degradation_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module \
--with-http_image_filter_module=dynamic --with-http_mp4_module --with-http_perl_module=dynamic \
--with-http_random_index_module --with-http_realip_module --with-http_secure_link_module \
--with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module \
--with-http_v2_module --with-http_xslt_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre \
--with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-stream_ssl_preread_module --with-threads \
--with-cc-opt='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security
-Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong
-specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -march=x86-64-v2 -mtune=generic -fasynchronous-unwind-tables
-fstack-clash-protection -fcf-protection' \
--with-ld-opt='-Wl,-z,relro -Wl,--as-needed -Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld
-specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -Wl,-E -L/usr/lib64' \
--add-module=/tmp/ModSecurity-nginx

How did I come up with this configure command?

I got this configuration by first installing nginx using dnf (dnf install nginx), then running nginx -V to see the compilation parameters that were used in the official package.

3.1 Make it

# Run make to actually compile the source code. This might take a moment
make

Install

# And then install it
make install

4. Create nginx user

A dedicated user account will be used to run nginx process

useradd --system --home-dir /var/cache/nginx --shell /sbin/nologin --comment "nginx user" --user-group nginx

5. Create a service file

touch /usr/lib/systemd/system/nginx.service

And then paste the following content into it:

[Unit]
Description=The nginx HTTP and reverse proxy server
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
# Nginx will fail to start if /run/nginx.pid already exists but has the wrong
# SELinux context. This might happen when running `nginx -t` from the cmdline.
# https://bugzilla.redhat.com/show_bug.cgi?id=1268621
ExecStartPre=/usr/bin/rm -f /run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
KillSignal=SIGQUIT
TimeoutStopSec=5
KillMode=mixed
PrivateTmp=true

[Install]
WantedBy=multi-user.target

6. Clone the core rule set

The steps above make nginx available and ready to serve.
While ModSecurity features are now available, they work by following specific rules. Without rules, ModSecurity does nothing.
So let’s gather rules from OWASP (Open Web Application Security Project) and place them on our server:

# Go to /tmp directory and clone. 
# So that files will be downloaded into '/tmp/coreruleset'

cd /tmp

git clone https://github.com/coreruleset/coreruleset.git

cd /tmp/coreruleset

# We are going to use rules from the branch v4.0/main
git checkout v4.0/main

7. Create a directory to hold those rules

mkdir /etc/nginx/ModSecurity

mv /tmp/coreruleset /etc/nginx/ModSecurity/owasp-modsecurity-crs

8. Modify files in rules directory

We need to make some changes to the files in the configuration files in the core rule set.

8.1 crs-setup.conf

cd /etc/nginx/ModSecurity/owasp-modsecurity-crs
cp crs-setup.conf.example crs-setup.conf

8.2 Rename files

# Rename files
cd /etc/nginx/ModSecurity/owasp-modsecurity-crs/rules/
mv REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
mv RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf

9. Create include file

Create file /etc/nginx/ModSecurity/modsec_includes.conf with the following content:

include /etc/nginx/ModSecurity/modsecurity.conf
include /etc/nginx/ModSecurity/owasp-modsecurity-crs/crs-setup.conf
include /etc/nginx/ModSecurity/owasp-modsecurity-crs/rules/*.conf

This includes.conf file will be used in an nginx directive, which we’ll configure shortly.
The modsecurity.conf file contains the base setup for ModSecurity and comes from the OWASP ModSecurity project. We don’t have this file yet, so we’ll need to download it next.
The crs-setup.conf file also contains base setup for ModSecurity, but it’s specifically designed to work with the Core Rule Set. As you saw in step 7, this file comes from the OWASP Core Rule Set project.

10. Download the base configuration file

Clone this repository

cd /tmp
git clone https://github.com/owasp-modsecurity/ModSecurity.git

# And checkout the branch v3/master
git checkout v3/master

Then copy the configuration file to the correct location:

cp /tmp/ModSecurity/modsecurity.conf-recommended /etc/nginx/ModSecurity/modsecurity.conf

This configuration file references a unicode.mapping file, so we also need to copy that file:

cp /tmp/ModSecurity/unicode.mapping /etc/nginx/ModSecurity/unicode.mapping

By default, ModSecurity only detect malicious requests but does not block them. You may want to change this behaviour by editing modsecurity.conf

# /etc/nginx/ModSecurity/modsecurity.conf
SecRuleEngine On

11. nginx.conf main configuration file

Check /etc/nginx/nginx.conf and make sure it contains a line that instructs nginx to include all configuration files from other locations. It should contain the line below or something similar:

# nginx.conf 
include /etc/nginx/conf.d/*.conf;

12. Check the setup at current stage

At this point, the configuration is basically complete and nginx should function correctly. You can test it by running:

nginx -t

It should print a message indicating that everything is working properly:

nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

CONFIGURE A SITE

Now we should be able to configure a specific site that is protected by ModSecurity

Create your site’s configuration file:

touch /etc/nginx/conf.d/th-loyal.com.conf

Its content:
server {
server_name ;
root /usr/share/nginx/html;
index index.html;

modsecurity on;
modsecurity_rules_file /etc/nginx/ModSecurity/modsec_includes.conf;

# ... other configuration

}