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.

30-45 minutes intermediate 8 steps
Last updated: Jul 18, 2026

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

1

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.

Tip: On CentOS/RHEL, the package is called mod_security and is configured through /etc/httpd/conf.d/mod_security.conf.
2

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.

3

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.
4

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.

5

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.

6

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"
7

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_rewrite with complex conditions: Caddy uses matchers instead of RewriteCond/RewriteRule. Most patterns translate, but complex chains need rethinking.
  • <Directory> / <Location> blocks: use Caddy's handle and matcher directives instead.
  • Authentication (mod_auth_basic): use Caddy's basic_auth directive.

Not available in Caddy

  • mod_php: Caddy does not embed PHP. Use PHP-FPM as a backend with php_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.
8

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