How to Add WAF Protection to Nginx
Two tested approaches to protect your Nginx web server with a WAF. Add Coraza as a reverse proxy in front of Nginx, or replace Nginx entirely with Caddy+Coraza for a single-container solution.
"nginx waf" is one of the most common searches in web application security, and the answer is less obvious than it should be. Nginx does not have a built-in WAF. Your options are to bolt one on (ModSecurity module, or a WAF reverse proxy in front of Nginx) or to switch to a web server that has WAF built in.
This guide covers two approaches, both tested and verified:
- Option A: Keep Nginx, add a WAF proxy in front -- run Caddy with the Coraza WAF plugin as a reverse proxy. Your Nginx config stays untouched. You get OWASP CRS v4 protection with zero changes to your application.
- Option B: Replace Nginx with Caddy+Coraza -- use Caddy as both your web server and WAF. One container, one config file. Caddy handles everything Nginx does (static files, reverse proxy, TLS, headers, gzip) plus WAF protection.
Both approaches use the same WAF engine (Coraza) and the same rule set (OWASP CRS v4). The difference is whether you want to keep Nginx in the picture or simplify your stack.
Prerequisites
- Docker installed on your system
- An existing Nginx setup you want to protect (or follow along with the examples)
- Basic familiarity with Nginx configuration
Step-by-Step Guide
Option A: Add WAF Proxy in Front of Nginx
This approach puts Caddy+Coraza in front of your existing Nginx. The architecture looks like this:
Client -> Caddy + Coraza WAF (:443) -> Nginx (:80) -> Your App
Your Nginx config stays exactly as it is. You add a Caddy container that inspects all traffic before forwarding clean requests to Nginx. Malicious requests are blocked before reaching your server.
See our dedicated step-by-step guide: How to Protect Nginx with Coraza WAF Using Docker. That guide walks through every file, build step, and test in detail.
When to choose this: you have a working Nginx setup, you do not want to change it, and you just want to add WAF protection on top.
Option B: Replace Nginx with Caddy+Coraza
Instead of running two containers, you can replace Nginx entirely. Caddy handles static files, reverse proxying, TLS, headers, and compression, just like Nginx. With the Coraza plugin, it also provides WAF protection.
The result: one container, one config file, web server + WAF + automatic HTTPS.
When to choose this: you are starting fresh, you want a simpler stack, or your Nginx config is straightforward enough to translate to Caddy.
Build the Caddy+Coraza Image
Both options use the same Docker image. Create a Dockerfile:
FROM caddy:2-builder AS builder
RUN xcaddy build \ --with github.com/corazawaf/coraza-caddy/v2
FROM caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
Build it:
docker build -t caddy-coraza .
Translate Your Nginx Config to a Caddyfile
Here is how common Nginx directives map to Caddy. Use this table to convert your config:
Static Files and Basics
# Nginx -> Caddy root /var/www/html; -> root * /srv index index.html; -> (automatic with file_server) try_files $uri $uri/ =404; -> file_server try_files $uri /index.html; -> try_files {path} /index.html gzip on; -> encode gzip listen 443 ssl; -> (automatic with domain name)
Reverse Proxy
# Nginx -> Caddy proxy_pass http://app:3000; -> reverse_proxy app:3000 proxy_set_header Host $host; -> (automatic) proxy_set_header X-Real-IP ...; -> (automatic) proxy_read_timeout 60s; -> reverse_proxy app:3000 { read_timeout 60s } proxy_buffering off; -> reverse_proxy app:3000 { flush_interval -1 }
Headers and Security
# Nginx -> Caddy add_header X-Frame-Options ...; -> header X-Frame-Options DENY add_header X-Content-Type ...; -> header X-Content-Type-Options nosniff server_tokens off; -> (off by default in Caddy)
Redirects and Rewrites
# Nginx -> Caddy return 301 https://$host$uri; -> redir https://{host}{uri} permanent rewrite ^/old(.*)$ /new$1; -> rewrite /old* /new{path.1} location /blog { ... } -> handle /blog/* { ... } location = /health { ... } -> handle /health { ... }
TLS / HTTPS
# Nginx -> Caddy ssl_certificate /path/cert.pem; -> tls /path/cert.pem /path/key.pem ssl_certificate_key /path/...; -> (or just use a domain name for auto-HTTPS) ssl_protocols TLSv1.2 TLSv1.3; -> (TLS 1.2+ by default)
Write the Caddyfile with WAF Protection
Here is a complete Caddyfile that replaces a typical Nginx static site config, 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 Referrer-Policy strict-origin-when-cross-origin } }
For a reverse proxy setup (e.g., proxying to a Node.js or Python app):
{ 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 ` }
reverse_proxy app:3000 }
Run and Test
Start the container (static file example):
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 that normal traffic works:
curl http://localhost/
Test that attacks are blocked:
# 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"
# Scanner - should return 403 curl -o /dev/null -w "%{http_code}\n" -H "User-Agent: nikto" http://localhost/
What Does Not Map 1:1
Some Nginx features do not have direct Caddy equivalents. Here is what to watch for:
Works differently
upstreamblocks with weights/health checks: Caddy handles this inline inreverse_proxywith different syntax. Load balancing policies (round_robin, least_conn, ip_hash) are supported but named differently.mapdirective: no direct equivalent. Usevarsor Caddy'sexpressionmatcher for simple cases.limit_req_zone/ rate limiting: use Caddy'srate_limitdirective (available as a plugin).- Complex
locationregex: Caddy uses path matchers and named matchers instead of regex locations. Most patterns translate cleanly, but deeply nested regex locations need rethinking.
Not available in Caddy
proxy_cache: Caddy does not have built-in response caching (there is a cache-handler plugin, but it is less mature than Nginx's). If you rely heavily on Nginx's proxy cache, keep Nginx as the backend.- Lua/njs scripting: no equivalent. If you have Nginx Lua scripts, they need to be rewritten as Caddy plugins (Go) or moved into your application.
- HTTP/2 push: deprecated in browsers and not supported in Caddy.
Use AI to Help Convert Your Config
If your Nginx config is long or complex, you can use an AI assistant to help with the conversion. Here is a prompt you can paste into ChatGPT, Claude, or any LLM:
Convert the following Nginx 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 - Preserve all existing functionality (redirects, headers, proxy targets) - Add comments explaining each section - Flag anything that does not have a direct Caddy equivalent
Here is my Nginx config:
[paste your nginx.conf or site config here]
Review the output carefully. Check that all your locations, redirects, and proxy targets are preserved. Test the converted config in a staging environment before switching production traffic.
Conclusion & Next Steps
You now have two proven paths to add WAF protection to your Nginx setup:
- Option A (reverse proxy): minimal change, add Caddy+Coraza in front of Nginx. Best when you have a working Nginx config you do not want to touch.
- Option B (full replacement): simplify your stack to one container. Best for new deployments or when your Nginx config is straightforward.
Both approaches give you the same OWASP CRS v4 protection against SQL injection, XSS, command injection, scanner probes, and hundreds of other attack patterns.
Next steps:
- Monitor WAF logs for false positives during the first few days
- Add custom SecRule directives for application-specific protection
- Consider adding CrowdSec integration for IP reputation blocking
- For detailed Docker setup instructions, see the Coraza + Nginx Docker guide
Troubleshooting
My application breaks after adding the WAF
CRS rules can trigger false positives on legitimate application traffic. Check the WAF logs to find which rule is firing, then add an exclusion: SecRuleRemoveById [rule_id] in your Caddyfile directives.
Caddy config errors on startup
Caddy validates the Caddyfile on startup. Common issues: missing closing braces, incorrect indentation, or using Nginx syntax. Run caddy validate --config Caddyfile --adapter caddyfile inside the container to see specific errors.
My regex location blocks do not work in Caddy
Caddy uses path matchers and named matchers instead of regex-based location blocks. For complex patterns, use named matchers: @api path /api/* then handle @api { ... }.
Frequently Asked Questions
Can Caddy fully replace Nginx?
For most use cases, yes. Caddy handles static file serving, reverse proxying, TLS (automatic via Let's Encrypt), headers, gzip compression, redirects, and load balancing. The main gaps are proxy_cache (less mature in Caddy), Lua/njs scripting (no equivalent), and some advanced load balancing features. If your Nginx config is a standard static site or reverse proxy, Caddy is a direct replacement with a simpler config.
Is Caddy as fast as Nginx?
For most workloads, yes. Caddy and Nginx both handle high concurrency well. Nginx has a slight edge in raw static file throughput at extreme scale (100k+ requests/second), but the difference is negligible for the vast majority of deployments. The WAF overhead (1-5ms per request from CRS processing) is the same regardless of which web server you use.
Do I need to learn a new config language?
Yes, but Caddy's config language (Caddyfile) is simpler than Nginx. A typical Nginx config translates to about one-third the lines in Caddy. The mapping table in this guide covers the most common directives. For complex configs, use the AI-assisted conversion prompt included in this guide.
Can I run both Nginx and Caddy+Coraza at the same time during migration?
Yes. Run Caddy+Coraza on different ports or use a load balancer to gradually shift traffic. Start by sending 10% of traffic through Caddy, monitor for issues, then increase. This is the safest way to migrate without downtime.
What about Nginx Plus features?
Nginx Plus adds commercial features like active health checks, session persistence, and a dashboard. Caddy has equivalents for most of these (active health checks in reverse_proxy, sticky cookies for session persistence). The main Nginx Plus feature without a Caddy equivalent is the live activity monitoring dashboard, though Caddy exposes metrics via Prometheus.
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.