Achieving Apache-Like Seamless Reloads for Coraza in Caddy – Tips, Workarounds, or Replacements?

1. The problem I’m having:

When using Caddy with the coraza-caddy WAF plugin, changes to Coraza rule files (.conf files containing SecRules) are not reflected after running caddy reload.

Setup: I have a Caddyfile that uses the coraza_waf directive with an Include statement to load a Coraza configuration file (coraza.conf), which in turn includes additional rule files via wildcard patterns like Include /etc/coraza/rule/custom/*.conf.

Expected behavior: When I modify any of the included Coraza rule files and run caddy reload, the updated rules should take effect (similar to Apache + mod_security).

Actual behavior: After caddy reload, changes to the Coraza rule files are NOT applied. Only a full container restart applies the changes.

Example scenario:

  1. I add a new SecRule to /etc/coraza/rule/custom/custom-dir.conf
  2. I run docker exec <container> caddy reload --config /etc/caddy/Caddyfile
  3. Caddy reports successful reload, but the new rule is not active
  4. Only docker restart <container> makes the rule active

What works vs. what doesn’t:

Rules written inline in the Caddyfile’s directives block DO reload properly. However, this only works for simple self-contained rules. Many production rules cannot be inlined because they depend on external data files. For example:

# This rule references an external data file - cannot be inlined
SecRule REQUEST_HEADERS:User-Agent "@pmFromFile /etc/coraza/rule/custom/blocked-UA.data" \
    "id:10002,phase:1,deny,status:403,msg:'Blocked User-Agent'"

Rules using @pmFromFile, @ipMatchFromFile, or other directives that read from external files must remain in separate .conf files with Include. These are the rules we need to hot-reload.

Why restart is not feasible:

Our WAF handles continuous production traffic. Restarting the container causes a brief service interruption and drops active connections. If we could reload the coraza.conf or specific custom rule files dynamically, we could automate rule updates without any downtime.

Background - Migration from mod_security:

We migrated from Apache + mod_security where this worked seamlessly. A simple apachectl graceful would re-read all rule files (including external data files) without dropping connections. We have an extensive rule infrastructure built on SecLang (ModSecurity rule language).

Question:

  1. Is hot-reloading of Coraza rules (including Include directives and external data files like @pmFromFile) supported in coraza-caddy?
  2. If not currently supported, is this a planned feature or on the roadmap?
  3. Are there any recommended workarounds for achieving zero-downtime rule updates? We have automation in place that can trigger reloads when rule files change, but currently it requires a full container restart which interrupts active connections.

2. Error messages and/or full log output:

$ docker exec caddy-coraza caddy reload --config /etc/caddy/Caddyfile
{"level":"info","ts":1766482874.6746414,"msg":"using config from file","file":"/etc/caddy/Caddyfile"}
{"level":"warn","ts":1766482874.675638,"logger":"caddyfile","msg":"Unnecessary header_up X-Forwarded-For: the reverse proxy's default behavior is to pass headers to the upstream"}
{"level":"warn","ts":1766482874.675701,"logger":"caddyfile","msg":"Unnecessary header_up X-Forwarded-Proto: the reverse proxy's default behavior is to pass headers to the upstream"}
{"level":"info","ts":1766482874.6766903,"msg":"adapted config to JSON","adapter":"caddyfile"}
{"level":"warn","ts":1766482874.6767473,"msg":"Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies","adapter":"caddyfile","file":"/etc/caddy/Caddyfile","line":3}

The reload completes without errors, but rule changes from included files are not applied.

3. Caddy version:

v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=

Built with xcaddy using:

  • github.com/corazawaf/coraza-caddy/v2

4. How I installed and ran Caddy:

a. System environment:

  • Host OS: Ubuntu 22.04.5 LTS (Jammy Jellyfish)
  • Architecture: x86_64
  • Runtime: Docker with docker compose
  • Base image: debian:bookworm-slim
  • Build: Custom image built with xcaddy

b. Command:

# Running Caddy
docker compose up -d

# Reloading Caddy config
docker exec caddy-coraza caddy reload --config /etc/caddy/Caddyfile

c. Service/unit/compose file:

services:
  caddy-coraza:
    build: .
    container_name: caddy-coraza
    ports:
      - "9900:9900"
      - "9901:9901"
    volumes:
      - ./config/Caddyfile:/etc/caddy/Caddyfile:ro
      - ./config/coraza.conf:/etc/caddy/coraza.conf:ro
      - ./rule:/etc/coraza/rule:ro
      - ./logs:/var/log/caddy:rw
      - ./logs:/var/log/coraza:rw
    restart: always

Dockerfile CMD:

CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

d. My complete Caddy config:

Caddyfile:

{
    order coraza_waf first
    admin 0.0.0.0:9901
}

:9900 {
    coraza_waf {
        directives `
            # IP Whitelist - this inline rule DOES reload properly
            SecRule REQUEST_HEADERS:X-Forwarded-For "@rx ^([0-9.]+)" "id:9001,phase:1,pass,nolog,capture,chain"
            SecRule TX:1 "@ipMatch 192.168.1.100,192.168.1.101" "t:none,pass,log,ctl:ruleEngine=DetectionOnly,msg:'Whitelisted IP: %{TX.1}'"
            
            # Include base Coraza configuration - changes here do NOT reload
            Include /etc/caddy/coraza.conf
        `
    }

    handle_errors {
        @403 expression {http.error.status_code} == 403
        handle @403 {
            rewrite * /403.html
            file_server {
                root /var/www/html/errors
            }
        }
    }

    reverse_proxy backend:80 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
    }

    log {
        output file /var/log/caddy/access.log
        format json
    }
}

coraza.conf (included via Caddyfile):

# Coraza WAF Configuration with CRS v4.21.0

SecRuleEngine On
SecRequestBodyAccess On
SecResponseBodyAccess On
SecRequestBodyLimit 13107200

# Audit Logging
SecAuditEngine RelevantOnly
SecAuditLog /var/log/coraza/audit.log
SecAuditLogFormat JSON

# CRS v4.21.0
Include /etc/coraza/rule/coreruleset-v4.21.0/crs-setup.conf
Include /etc/coraza/rule/coreruleset-v4.21.0/REQUEST-*.conf
Include /etc/coraza/rule/coreruleset-v4.21.0/RESPONSE-*.conf

# Custom Rules - changes here don't apply on reload
Include /etc/coraza/rule/custom/*.conf

5. Links to relevant resources:

This is expected behaviour, I believe.
caddy reload only reloads the configuration if Caddy’s own config has changed. Coraza rulesets aren’t part of that.

Try running caddy reload --force to see if that makes a difference.

1 Like

Nope, it just reloads the contents of the Caddyfile’s changes.
I need a way to reload the included file’s changes as well.