This is the third and final blog post of a three-part series describing the new CRS-upgrading-plugin. In the previous two posts I introduced the new plugin (part one) and covered the technical implementation details (part two).
This post walks through a real-world migration at one of our enterprise customers and the experiences gathered during the shift from CRS v3 to CRS v4.
Customer Background
The customer is a large company with millions of requests per day across various websites and web applications.
Before the migration, they had been running CRS v3 for several years at paranoia level 3 with an anomaly score threshold of 5. Their heterogeneous traffic required several hundred exclusion rules.
The motivation to migrate was straightforward: to stay current with the latest vulnerabilities and exploits by moving to CRS v4.
The broad scope of this customer and the sheer volume of traffic made the migration both interesting and challenging.
First Findings: Lots of new False Positives
To be honest, I didn’t expect so many false positives. But in hindsight, running at paranoia level 3 with a large volume of diverse traffic, combined with CRS v4’s new and extended rules, the volume was not really surprising.
The number of false was so large our C-Rex tool C-Rex Fangs ran into performance issues when analyzing them. A positive side effect: this triggered a new, performance-optimized version of C-Rex Fangs!
One rule that triggered frequently and required a lot of tunings was the new RCE rule 932236 at paranoia level 2. Legitimate requests containing strings like “La France” or “Route des Alpes” triggered because “La” and “Route” are also Unix commands.
The parallel mode was absolutely the right approach. Without it, the volume of new false positives would have caused serious disruption to the application.
Tuning Strategy: Migrate Existing Exclusions or Start Fresh?
The customer had invested a considerable effort in tuning CRS v3, so we used a mixed strategy: narrowly written exclusions were kept, overly broad ones were removed. The migration was a good and unique opportunity to clean up, because broad exclusions had been silently preventing ModSecurity from logging certain traffic.
By “too broad” I mean rule exclusions written per tag, such as excluding everything tagged attack-sqli, or removing entire rules instead of targeting specific arguments or parameters. Removing these gave us new insight into the actual traffic patterns and a pleasant surprise: many of the broad exclusions turned out to be unnecessary in that form. Once we looked more closely, only a small part of them was actually needed, for example, excluding a specific rule for a single argument or cookie, rather than suppressing everything under a tag.
This phase also revealed an important technical finding: to make the parallel exclusion approach work correctly, it is not sufficient to renumber the CRS v4 rule IDs to the 89xxxxx range. The tags also need to be renamed: for example from attack-sqli to attack-sqli_crs4. Without this, the CRS v3 exclusion would also silence the CRS v4 rules, and we would lose the visibility we were trying to gain. I have added this step to the Prerequisites section in the plugin README on GitHub.
We used C-Rex Fangs to identify false positives in the logs and C-Rex Arms to write the exclusion rules. The parallel mode tuning phase ran from mid-December through end of March, at which point we moved to sampling mode.
Path-Based Rollout in Practice
We did not use path-based rollout at all. Given the number of applications involved, defining paths for CRS v3 vs. CRS v4 would have required too much application-specific knowledge and taken too long. We went directly from parallel mode to sampling mode.
Sampling Mode: Numbers and Observations
At the end of March we started the sampling mode at 10%, then gradually increased to 30%, 50%, 75% and finally 100% by end of May.
Shortly after moving to 10%, we noticed that CRS v4 false positives were no longer appearing in C-Rex Fangs. The reason was a variable name change: CRS v4 uses TX.blocking_outbound_anomaly_score instead of TX.outbound_anomaly_score and the per-paranoia-level variables changed as well. Since Fangs reads anomaly scores from the extended access log format (as described here) and those values were all zero for CRS v4, the false positives were invisible. We temporarily reverted to parallel mode and fixed the issue with a custom ModSecurity rule that maps the new CRS v4 variable names back to the ones expected by the log format:
code here
Lessons Learned
Several unexpected things came up during the migration, some of which I have already described above.
One thing that was not unexpected, but worth mentioning again: tuning false positives requires application knowledge, not just WAF expertise. I frequently had to consult the application owners to determine whether a triggered request was legitimate or not.
What was unexpected was what happened during the 30% sampling phase: we upgraded from 4.22.0 to the LTS release 4.25.0. Minor releases had not caused issues, but the LTS release introduced new rules and rule changes that led to false positives and blocked legitimate users. Fortunately, it only affected a single argument in each of two applications, and only 30% of their traffic. But still, it showed that a ruleset update can always bring new surprises.
To be honest, during the migration I mostly focused on the false positives that Fangs showed and rarely looked at the error log where the real attacks would have shown up. But in general, it is safe to say that CRS v4 is the most secure CRS release ever, thanks in part to the CRS bug bounty program in 2022, which led to stronger and broader attack detection.
Overall the project went well and I would not change much. The one thing I would do differently: install a major ruleset update like the LTS release during parallel mode rather than during sampling mode. But in our case the LTS release 4.25.0 simply came out at the time we were already at 30% sampling.
Time and Effort
We started the project on October 27, 2025, with a kickoff with the development team on October 29. Having the developers on board from the start is important, as their application knowledge is essential for the tuning work later on. In weeks 44 and 45 I cleaned up the existing rule exclusions, removing the overly broad ones as described above. In weeks 46 and 47 we deployed the plugin to a staging environment and verified that it worked correctly. In week 49 the plugin was deployed to two production services and in week 50 it was rolled out to full production.
Parallel mode and the review of false positives started in week 51. It was here that we discovered C-Rex Fangs had performance issues with the high volume of false positives, so alongside the tuning work I also had to make performance improvements to Fangs. Parallel mode ended in week 12 and since week 13 of 2026 we have been running in sampling mode. Sampling mode ends this week and from week 22 onwards we run with 100% CRS v4.
All in all, the migration took around seven months. We could have moved faster, but we always chose to take more time rather than risk disruption for the customer. Looking at the overall effort: roughly 22% went into project setup and plugin deployment, 44% into tuning during parallel mode (including the Fangs performance work), 28% into the sampling mode phase, and the remaining 6% will be the final cleanup.
The plugin was absolutely worth it. Doing this migration manually would have been significantly more complex and error-prone.
Conclusion
The new CRS v4 setup is in good shape. The customer is now running the LTS release 4.25.0 with a cleaned-up, minimal set of exclusion rules. Compared to the old CRS v3 installation, the security posture is better and the configuration is leaner.
If you are still running CRS v3, the migration is worth the effort and the plugin makes it a lot more manageable.
I would recommend the plugin to anyone upgrading from CRS v3 to CRS v4. Without it, you risk breaking your application with new false positives, which, as we now know, will happen anyway. I don’t think it is so much a question of size or complexity. As soon as you have a blocking WAF setup with a low anomaly threshold, you should run CRS v4 in parallel mode first before enabling blocking and the plugin is the easiest way to do that.
The plugin is available at https://github.com/netnea/netnea-crs-upgrading-plugin. Feedback and issues are always welcome!

Franziska Bühler
