1. The problem I’m having:
owasp is rejecting valid requests : uri “/?v-r=heartbeat&v-uiId=0”
I’ve tried chat gpt but it keeps going around in circles.
2. Error messages and/or full log output:
{"level":"error","ts":1759296096.8112571,"logger":"http.handlers.waf","msg":"[client \"223.123.105.210\"] Coraza: Warning. Inbound Anomaly Score Exceeded (Total Score: 5) [file \"@owasp_crs/REQUEST-949-BLOCKING-EVALUATION.conf\"] [line \"7441\"] [id \"949110\"] [rev \"\"] [msg \"Inbound Anomaly Score Exceeded (Total Score: 5)\"] [data \"\"] [severity \"emergency\"] [ver \"OWASP_CRS/4.15.0\"] [maturity \"0\"] [accuracy \"0\"] [tag \"anomaly-evaluation\"] [tag \"OWASP_CRS\"] [hostname \"\"] [uri \"/?v-r=heartbeat&v-uiId=0\"] [unique_id \"LCpuCdULGzCkuPnA\"]"}
PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.
3. Caddy version:
caddy --version
v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=
4. How I installed and ran Caddy:
I’m running caddy in a docker container:
# we build our own caddy file as we need the cloudflare module.
FROM caddy:2.10.0-builder AS builder
# https://caddyserver.com/docs/modules/dns.providers.cloudflare
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare \
--with github.com/WeidiDeng/caddy-cloudflare-ip \
--with github.com/corazawaf/coraza-caddy/v2
FROM caddy:2.10.0
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
# Set custom directories for data and config
ENV XDG_CONFIG_HOME=/opt/onepub/caddy/config
ENV XDG_DATA_HOME=/opt/onepub/caddy/data
# RUN mkdir -p /caddy
# coraza WAF configuation directory
RUN mkdir -p /opt/onepub/caddy/coraza
COPY config/caddy/ /opt/onepub/caddy/
# waf configuration files.
COPY config/coraza /opt/onepub/caddy/coraza
# so we know where to put any custom waf commands.
RUN touch /opt/onepub/caddy/coraza/waf-directives.conf
CMD ["caddy", "run", "--config", "/opt/onepub/caddy/Caddyfile", "--adapter", "caddyfile"]
a. System environment:
docker
b. Command:
PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.
n/a
c. Service/unit/compose file:
PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.
caddy:
container_name: caddy
image: onepub/onepub-caddy:${ONEPUB_VERSION}
restart: always
network_mode: "host"
cap_add:
- NET_ADMIN
environment:
ACME_AGREE: "true"
ACME_URL: ${ACME_URL} # staging or production
EMAIL: ${AUTH_PROVIDER_EMAIL_ADDRESS}
CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
ACTIVE_SITE: ${ACTIVE_SITE}
ports:
- 80:80
- 443:443
- 443:443/udp
volumes:
# Persist certificates
- caddy:/caddy
- filestore:/opt/onepub/filestore
#- /opt/onepub/caddy:/opt/onepub/caddy
# Logs
- /tmp/caddy:/var/log/caddy
logging:
driver: "journald"
d. My complete Caddy config:
PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.
5. Links to relevant resources:
My caddy config:
{
# ===== Global =====
email {$EMAIL}
acme_ca {$ACME_URL:"https://acme-staging-v02.api.letsencrypt.org/directory"}
servers {
trusted_proxies cloudflare {
interval 12h
timeout 15s
}
}
order coraza_waf first
}
import common.caddy
# Only these will be loaded; set ACTIVE_IMPORTS to a space-separated list of site files.
# e.g.
# ACTIVE_SITE="sites/production/*.caddy"
# ACTIVE_SITE="sites/beta/*.caddy"
# ACTIVE_SITE="sites/dev/*.caddy"
import "{$ACTIVE_SITE}"
The 'ACTIVE_SITE' import
# Primary: onepub.dev + wildcard (prod & beta share app routes)
# The wild card is required to support www.onepub.dev
onepub.dev, *.onepub.dev {
import cloudflare-tls
import common-security
import common-app-routes
# Beta-specific robots
@beta host beta.onepub.dev
header @beta {
X-Robots-Tag "noindex, nofollow"
}
# MailHog ONLY for beta.onepub.dev
handle @beta {
import common-mailhog
}
}
common.caddy
# ===== Reusable TLS (single CF token for all zones) =====
(cloudflare-tls) {
tls {
dns cloudflare {
api_token {$CLOUDFLARE_API_TOKEN}
}
}
}
# ===== Reusable snippets =====
(common-security) {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
}
@block_ai header_regexp User-Agent "(?i)(GPTBot|ChatGPT|Google-Extended|Claude-Web|Anthropic|Amazonbot|FacebookBot|cohere-ai|Bytespider|YouBot)"
respond @block_ai 403
@block_php path *.php
abort @block_php
@multiple_slashes path_regexp multipleSlashes ^(.*)//+(.*)$
redir @multiple_slashes {scheme}://{host}{re.multipleSlashes.1}/{re.multipleSlashes.2}{query} permanent
encode gzip
}
(common-waf) {
coraza_waf {
load_owasp_crs
directives `
Include @coraza.conf-recommended
Include @crs-setup.conf.example
Include @owasp_crs/*.conf
Include /opt/onepub/caddy/coraza/waf-directives.conf
`
}
}
(common-proxy8080) {
reverse_proxy 127.0.0.1:8080 {
header_up Host {host}
header_up X-Real-IP {remote_host}
transport http {
read_timeout 300s
}
header_up Connection {header.Connection}
header_up Upgrade {header.Upgrade}
}
}
(common-app-routes) {
# we need to bypass WAF for some of our internal
# end points that look a litte odd to WAF.
# WebSocket upgrades (keep first)
@ws {
header_regexp Upgrade (?i)websocket
header_regexp Connection (?i)\bupgrade\b
}
handle @ws {
import common-proxy8080
}
# UIDL calls like "/?v-r=uidl&v-uiId=..."
@vaadin_uidl {
path /
query v-r=uidl
query v-uiId=*
}
handle @vaadin_uidl {
import common-proxy8080
}
# Vaadin Push long-poll/stream: "/VAADIN/push?v-r=push&v-uiId=...&v-pushId=..."
@vaadin_push {
path /VAADIN/push
query v-r=push
query v-uiId=*
query v-pushId=*
}
handle @vaadin_push {
import common-proxy8080
}
# Your endpoint that currently sends text/plain
@visibility path /api/visibility
handle @visibility {
import common-proxy8080
}
# Heartbeat (already bypassed, keep consistent)
@heartbeat path /api/clientheartbeat
handle @heartbeat {
import common-proxy8080
}
# Heartbeat (root path, query v-r=heartbeat)
@vaadin_heartbeat {
path /
query v-r=heartbeat
query v-uiId=*
}
handle @vaadin_heartbeat {
import common-proxy8080
}
# Everything else under WAF
handle {
import common-waf
import common-proxy8080
}
# API error mapping
handle_errors {
@api path /api/*
handle @api {
root * /etc/caddy/json
rewrite 502 /500.json
rewrite 404 /404.json
file_server
}
}
# Default upstream
import common-proxy8080
}
(common-mailhog) {
handle /mailhog/* {
basic_auth {
{$EMAIL} XXXXXXXXXXXXXXXXXXXXXXxxx
}
reverse_proxy 127.0.0.1:8025
}
}
(common-video) {
# TLS via Cloudflare DNS for video hosts
import cloudflare-tls
# Security headers, gzip, etc.
import common-security
# Apply WAF by default (your app is serving the video files)
import common-waf
# MP4 proxy (shared)
@mp4 path *.mp4
handle @mp4 {
reverse_proxy 127.0.0.1:8080 {
header_up Host {host}
header_up X-Real-IP {remote_host}
transport http {
read_timeout 300s
}
header_up Connection {header.Connection}
header_up Upgrade {header.Upgrade}
}
}
}