Technology

The Calm WAF: How I Tune ModSecurity + OWASP CRS to Cut False Positives and Keep Sites Fast

It started with a late-night ping from a friend: “Our checkout is throwing weird errors, but only for some users. We didn’t change anything.” You know that stomach drop when you realize the Web Application Firewall is probably doing its job a little too enthusiastically? I’ve been there more times than I’d like to admit. The logs are full of scary-looking alerts, the devs are frustrated, the marketing team is waving a launch deadline, and all you want is a WAF that stops the bad stuff without stepping on legitimate traffic.

Here’s the thing: ModSecurity with the OWASP Core Rule Set (CRS) can be a lifesaver, but it needs a little love. Out of the box, it’s like a guard dog that barks at every unfamiliar noise. With some thoughtful tuning, though, it becomes calm, confident, and accurate. In this post, I’ll walk you through how I approach false positive reduction and performance tuning for ModSecurity + CRS, with stories from real-world rollouts, the why behind each tweak, and the settings that have earned their spot in my go-to template.

We’ll talk about a workflow that catches issues early, a safe way to roll out rules, practical examples of exclusions that don’t turn your firewall into a screen door, and how to keep request inspection snappy. By the end, you’ll have a playbook you can pick up tomorrow morning and apply to your own stack.

İçindekiler

What I Aim For When I Deploy ModSecurity + CRS

Whenever I add a WAF to a site, I set two goals. First, stop actual bad requests—SQL injection payloads, XSS attempts, protocol anomalies, and bot noise. Second, don’t break normal users or admin workflows. Sounds obvious. But in practice, that balance takes a bit of craftsmanship.

My mindset is simple: treat the WAF like a safety net layered under good app hygiene and smart edge rules. It’s not your only defense, and it shouldn’t feel like handcuffs during a release. That’s why I never slam a WAF into full block mode on day one. I stage it, I learn from its alerts, and then I dial in the rules until “silent, accurate, fast” becomes the default state.

If you like layered defense talk, I’ve shared a bigger picture of that approach here: the layered shield I trust with Cloudflare, ModSecurity, and Fail2ban. Think of this article as the deep-dive on the ModSecurity + CRS part of that shield.

A Friendly Crash Course: ModSecurity, CRS, and Why False Positives Happen

Let’s set the scene with a quick, conversational overview. ModSecurity is the engine—the part that sits in your web server and inspects requests and responses. The OWASP Core Rule Set (CRS) is the brain—the curated set of signatures, behaviors, and anomaly checks that tells ModSecurity what to look for. You can run ModSecurity with Apache, Nginx, or even as part of some proxies. It’s flexible, which is one reason I keep coming back to it.

CRS has personality. It looks not only for obvious attacks but also for suspicious patterns across headers, URLs, bodies, and encodings. That thoroughness is why you’ll see false positives: real-world apps are messy. Someone’s API uses JSON fields called “select” or “union.” Your search box accepts angle brackets because it powers an internal template editor. A plugin sends encoded payloads that look exactly like what a WAF would block in a different context. None of this means CRS is wrong—it just means your app deserves some custom fit.

If you haven’t browsed the docs, they’re straightforward and worth a skim: the OWASP Core Rule Set documentation lays out the rule groups, paranoia levels, and tuning methods in plain language. I’ll distill the bits I actually touch in production below.

My Rollout Workflow: Quiet First, Then Confident

Step 1: Detection only, with anomaly scoring

When I first deploy CRS on a live site, I start in “DetectionOnly” mode. In CRS 3.x, anomaly scoring is the default strategy. Each suspicious pattern adds points. A typical flow is: let the WAF score requests silently, ship logs, and watch what bubbles up. The goal is to learn the app’s shape without blocking users on day one.

# Global engine in detection-only
SecRuleEngine DetectionOnly

