Hardening Caddy: Why I Ditched Teler WAF for Coraza & CrowdSec (and Cloudflare Alternatives)

Running a modern self-hosted infrastructure or homelab is a constant balancing act between security and convenience. We want to expose our web applications—whether it’s WordPress, automated workflow tools, or local LLM interfaces—to the internet without leaving the front door wide open to threat actors.

For a long time, Caddy has been my reverse proxy of choice. Its automatic HTTPS handling via ACME, lightweight footprint, and intuitive Caddyfile syntax make it unbeatable.

However, edge defense is where things get tricky. In my search for a lightweight security layer, I initially turned to Teler WAF. While it started out promising, it quickly turned into a maintenance headache. Here is why I moved away from Teler, how I replaced it with a multi-layered CrowdSec + Coraza WAF setup, and why Cloudflare remains a solid alternative worth considering.

The Problem with Teler WAF: The Maintenance Trap

When setting up a Web Application Firewall (WAF) in Caddy, Teler often comes up as a lightweight, Go-based option. It analyzes HTTP traffic in real-time against threat intelligence feeds, known malicious user agents, CVE patterns, and basic application attack signatures.

On paper, Teler sounds ideal. In practice, running Teler on dynamic, modern web applications led to constant friction:

1. High Rate of False Positives

Teler relies heavily on static pattern matching and simple regex rules. Modern web applications—especially rich single-page apps (SPAs), webhooks, and REST APIs—regularly send complex JSON payloads, encoded strings, and dynamic query parameters. Teler routinely flagged legitimate API requests as SQL injections or cross-site scripting (XSS) attempts, causing silent application breakages for valid users.

2. Lack of Fine-Grained Exclusion Rules

Unlike mature WAF engines, tuning Teler to ignore specific false positives without completely disabling entire threat categories was cumbersome. If a legitimate admin action on WordPress triggered a rule, your choices were often limited to living with broken functionality or turning off significant security checks across the board.

3. Exhausting Rule Maintenance

Security shouldn’t require fixing reverse proxy logs every time an app updates its API schema. The administrative overhead of maintaining whitelists for every edge-case request eventually outweighed the security benefits Teler was providing.

I needed a system that offered true layer-7 web application security with low false positives, paired with proactive layer-3/4 threat intelligence.

The New Defense Architecture: CrowdSec + Coraza WAF

To fix this, I completely restructured my edge security around two complementary open-source tools integrated directly into Caddy using custom Go modules: CrowdSec and Coraza WAF.

[ Incoming Traffic ]
           │
           ▼
 ┌───────────────────┐
 │   CrowdSec LAPI   │  ◄── Layer 3/4: Blocks malicious IPs via crowd-sourced threat intelligence
 └─────────┬─────────┘
           │ (Allowed IPs)
           ▼
 ┌───────────────────┐
 │   Coraza WAF      │  ◄── Layer 7: Deep packet inspection via OWASP Core Rule Set v4
 └─────────┬─────────┘
           │ (Sanitized Requests)
           ▼
 ┌───────────────────┐
 │  Backend Services │  (WordPress, Open WebUI, APIs)
 └───────────────────┘

Layer 1: CrowdSec (Reputation & IP Blocking)

Instead of relying purely on pattern matching inside request payloads, CrowdSec operates at the IP behavior level. Think of it as a modern, crowd-sourced alternative to Fail2ban.

  • How it works: CrowdSec parses logs across your services (SSH, Caddy access logs, authentication endpoints). When an IP behaves maliciously—like brute-forcing an admin panel or scanning for vulnerabilities—CrowdSec issues a local ban.
  • Crowd-Sourced Intelligence: The real power of CrowdSec is its consensus network. When an IP gets banned on thousands of other servers around the world, that IP is pushed to your local blocklist before it ever reaches your server.

By enforcing CrowdSec at the top of the Caddy processing pipeline, bad actors and automated botnets are dropped at the connection level, saving CPU cycles and preventing them from even reaching the application layer.

Layer 2: Coraza WAF with OWASP CRS v4 (Deep Request Inspection)

For traffic coming from clean IPs, Coraza WAF provides true enterprise-grade Layer 7 protection. Coraza is a modern, high-performance Go-based replacement for ModSecurity, designed to natively integrate into reverse proxies like Caddy.

Key advantages of Coraza over Teler include:

  • OWASP Core Rule Set (CRS) v4: Coraza runs the latest CRS v4 rule set, which dramatically reduces false positives compared to older rules.
  • Modular Plugin Architecture: CRS v4 features dedicated rule exclusions for major platforms like WordPress, Nextcloud, and Drupal. Instead of manually writing regex whitelists, importing plugin rules automatically tunes out false positives for legitimate application workflows (like WordPress post saving or XML-RPC handling).
  • Granular Controls: Rules can be tuned, skipped, or modified per-route without sacrificing safety across the rest of the stack.

