Web application firewall¶
Coraza with the OWASP Core Rule Set, running per service in its own container, inspects requests and responses before they reach your origin or your visitors. Its place in the request path: pipeline. Requests need API access.
Endpoints¶
| Method | Path | Key | Purpose |
|---|---|---|---|
PATCH |
/services/{fqdn} |
write | Change a vService |
PATCH /services/{fqdn}
Body: ServicePatch
Returns: 200, Service
Request: waf_and_limit
{
"waf": {
"enabled": true
},
"rate_limiting": {
"req_per_10s": 800
}
}
Request: replace_origins
{
"origins": [
{
"name": "origin-1",
"address": "192.0.2.10",
"port": 443
}
]
}
Answer 200: waf_and_limit
{
"fqdn": "a.example.com",
"tenant": "tenant-a",
"enabled": true,
"mode": "https",
"ports": [
443
],
"udp_ports": [],
"created": "2026-10-01T00:00:00+00:00",
"created_by": "",
"account_id": "acc-a",
"network": {
"vips": [
{
"anycast_ipv4": "203.0.113.1",
"anycast_ipv6": "2001:db8:a::1"
}
],
"pool": null,
"edge_hostname": "a-example-com.edge.protect.zeroservices.eu"
},
"network_protection": {
"enabled": true
},
"backend": {
"algorithm": "roundrobin",
"health_check": {}
},
"origins": [
{
"name": "origin-1",
"address": "192.0.2.10",
"port": 443,
"tls": true,
"ssl_verify": true,
"weight": 100,
"maxconn": 100
}
],
"waf": {
"enabled": true,
"fail_open": true,
"mode": "On",
"paranoia_level": 1,
"inbound_threshold": 5,
"disabled_rules": []
},
"rate_limiting": {
"challenge_req_per_10s": 200,
"req_per_10s": 800,
"conn_per_10s": 50,
"conn_drop_per_10s": 500,
"path_default": 1000,
"path_aggregate_per_10s": 5000,
"geo_tcp_reject_sess_rate": 200,
"ja4_enabled": false,
"ja4_req_per_10s": 100,
"ja4_action": "challenge"
},
"pow": {
"enabled": true,
"difficulty": 18,
"cookie_ttl": 86400,
"always_challenge": false,
"exempt_crawlers": true,
"bind_ip": true,
"botcheck_per_10s": 1000
},
"admission": {
"enabled": false,
"credit_required": 5,
"credit_window": "60s",
"known_ttl": "24h",
"unknown_conn_rate": 1500,
"table_size": "1m"
},
"ssl": {
"mode": "acme",
"acme_provider": "zerossl",
"acme_email": null,
"https_redirect": false,
"cert_status": "none",
"cert_issued": null,
"cert_expires": null,
"cert_last_renewal": null,
"days_remaining": null
},
"aliases": [
{
"fqdn": "www.a.example.com",
"ssl": {
"mode": "acme",
"acme_provider": "zerossl",
"acme_email": null,
"https_redirect": false,
"cert_status": "none",
"cert_issued": null,
"cert_expires": null,
"cert_last_renewal": null,
"days_remaining": null
}
}
],
"cache": {
"enabled": true
},
"slow_http": {
"enabled": true,
"timeout_http_request": "5s",
"timeout_client_fin": "5s"
},
"tcp_timeout": {
"client": "30s",
"server": "30s",
"tunnel": "30s"
},
"udp_timeout": {
"idle": "300s",
"unreplied": "30s",
"unreplied_pkts": 32,
"src_pps": 0
},
"sni_policy": "allowlist",
"proxy_protocol": false,
"tls_termination": false,
"priority": {
"enabled": true,
"session_signal": []
},
"response_headers": {
"hsts": {},
"cors": {},
"x_frame_options": null,
"x_content_type_options": false,
"referrer_policy": null
},
"content_negotiation": {
"enabled": false,
"encodings": [
"zstd",
"gzip"
],
"file_types": [
"css",
"js",
"html"
]
},
"logging": {
"enabled": false
},
"threat_intel": {
"mode": "log",
"abuse": "log",
"tor": "log"
},
"redirects": [],
"origin_host": null,
"origin_sni": null,
"maintenance_mode": false,
"auto_mitigation": false,
"defense_posture": null,
"protection_allowlist": null,
"edges": [],
"protection_rules": [],
"append_trailing_slash": false,
"exceed_no_rtbh": false,
"exceed_no_flowspec": false,
"origin_kind": "dynamic",
"alias_limit": 20,
"serving_edges": [
"shared-1"
],
"posture_drift": [],
"baseline": null
}
Answer 200: replace_origins
{
"fqdn": "a.example.com",
"tenant": "tenant-a",
"enabled": true,
"mode": "https",
"ports": [
443
],
"udp_ports": [],
"created": "2026-10-01T00:00:00+00:00",
"created_by": "",
"account_id": "acc-a",
"network": {
"vips": [
{
"anycast_ipv4": "203.0.113.1",
"anycast_ipv6": "2001:db8:a::1"
}
],
"pool": null,
"edge_hostname": "a-example-com.edge.protect.zeroservices.eu"
},
"network_protection": {
"enabled": true
},
"backend": {
"algorithm": "roundrobin",
"health_check": {
"method": "GET",
"path": "/",
"host": null,
"interval": "10s",
"rise": 2,
"fall": 3,
"expect": null
}
},
"origins": [
{
"name": "origin-1",
"address": "192.0.2.10",
"port": 443,
"tls": true,
"ssl_verify": true,
"weight": 100,
"maxconn": 100
}
],
"waf": {
"enabled": true,
"fail_open": true,
"mode": "On",
"paranoia_level": 1,
"inbound_threshold": 5,
"disabled_rules": []
},
"rate_limiting": {
"challenge_req_per_10s": 200,
"req_per_10s": 800,
"conn_per_10s": 50,
"conn_drop_per_10s": 500,
"path_default": 1000,
"path_aggregate_per_10s": 5000,
"geo_tcp_reject_sess_rate": 200,
"ja4_enabled": false,
"ja4_req_per_10s": 100,
"ja4_action": "challenge"
},
"pow": {
"enabled": true,
"difficulty": 18,
"cookie_ttl": 86400,
"always_challenge": false,
"exempt_crawlers": true,
"bind_ip": true,
"botcheck_per_10s": 1000
},
"admission": {
"enabled": false,
"credit_required": 5,
"credit_window": "60s",
"known_ttl": "24h",
"unknown_conn_rate": 1500,
"table_size": "1m"
},
"ssl": {
"mode": "acme",
"acme_provider": "zerossl",
"acme_email": null,
"https_redirect": false,
"cert_status": "none",
"cert_issued": null,
"cert_expires": null,
"cert_last_renewal": null,
"days_remaining": null
},
"aliases": [
{
"fqdn": "www.a.example.com",
"ssl": {
"mode": "acme",
"acme_provider": "zerossl",
"acme_email": null,
"https_redirect": false,
"cert_status": "none",
"cert_issued": null,
"cert_expires": null,
"cert_last_renewal": null,
"days_remaining": null
}
}
],
"cache": {
"enabled": true
},
"slow_http": {
"enabled": true,
"timeout_http_request": "5s",
"timeout_client_fin": "5s"
},
"tcp_timeout": {
"client": "30s",
"server": "30s",
"tunnel": "30s"
},
"udp_timeout": {
"idle": "300s",
"unreplied": "30s",
"unreplied_pkts": 32,
"src_pps": 0
},
"sni_policy": "allowlist",
"proxy_protocol": false,
"tls_termination": false,
"priority": {
"enabled": true,
"session_signal": []
},
"response_headers": {
"hsts": {
"enabled": false,
"max_age": 31536000,
"include_subdomains": false,
"preload": false
},
"cors": {
"enabled": false,
"rules": []
},
"x_frame_options": null,
"x_content_type_options": false,
"referrer_policy": null
},
"content_negotiation": {
"enabled": false,
"encodings": [
"zstd",
"gzip"
],
"file_types": [
"css",
"js",
"html"
]
},
"logging": {
"enabled": false
},
"threat_intel": {
"mode": "log",
"abuse": "log",
"tor": "log"
},
"redirects": [],
"origin_host": null,
"origin_sni": null,
"maintenance_mode": false,
"auto_mitigation": false,
"defense_posture": null,
"protection_allowlist": null,
"edges": [],
"protection_rules": [],
"append_trailing_slash": false,
"exceed_no_rtbh": false,
"exceed_no_flowspec": false,
"origin_kind": "dynamic",
"alias_limit": 20,
"serving_edges": [
"shared-1"
],
"posture_drift": [],
"baseline": null
}
Fields¶
Part of the service: send waf with PATCH /services/{fqdn}; send only the keys you
change. A rule list you send replaces the stored one.
Waf¶
| Field | Type | Notes |
|---|---|---|
enabled |
boolean | default false; Inspect requests with the WAF: headers and the first 32,768 bytes of the body. Requests answered from the cache skip it; no rule exempts from it |
fail_open |
boolean | default true; read-only; Set by support on request |
mode |
string | one of On, DetectionOnly; default "On"; On: block a request whose anomaly score reaches the threshold with 403. DetectionOnly: only log |
paranoia_level |
integer | 1–4; default 1; Core Rule Set paranoia level, 1 to 4: each level adds stricter rules and more false positives; 1 suits most applications |
inbound_threshold |
integer | 1–10; default 5; Anomaly score at which a request is blocked, 1 to 10; a critical rule adds 5, an error 4, a warning 3 |
disabled_rules |
list of integer | at most 200; Core Rule Set rule ids switched off for this vService, up to 200 |
Example¶
Switch it on in detection mode, then block, with one rule switched off that fires on your application:
curl -s -X PATCH -H "Authorization: Bearer $ZP_KEY" -H 'Content-Type: application/json' \
$API/services/www.example.com -d '{"waf": {"enabled": true, "mode": "DetectionOnly"}}'
curl -s -X PATCH -H "Authorization: Bearer $ZP_KEY" -H 'Content-Type: application/json' \
$API/services/www.example.com -d '{"waf": {"mode": "On", "disabled_rules": [930120]}}'
Behaviour¶
mode:Onblocks,DetectionOnlyonly logs what it would have blocked.paranoia_level1 to 4: higher levels block more and produce more false positives.- Findings add up to an anomaly score; a request is blocked once it reaches
inbound_threshold. A critical finding is worth 5, an error 4, a warning 3, so at the default 5 one critical match blocks; 10 demands two findings. - Rollout: start at level 1 in
DetectionOnly, watch the WAF log for a few days, switch false positives off withdisabled_rules, then block and raise the level if needed. disabled_rulesswitches Core Rule Set ids off. The WAF inspects requests, not responses.- Headers and cookies are inspected too: a match on a session cookie blocks that visitor on every request until the cookie changes. A WAF block is logged as
coraza_action=denywith the rule id, see Logs. fail_open(default on, set by support): if the WAF is unavailable or saturated, traffic passes, logged ascoraza_failed. Fail-closed on request returns an error instead.- Cached responses skip the WAF, see Cache. Neither
allownorskipexempts from it.
In the portal: your service → WAF.