Skip to content

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: On blocks, DetectionOnly only logs what it would have blocked.
  • paranoia_level 1 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 with disabled_rules, then block and raise the level if needed.
  • disabled_rules switches 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=deny with the rule id, see Logs.
  • fail_open (default on, set by support): if the WAF is unavailable or saturated, traffic passes, logged as coraza_failed. Fail-closed on request returns an error instead.
  • Cached responses skip the WAF, see Cache. Neither allow nor skip exempts from it.

In the portal: your service → WAF.