Overcoming Real-World Edge Cases: The WebSocket Stumbling Block

Building a custom security pipeline isn’t without its own quirks. Shortly after deploying Coraza, I ran into an issue with interactive services like Open WebUI (used for interfacing with local LLM models): WebSocket connections were failing, and token streaming was broken.

Why WebSockets Break WAFs

When establishing a WebSocket or Socket.IO connection, the browser sends an HTTP Upgrade request containing protocol parameters like ?EIO=4&transport=websocket. Coraza inspects the initial request parameters, and standard WAF buffering can delay or reject the 101 Switching Protocols handshake. Furthermore, once a WebSocket connection opens, it transitions into a persistent raw TCP stream that a Layer 7 HTTP WAF cannot inspect anyway.

The Solution: Caddy Native Route Splitting

Instead of forcing Coraza to parse non-HTTP stream handshakes, I leveraged Caddy’s native handle directives to bypass the WAF specifically for WebSocket paths while keeping Coraza active for standard web and API routes:

# ─── Global Options ────────────────────────────────────────────
{
    order crowdsec first
    order coraza_waf first

    crowdsec {
        api_url http://127.0.0.1:8080/
        api_key YOUR_CROWDSEC_API_KEY
    }
}

# ─── WAF Snippet Definitions ──────────────────────────────────

(coraza_wp) {
    coraza_waf {
        directives `
            Include /etc/caddy/coraza/crs-setup.conf
            Include /etc/caddy/coraza/plugins/*-config.conf
            Include /etc/caddy/coraza/plugins/*-before.conf
            Include /etc/caddy/coraza/rules/*.conf
            Include /etc/caddy/coraza/plugins/*-after.conf
            SecRuleEngine On
        `
    }
}

# ─── Open WebUI (ai.example.com) ─────────────────────────────

ai.example.com {
    # 1. Direct route for WebSockets (bypass WAF buffering)
    @websockets path /ws/* /socket.io/*
    handle @websockets {
        reverse_proxy 10.0.0.50:3500 {
            header_up Host {host}
            header_up X-Real-IP {remote_host}
            flush_interval -1
        }
    }

    # 2. Standard HTTP route (protected by Coraza WAF)
    handle {
        import coraza_wp
        reverse_proxy 10.0.0.50:3500 {
            header_up Host {host}
            header_up X-Real-IP {remote_host}
            flush_interval -1
        }
    }
}

This routing split completely eliminated streaming latency while keeping standard HTTP requests protected against zero-day exploits.

The Alternative: Cloudflare (Managed Edge Security)

While building your own CrowdSec + Coraza stack inside Caddy gives you complete ownership over your data and infrastructure, it is not the only path forward. Cloudflare remains an attractive, turn-key alternative for many environments.

FeatureSelf-Hosted (Caddy + CrowdSec + Coraza)Cloud-Native (Cloudflare Free/Pro)
DDoS MitigationLimited to origin bandwidth & host capacityUnmetered edge mitigation (terabit scale)
Data Privacy100% self-hosted; non-custodialTraffic decrypted at Cloudflare edge nodes
MaintenanceRequires building custom Caddy binaries (xcaddy)Zero local installation; UI-managed
False Positive TuningLocal configuration via Caddyfile / rulesManaged Rulesets with visual toggle controls
CostFree & Open SourceFree tier available; paid for advanced WAF

When to Choose Cloudflare:

  • You want to hide your home server’s residential IP address behind a proxy.
  • You need protection against massive volumetric (L3/L4) DDoS attacks that would saturate your home internet pipe before Caddy could even process the traffic.
  • You prefer a managed dashboard over writing custom rule exclusions and updating local binaries.

When to Choose Caddy + Coraza + CrowdSec:

  • You prioritize complete privacy and don’t want a third party decrypting your TLS traffic.
  • You run internal or hybrid services where external cloud proxies add unnecessary latency or complexity.
  • You enjoy having full programmatic control over your reverse proxy pipeline without relying on proprietary cloud lock-in.

Final Thoughts

Security doesn’t have to mean spending every weekend debugging broken applications.

Moving away from Teler WAF to a CrowdSec + Coraza (OWASP CRS v4) architecture solved both ends of the problem: CrowdSec keeps automated scanners and bad actors from ever hitting my stack, while Coraza provides deep payload protection with minimal false positives.

If you are currently running Caddy with Teler WAF and fighting high false positive rates, separating your IP reputation layer from your application firewall is well worth the effort.