# CRS core includes (example path; adapt to your distro)
Include /etc/modsecurity/crs/crs-setup.conf
Include /etc/modsecurity/crs/rules/*.conf

Why anomaly scoring? It’s gentler than single-rule blocking. Users rarely trigger multiple independent rule groups unless they’re doing something sketchy. That makes it easier to separate noisy patterns from truly risky requests.

Step 2: Set a blocking threshold and monitor

After a few days, I enable blocking with a threshold that fits the app’s behavior. For many apps, a threshold of 5 is a decent starting point. If you’re seeing borderline cases in the logs, you can go a notch higher and still catch the majority of real attacks. Then we iterate.

# Switch to blocking mode once you trust your telemetry
SecRuleEngine On

# In crs-setup.conf, set anomaly thresholds (example values)
# tx.inbound_anomaly_score_threshold=5
# tx.outbound_anomaly_score_threshold=4

Don’t rush this phase. Those first weeks teach you a ton about the app’s rough edges. I like to schedule quick “WAF check-ins” with the dev team to review logs and spot patterns early, before someone reports a broken workflow.

Step 3: Nudge the paranoia level gradually

CRS gives you “paranoia levels,” essentially sensitivity presets. Level 1 is friendly and broadly compatible; level 2 adds depth; higher levels get very strict. My approach: start at 1, fix the low-hanging false positives, then try level 2 in detection only for a period. If it’s quiet, promote it to block mode. Rinse and repeat. The official CRS paranoia levels guide explains what each level adds, and it matches what I see in the real world.

False Positive Tuning: A Playbook That Doesn’t Weaken Your Shield

I learned the hard way that quick fixes—like turning off huge chunks of rules—feel good in the moment and hurt later. The trick is to be precise. Think in terms of scope: what path, what parameter, what header, and in what context? Here’s how I keep the shield strong while removing the friction.

1) Build a short feedback loop

When an alert lands, I ask three questions: is this user flow legitimate, is the matched pattern actually risky, and can I narrow the context so the rule still catches real attacks elsewhere? I scan the ModSecurity audit log for the Rule ID, the matched variable (ARGS, REQUEST_HEADERS, REQUEST_COOKIES), and the exact snippet flagged. That tells me where to aim the exclusion.

2) Exclude by surgical path or parameter

Let’s say a REST endpoint legitimately accepts JSON fields like “order[select]” or raw HTML. Instead of disabling an entire rule group globally, I’ll either remove a specific rule for that location or remove a specific variable (target) from inspection. For example, excluding a single parameter from a single rule that’s noisy:

# Example: For a specific path, remove ARGS:comment from a rule (e.g., XSS rule 941100)
# Adapt syntax to Apache/Nginx context blocks as needed.

SecRule REQUEST_URI "^/api/review$" "id:10010,phase:1,pass,t:none,ctl:ruleRemoveTargetById=941100;ARGS:comment"

If a whole endpoint is special (like a WYSIWYG admin path), I’ll limit exclusions to that path only. That way, the rest of the app continues to enjoy full protection.

3) Whitelist admin-only routes—carefully

Admin dashboards often include features—file uploads, embedded HTML, complex filters—that can trigger CRS. If the route is behind authentication and additional checks, I’m okay with lighter inspection there. But I don’t go straight to “WAF off.” I might remove high-noise rules and keep protocol and anomaly checks in place. If you absolutely must disable inspection, scope it tightly:

# Apache-style example for a very specific admin uploader path
<LocationMatch "/admin/uploader">
    SecRuleEngine Off
</LocationMatch>

In Nginx with the ModSecurity connector, use a location block and turn modsecurity off for that route. The goal is to be intentional and explicit. Future you will thank you.

4) Tackle common hot spots: SQLi and XSS alerts on legit inputs

If you’ve run CRS for more than a week, you’ve seen this: an input that contains words like “select” or “union” or a search box using “” for templates. My approach is: first, check the request to confirm the context really needs those strings. Second, prefer removing only the relevant rule(s) for that path or parameter. For example, a review comment field that allows angle brackets could trigger 941100 (XSS). I’ll narrow that rule’s target to exclude just ARGS:comment on the review endpoint, as shown earlier.

On SQLi, pay attention to content type. A lot of false positives happen when JSON bodies contain short tokens that match SQL keywords. If the endpoint is internal or authenticated, and the pattern is expected, I’ll remove SQLi checks just for that parameter or endpoint, not globally.

5) Be kind to uploads

Large file uploads are an easy way to both slow down your WAF and create confusion. I usually keep a higher body limit for multipart forms while capping in-memory buffering and making sure timeouts are sane. Here’s a baseline I’ve used often:

# Body limits (tune to your infra)
SecRequestBodyLimit 13107200            # 12.5MB
SecRequestBodyNoFilesLimit 131072       # 128KB for non-file bodies
SecRequestBodyInMemoryLimit 131072      # Keep memory usage moderate
SecResponseBodyAccess Off               # Usually off unless you need response inspection

Uploads will generate fewer false positives if you treat them differently and confirm allowed MIME types at the app layer. CRS does its best, but the app is the most reliable bouncer at the door for what files should be accepted.

6) Don’t forget outbound rules

CRS can also inspect responses. It’s not as commonly used, but it can catch server leaks or unintended data. For most sites, I keep response inspection minimal for performance reasons. If you enable it, start in detection only and keep your thresholds conservative.

Performance Tuning: Make It Fast Without Losing Sleep

I used to think WAF slowness was inevitable. It isn’t. Most of the perceived slowness comes from a few hotspots: inspecting static files, overly broad rules applied to traffic that doesn’t need them, and debug-heavy logging. Trim those, and ModSecurity hums along quietly.

Skip what doesn’t need scanning

There’s no reason to inspect every PNG, CSS, or JS as if it were a risky POST request. I exclude static assets and health checks from deep inspection. In Apache, I’ll wrap those paths and switch off the engine. In Nginx, I’ll set modsecurity off in the locations that serve static files.

# Apache-style example for static paths
<LocationMatch ".*.(?:css|js|png|jpg|jpeg|gif|ico|svg)$">
    SecRuleEngine Off
</LocationMatch>

Even when the engine is on, make sure your rule set and request phases aren’t wasting time on traffic that can’t possibly be malicious.

Right-size your PCRE and logging

CRS uses a lot of regular expressions. Enabling PCRE JIT at build time can help on busy systems. Also, set reasonable PCRE match limits to prevent pathological cases from causing CPU spikes. And in production, keep debug logging low. It’s helpful in a pinch but expensive if left on.

# Good production defaults (tune as needed)
SecDebugLogLevel 0
SecAuditLogRelevantStatus "@lt 400"
SecPcreMatchLimit 100000
SecPcreMatchLimitRecursion 100000

I once found a staging server crawling because debug logging was left at a high level. The fix was as glamorous as flipping a switch. Don’t overlook the boring wins.

Tune request phases and transformations

CRS rules run in phases (think “early request checks” vs “post-body checks”). Most of the heavy lifting happens after the request body is available. If your app almost never uses request bodies on certain paths, you can short-circuit deep inspection there. Likewise, transformations (like URL decoding or lowercasing) are essential for catching obfuscated attacks, but you can remove a target from a specific rule if the cost outweighs the benefit in a narrow context.

Scope your configuration to your traffic

If your app is almost entirely JSON APIs, treat it like that. If it’s mostly static with a handful of dynamic endpoints, embrace that shape. The same WAF config for every path is the slowest and least accurate. The more you reflect your app’s reality in your rules, the less work ModSecurity has to do.

A Real-World Tuning Story: The Checkout That Kept Tripping XSS

A client’s checkout was fine under test but sporadically broke in production. We dug into the audit logs and found a pattern: a loyalty field was triggering an XSS rule because some users pasted coupon codes containing template-like strings. The rule wasn’t wrong. The input could have been risky. But we had several layers of server-side output encoding, and the field wasn’t rendered back to users without escaping.

We made a narrow exclusion: remove that specific field from the specific XSS rule on the checkout endpoint, and only for authenticated sessions. Then we set an alert for any multi-rule anomaly on that endpoint. False positive gone, protection still in place. Performance also improved because the app wasn’t retrying failed steps.

The punchline is that the most effective WAF tuning tends to be boring: small, scoped, and logged. Save the dramatic changes for last resort.

Your First 90 Minutes: A Practical Checklist

1) Start quiet

Switch ModSecurity to DetectionOnly. Enable CRS. Keep your current deployment paths untouched. Let it run for a couple of days to collect data without impacting users.

2) Turn on anomaly scoring dashboards

Point the audit log to a place where you can query it. I like to track top Rule IDs, affected paths, and client IP segments. A few hot paths will show up quickly. Fix those first.

3) Set thresholds and move to blocking

Pick a threshold (5 is a good middle ground), enable blocking, and continue to monitor. Create small, focused exclusions for known-good flows. If you’re nervous, start with blocking on staging and detection-only on production, then flip the switch once you’re comfortable.

4) Try paranoia level 2, gently

Crank CRS to level 2 in detection-only. Let it sit for a week. If it’s quiet, enable it in blocking mode. You’ll be pleasantly surprised how much extra coverage you gain with a handful of surgical exclusions.

5) Trim the fat for performance

Exclude static assets from scanning. Re-check your debug log levels. Make sure PCRE limits are set. Confirm body limits match your app’s upload habits. Each of these takes minutes and pays off every day.

Examples You Can Copy-Paste (Adapt With Care)

Exclude static assets from WAF inspection

# Apache example
<LocationMatch ".*.(?:css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$">
    SecRuleEngine Off
</LocationMatch>

Remove one noisy rule for one parameter on one path

# Targeted exclusion for XSS rule on a comment field
SecRule REQUEST_URI "^/api/review$" "id:10010,phase:1,pass,t:none,ctl:ruleRemoveTargetById=941100;ARGS:comment"

Allow larger file uploads without blowing memory

SecRequestBodyLimit 13107200
SecRequestBodyInMemoryLimit 131072
SecRequestBodyNoFilesLimit 131072

Switch from detection-only to blocking with thresholds

# Engine On, anomaly thresholds set in crs-setup.conf
SecRuleEngine On
# In crs-setup.conf:
# setvar:tx.inbound_anomaly_score_threshold=5
# setvar:tx.outbound_anomaly_score_threshold=4

How I Think About Safety Nets: Don’t Tune in a Vacuum

WAFs do their best work as part of a broader plan. If you’re already using an edge provider with basic bot rules and rate limiting, let that layer absorb some junk traffic. Your origin WAF then deals with the trickier stuff that slips through. Likewise, if your app sanitizes output correctly and validates inputs thoroughly, you can afford to be more forgiving in certain endpoints without opening doors you’ll regret.

The layers don’t compete—they relax each other. That’s the quiet magic of a balanced stack. Your pages feel fast, your logs stay readable, and your team only hears about security when it matters.

Troubleshooting: Reading the Audit Log Without Losing Your Mind

Audit logs can feel like a wall of text at first. Here’s how I skim efficiently. I look for the section with the matched Rule ID and the “Matched Data” snippet. That usually points me straight to the parameter or header that tripped the rule. Next, I scan the rule tag—XSS, SQLi, protocol anomaly—to understand the family. Then, I check the request path and method. If it’s a POST to a JSON endpoint, I already have a mental shortlist of typical false positives.

When I’m ready to write an exclusion, I copy the path pattern, note the parameter name, and decide between “remove this rule for this path” or “remove this rule’s target for this parameter.” I save global rule removals for the rare cases where a rule simply doesn’t fit the app at all. And I document every change with the Rule ID and why we did it. Future merges go smoother when your config tells a story.

Useful Docs and Where I Look Things Up

When I get stuck or want to double-check syntax, I go straight to the official docs. The OWASP Core Rule Set documentation is my home base for rule behavior and concepts. For a deeper dive into the engine itself, the ModSecurity reference manual covers directives, variables, and gotchas. And if you’re experimenting with stricter policies, the CRS paranoia levels guide helps you set expectations before you flip the switch.

Wrap-Up: Keep the WAF Calm, and It’ll Keep You Calm

I remember the first time I rolled out CRS without a plan. It felt like tossing a net into a storm and hoping for the best. Since then, the playbook has become almost meditative: start quiet, learn the app’s voice, tighten the rules, and stay specific with exclusions. The result is a WAF that rarely surprises you, a team that trusts it, and users who never know it’s there.

If you’re about to start tuning, keep it simple. Set anomaly thresholds that feel comfortable. Trim static paths from inspection. Tackle the loudest false positives with one-parameter, one-path exclusions. Slowly elevate paranoia levels once the noise dies down. And revisit your settings when the app changes—new features deserve a quick “WAF warm-up” just like they deserve tests and monitoring.

Hope this was helpful! If you want more on the bigger security picture, check out the layered approach I mentioned earlier. And if you found a tweak that made your WAF both calmer and faster, I’d genuinely love to hear about it. See you in the next post, and may your logs be boring in the best possible way.

Frequently Asked Questions

Great question! Start in DetectionOnly so nothing gets blocked. Let it run for a few days, review the audit logs, then enable blocking with an anomaly threshold (I like 5). Fix noisy paths with small, scoped exclusions. Once it’s quiet, consider raising the paranoia level in detection-only, monitor again, and promote when it stays calm.

Think surgical, not global. Remove a rule’s target for a single parameter on a single path, or disable a specific rule only on a specific endpoint. Keep protocol and anomaly checks on. Document every exclusion with the Rule ID and reason, and avoid turning off entire rule groups unless you truly have to.

Skip static assets, right-size body limits, keep response inspection off unless needed, and keep debug logging low in production. Make sure PCRE JIT is enabled if available and set reasonable PCRE match limits. Above all, scope rules to the shape of your app so the WAF doesn’t waste cycles on traffic that can’t be malicious.