How to Add WAF Protection to Apache
Two approaches to protect Apache with a WAF. Use ModSecurity as a native Apache module, or replace Apache with Caddy+Coraza for a simpler, modern setup with the same OWASP CRS rules.
Apache has the longest history of any web server when it comes to WAF protection. ModSecurity was originally built as an Apache module in 2002, and the combination of Apache + ModSecurity + CRS was the reference open source WAF deployment for over two decades.
One thing to know going in: Trustwave ended commercial support for ModSecurity on 1 July 2024, and custodianship of the engine passed to OWASP, where it is now maintained in a stability/maintenance mode (security and bug fixes, with a slow feature pace). The Apache module still works and is still packaged by every major distribution, but this is part of why we present the modern Coraza option alongside it. Both run the same OWASP CRS rules.
This guide covers two approaches:
- Option A: ModSecurity as a native Apache module -- the traditional approach. Install mod_security2 directly into Apache. Mature, well-documented, battle-tested. Best if you have an existing Apache setup and want to add WAF protection without changing your architecture.
- Option B: Replace Apache with Caddy+Coraza -- the modern approach. Use Caddy as both your web server and WAF, powered by Coraza (a Go reimplementation of ModSecurity). Same CRS rules, simpler config, automatic HTTPS, one container. Best for new deployments or if you are already considering a migration away from Apache.
Both approaches use the OWASP Core Rule Set (CRS) for attack detection. Your protection coverage is the same regardless of which engine runs the rules.
Prerequisites
- An existing Apache setup you want to protect, or Docker for the Caddy+Coraza approach
- Root or sudo access for the Apache ModSecurity approach
- Basic familiarity with Apache configuration (.htaccess, VirtualHost blocks)
Step-by-Step Guide
Option A: ModSecurity as a Native Apache Module
This is the traditional approach. ModSecurity was originally built for Apache and integrates as a native module (mod_security2). It runs inside the Apache process, inspecting every request.
Install on Ubuntu/Debian:
sudo apt update
sudo apt install -y libapache2-mod-security2
sudo a2enmod security2
sudo systemctl restart apache2
Enable the base ModSecurity config:
The package ships the core engine settings only as a sample file. You must copy it into place. Without this, the IncludeOptional /etc/modsecurity/modsecurity.conf line in the config below matches no file (IncludeOptional silently ignores a missing path), and the base settings for request/response body access, PCRE limits, and audit logging are never loaded, which makes CRS rules that depend on them misbehave:
sudo cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
This file ships with SecRuleEngine DetectionOnly (log matches but do not block). The SecRuleEngine On in the Apache config below overrides it to actively block. Leave it on DetectionOnly for an initial observation period if you want to review false positives in the audit log before enforcing.
Install the OWASP CRS:
cd /etc/modsecurity
sudo git clone https://github.com/coreruleset/coreruleset.git /etc/modsecurity/crs
sudo cp /etc/modsecurity/crs/crs-setup.conf.example /etc/modsecurity/crs/crs-setup.conf
Configure Apache to use CRS:
Edit /etc/apache2/mods-enabled/security2.conf:
<IfModule security2_module>
SecDataDir /var/cache/modsecurity
SecRuleEngine On
IncludeOptional /etc/modsecurity/modsecurity.conf
IncludeOptional /etc/modsecurity/crs/crs-setup.conf
IncludeOptional /etc/modsecurity/crs/rules/*.conf
</IfModule>
Restart Apache: sudo systemctl restart apache2
For a complete walkthrough, see our ModSecurity provider page.
Option B: Replace Apache with Caddy+Coraza
If you want to simplify your stack, Caddy+Coraza can replace Apache entirely. You get a web server, WAF, and automatic HTTPS in one binary. The WAF uses Coraza (a Go reimplementation of ModSecurity) with the same CRS rules.
The key advantage: no C dependencies and no complex Apache config. You do build a custom Caddy binary (the WAF ships as a Go plugin that xcaddy compiles in, shown in a later step), but the result is a single static binary with no C toolchain or per-request filesystem checks. A Caddyfile that replaces a typical Apache VirtualHost is 10-15 lines.
Apache to Caddy Directive Mapping
Here is how common Apache directives translate to Caddy:
Basics
# Apache -> Caddy
DocumentRoot /var/www/html -> root * /srv
DirectoryIndex index.html -> (automatic with file_server)
Options -Indexes -> file_server { browse } (or omit for no listing)
ServerName example.com -> example.com { ... }
Reverse Proxy
# Apache -> Caddy
ProxyPass / http://app:3000/ -> reverse_proxy app:3000
ProxyPassReverse / http://app:3000/ -> (automatic in Caddy)
ProxyPreserveHost On -> (automatic in Caddy)
ProxyTimeout 60 -> reverse_proxy app:3000 { transport http { read_timeout 60s } }
Headers
# Apache -> Caddy
Header set X-Frame-Options DENY -> header X-Frame-Options DENY
Header set X-Content-Type-Options ... -> header X-Content-Type-Options nosniff
ServerTokens Prod -> (off by default in Caddy)
Redirects and Rewrites
# Apache -> Caddy
Redirect 301 /old /new -> redir /old /new permanent
RewriteRule ^/blog(.*)$ /news$1 [R=301]-> redir /blog* /news{path} permanent
RewriteCond %{HTTPS} off -> (automatic with Caddy auto-HTTPS)
RewriteRule (.*) https://%{HTTP_HOST} -> (automatic)
TLS
# Apache -> Caddy
SSLEngine on -> (automatic with domain name)
SSLCertificateFile /path/cert.pem -> tls /path/cert.pem /path/key.pem
SSLCertificateKeyFile /path/key.pem -> (or just use a domain name for auto-HTTPS)
SSLProtocol all -SSLv3 -TLSv1 ... -> (TLS 1.2+ by default)
.htaccess
# Apache .htaccess is not supported in Caddy.
# Move all .htaccess rules into the Caddyfile directly.
# This is actually an advantage: no per-request filesystem checks.
Write the Caddyfile with WAF
Here is a Caddyfile that replaces a typical Apache VirtualHost with WAF protection:
{
order coraza_waf first
}
yourdomain.com {
coraza_waf {
load_owasp_crs
directives `
Include @coraza.conf-recommended
Include @crs-setup.conf.example
Include @owasp_crs/*.conf
SecRuleEngine On
`
}
root * /srv
file_server
encode gzip
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
}
}
Compare this to the Apache equivalent:
<VirtualHost *:443>
ServerName yourdomain.com
DocumentRoot /var/www/html
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.com/privkey.pem
<IfModule security2_module>
SecRuleEngine On
IncludeOptional /etc/modsecurity/modsecurity.conf
IncludeOptional /etc/modsecurity/crs/crs-setup.conf
IncludeOptional /etc/modsecurity/crs/rules/*.conf
</IfModule>
Header set X-Content-Type-Options nosniff
Header set X-Frame-Options DENY
<Directory /var/www/html>
Options -Indexes
AllowOverride None
Require all granted
</Directory>
</VirtualHost>
The Caddyfile is roughly one-third the size. TLS is automatic, and the WAF configuration is simpler because Coraza bundles CRS directly.
Build a Caddy Binary with the Coraza Plugin
Vanilla Caddy does not understand the coraza_waf directive or order coraza_waf first. Those come from the coraza-caddy plugin, which you compile into a custom Caddy binary with xcaddy. The official caddy:builder image bundles xcaddy, so a multi-stage Dockerfile does the whole build and overlays the new binary on the standard caddy image:
# Dockerfile
FROM caddy:builder AS builder
RUN xcaddy build --with github.com/corazawaf/coraza-caddy/v2
FROM caddy
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
The module path must include the /v2 suffix; that is the current major version of coraza-caddy. If you have Go and xcaddy installed on the host, you can build the binary directly instead of using Docker:
xcaddy build --with github.com/corazawaf/coraza-caddy/v2
The OWASP CRS is embedded in the plugin, so the load_owasp_crs subdirective in the Caddyfile is what makes the @crs-setup.conf.example and @owasp_crs/*.conf includes resolve. No separate CRS download is needed for this option.
Build, Run, and Test
Build the Caddy+Coraza image from the Dockerfile above and run it:
# Build
docker build -t caddy-coraza -f Dockerfile .
# Run (static files)
docker run -d -p 80:80 -p 443:443 \
-v ./Caddyfile:/etc/caddy/Caddyfile:ro \
-v ./site:/srv:ro \
-v caddy_data:/data \
caddy-coraza
Test normal access and WAF blocking:
# Normal request - should return 200
curl -o /dev/null -w "%{http_code}\n" http://localhost/
# SQL injection - should return 403
curl -o /dev/null -w "%{http_code}\n" "http://localhost/?id=1%20OR%201=1"
# XSS - should return 403
curl -o /dev/null -w "%{http_code}\n" "http://localhost/?q=%3Cscript%3Ealert(1)%3C/script%3E"
What Does Not Map 1:1 from Apache
Some Apache features work differently or are not available in Caddy:
Works differently
.htaccess: not supported. Move all rules into the Caddyfile. This is actually faster because Caddy does not scan the filesystem for .htaccess files on every request.mod_rewritewith complex conditions: Caddy uses matchers instead of RewriteCond/RewriteRule. Most patterns translate, but complex chains need rethinking.<Directory>/<Location>blocks: use Caddy'shandleand matcher directives instead.- Authentication (
mod_auth_basic): use Caddy'sbasic_authdirective.
Not available in Caddy
mod_php: Caddy does not embed PHP. Use PHP-FPM as a backend withphp_fastcgi.mod_perl/mod_python: no equivalent. Run your app as a separate process and reverse proxy to it.mod_cache: Caddy has a cache-handler plugin but it is less mature than Apache's caching modules.
Use AI to Convert Your Apache Config
For complex Apache configurations with many VirtualHosts, use this prompt with any AI assistant:
Convert the following Apache VirtualHost configuration to a Caddy (Caddyfile) configuration. Also add Coraza WAF protection using the coraza-caddy plugin with OWASP CRS.
Requirements: - Use the coraza_waf directive with load_owasp_crs - Include @coraza.conf-recommended, @crs-setup.conf.example, and @owasp_crs/*.conf - Set SecRuleEngine On - Use "order coraza_waf first" in the global options block - Convert all .htaccess rules into the Caddyfile directly - Preserve all redirects, headers, and proxy targets - Replace mod_rewrite rules with Caddy matchers - Add comments explaining each section - Flag anything that does not have a direct Caddy equivalent
Here is my Apache config:
[paste your Apache VirtualHost config here]
Review the output carefully, especially mod_rewrite conversions and any .htaccess rules that were inlined.
Conclusion & Next Steps
Apache has solid WAF support through its native ModSecurity module. If you are happy with Apache and just want to add WAF protection, Option A (ModSecurity) is the straightforward path. Keep in mind that ModSecurity is now an OWASP maintenance-mode project (Trustwave commercial support ended 1 July 2024), so it receives security and bug fixes rather than active feature development.
If you are looking to modernize your stack, Caddy+Coraza offers the same CRS-based protection with a simpler configuration, automatic HTTPS, and no C dependency chain. The same rules that run on ModSecurity run on Coraza, so your protection level is identical.
Summary:
- Existing Apache setup, just want WAF -> install mod_security2 + CRS
- New deployment or ready to simplify -> Caddy+Coraza in Docker
- Complex Apache config with mod_rewrite, .htaccess, mod_php -> keep Apache + ModSecurity, or migrate incrementally
Troubleshooting
ModSecurity blocks legitimate requests on Apache
Check the Apache error log or ModSecurity audit log (/var/log/apache2/modsec_audit.log) to find which rule is firing. Disable specific rules with SecRuleRemoveById in your Apache config.
Apache is slow after enabling ModSecurity
ModSecurity adds overhead for each request. Reduce it by lowering the CRS paranoia level in crs-setup.conf (default is 1, which is the lightest). Also check that you are not logging every request in full audit mode (SecAuditEngine RelevantOnly instead of On).
.htaccess rules do not work in Caddy
Caddy does not support .htaccess files. All rules need to go in the Caddyfile. Search your .htaccess files for RewriteRule, Redirect, Header, and auth directives and translate them using the mapping table in this guide.
Frequently Asked Questions
Should I stick with Apache + ModSecurity or switch to Caddy + Coraza?
If Apache is working well for you and you just need WAF protection, stick with Apache + ModSecurity. It is a mature open source WAF combination, though the engine is now in OWASP maintenance mode after Trustwave ended commercial support on 1 July 2024. Switch to Caddy + Coraza if you want a simpler config, automatic HTTPS, Docker-native deployment, an actively developed engine, or are already planning to move away from Apache.
Are the CRS rules the same on ModSecurity and Coraza?
Yes. Both engines run the OWASP Core Rule Set. CRS v4 is tested against ModSecurity v2, ModSecurity v3, and Coraza on every release. The protection coverage is identical. The difference is the engine that evaluates the rules (C for ModSecurity, Go for Coraza), not the rules themselves.
Can I migrate my custom ModSecurity rules to Coraza?
In most cases, yes. Coraza supports the SecLang rule language that ModSecurity uses. Your custom SecRule directives, rule exclusions, and CRS tuning will work on Coraza without modification. Some advanced ModSecurity features have partial compatibility in Coraza, so test your custom rules before switching production traffic.
What about Apache modules like mod_php?
Caddy does not embed PHP like Apache's mod_php. Instead, use PHP-FPM (FastCGI Process Manager) as a separate process and configure Caddy with the php_fastcgi directive. This is actually the recommended approach even in Apache for performance. The conversion is straightforward for standard PHP applications like WordPress, Laravel, or Drupal.
Related Guides
How to Install and Configure ModSecurity with NGINX
Complete guide to deploying ModSecurity 3.x with NGINX for free, open-source WAF protection using the OWASP Core Rule Set.
How to Protect Nginx with Coraza WAF Using Docker
Step-by-step guide to deploying Coraza WAF as a reverse proxy in front of Nginx using Docker and docker-compose, with OWASP CRS protection out of the box.