Have a Question?
< All Topics
Print

ModSecurity with Nginx on CentOS 8

Tutorial on how to configure ModSecurity with Nginx on CentOS 8

ModSecurity, sometimes called Modsec, is an open-source web application firewall (WAF) to provide protections against generic classes of vulnerabilities using the OWASP ModSecurity Core Rule Set (CRS)

Preparation of CentOS 8

  1. Install a fresh copy of CentOS 8 with minimal install in VMware ESXi Host
  2. Perform System Update
dnf check-update
dnf update

## Free up disk space by deleting all downloaded software package and all cached repository 
dnf clean all
  1. Install required build tools and dependencies
dnf install gcc-c++ flex bison yajl curl-devel curl zlib-devel pcre-devel autoconf automake git curl make libxml2-devel pkgconfig libtool httpd-devel redhat-rpm-config git wget openssl openssl-devel vim nano
  1. Install EPEL & Remi Repository
dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
dnf --enablerepo=PowerTools install doxygen yajl-devel
  1. Install GeoIP Module to allows system administrators to redirect or block web traffic according to the client’s geographical location
dnf --enablerepo=remi install GeoIP-devel

Install ModSecurity

  1. Clone the latest LibModsecurity GitHub repository to /opt/modsec
mkdir /opt/modsec
cd /opt/modsec

git clone --depth 1 -b v3/master --single-branch https://github.com/SpiderLabs/ModSecurity
  1. Compile and install ModSecurity
cd ModSecurity
git submodule init
git submodule update

./build.sh
./configure

make
make install

Compile Nginx to support ModSecurity

  1. Download the ModSecurity-nginx connector which provides a communication channel between Nginx and LibModsecurity
cd /opt/modsec 
git clone https://github.com/SpiderLabs/ModSecurity-nginx.git
  1. Create a non-privileged system user and group for Nginx
useradd -r -M -s /sbin/nologin -d /usr/local/nginx nginx
  1. Download Nginx and extract it to /opt/nginx
cd /opt
wget http://nginx.org/download/nginx-1.17.6.tar.gz
tar xzf nginx-1.17.6.tar.gz
  1. Compile Nginx from source code
cd nginx-1.17.6

./configure --user=nginx --group=nginx --with-pcre-jit --with-debug --with-http_ssl_module --with-http_v2_module --with-http_realip_module --add-module=/opt/modsec/ModSecurity-nginx

make
make install
  1. Create a systemd service for Nginx
nano /etc/systemd/system/nginx.service

    [Unit]
    Description=The nginx HTTP and reverse proxy server
    After=network.target remote-fs.target nss-lookup.target
    
    [Service]
    Type=forking
    PIDFile=/run/nginx.pid
    ExecStartPre=/usr/bin/rm -f /run/nginx.pid
    ExecStartPre=/usr/sbin/nginx -t
    ExecStart=/usr/sbin/nginx
    ExecReload=/bin/kill -s HUP $MAINPID
    KillSignal=SIGQUIT
    TimeoutStopSec=5
    KillMode=mixed
    PrivateTmp=true
    
    [Install]
    WantedBy=multi-user.target
  1. Create a symbolic link of Nginx binary to /usr/sbin/ path.
ln -s /usr/local/nginx/sbin/nginx /usr/sbin/
  1. Change the location of pid to /run/nginx.pid in Nginx config file
nano /usr/local/nginx/conf/nginx.conf
	  pid        /run/nginx.pid;
  1. Enable and start Nginx service
systemctl enable --now nginx
  1. Allow Firewalld for HTTP & HTTPS
firewall-cmd --permanent --zone=public --add-service=http
firewall-cmd --permanent --zone=public --add-service=https
# Restart Firewalld
systemctl restart firewalld.service

# Verify HTTP & HTTPS is allowed
 firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: ens192
  sources:
  services: cockpit dhcpv6-client http https ssh
  ports:
  1. You should be able to access to Nginx Web Server via http://IP-ADDRESS now

ModSecurity with Nginx on CentOS 8

Let’s Encrypt Wildcard SSL Certificate

Generate Let’s Encrypt wildcard SSL Certificate with certbot with DNS Plugin for DnsMadeEasy for *.aventis.dev

#Install Certbot 
sudo dnf install certbot
#Install DNS Plugin 
sudo dnf install python3-certbot-dns-dnsmadeeasy

Obtain the API & Secret Key from DnsMadeEasy and save it to ~/.secrets/dnsmadeeasy.ini

mkdir ~/.secrets
nano ~/.secrets/dnsmadeeasy.ini

# DNS Made Easy API credentials used by Certbot
dns_dnsmadeeasy_api_key = XXXXXXXXXXXXXXXXXXXXXXXXX
dns_dnsmadeeasy_secret_key = XXXXXXXXXXXXXXXXXXXXXX

