bokumin.org

Github

Build mod_security + OWASP CRS + fail2ban environment [OpenSUSE]

This article is a translation of the following my article:

 

 

* Translated automatically by Google.
* Please note that some links or referenced content in this article may be in Japanese.
* Comments in the code are basically in Japanese.

 

by bokumin

 

Setting up mod_security + OWASP CRS + fail2ban on OpenSUSE

 

Introduction

 

In the previous article, we explained how to prevent simple brute force attacks using PF, but this time we will explain how to build a more advanced attack detection and defense system.

 

Traditional brute force defenses are designed to detect consecutive login attempts and mass access on a number-of-times basis. However, attacks such as SQL injection, XSS, and directory traversal can be successful with just a single request. Additionally, in a distributed attack using multiple IP addresses, even if the number of attempts from each IP is small, the overall attack will be large-scale. To respond to such sophisticated attacks, a mechanism is needed to inspect the request content itself.

 

This time, we used OWASP CRS as the ruleset for this inspection. The OWASP Core Rule Set is a comprehensive attack detection ruleset that covers major attack patterns such as SQL injection, XSS, remote code execution, and file inclusion. Creating these rules on your own requires considerable effort, but with OWASP CRS, anyone can easily implement world-standard security measures. The license is ASLv2 and can be used for commercial purposes.

 

In this article, we will build a system that incorporates ModSecurity2 into Apache, detects attacks using OWASP CRS, and automatically blocks attack source IPs using fail2ban using iptables.

 

Execution environment

 

OS: openSUSE Tumbleweed 20251127
Web server: Apache 2.4.65 (prefork MPM)
Hardware: Intel Xeon E5-2650L v4 (14 cores) / Memory 32GB
Operation site: WordPress, static sites, web apps, etc. *Basically, any distribution can implement these. Please refer to the manual or official page for detailed differences such as the location of the configuration file.

 

Install

 

We will actually build the system.
First, install the required packages. openSUSE uses the zypper command to manage packages.

 

# mod_security2とOWASP CRSのインストール
sudo zypper install apache2-mod_security2 owasp-modsecurity-crs

# fail2banのインストール
sudo zypper install fail2ban

# 依存パッケージの確認
rpm -qa | grep mod_security
rpm -qa | grep owasp

 

Once the installation is complete, check whether the package is installed correctly. For OpenSUSE, you can check the version and build information by displaying the package information with the rpm command.

 

$ rpm -qi apache2-mod_security2
Name        : apache2-mod_security2
Version     : 2.9.12
Release     : 1.2
Architecture: x86_64
Install Date: Tue 11 Nov 2025 02:01:44 PM JST
Group       : Productivity/Networking/Web/Servers
Size        : 1045515
License     : Apache-2.0
Signature   : RSA/SHA512, Fri 10 Oct 2025 01:07:25 AM JST, Key ID 35a2f86e29b700a4
Source RPM  : apache2-mod_security2-2.9.12-1.2.src.rpm
Build Date  : Thu 07 Aug 2025 03:25:52 AM JST
Build Host  : reproducible
Packager    : https://bugs.opensuse.org
Vendor      : openSUSE
URL         : https://www.modsecurity.org/
Summary     : Web Application Firewall for Apache httpd
Description :
ModSecurity is an intrusion detection and prevention
engine for web applications (or a web application firewall). Operating
as an Apache Web server module or standalone, the purpose of
ModSecurity is to increase web application security, protecting web
applications from known and unknown attacks.
Distribution: openSUSE Tumbleweed

 

After confirming that the package is installed successfully, enable mod_security2 as a module in Apache.

 

# mod_security2モジュールの有効化
sudo a2enmod security2

 

mod_security2 basic settings

 

OpenSUSE automatically loads .conf files located in the conf.d/ directory, so creating a custom configuration file is easier to maintain than directly editing the original package file.
Basically, the default settings will work, but if you need large file uploads such as WordPress, or if you want to tune performance, we recommend creating custom settings. Below is an example of custom settings.

 

sudo vim /etc/apache2/conf.d/mod_security2_custom.conf


