Getting Started with Apache ModSecurity on Debian and Ubuntu

ModSecurityLogo.png ModSecurity is a Web Application Firewall: a program that can be used to inspect information as it passes through your web server, intercepting malicious requests before they are processed by your web application. This tutorial will show you how to install ModSecurity on Apache, and configure it with some sensible rules provided by the Open Web Application Security Project's Core Rule Set (OWASP CRS), which will help to protect your server against SQL injection, denial of service attacks, malformed requests, cross site scripting attacks, and more. And yes, you can use this guide with your Raspberry Pi if you're running Raspbian, Ubuntu, or another Debian derivative on it.

The Open Web Application Security Project's Core Rule Set (OWASP CRS)

ModSecurity is configured using a set of rules that provide the logic used to decide which requests to block. Probably the most common set of rules is the Open Web Application Security Project's Core Rule Set (OWASP CRS), which is in the repos as modsecurity-crs. The CRS can run in two modes: traditional and anomaly scoring. In traditional mode, the first rule that matches will block the request; in anomaly scoring mode the rules increment counters that "enumerate badness", and if the rule exceeds a threshold then the request is blocked. In this tutorial, we will configure Apache to run the core rule set in anomaly scoring mode, since it allows for a more intelligent approach to blocking.

Installing ModSecurity and the CRS

I'm assuming at this point that you already have Apache2 installed and some kind of web application (Wordpress, ownCloud etc.) running. If you don't, running the following commands will install apache2 as a dependency anyway, but it's a good idea to get something up and running on the server before you install ModSecurity. ModSecurity is a module for Apache2, so the name of the package follows the apache module conventions: libapache2-mod-security2. You can install it like this:

sudo apt-get update
sudo apt-get install libapache2-mod-security2

On most systems, this will pull in the CRS too, which is packaged as modsecurity-crs, as it is marked as a suggested package for modsecurity. This command will make sure it is set to manually installed so it won't get auto-removed:

sudo apt-get install modsecurity-crs

You can download new versions of the CRS directly from the project's GitHub repo, but don't! Some newer versions of the CRS make use of new features and directives in ModSecurity, so trying to run the newest rules with an "old" (read: not bleeding edge) version of ModSecurity will not work. I found this out the hard way...


ModSecurity creates a directory at /etc/modsecurity during installation. This is the directory you should use to store all of your ModSecurity rules and configuration. The module configuration file that comes with ModSecurity will read any files in this directory that end in .conf when apache starts up. This is what the module config file looks like:

<IfModule security2_module>
        # Default Debian dir for modsecurity's persistent data
        SecDataDir /var/cache/modsecurity

        # Include all the *.conf files in /etc/modsecurity.
        # Keeping your local configuration in that directory
        # will allow for an easy upgrade of THIS file and
        # make your life easier
        IncludeOptional /etc/modsecurity/*.conf

This file should be at /etc/apache2/mods-available/security2.conf; there is also a file called security2.load that loads the module itself. We need to make sure that these two files are read by apache2 when it starts up, by creating symlinks into the /etc/apache2/mods-enabled directory. You could create the symlinks manually with ln -s, but the easiest and most reliable way is to use the a2enmod command:

sudo a2enmod security2

ModSecurity also makes use of the headers module, which we can enable like this:

sudo a2enmod headers

Don't reload apache2 yet!

ModSecurity Configuration File

At the moment, you should have two files in /etc/modsecurity: unicode.mapping and modsecurity.conf-recommended. The latter is a template config file. First we need to copy it so that it has a name that will be read by apache:

sudo cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf

You shouldn't have to change much in this configuration file, but I'd like to bring your attention to the SecRuleEngine parameter, which decides whether ModSecurity is On, Off, or in DetectionOnly mode where it will process requests and write the audit log but not actually block anything:

SecRuleEngine DetectionOnly

You can leave this at DetectionOnly for now: there will be tons of false positives to begin with, and until you have written a whitelist (more on that later), turning the engine on will likely just break your site! When you have written the whitelist, you can change the parameter to On.

Filesystem Configuration

As can be seen in the module configuration file (/etc/apache2/mods-available/security2.conf above), ModSecurity creates a directory for storing temporary files at /var/cache/modsecurity during installation and makes it writable by Apache. The default configuration file specifies that temporary files should be stored in /tmp/, but this data is cleared after a reboot, whereas the data in /var/cache really is persistent. This is important because some of the session hijacking rules in the CRS check to see if secure session ID cookies set by web apps and PHP are ones that ModSecurity has seen before, and block the request if they are "new"... so if you log in to a web app like ownCloud and then reboot the web server and visit again later, the request will be blocked because the old cookie is "new" to ModSecurity. To fix this, navigate to the "Filesystem configuration" section in /etc/modsecurity/modsecurity.conf and change both SecTmpDir and SecDataDir from /tmp/ to /var/cache/modsecurity, i.e.

SecDataDir /var/cache/modsecurity

Now the session hijacking rules should work as intended (if you choose to enable them!).

Loading the CRS Config Files

The Core Rule Set is installed into /usr/share/modsecurity-crs/, and is separated into several directories such as base_rules, experimental_rules, optional_rules and slr_rules. SLR stands for SpiderLabs Research, which is the research team of the company that currently maintains the CRS. We are going to create symbolic links in the /etc/modsecurity directory that point to the rule files we want to load. You can ignore the activated_rules directory - on other distros or if you compiled everything from source, you might unpack the CRS tarball directly into /etc/modsecurity and then use the /etc/apache2/mods-enabled/security2.conf to make Apache read files in /etc/modsecurity/activated_rules instead of everything in the /etc/modsecurity directory. It's entirely up to you which rule files you choose to load, but at a minimum you should be using the base_rules. I would also recommend loading all of the optional_rules and then choosing a few of the experimental rules you think might be useful for your site or app (for example, I chose to enable the brute force and DoS protection rule files). You probably don't want to enable the appsensor_detection_point_* files (if you're curious what these do, refer to the OWASP documentation). Bear in mind that the experimental rules are likely to require more work tweaking/whitelisting to remove false positives for your site. Now, you could create each symlink individually, but that would be extremely tedious. If you want to use all of the rule files in one or more of those directories, this is much faster: For the base_rules:

cd /usr/share/modsecurity-crs/base_rules/
for ruleFile in * ; do sudo ln -s /usr/share/modsecurity-crs/base_rules/$ruleFile /etc/modsecurity/$ruleFile ; done

For the optional_rules:

cd /usr/share/modsecurity-crs/optional_rules/
for ruleFile in * ; do sudo ln -s /usr/share/modsecurity-crs/optional_rules/$ruleFile /etc/modsecurity/$ruleFile ; done

You probably don't want to enable all of the experimental_rules, but if you did you could use this command:

cd /usr/share/modsecurity-crs/experimental_rules/
for ruleFile in * ; do sudo ln -s /usr/share/modsecurity-crs/experimental_rules/$ruleFile /etc/modsecurity/$ruleFile ; done

For the slr_rules, I would recommend yo.u only load rule files when they are relevant to the type of site you run, e.g. if you have a wordpress site, you could run these two commands to load the configuration and data files:

sudo ln -s /usr/share/modsecurity-crs/slr_rules/modsecurity_crs_46_slr_et_wordpress_attacks.conf /etc/modsecurity/modsecurity_crs_46_slr_et_wordpress_attacks.conf
sudo ln -s /usr/share/modsecurity-crs/slr_rules/ /etc/modsecurity/

You should now have a bunch of symlinks in the /etc/modsecurity directory pointing to the rule files, but we still need to copy across the most important file: modsecurity_crs_10_setup.conf, which contains configuration for the CRS itself. We symlinked the rule files so that when the package manager updates modsecurity-crs, a new version of the rule files will be automatically loaded by apache, meaning we will always have the latest version of the CRS for our version of ModSecurity. We will never edit the files directly to modify the rules, because those changes would be lost when the package is updated, instead we will create new rules in our own files that will override the defaults (more on that later). Since we intend to change the configuration in the modsecurity_crs_10_setup.conf file, we must copy it instead of symlinking so that we can make permanent changes:

sudo cp /usr/share/modsecurity-crs/modsecurity_crs_10_setup.conf /etc/modsecurity/modsecurity_crs_10_setup.conf

Configuring the CRS

Turn On Anomaly Scoring

The configuration in modsecurity_crs_10_setup.conf determines the blocking mode that the core rule set will use - the traditional vs anomaly scoring modes I mentioned in the introduction. SecDefaultAction determines the action taken by ModSecurity when a rule's action list contains the block keyword. This is unfortunate choice of language in my opinion, because the word "block" implies that the request will be intercepted, but that isn't necessarily the case depending on what SecDefaultAction is set to. I think the language is probably a relic from the way the CRS worked in early versions (traditional mode), where individual rules determined whether to intercept requests, and this parameter was primarily used to decide which phase to intercept the request in, and how to log it (i.e. it always blocked, but the details could be configured differently). Anyway, here's what the keywords mean:

  • The pass keyword means that ModSecurity will continue to process rules even though this rule matched.
  • To stop processing the transaction and intercept the request, rules can use the keyword deny.
  • In rare cases such as the Denial of Service rules, the rule may specify the drop action, which is even more severe: it closes the TCP connection completely by sending a FIN packet.

The default configuration is:

SecDefaultAction "phase:1,deny,log"

This means that the default block action intercepts the request in the first phase, and writes messages to both the error log and audit log. Since we don't want ModSecurity to decide whether to intercept requests until until all of the rules have been evaluated, locate that line in the file, and change it to this:

SecDefaultAction "phase:2,pass,log"

We also need to turn anomaly_score_blocking on by locating rule 900004 in the modsecurity_crs_10_setup.conf file under the "Collaborative Detection Blocking" heading and uncommenting the first line (the one with SecAction):

SecAction \
  "id:'900004', \
  phase:1, \
  t:none, \
  setvar:tx.anomaly_score_blocking=on, \
  nolog, \

So, how does this result in blocking? If you look carefully at any of the rule files (modsecurity_crs_20_protocol_violations.conf would be a good place to start), you will notice that the action section of each rule sets transaction variables like this:


And messages like this:


If the rule matches, the anomaly_score variable is being incremented based on the severity of the rule. In this case the anomaly score was incremented by two points, in accordance with the variables set at the start of the transaction by a rule in modsecurity_crs_10_setup.conf under the "Collaborative Detection Severity Levels" heading:

SecAction \
  "id:'900001', \
  phase:1, \
  t:none, \
  setvar:tx.critical_anomaly_score=5, \
  setvar:tx.error_anomaly_score=4, \
  setvar:tx.warning_anomaly_score=3, \
  setvar:tx.notice_anomaly_score=2, \
  nolog, \

The rule files are processed in alphanumerical order, so near the end of the chain in modsecurity_crs_49_inbound_blocking.conf you will find this rule, which uses the anomaly_score transaction variable:

# Alert and Block based on Anomaly Scores
SecRule TX:ANOMALY_SCORE "@gt 0" \
    "chain,phase:2,id:'981176',t:none,deny,log,msg:'Inbound Anomaly Score Exceeded (Total Score: %{TX.ANOMALY_SCORE}, SQLi=%{TX.SQL_INJECTION_SCORE}, XSS=%{TX.XSS_SCORE}): Last Matched Message: %{tx.msg}',logdata:'Last Matched Data: %{matched_var}',setvar:tx.inbound_tx_msg=%{tx.msg},setvar:tx.inbound_anomaly_score=%{tx.anomaly_score}"
        SecRule TX:ANOMALY_SCORE "@ge %{tx.inbound_anomaly_score_level}" chain
                SecRule TX:ANOMALY_SCORE_BLOCKING "@streq on" chain
                        SecRule TX:/^\d+\-/ "(.*)"

Looks a little scary, but roughly translated this means:

If the anomaly_score is greater than or equal to the inbound_anomaly_score_level, and anomaly_score_blocking is turned on, and there is a transaction variable that looks something like 000001-FOO/BAR, write a log message containing the score and variables that were matched and intercept the request

There's a similar rule for outbound blocking, which runs in a later phase and can intercept requests based on the outbound data to prevent information leakage. With a couple of exceptions such as the Denial of Service protection rules, when the CRS is run in anomaly scoring mode these two rules handle all of the interceptions, and the rest simply modify transaction variables to help them make a decision.

Use Brute Force and DoS Protection

If you enabled the brute force and DoS rule files, you need to modify some SecAction statements in modsecurity_crs_10_setup.conf that are used for these rules. This SecAction sets some transaction variables that are used in denial of service rules. The rules work by dropping new connections for a period of time (dos_block_timeout) if the number of requests exceeds a limit (dos_counter_thrreshold) in a certain amount of time (dos_burst_time_slice). You can probably leave the variables as they are, just uncomment the SecAction:

SecAction \
  "id:'900015', \
  phase:1, \
  t:none, \
  setvar:'tx.dos_burst_time_slice=60', \
  setvar:'tx.dos_counter_threshold=100', \
  setvar:'tx.dos_block_timeout=600', \
  nolog, \

The brute force protection rules work in a similar way. This time you should edit the paths in brute_force_protected_urls so that the login urls for your server are protected, as well as uncommenting SecAction:

SecAction \
  "id:'900014', \
  phase:1, \
  t:none, \
  setvar:'tx.brute_force_protected_urls=#/user/login# #/wp_login.php#', \
  setvar:'tx.brute_force_burst_time_slice=60', \
  setvar:'tx.brute_force_counter_threshold=10', \
  setvar:'tx.brute_force_block_timeout=300', \
  nolog, \


Under the GeoIP Database section, there is a SecGetoLookupDb parameter that is used to provide modsecurity with a database it can use to guess the country of clients by their IP addresses. If you want to use rules that make use of the database, make sure you have the GeoIP database installed:

sudo apt-get update
sudo apt-get install geoip-database

Then uncomment the line and update the path like this:

SecGeoLookupDb /usr/share/GeoIP/GeoIP.dat

Finishing Up

Now we've turned on ModSecurity, configured it to use the CRS, and set some sensible options, all that is left is to reload apache to read the new configuration:

sudo service apache2 reload

If you didn't get any errors, congratulations! ModSecurity is running in DetectionOnly mode. Now comes the difficult part: whitelisting to remove false positives, before you turn ModSecurity on for real. Before we dive into whitelisting, let's make sure you know how to read the three types of log file that are written by ModSecurity...

Reading the Logs (Error, Audit and Debug)

There are three types of log file that are written by ModSecurity:

  • The error log is the same log file that is used by Apache to write error messages, normally stored at /var/log/apache2/error.log.
  • The audit log is modsecurity's own log file, normally stored at /var/log/apache2/modsec_audit.log, which can contain a complete record of all the data in a transaction, and how ModSecurity processed it.
  • The debug log produces a huge amount of information about how each rule is evaluated and how transaction variables are set and incremented. As a result, it can grow very large very quickly, so you should only turn it on when you have to. Even then, it's a good idea to turn it on selectively for the specific types of request you are interested in. I won't spend any time explaining the debug log, because you hopefully won't have to use it!

Error log

Typically, each rule that matches will write a message to the error log, although this behaviour is controlled by the log or nolog keywords specified in the rule's action list, or in the default action we configured earlier. log means a message will be written to the error log and the audit log, and auditlog means the message will only be written to the auditlog, so if you only want to log in the audit log you must specify nolog,auditlog. Here's an example of a typical error log message from rule 960911, which verifies that the request line sent by the client follows the format specified in the RFC.

[Sun Mar 13 15:54:12.675099 2016] [:error] [pid 22832] [client] ModSecurity: Warning. Match of "rx ^(?i:(?:[a-z]{3,10}\\\\s+(?:\\\\w{3,7}?://[\\\\w\\\\-\\\\./]*(?::\\\\d+)?)?/[^?#]*(?:\\\\?[^#\\\\s]*)?(?:#[\\\\S]*)?|connect (?:\\\\d{1,3}\\\\.){3}\\\\d{1,3}\\\\.?(?::\\\\d+)?|options \\\\*)\\\\s+[\\\\w\\\\./]+|get /[^?#]*(?:\\\\?[^#\\\\s]*)?(?:#[\\\\S]*)?)$" against "REQUEST_LINE" required. [file "/etc/modsecurity/modsecurity_crs_20_protocol_violations.conf"] [line "52"] [id "960911"] [rev "2"] [msg "Invalid HTTP Request Line"] [data "CONNECT HTTP/1.1"] [severity "WARNING"] [ver "OWASP_CRS/2.2.8"] [maturity "9"] [accuracy "9"] [tag "OWASP_CRS/PROTOCOL_VIOLATION/INVALID_REQ"] [tag "CAPEC-272"] [hostname ""] [uri "/"] [unique_id "VuWNJH8AAQEAAFkwANMAAAAI"]

Let's break this down a bit:

  • The first part of the log entry is a timestamp in the Apache format, followed by the process ID of the Apache handler that handled the request, and the client IP address.
  • Next is the test that was used in the matching rule, and the data that matched it.
  • The third part gives some information about the rule: which file it was loaded from, its position in the file, the unique ID of the rule, and an optional revision of the rule.
  • Following this is some data chosen by the rule. The message is set with the msg parameter (msg:'Invalid HTTP Request Line'); the data part is set with the logdata parameter, and is used to include the relevant part of the request that triggered the rule (logdata:'%{request_line}').
  • A series of CRS specific information follows this: the severity, version of the CRS that the rule belongs to, the rule "maturity" (how heavily tested the rule is, where 10 is the highest), the "accuracy" of the rule (where 10 is highest and scores below 5 indicate that there have been some false positives reported), and finally some tags that can be used to identify the category of attack that the rule was written to catch
  • The hostname and URI sent to the server are the domain name of the site the client wanted the information from, and the unique resource identifier (e.g. the URL) of the resource it wanted.
  • Finally, the unique_id is a unique identifier for that specific request, which can be used to identify the same request in the audit and debug logs.

When a transaction matches more than one rule, more than one log entry may be written, and the rules that do the collaborative blocking based on anomaly scores also write their own log messages on top of that. The error log is often the best place to look to get an overview of why a transaction has been blocked, but it doesn't contain the whole request sent to the server, and the complete reply. For that, you must look in the audit log.

Audit log

Sometimes, you need more information than the snippets of data selected by the rules that get printed in the error log. The audit log contains a record of complete transactions in text format. These three lines in /etc/modsecurity/modsecurity.conf set out what gets written to the audit log:

SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
SecAuditLogParts ABIJDEFHZ

Lines 1 and 2 mean that only requests with response codes 5XX (server error) or a 4XX (client error), excluding 404 (not found) are written to the audit log. This keeps the log size down, although you will find it still grows pretty quickly! Line 3 controls which parts of the audit log are written by default for matching requests. The parts available are:

  • A - audit log header
  • B - request headers
  • C - request body
  • D - intended response header (not implemented yet)
  • E - intended response body
  • F - response headers
  • G - response body
  • H - audit log trailer
  • I - reduced multipart request body
  • J - multipart files information
  • K - matched rules
  • Z - audit log footer

Some rules add to the default list of parts logged where that information is particularly useful or relevant, for example many of the outbound rules add part E to the log by specifying ctl:auditLogParts=+E in the action list. Most of the sections are pretty self explanatory, but I find the most useful section to be part H, which contains one message for each rule that was matched. You can get more information about each section of the log in the ModSecurity Documentation. When investigating intercepted transactions, you will often find yourself trying to find specific requests in the audit log after identifying them in the access and error logs. You may find it useful to copy the unique_id (which is in both of the other logs), and then after opening the audit log with less, type / and then paste the unique id (ctrl+shift+v) and press enter to search for the request. Here's the audit log entry that matches the error log snippet I used earlier:

[13/Mar/2016:15:54:12 +0000] VuWNJH8AAQEAAFkwANMAAAAI 61122 80
Proxy-Connection: Keep-Alive

HTTP/1.1 403 Forbidden
Content-Length: 283
Content-Type: text/html; charset=iso-8859-1

<title>403 Forbidden</title>
<p>You don't have permission to access /
on this server.


<hr> <address>Apache/2.4.7 (Ubuntu) Server at Port 443</address> </body></html> --09989b78-H-- Message: Warning. Match of "rx ^(?i:(?:[a-z]{3,10}\\s+(?:\\w{3,7}?://[\\w\\-\\./]*(?::\\d+)?)?/[^?#]*(?:\\?[^#\\s]*)?(?:#[\\S]*)?|connect (?:\\d{1,3}\\.){3}\\d{1,3}\\.?(?::\\d+)?|options \\*)\\s+[\\w\\./]+|get /[^?#]*(?:\\?[^#\\s]*)?(?:#[\\S]*)?)$" against "REQUEST_LINE" required. [file "/etc/modsecurity/modsecurity_crs_20_protocol_violations.conf"] [line "52"] [id "960911"] [rev "2"] [msg "Invalid HTTP Request Line"] [data "CONNECT HTTP/1.1"] [severity "WARNING"] [ver "OWASP_CRS/2.2.8"] [maturity "9"] [accuracy "9"] [tag "OWASP_CRS/PROTOCOL_VIOLATION/INVALID_REQ"] [tag "CAPEC-272"] Message: Warning. Match of "within %{tx.allowed_methods}" against "REQUEST_METHOD" required. [file "/etc/modsecurity/modsecurity_crs_30_http_policy.conf"] [line "31"] [id "960032"] [rev "2"] [msg "Method is not allowed by policy"] [data "CONNECT"] [severity "CRITICAL"] [ver "OWASP_CRS/2.2.8"] [maturity "9"] [accuracy "9"] [tag "OWASP_CRS/POLICY/METHOD_NOT_ALLOWED"] [tag "WASCTC/WASC-15"] [tag "OWASP_TOP_10/A6"] [tag "OWASP_AppSensor/RE1"] [tag "PCI/12.1"] Apache-Error: [file "mod_authz_core.c"] [line 866] [level 3] AH01630: client denied by server configuration: %s%s Stopwatch: 1457884452674435 222736 (- - -) Stopwatch2: 1457884452674435 222736; combined=222248, p1=221271, p2=0, p3=75, p4=416, p5=395, sr=102, sw=91, l=0, gc=0 Response-Body-Transformed: Dechunked Producer: ModSecurity for Apache/2.7.7 (; OWASP_CRS/2.2.8. Server: Apache/2.4.7 (Ubuntu) Engine-Mode: "ENABLED" --09989b78-Z-- In this case, as you can see in the messages it was the base Apache configuration that denied the request, not ModSecurity (i.e. the request wasn't intercepted). However, two rules did match (960911 and 960032), and we can see some information about those. Here's what a request that is intercepted looks like:

[13/Mar/2016:23:43:25 +0000] VuX7HX8AAQEAAGRzdAsAAAAG 41386 443
GET /?test=test-attack HTTP/1.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:44.0) Gecko/20100101 Firefox/44.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
DNT: 1
Cookie: Drupal.toolbar.collapsed=0; SSESSfoobar; has_js=1
Connection: keep-alive

HTTP/1.1 403 Forbidden
X-Content-Type-Options: nosniff
X-Powered-By: PHP/5.5.9-1ubuntu4.14
Strict-Transport-Security: max-age=15768000
X-Clacks-Overhead: GNU Terry Pratchett
Content-Length: 707
Connection: close
Content-Type: text/html; charset=UTF-8

Message: Access denied with code 501 (phase 1). Pattern match "test-attack" at ARGS:test. [file "/etc/modsecurity/modsecurity_crs_15_customrules.conf"] [line "22"] [id "000001"]
Action: Intercepted (phase 1)
Apache-Handler: application/x-httpd-php
Stopwatch: 1457912605362266 957 (- - -)
Stopwatch2: 1457912605362266 957; combined=300, p1=142, p2=0, p3=0, p4=0, p5=129, sr=43, sw=29, l=0, gc=0
Producer: ModSecurity for Apache/2.7.7 (; OWASP_CRS/2.2.8.
Server: Apache/2.4.7 (Ubuntu)
Engine-Mode: "ENABLED"


When I started whitelisting, I found that I wanted an easy way to sort through audit logs and inspect groups of requests (e.g. all of the requests from a certain IP address, or all requests that tripped a certain rule). This isn't something that is easily achieved using a text file where the information is split across multiple lines, so I wrote a commandline utility that can read the audit log into a sqlite database. At the time of writing it is in dire need of a rewrite because it was the first thing I wrote in C++, but it does work and it may be useful to you, so feel free to give it a go.


Now that you have ModSecurity up and running, and you know how to inspect requests, it's time to write a whitelist. Whitelisting is not easy, but I hope my extensive guide with example whitelisting rules for ModSecurity will prove useful to you. You should also refer to the ModSecurity Reference Manual on GitHub, which is full of useful information. Once you have written your whitelisting rules, remember to set the SecRuleEngine On and reload apache! If you run more than one site or web app on your server, you might want to run ModSecurity in two different modes on different parts (e.g. DetectionOnly on one part while you whitelist, On on another part that you have finished whitelisting). You can do this, as well as setting custom log locations, by changing the settings in your virtualhost file. Make sure you use an IfModule guard so that the config is only loaded if ModSecurity is installed/running:

<IfModule mod_security2.c>
        SecRuleEngine On
        SecAuditLog     ${APACHE_LOG_DIR}/samhobbs/modsec_audit.log
        SecDebugLog ${APACHE_LOG_DIR}/samhobbs/modsec_debug.log

If you have a bit of cash to spare, I would highly recommend buying the ModSecurity Handbook, which you can buy quite cheaply directly from the independent publisher. Again, you may find my commandline utility that can read the audit log into a sqlite database useful. If you want to learn more about the types of attack that the CRS protects against, I would recommend for a nice easy overview, followed by the OWASP website for articles with more technical detail.



Hi Sam,

Modsecurity modsec_audit is still opaque (to me). When modsecurity is active (Engine ON) I can access my normal web-site either directly or without error. However, when accessing webmail via or https://.../ourmail I get:

You do not have permission to access /ourmail/src/redirect.php on this server.

modsec_audit considers that the anomaly count has been exceeded.

I am content to leave Engine Off. I doubt there is a simple explanation for this - but if there is a simple answer, I would be glad to have advice. Otherwise, don't worry about it and enjoy the weekend.


In reply to by John (not verified)


Quite a lot of whitelisting is normally required before anything that accepts text input (comment fields etc.) will work without false positives. It does take quite a while to get used to the audit log format. I'd have to see the log for the whole request to say what it was (particularly section H). If you like, you can turn modsecurity on for the bulk of your site and put it in detectiononly mode just for webmail using this example from the whitelisting tutorial:
SecRule REQUEST_URI "@beginsWith /webapp" \
  "id:'000080', \
  phase:1, \
  t:none, \
  ctl:ruleEngine=DetectionOnly, \
  nolog, \

Hi Sam,

I would not want to spoil your weekend with the 3 --H- blocks that are generated with one web mail call. I'll start with the whitelisting tutorial and insert the example you gave me.


Hi Sam,

Your 'fix' to disable Engine for webmail but leave Engine On otherwise works perfectly. Thanks...Perhaps it is time to buy the Big Book.



Hi Sam,

Phew! I think I am winning.

1. Back to scratch reviewing squirrelmail. I decided to add another A record and made:
2. Set-up everything again according to the tutorial using new Virtual host records etc.
3. Everything works OK - ie automatic switch to
4. Now turn on Modsecurity and look for 403:
Message: Access denied with code 403 (phase 2). Pattern match "(.*)" at TX:960015-OWASP_CRS/PROTOCOL_VIOLATION/MISSING_HEADER-REQUEST_HEADERS. [file "/etc/modsecurity/modsecurity_crs_49_inbound_blocking.conf"] [line "26"] [id "981176"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5, SQLi=0, XSS=0): Last Matched Message: Host header is a numeric IP address"] [data "Last Matched Data: 0"]
5. I cannot interpret this error message. What header can be missing?
6. So I modified your fix as follows:
SecRule REQUEST_URI "@beginsWith /src" \
7. Now, apart from the mystery about Protocol Violation - web-mail and web-site are working with modsecurity engine ON

Final question: is Protocol Violation as serious as it sounds?

As before: thanks for guidance and advice.


In reply to by John (not verified)


Nice! Are you sure that request wasn't from accessing your server using IP address in the URL bar? It's a protocol violation because you're supposed to use a domain name rather than an IP address in the GET request. Sam

Hi Sam,
Quite sure this was not my IP address in the call. I use your scheme to block the use of IP Addressing. Without the change the 403 error is reported as "You don't have permission to access /src/redirect.php" which does not make sense. With Engine in Detection Only mode, the same call works. I notice on Google that Modsecurity and 403 errors are long-standing....


Hi Sam,

I am (very) slowly finding my way through Modsecurity with the help of the Big Handbook. (AuditConsole is also hugely helpful.) Previously I found I had to add ruleRemoveById 981176 (in crs_49_inbound_blocking) in order to get through the login stage of Squirrelmail. If I leave 981176 active, the error I get in modsec_audit.log is:

Message: Warning. Operator EQ matched 1 at SESSION:IS_NEW. [file "/etc/modsecurity/modsecurity_crs_16_session_hijacking.conf"] [line "24"] [id "981054"] [msg "Invalid SessionID Submitted."] followed by: from 981176:
Message: Access denied with code 403 (phase 2). Pattern match "(.*)" at TX:981054-WEB_ATTACK/INVALID_SESSIONID-SESSION

Now 981054 has this regex:
SecRule REQUEST_COOKIES:'/(j?sessionid|(php)?sessid|(asp|jserv|jw)?session[-_]?(id)?|cf(id|token)|sid)/' ".*" and is chained to:
SecRule SESSION:IS_NEW "@eq 1" "t:none,setvar:tx.anomaly_score=+%{tx.critical_anomaly_score},setvar:tx.%{}-WEB_ATTACK/INVALID_SESSIONID-%{matched_var_name}=%{tx.0}"

and the match will fail since SquirrelMail's sessionid (SQMSESSID) is not in this list. OK- so add SQMSESSID to the regex in 981054.

Result? the same. I have not been able find what Invalid SessionID Submitted might mean - or what is wrong with SQMSESSID below or what SESSION:IS_NEW is actually testing from the previous SecRule. Where is IS_NEW as part of the SESSION collection set?

> Taken from modsec_audit.log: Set-Cookie: SQMSESSID=nrta9ogt7vbbplgujpfn843em7; path=/; secure; HttpOnly

At "modsecurity SESSION:IS_NEW" I followed this advice but with SQMSESSID instead of PHPSESSID
Description: Special-purpose action that initialises the SESSION collection.
Action Group: Non-disruptive


# Initialise session variables using the session cookie value

but with the same result: Access denied 403.

Another comment was: "but as far as I can tell SESSION:IS_NEW is broken, I can see that session IDs make it into the persistent store, but that state does not seem to be detected on subsequent hits."

So I tried another tack: in 981054 delete chain and comment out SecRule SESSION:IS_NEW: Now the anomaly score is not incremented and SquirrelMail works with 981176 enabled. In this case the additional SecRule to setsid is needed. (It follows that I don't understand how 981054 works.)

I don't want to take up a lot of your time, since I can clearly get SquirrelMail to work perfectly by either disabling 981176, or the 981054 chained rule, but it would help to understand why the Squirrelmail sessionid is causing a lot of problems, and how SESSION:IS_NEW is defined....


PS: I found this brilliant URL:{{flavorModel.val}} You can paste a PCRE regex in a window and it then provides a graphical representation of the various paths available in the test. It made the regex at 981054 instantly understandable.

A few weeks ago I added a subsection called "Filesystem configuration" under the "ModSecurity Configuration File" section in the getting started tutorial, which might explain this - I was getting similar behaviour with ownCloud. Unless you change the SecDataDir, modsecurity's data is not persistent across reboots, which makes it think some sessions are new when they're just older than the last reboot of apache or the server itself. Sam

Hi Sam,

I had picked up this comment earlier and SecDataDir defined in security2.conf. As well as changing SecTmpDir and SecDataDir in modsecurity.conf. And I notice that /var/cache/modsecurity is populated when I run.

However, even if I force cache/modsecurity to be empty, then a new session is a perfectly valid occurence and should not trigger an error - which it does.

Something is still not right. My tests are normally done after restarting apache and emptying the cache. Restarting the server itself is rather drastic - I don't do this. Finding out where SESSION:IS_NEW is defined would be helpful.


SESSION:IS_NEW is just looking up the session ID to see if it's in the database (and it matches if it isn't). The rule that captures the session ID in the first place is rule 981062. When a client connects for the first time, they don't have a session ID cookie so the request won't trip 981054. In the response to the request, the web app/php sets a session ID cookie, which is captured by rule 981062. 981063 and 982064 also store the user agent and IP address that were used in the request. In subsequent requests, the cookie will be recognised (but not new, so 981054 will be fine) and modsecurity will verify that the user agent and IP match the ones stored with the session ID. If the IP address is different, the anomaly score is increased by 2. Same for the user agent. If both change, it is increased by 5 (981061). I think what's happening is you changed the regex in one place (the test for new sessions in 981054) without also changing it in the place where modsecurity captures the sessions as they are set (rule 981062) - if you don't change this regex to match the session cookie then the session will never be in the database, so it will always be "new". Sam

Hi Sam,

Thanks for the comment about 981062. I did not originally insert SQMSESSID in that rule since it is only actioned in phase 5 which, as the handbook states: "By the time this phase runs, the transaction will have finished" . It seemed to me that all the problems were taking place at the beginning (phase 1) of the transaction and that phase 5 would be a bit late to have any effect. I didn't appreciate that after phase 5 it is possible for phase 1 to be called again.

I inserted SQMSESSID in the regex for 981062 and removed all the additions/subtractions that I had previously made. And started with an empty cache.
But problems remain. :Access denied 403..

Finally, and maybe this points to the real problem: I removed SQMSESSID from all regex rules in crs_16_session and left the only change being to comment SecRule SESSION:IS_NEW (and removing chain from 981054). Squirrel mail works but the only warning message is:

Message: Warning. Pattern match ".*" at REQUEST_COOKIES:SQMSESSID. [file "/etc/modsecurity/modsecurity_crs_16_session_hijacking.conf"] [line "29"] [id "981054"] [msg "Invalid SessionID Submitted."]

So the system is picking up SQMSESSID but I don't understand how the regex on that line is matched when SQMSESSID is not in the regex, nor what is invalid about SQMSESSID.

SecRule REQUEST_COOKIES:'/(j?sessionid|(php)?sessid|(asp|jserv|jw)?session[-_]?(id)?|cf(id|token)|sid)/' ".*" "phase:1,id:'981054',t:none,block,log,msg:'Invalid SessionID Submitted.',setsid:%{matched_var},setvar:tx.sessionid=%{matched_var},skipAfter:END_SESSION_STARTUP"

But I have already taken up your time. I'll revert to commenting the SESSION:IS_NEW rule for the time being while I try to search on "invalid sessionid".

I wish modsecurity could provide more information about errors.


John, Modsecurity isn't calling phase 1 after phase 5, the cookie capturing rule happens in phase 5 of the request where the cookie is sent, and then the cookie is matched but not new in phase 1 of subsequent requests. By removing the second part of the chain in 981054, you have removed the part that increments the anomaly score (the chain), but the first part still matches and causes a message to be written to the log. Could you post the full log for the blocked request, with any sensitive info redacted (contents of cookies but not the names)? I'll have a look and see if anything jumps out at me. Sam

Hi Sam,

> Cookie is matched in phase 1 of subsequent requests.

Understood - I only see one transaction in modsec_audit which is being emailed to you.

I remove the second part of the 981054 chain precisely to prevent the anomaly score from being incremented so as to avoid the subsequent fatal block from _crs_49_inbound blocking.

I use SecAction...santiseArg rub-out password info.

Any help to understand what 981054 is actually matching on would be great.

Modsec_audit is on its way.


In reply to by John (not verified)


I thought I explained what 981054 is matching on, maybe I did a bad job of it - if a session id cookie is found, a test is performed to see if a session with a matching session ID is in the database. The database itself is initialised by the setsid in rule 981062 - see persistent storage and setsid. Thanks for the log. The reason you only see the request that got blocked in the log is that the capturing rule doesn't write anything to either log (nolog). I've spent quite a while trying to work out why these rules don't work as expected, I even installed squirrelmail again! I changed the capturing rule (981062) so it logs to the audit log but not the error log (nolog,auditlog), and adds a useful message that tells us what the session ID is (msg:'Captured session ID ' and logdata:%{session.sessionid}).
SecRule RESPONSE_HEADERS:/Set-Cookie2?/ "(?i:(j?sessionid|(php)?sessid|(asp|jserv|jw)?session[-_]?(id)?|cf(id|token)|sid).*?=([^\s].*?)\;\s?)" \
  msg:'Captured session ID ',\
        SecRule UNIQUE_ID "(.*)" "t:none,t:sha1,t:hexEncode,capture,setvar:session.csrf_token=%{TX.1}"
I also changed rule 981054 so that it tells you which sessionid it thinks is new (logdata:%{tx.sessionid}):
SecRule REQUEST_COOKIES:'/(j?sessionid|(php)?sessid|(asp|jserv|jw)?session[-_]?(id)?|cf(id|token)|sid)/' ".*" \
  msg:'Invalid SessionID Submitted.',\
        SecRule SESSION:IS_NEW "@eq 1" \
The log shows squirrelmail set 4 cookies in response to the first request (contents removed):
Set-Cookie: SQMSESSID=foo; path=/squirrelmail/
Set-Cookie: SQMSESSID=foo; path=/squirrelmail/; secure; HttpOnly
Set-Cookie: SQMSESSID=bar; path=/squirrelmail/
Set-Cookie: SQMSESSID=bar; path=/squirrelmail/; secure; HttpOnly
The first three are overwritten by the last one in your browser. Although the logdata parts I added to the rules show matching session IDs in the log, I wonder if what appears in the log is the last match, whereas the setsid rule can only be run once per request and therefore the last cookie (with contents "bar") is sent in the subsequent requests, but the first cookie (with contents "foo") is used in the setsid! Anyway, I'm going to admit defeat on this one. I also tried to find out why squirrelmail sets so many cookies in a row, but didn't get far. I've been looking at the new modsecurity CRS (version 3.0), which seems to have been simplified quite a lot, and is apparently ready for use - I might try and switch over to that. Sam

Hi Sam,

Many thanks for taking the time to investigate the problem in such detail. I will insert your mods into 981054/62 and look at these. It is true that SMail generates loads of cookies - multiple times for one attempt to login

Set-Cookie: SQMSESSID=rgaabbdb2evt3t2j6e52b6vss5; path=/; secure; HttpOnly
Set-Cookie: SQMSESSID=lhh25ddcj35gbp0uitd1n9n562; path=/; secure; HttpOnly
and later
Set-Cookie: SQMSESSID=rgaabbdb2evt3t2j6e52b6vss5; path=/; secure; HttpOnly
Set-Cookie: SQMSESSID=kf52sft09fvgia97dd090ps5c5; path=/; secure; HttpOnly

perhaps it's no wonder that modsecurity gets upset.

I will revert to your original suggestion which was to look at the first POST and if this is /src then turn Mod's Engine to DETECTION ONLY.



Hi, I followed your guide (except whitelisting) step by step, but I enable security mod launching the command tail -f /var/log/modsec_audit.log to check if mod security detects attacks, but when on the Drupal site launch a script, the folder does not detect anything. What do you think I'm wrong ?

You won't see entries in the log for every request, only ones where some rules that have 'auditlog' in them were matched. However, if you're enabling modsecurity for the first time I'd expect there to be lots of false positives, so it's likely the module isn't enabled (either globally or in the virtualhost you're using). Did you enable the module (sudo a2enmod security2) and then restart Apache (sudo systemctl restart apache2)? Did you definitely load some rules in /etc/modsecurity? Sam

Hi Sam

Great contents you have here, learning a lot! Thanks for that.

I just have a simple question, where I don't feel the answer is elaborated enough. The book you mention, ModSecurity Handbook (, appears in most searches for ModSecurity. Seeing as the book is last updated in 2012, is it still viable to read it and trust the contents? I'm not sure exactly how much ModSec has changed, code wide, in the years following 2012 to now.

We're using ModSec as a type of Virtual Patching; writing all rules from the ground up, both to learn, but also to shape them into our environment as we see fit.

Thanks again

It's still a really useful book. The Core Rule Set is updated frequently but modsecurity itself hasn't changed fundamentally since then, although there are a few new features. Sam

Hi there Sam,

First of all, thank you for sharing all about modsecurity, personally I've been struggling in order to understand everything so here it goes my question:

I've tried using your latest whitelisting tool, but I'm completely confused. I opened the .db file with SQLite Manager (for Firefox) and while I can see IDs from certain tables I don't know what IDs I've to include in the whitelist.

If you can give me a hand I'll really appreciate it.


Hi Sam,

I'm attempting to set up the firewall again (I unsuccessfully tried many months ago).

I'm at the Brute Force section and was wondering about the single quote at the end of the url line:

setvar:'tx.brute_force_protected_urls=#/squirrelmail/src/login.php# #/phpmyadmin/# #/owncloud/index.php/login#', \

I see in some of your answers to comments you didn't use it. Is it criticial to the code to be there?
I also have squirrelmail, phpmyadmin and owncloud - did I set it up properly in the above line for brute force?

Please advise.




Hi Sam,

Apache2 Reload failed...

Here are the logs:

admin@pi-box:~ $ sudo systemctl status apache2.service
● apache2.service - LSB: Apache2 web server
Loaded: loaded (/etc/init.d/apache2)
Drop-In: /lib/systemd/system/apache2.service.d
Active: active (running) (Result: exit-code) since Wed 2017-01-04 12:10:49 UTC; 53min ago
Process: 23677 ExecStop=/etc/init.d/apache2 stop (code=exited, status=0/SUCCESS)
Process: 24574 ExecReload=/etc/init.d/apache2 reload (code=exited, status=1/FAILURE)
Process: 23703 ExecStart=/etc/init.d/apache2 start (code=exited, status=0/SUCCESS)
CGroup: /system.slice/apache2.service
├─23727 /usr/sbin/apache2 -k start
├─23731 /usr/sbin/apache2 -k start
├─23732 /usr/sbin/apache2 -k start
├─23734 /usr/sbin/apache2 -k start
├─23735 /usr/sbin/apache2 -k start
├─24442 /usr/sbin/apache2 -k start
├─24444 /usr/sbin/apache2 -k start
├─24492 /usr/sbin/apache2 -k start
├─24494 /usr/sbin/apache2 -k start
├─24495 /usr/sbin/apache2 -k start
└─24503 /usr/sbin/apache2 -k start

Jan 04 13:01:40 pi-box systemd[1]: Reloading LSB: Apache2 web server.
Jan 04 13:01:41 pi-box apache2[24574]: Reloading web server: apache2 failed!
Jan 04 13:01:41 pi-box apache2[24574]: The apache2 configtest failed. Not doing anything. ... (warning).
Jan 04 13:01:41 pi-box apache2[24574]: Output of config test was:
Jan 04 13:01:41 pi-box apache2[24574]: AH00526: Syntax error on line 51 of /etc/modsecurity/modsecurity_crs_16_session_hijacking.conf:
Jan 04 13:01:41 pi-box apache2[24574]: ModSecurity: Disruptive actions can only be specified by chain starter rules.
Jan 04 13:01:41 pi-box apache2[24574]: Action 'configtest' failed.
Jan 04 13:01:41 pi-box apache2[24574]: The Apache error log may have more information.
Jan 04 13:01:41 pi-box systemd[1]: apache2.service: control process exited, code=exited status=1
Jan 04 13:01:41 pi-box systemd[1]: Reload failed for LSB: Apache2 web server.

admin@pi-box:~ $ sudo journalctl -xn
-- Logs begin at Sun 2017-01-01 19:55:30 UTC, end at Wed 2017-01-04 13:07:18 UTC. --
Jan 04 13:03:47 pi-box postfix/smtpd[24593]: disconnect from unknown[]
Jan 04 13:03:49 pi-box sudo[24607]: pam_unix(sudo:session): session closed for user root
Jan 04 13:03:59 pi-box sudo[24615]: admin : TTY=pts/0 ; PWD=/home/admin ; USER=root ; COMMAND=/bin/systemctl status apache2.service
Jan 04 13:03:59 pi-box sudo[24615]: pam_unix(sudo:session): session opened for user root by (uid=0)
Jan 04 13:03:59 pi-box sudo[24615]: pam_unix(sudo:session): session closed for user root
Jan 04 13:07:07 pi-box postfix/anvil[24556]: statistics: max connection rate 1/60s for (smtp: at Jan 4 13:00:28
Jan 04 13:07:07 pi-box postfix/anvil[24556]: statistics: max connection count 1 for (smtp: at Jan 4 13:00:28
Jan 04 13:07:07 pi-box postfix/anvil[24556]: statistics: max cache size 1 at Jan 4 13:00:28
Jan 04 13:07:18 pi-box sudo[24626]: admin : TTY=pts/0 ; PWD=/home/admin ; USER=root ; COMMAND=/bin/journalctl -xn
Jan 04 13:07:18 pi-box sudo[24626]: pam_unix(sudo:session): session opened for user root by (uid=0)

Any ideas? Please help.



As a general rule, don't bother with journalctl -xn. I know the output of systemctl status tells you to use it, but it just gives you the last 10 lines of the journal, which may or may not be relevant. To check apache config, use sudo apachectl configtest, which will tell you which line your error is on. Sam

Hi Sam,

It says the problem is on line 51, which is below, but I didn't do anything with this file...Below that I added the error.log file as well.

Line 51/52 of configtest:

SecRule &SESSION:SESSIONID "@eq 1" "chain,phase:5,id:'981064',nolog,pass,t:none"
SecRule REQUEST_HEADERS:User-Agent ".*" "t:none,t:sha1,t:hexEncode,nolog,setvar:session.ua_hash=%{matched_var}"

Apache error.log file:

[Wed Jan 04 06:25:11.482882 2017] [mpm_prefork:notice] [pid 1099] AH00163: Apache/2.4.10 (Raspbian) OpenSSL/1.0.1t configured -- resuming normal operations
[Wed Jan 04 06:25:11.483042 2017] [core:notice] [pid 1099] AH00094: Command line: '/usr/sbin/apache2'
[Wed Jan 04 12:10:44.918301 2017] [mpm_prefork:notice] [pid 1099] AH00169: caught SIGTERM, shutting down
[Wed Jan 04 12:10:48.000495 2017] [:notice] [pid 23722] ModSecurity for Apache/2.8.0 ( configured.
[Wed Jan 04 12:10:48.000689 2017] [:notice] [pid 23722] ModSecurity: APR compiled version="1.5.1"; loaded version="1.5.1"
[Wed Jan 04 12:10:48.000743 2017] [:notice] [pid 23722] ModSecurity: PCRE compiled version="8.35 "; loaded version="8.35 2014-04-04"
[Wed Jan 04 12:10:48.000830 2017] [:notice] [pid 23722] ModSecurity: LUA compiled version="Lua 5.1"
[Wed Jan 04 12:10:48.000865 2017] [:notice] [pid 23722] ModSecurity: LIBXML compiled version="2.9.1"
[Wed Jan 04 12:10:48.000893 2017] [:notice] [pid 23722] Status engine is currently disabled, enable it by set SecStatusEngine to On.
[Wed Jan 04 12:10:49.021507 2017] [mpm_prefork:notice] [pid 23727] AH00163: Apache/2.4.10 (Raspbian) OpenSSL/1.0.1t configured -- resuming normal operations
[Wed Jan 04 12:10:49.021790 2017] [core:notice] [pid 23727] AH00094: Command line: '/usr/sbin/apache2'

Any ideas what it might be?



Add new comment

The content of this field is kept private and will not be shown publicly.

Filtered HTML

  • Web page addresses and email addresses turn into links automatically.
  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Theme by Danetsoft and Danang Probo Sayekti inspired by Maksimer