Request the SSL Certificate from Let’s Encrypt

[root@centos /]# certbot certonly \
>   --dns-dnsmadeeasy \
>   --dns-dnsmadeeasy-credentials ~/.secrets/dnsmadeeasy.ini \
>   -d *.aventis.dev

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator dns-dnsmadeeasy, Installer None
Enter email address (used for urgent renewal and security notices)
 (Enter 'c' to cancel): [email protected]

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for aventis.dev
Waiting 60 seconds for DNS changes to propagate
Waiting for verification...
Cleaning up challenges
Subscribe to the EFF mailing list (email: [email protected]).

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/aventis.dev/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/aventis.dev/privkey.pem
   Your cert will expire on 2020-12-06. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

Let’s Encrypt wildcard SSL Certificate is stored in /etc/letsencrypt/live/aventis.dev

[root@centos /]# ls  /etc/letsencrypt/live/aventis.dev
cert.pem  chain.pem  fullchain.pem  privkey.pem  README

Enable SSL & HTTP/2 Support

Copy the cert.pem & privkey.pem to /usr/local/nginx/ssl/

mkdir /usr/local/nginx/ssl
cp /etc/letsencrypt/live/aventis.dev/cert.pem  /usr/local/nginx/ssl/
cp /etc/letsencrypt/live/aventis.dev/privkey.pem  /usr/local/nginx/ssl/

Enable SSL Support in /usr/local/nginx/conf/nginx.conf

nano /usr/local/nginx/conf/nginx.conf

server {
        #Enable SSL
        listen       443 ssl http2;
        server_name  localhost uat.aventis.dev;

        #enable SSL
        ssl_certificate /usr/local/nginx/ssl/cert.pem;
        ssl_certificate_key /usr/local/nginx/ssl/privkey.pem;
}

Verify the configuration file of Nginx and restart the service

nginx -t
systemctl restart nginx

Go to https://uat.aventis.dev and verify that HTTP/2 is enabled with Google Chrome – Developer Tool

PHP-FPM with NGINX

PHP 7.2 is the default installation profiles

$ sudo dnf module list php

Last metadata expiration check: 0:48:01 ago on Mon 07 Sep 2020 03:54:30 PM +08.
CentOS-8 - AppStream
Name                             Stream                                  Profiles                                              Summary
php                              7.2 [d]                                 common [d], devel, minimal                            PHP scripting language
php                              7.3                                     common [d], devel, minimal                            PHP scripting language

Remi's Modular repository for Enterprise Linux 8 - x86_64
Name                             Stream                                  Profiles                                              Summary
php                              remi-7.2                                common [d], devel, minimal                            PHP scripting language
php                              remi-7.3                                common [d], devel, minimal                            PHP scripting language
php                              remi-7.4 [e]                            common [d], devel, minimal                            PHP scripting language

Install the latest PHP 7.4 Module

$ sudo dnf module reset php 
#Enable PHP 7.4
$ sudo dnf module enable php:remi-7.4
#Install PHP 7.4
$ sudo dnf install php php-opcache php-gd php-curl php-mysqlnd
#Verify the PHP version installed
[kwyong@centos ~]$ php -v
PHP 7.4.10 (cli) (built: Sep  1 2020 13:58:08) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.10, Copyright (c), by Zend Technologies

Change the PHP-FPM to use nginx instead of Apache user in

$ nano /etc/php-fpm.d/www.conf

user = nginx
group = nginx

Enable and start PHP-FPM

$ sudo systemctl enable php-fpm
$ sudo systemctl start php-fpm

Configure PHP-FPM with Nginx by modifying /usr/local/nginx/conf/nginx.conf

$ sudo nano /usr/local/nginx/conf/nginx.conf

  location ~ \.php$ {
    root           html;
    fastcgi_pass  unix:/run/php-fpm/www.sock;
    fastcgi_index  index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include        fastcgi_params;
    }

Restart Nginx & PHP-FPM services

$ sudo systemctl restart nginx
$ sudo systemctl restart php-fpm

Create info.php in /usr/local/nginx/html (Default Root Directory for Nginx)

$ nano /usr/local/nginx/html/info.php
<?php
 phpinfo();
?>

Go to https://uat.aventis.dev/info.php to verify PHP is installed successfully

Confgure Nginx with ModSecurity

  1. Copy the Sample ModSecurity config file and unicode.mapping from ModSecurity source directory to Nginx configuration directory
cp /opt/modsec/ModSecurity/modsecurity.conf-recommended /usr/local/nginx/conf/modsecurity.conf
cp /opt/modsec/ModSecurity/unicode.mapping /usr/local/nginx/conf/
  1. Create a backup for Nginx config file