<IfModule mod_security2.c>

    SecRuleEngine On
    
    SecRequestBodyLimit 13107200
    SecRequestBodyNoFilesLimit 131072
    
    # レスポンスボディ検査を有効化(デフォルトはOff)
    SecResponseBodyAccess On
    SecResponseBodyMimeType text/plain text/html text/xml application/json
    SecResponseBodyLimit 524288
    
    # 作業ディレクトリの明示的指定
    SecTmpDir /var/lib/mod_security/tmp
    SecDataDir /var/lib/mod_security/data
    SecUploadDir /var/lib/mod_security/upload
    SecUploadKeepFiles Off
    
    # 監査ログ設定の上書き(fail2ban連携用)
    SecAuditEngine RelevantOnly
    SecAuditLogRelevantStatus "^(?:5|4(?!04))"
    SecAuditLogParts ABIJDEFHZ
    SecAuditLogType Serial
    SecAuditLog /var/log/apache2/modsec_audit.log
    
    # デバッグログ(本番では0に設定)
    SecDebugLog /var/log/apache2/modsec_debug.log
    SecDebugLogLevel 0
    
    # パフォーマンスチューニング
    SecPcreMatchLimit 100000
    SecPcreMatchLimitRecursion 100000
    
    # OWASP CRS読み込み
    IncludeOptional /etc/apache2/mod_security2.d/*.conf
    IncludeOptional /etc/apache2/mod_security2.d/rules/*.conf
</IfModule>

 

The contents of the customized configuration file are
SecRuleEngine On → State to actually block the attack
SeSecRequestBodyLimit 13107200 → Set the maximum size of the request body to be inspected to 13MB
*Please change as per your preference
SecResponseBodyAccess On → Enable response body inspection
SecResponseBodyMimeType text/plain text/html text/xml application/json → MIME type to be inspected
*Inspecting binary files such as images and videos wastes performance
SecAuditEngine RelevantOnly → Detailed logs are recorded only when an attack is detected. These are upper limits for regular expression matching, and are settings to avoid using too much CPU when performing complex pattern matching.

 

Reflect and confirm the settings.

 

sudo apachectl configtest
# 問題なければ
sudo systemctl restart apache2

 

Check OWASP CRS placement

 

In the OpenSUSE package, the rules are placed in /usr/share/owasp-modsecurity-crs/rules/ and symlinked from /etc/apache2/mod_security2.d/rules/

 

$ ls /etc/apache2/mod_security2.d/rules/
iis-errors.data                                      REQUEST-934-APPLICATION-ATTACK-GENERIC.conf
java-classes.data                                    REQUEST-941-APPLICATION-ATTACK-XSS.conf
java-code-leakages.data                              REQUEST-942-APPLICATION-ATTACK-SQLI.conf
java-errors.data                                     REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf
lfi-os-files.data                                    REQUEST-944-APPLICATION-ATTACK-JAVA.conf
php-config-directives.data                           REQUEST-949-BLOCKING-EVALUATION.conf
php-errors.data                                      RESPONSE-950-DATA-LEAKAGES.conf
php-errors-pl2.data                                  RESPONSE-951-DATA-LEAKAGES-SQL.conf
php-function-names-933150.data                       RESPONSE-952-DATA-LEAKAGES-JAVA.conf
php-function-names-933151.data                       RESPONSE-953-DATA-LEAKAGES-PHP.conf
php-variables.data                                   RESPONSE-954-DATA-LEAKAGES-IIS.conf
REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example  RESPONSE-955-WEB-SHELLS.conf
REQUEST-901-INITIALIZATION.conf                      RESPONSE-959-BLOCKING-EVALUATION.conf
REQUEST-905-COMMON-EXCEPTIONS.conf                   RESPONSE-980-CORRELATION.conf
REQUEST-911-METHOD-ENFORCEMENT.conf                  RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example
REQUEST-913-SCANNER-DETECTION.conf                   restricted-files.data
REQUEST-920-PROTOCOL-ENFORCEMENT.conf                restricted-upload.data
REQUEST-921-PROTOCOL-ATTACK.conf                     scanners-user-agents.data
REQUEST-922-MULTIPART-ATTACK.conf                    sql-errors.data
REQUEST-930-APPLICATION-ATTACK-LFI.conf              ssrf.data
REQUEST-931-APPLICATION-ATTACK-RFI.conf              unix-shell.data
REQUEST-932-APPLICATION-ATTACK-RCE.conf              web-shells-php.data
REQUEST-933-APPLICATION-ATTACK-PHP.conf              windows-powershell-commands.data

 

CRS configuration files are all commented out by default. If commented out, OWASP CRS uses the internal default value. The default value is as follows.

 

Paranoia Level: 1 (default)
Inbound threshold: 5 (block after one critical attack)
Outbound threshold: 4
Allowed HTTP methods: GET HEAD POST OPTIONS
Early Blocking: Disabled

 

If tuning is required, uncomment the following and enable it. Paranoia Level is a parameter that controls the severity of detection, with 1 being the most lenient and 4 being the strictest. We recommend starting with 1 and increasing it gradually after making sure there are few false positives.

 

sudo vim /etc/apache2/mod_security2.d/modsecurity-crf-setup.conf
#Paranoia Level設定 
# コメントを外して有効化

SecAction \
    "id:900000,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    tag:'OWASP_CRS',\
    ver:'OWASP_CRS/4.10.0',\
    setvar:tx.blocking_paranoia_level=1"

# 検知専用Paranoia Level 

SecAction \
    "id:900001,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    tag:'OWASP_CRS',\
    ver:'OWASP_CRS/4.10.0',\
    setvar:tx.detection_paranoia_level=1"

# Anomaly Score閾値

SecAction \
    "id:900110,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    tag:'OWASP_CRS',\
    ver:'OWASP_CRS/4.10.0',\
    setvar:tx.inbound_anomaly_score_threshold=5,\
    setvar:tx.outbound_anomaly_score_threshold=4"

# 許可HTTPメソッド

SecAction \
    "id:900200,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    tag:'OWASP_CRS',\
    ver:'OWASP_CRS/4.10.0',\
    setvar:'tx.allowed_methods=GET HEAD POST OPTIONS'"

# 許可Content-Type

SecAction \
    "id:900220,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    tag:'OWASP_CRS',\
    ver:'OWASP_CRS/4.10.0',\
    setvar:'tx.allowed_request_content_type=|application/x-www-form-urlencoded| |multipart/form-data| |text/xml| |application/xml| |application/soap+xml| |application/json|'"

# Early Blocking有効化

SecAction \
    "id:900120,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    tag:'OWASP_CRS',\
    ver:'OWASP_CRS/4.10.0',\
    setvar:tx.early_blocking=1"

 

Also, mod_security logs grow rapidly. Make sure to set up proper log rotation. The following settings will save audit logs for 14 days and debug logs for 7 days.

 

sudo vim /etc/logrotate.d/apache2-modsecurity

/var/log/apache2/modsec_audit.log {
    daily
    rotate 14
    compress
    delaycompress
    notifempty
    create 640 root root
    sharedscripts
    postrotate
        /usr/sbin/apache2ctl graceful > /dev/null 2>&1 || true
    endscript
}

/var/log/apache2/modsec_debug.log {
    daily
    rotate 7
    compress
    delaycompress
    notifempty
    create 640 root root
    sharedscripts
    postrotate
        /usr/sbin/apache2ctl graceful > /dev/null 2>&1 || true
    endscript
}

 

Coordination with fail2ban

 

Let’s set up fail2ban. In my case, I add the IP address of a CDN service such as CloudFlare to ignoreip. If you are accessing your site via CloudFlare, the CloudFlare IP address is logged instead of the actual client IP. If you ban CloudFlare’s IP, you will no longer be able to access the entire service.
*Please refer to the article below for information on how to obtain a real IP with Cloudflare.

 

 

With this setting, if two attacks are detected within 10 minutes, that IP address will be blocked for 7 days. The reason we set maxretry to 2 is that when ModSecurity detects an attack, it can clearly determine that it is malicious access. The bantime is set to a long 7 days to prevent automated attack tools from repeatedly attempting access.

 

sudo vim /etc/fail2ban/jail.d/apache-modsecurity.conf

[apache-modsecurity]
enabled = true
ignoreip = 127.0.0.1/8
           ::1
           162.158.0.0/15
           173.245.48.0/20
           103.21.244.0/22
           103.22.200.0/22
           103.31.4.0/22
           141.101.64.0/18
           108.162.192.0/18
           190.93.240.0/20
           188.114.96.0/20
           197.234.240.0/22
           198.41.128.0/17
port = http,https
filter = apache-modsecurity
logpath = /var/log/apache2/error.log

maxretry = 2
bantime = 7d
findtime = 10m

 

After saving the settings, restart fail2ban

 

sudo fail2ban-client -t

# 問題なければ
sudo systemctl restart fail2ban

 

Check log

 

Check if fail2ban is working properly. You can view the state of the apache-modsecurity jail using the fail2ban-client command.

 


# jail状態の確認
sudo fail2ban-client status apache-modsecurity
Status for the jail: apache-modsecurity
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     50
|  `- File list:        /var/log/apache2/modsec_audit.log 
`- Actions
   |- Currently banned: 30
   |- Total banned:     31
   `- Banned IP list:   106.54.124.78 136.144.35.160 138.197.167.75 138.68.86.32 142.93.107.190 154.8.198.199 159.223.193.66 192.34.63.233 195.178.110.201 195.178.110.242 204.76.203.8 209.97.137.68 35.216.183.140 45.133.74.43 45.148.10.154 45.148.10.158 45.148.10.63 47.251.13.59 49.248.192.204 74.208.7.160 78.153.140.128 78.153.140.178 78.153.140.179 78.153.140.195 78.153.140.203 95.214.52.169 96.41.38.202 194.180.49.174 74.7.242.23 134.199.168.23

 

In this example output, 28 IP addresses have already been blocked. Currently banned is the number of IP addresses that are currently blocked, and Total banned is the cumulative number of IP addresses that have been blocked so far. The Banned IP list displays the IP addresses that are actually blocked.

 

In actual operation, it is important to periodically check this list to understand which IP addresses are receiving attacks. If there are many attacks coming from the same country or the same ASN, you may consider broadly blocking them at the firewall level.

 

Supplementary false positives for specific rules

 

Once operational, legitimate requests may be incorrectly determined to be attacks. If a particular rule causes false positives, work around it by creating an exclusion rule.

 

Exclusion rules are configured in the RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf file. This file is read after all CRS rules are evaluated, so you can disable certain rules or skip rules under certain conditions.
For example, if rule ID 942100 is causing false positives, add exclusion settings as below.
*The first example completely disables rule 942100. The second example disables rule 942100 only for URL paths starting with /api/. The third example disables rule 942100 only for the parameter named search.

 

sudo vim /etc/apache2/mod_security2.d/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf

# 1
SecRuleRemoveById 942100 

# 2
SecRule REQUEST_URI "@beginsWith /api/" \
    "id:2000,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveById=942100"
    
# 3
SecRule ARGS:search "@rx .*" \
    "id:2001,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveById=942100"

 

In WordPress, etc., legitimate requests may be falsely detected. If you receive a false positive, please create an exclusion rule as below

 

# WordPress管理画面の除外
SecRule REQUEST_URI "@beginsWith /wp-admin/" \
    "id:1000,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveById=920440,\
    ctl:ruleRemoveById=942100,\
    ctl:ruleRemoveById=942190,\
    ctl:ruleRemoveById=942200,\
    ctl:ruleRemoveById=942260,\
    ctl:ruleRemoveById=942340,\
    ctl:ruleRemoveById=942370"

# WordPress AJAX処理の除外
SecRule REQUEST_URI "@beginsWith /wp-admin/admin-ajax.php" \
    "id:1001,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveById=942100,\
    ctl:ruleRemoveById=942200"

# WordPress REST APIの除外
SecRule REQUEST_URI "@beginsWith /wp-json/" \
    "id:1002,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveById=920300,\
    ctl:ruleRemoveById=942100"

 

 

By using OWASP CRS, you can implement world-standard security measures without the effort of creating your own rules. Continuously updated rulesets keep you up to date with new threats.
I hope this will be helpful to someone.

When operating, we recommend starting with the default values without changing any settings, and adjusting the Paranoia Level as necessary while observing the logs. If you receive a false positive, please respond by setting exclusion rules.
This is about how to do mod_security2+OWASP+fail2ban.

 

End

 

 

参考:
Modsecurity
https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v2.x)
OWASP CRS
https://coreruleset.org/
https://github.com/coreruleset/coreruleset
https://coreruleset.org/docs/1-getting-started/1-1-crs-installation/
fail2ban
https://github.com/fail2ban/fail2ban
https://fail2ban.readthedocs.io/en/latest/
OpenSUSE
https://software.opensuse.org/package/apache2-mod_security2