Scott Diemer
October 23, 2026•IT / Server Administration

ModSecurity and Fail2Ban Web Server Security

Server Environment

  • Ubuntu Server
  • Nginx
  • Frontend: farmcityfeed.com
  • Backend: admin.farmcityfeed.com
  • Fail2Ban already installed and running
  • No IPv6 configured on the server

Package Updates

Before making any changes, I updated the server packages:

sudo apt update
sudo apt upgrade

No reboot was required afterward.

I verified that Nginx was still running normally before continuing.

Installing ModSecurity and OWASP CRS

Installed the Nginx ModSecurity module and the OWASP Core Rule Set:

sudo apt install libnginx-mod-http-modsecurity modsecurity-crs

The ModSecurity Nginx module was automatically enabled under:

/etc/nginx/modules-enabled/50-mod-http-modsecurity.conf

The OWASP CRS rules were installed under:

/usr/share/modsecurity-crs/rules/

The main ModSecurity configuration was located at:

/etc/nginx/modsecurity.conf

Loading the OWASP Core Rule Set

The packaged OWASP loader contained IncludeOptional directives that the Nginx ModSecurity parser did not accept.

Instead, I configured:

/etc/nginx/modsecurity_includes.conf

to load the files directly:

include modsecurity.conf
include /etc/modsecurity/crs/crs-setup.conf
include /etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
include /usr/share/modsecurity-crs/rules/*.conf

Nginx successfully loaded 921 ModSecurity rules.

Enabling ModSecurity on the Websites

I enabled ModSecurity on both HTTPS server blocks.

The frontend is proxied to:

localhost:3000

The backend is proxied to:

localhost:5000

The following directives were added inside the HTTPS server blocks:

modsecurity on;
modsecurity_rules_file /etc/nginx/modsecurity_includes.conf;

The HTTP-only redirect blocks were left alone.

Detection-Only Testing

I initially left ModSecurity in detection-only mode:

SecRuleEngine DetectionOnly

This allowed the OWASP rules to inspect traffic without blocking anything.

The audit log was configured at:

/var/log/nginx/modsec_audit.log

I tested SQL injection detection with:

curl -k "https://farmcityfeed.com/?test=%27%20OR%20%271%27%3D%271"

ModSecurity correctly detected the request using OWASP CRS rule:

942100 - SQL Injection Attack Detected via libinjection

The anomaly score reached 5, which is the normal blocking threshold.

Real Scanner Traffic Detected

Almost immediately, ModSecurity started detecting real automated probes against the website.

Some of the requested paths included:

/.env
/.env.local
/.env.production
/.env.staging
/.env.old
/.env.bak
/.env.backup
/api/.env
/public/.env
/storage/.env
/.git/config
/.git/HEAD

Rule 930130 identified these as restricted file access attempts.

Rule 920440 also detected requests using restricted extensions such as:

.backup
.old
.bak

I verified that sensitive files were not publicly accessible.

Requests to the frontend returned 404, and the backend redirected unauthorized requests to the sign-in page.

Enabling Blocking

After verifying detection worked, I changed:

SecRuleEngine DetectionOnly

to:

SecRuleEngine On

After testing and reloading Nginx, malicious requests began returning:

HTTP/1.1 403 Forbidden

Both the SQL injection test and /.env probe were successfully blocked.

GraphQL False Positive

After enabling blocking, I found an important false positive.

Normal users browsing the Products page were generating requests to:

POST /api/graphql

OWASP CRS rule:

932100 - Remote Command Execution: Unix Command Injection

was incorrectly identifying the GraphQL query syntax as shell command injection.

This caused legitimate GraphQL requests to return 403.

The problem specifically involved:

ARGS:json.query

Rather than disabling rule 932100 globally, I created a narrow exclusion for only the GraphQL query argument on /api/graphql.

The exclusion file is:

/etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf

The exclusion added was:

# Farm City GraphQL false positive
# CRS 932100 interprets GraphQL query syntax as Unix command injection.
SecRule REQUEST_URI "@streq /api/graphql" \
    "id:1000001,phase:1,pass,nolog,t:none,ctl:ruleRemoveTargetById=932100;ARGS:json.query"

After reloading Nginx, legitimate GraphQL traffic returned:

HTTP 200

while malicious tests continued to return:

HTTP 403

This confirmed that the exclusion fixed the false positive without disabling the underlying security rule globally.

Fail2Ban Integration

The original plan was to have Fail2Ban read the ModSecurity audit log.

Because the audit log stores the client IP and the Access denied message in different sections, this would have required a multiline Fail2Ban filter.

Instead, I found that Nginx already writes ModSecurity blocks to:

/var/log/nginx/error.log

with the client IP on the same line.

Example:

[client 107.173.164.148] ModSecurity: Access denied with code 403

This made the Fail2Ban configuration much simpler.

Fail2Ban Filter

Created:

/etc/fail2ban/filter.d/nginx-modsecurity.conf

with:

[Definition]

failregex = ^.*\[client <HOST>\] ModSecurity: Access denied with code 403 .*$
ignoreregex =

Tested the filter with:

sudo fail2ban-regex /var/log/nginx/error.log /etc/fail2ban/filter.d/nginx-modsecurity.conf

The test found:

6 matched
1 missed

The missed line was only an unrelated Nginx notice, so the filter was working correctly.

Fail2Ban Jail

Created:

/etc/fail2ban/jail.d/nginx-modsecurity.local

with:

[nginx-modsecurity]

enabled = true
filter = nginx-modsecurity
logpath = /var/log/nginx/error.log

port = http,https
maxretry = 5
findtime = 10m
bantime = 1h

This means an IP that triggers five ModSecurity blocks within ten minutes will be banned for one hour.

The Fail2Ban configuration was tested with:

sudo fail2ban-client -t

and passed successfully.

IPv6 Setting

Because the server does not use IPv6, I explicitly disabled IPv6 support in Fail2Ban.

Created or edited:

/etc/fail2ban/fail2ban.local

with:

[Definition]
allowipv6 = false

This also removed the Fail2Ban warning about the allowipv6 setting.

Final Verification

The ModSecurity Fail2Ban jail is active:

sudo fail2ban-client status nginx-modsecurity

Current behavior is now:

Internet Request
      ↓
Nginx
      ↓
ModSecurity
      ↓
OWASP Core Rule Set
      ↓
Malicious Request?
   ↙            ↘
 No             Yes
 ↓               ↓
Application     403 Block
                  ↓
               Fail2Ban
                  ↓
        Repeated Offender?
             ↙        ↘
           No          Yes
           ↓            ↓
        Continue     IP Ban

Result

The Farm City web server now has an additional layered security setup.

ModSecurity and OWASP CRS inspect incoming web requests and block suspicious activity such as SQL injection attempts and probes for sensitive files.

Fail2Ban watches the ModSecurity blocks and escalates repeated offenders into temporary firewall bans.

The setup also revealed a legitimate GraphQL false positive during testing, which was corrected with a narrowly scoped rule exclusion instead of weakening the WAF globally.

Overall, the final setup adds meaningful protection while allowing the website and its GraphQL API to continue functioning normally.