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:
- I add a new SecRule to
/etc/coraza/rule/custom/custom-dir.conf - I run
docker exec <container> caddy reload --config /etc/caddy/Caddyfile - Caddy reports successful reload, but the new rule is not active
- 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:
- Is hot-reloading of Coraza rules (including
Includedirectives and external data files like@pmFromFile) supported incoraza-caddy? - If not currently supported, is this a planned feature or on the roadmap?
- 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:
- coraza-caddy GitHub
- Coraza WAF
- Related context: Migrating from Apache + mod_security where graceful reload re-read all rule files seamlessly