cp /usr/local/nginx/conf/nginx.conf /usr/local/nginx/conf/nginx.conf.bak
  1. Create /var/log/nginx to keep all the access & error log
mkdir /var/log/nginx
  1. Add the following to the nginx.conf file
nano /usr/local/nginx/conf/nginx.conf

user nginx;

http {
    server {
        modsecurity  on;
        modsecurity_rules_file  /usr/local/nginx/conf/modsecurity.conf;
        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;
        }
  1. Restart Nginx service
systemctl restart nginx

By default, ModSecurity is set on detection only mode where it only logs the requests based on the triggered rules without blocking anything

  1. Turn On ModSecurity Rule Engine
nano /usr/local/nginx/conf/modsecurity.conf
  1. Change the default log directory for ModSecurity to /var/log/nginx/modsec_audit.log
# Use a single file for logging. This is much easier to look at, but
# assumes that you will use the audit log only ocassionally.
SecAuditLogType Serial
SecAuditLog /var/log/nginx/modsec_audit.log

Install OWASP ModSecurity Core Rule Set (CRS)

The OWASP ModSecurity Core Rule Set (CRS) is a set of generic attack detection rules for use with ModSecurity. It aims at protecting the web applications from a wide range of attacks, including the OWASP Top Ten, minimum of false alerts.

  1. Clone the CRS from GitHub Repository to /usr/local/nginx/conf
git clone https://github.com/SpiderLabs/owasp-modsecurity-crs.git /usr/local/nginx/conf/owasp-crs
  1. Rename the crs-setup.conf.example to crs-setup.conf
cp /usr/local/nginx/conf/owasp-crs/crs-setup.conf.example /usr/local/nginx/conf/owasp-crs/crs-setup.conf
  1. Include the OWASP rule to /usr/local/nginx/conf/modsecurity.conf
nano /usr/local/nginx/conf/modsecurity.conf
	Include owasp-crs/crs-setup.conf
	Include owasp-crs/rules/*.conf	
  1. Verify Nginx configuration file and restart Nginx service
nginx -t
systemctl restart nginx

Testing ModSecurity on Nginx

Run the following command injection

[root@prod-centos conf]# curl localhost/index.html?exec=/bin/bash
    <html>
    <head><title>403 Forbidden</title></head>
    <body>
    <center><h1>403 Forbidden</h1></center>
    <hr><center>nginx/1.17.6</center>
    </body>
    </html>

Check the log file

[root@prod-centos conf]# tail /var/log/nginx/modsec_audit.log
ModSecurity: Warning. Matched "Operator `Rx' with parameter `^[\d.:]+$' against variable `REQUEST_HEADERS:Host' (Value: `192.168.1.236' ) [file "/usr/local/nginx/conf/owasp-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "722"] [id "920350"] [rev ""] [msg "Host header is a numeric IP address"] [data "192.168.1.236"] [severity "4"] [ver "OWASP_CRS/3.2.0"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/PROTOCOL_VIOLATION/IP_HOST"] [tag "WASCTC/WASC-21"] [tag "OWASP_TOP_10/A7"] [tag "PCI/6.5.10"] [hostname "192.168.1.236"] [uri "/index.html"] [unique_id "1591167373"] [ref "o0,13v46,13"]
ModSecurity: Warning. Matched "Operator `PmFromFile' with parameter `unix-shell.data' against variable `ARGS:exec' (Value: `/bin/bash' ) [file "/usr/local/nginx/conf/owasp-crs/rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf"] [line "496"] [id "932160"] [rev ""] [msg "Remote Command Execution: Unix Shell Code Found"] [data "Matched Data: bin/bash found within ARGS:exec: /bin/bash"] [severity "2"] [ver "OWASP_CRS/3.2.0"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-shell"] [tag "platform-unix"] [tag "attack-rce"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/WEB_ATTACK/COMMAND_INJECTION"] [tag "WASCTC/WASC-31"] [tag "OWASP_TOP_10/A1"] [tag "PCI/6.5.2"] [hostname "192.168.1.236"] [uri "/index.html"] [unique_id "1591167373"] [ref "o1,8v21,9t:urlDecodeUni,t:cmdLine,t:normalizePath,t:lowercase"]
ModSecurity: Access denied with code 403 (phase 2). Matched "Operator `Ge' with parameter `5' against variable `TX:ANOMALY_SCORE' (Value: `8' ) [file "/usr/local/nginx/conf/owasp-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "80"] [id "949110"] [rev ""] [msg "Inbound Anomaly Score Exceeded (Total Score: 8)"] [data ""] [severity "2"] [ver "OWASP_CRS/3.2.0"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "192.168.1.236"] [uri "/index.html"] [unique_id "1591167373"] [ref ""]

Table of Contents
Scroll to Top