Skip to content

Web application firewall

Requires a license with the waf feature. See pricing.

The web application firewall inspects every request to both APIs and blocks the ones that look like attacks. It ships with 37 built-in rules, scores each request by how many rules it trips and how serious they are, and logs every match so you can see what it caught and tune what it gets wrong.

The path, query string, headers, cookies and body are inspected. Encoded input is decoded first, and JSON, form and multipart bodies are read field by field. Each match adds to the request's score by the rule's severity:

SeverityScore
critical5
high3
medium2
low1

A request is blocked when a rule with the action block matches, or when the total score is above the anomaly threshold (5 by default). A match that does not block is still logged.

These are not inspected:

  • Sign-in routes under /api/admin/auth/, the firewall's own routes under /api/admin/waf/, and first-run setup at /api/admin/setup.
  • The body of a write (POST, PUT, PATCH) to /api/admin/content, /api/admin/content/* or /api/v1/content/*. A content entry is stored as data, never run as a query. The path, query string, headers and cookies of those requests are still inspected.
  • A body beyond max_inspect_body_bytes, 128 KiB by default. The rest of the body still reaches the API.

There is no switch that turns inspection off. To stop a rule from blocking, set it to log only.

  1. Run with a license that carries waf, and restart the instance. The built-in rules apply from that start. See licensing and tiers.
  2. Watch the violation log for a few days, and add exceptions for false positives before you tighten anything.

If you set LYEVE_PLUGINS, include waf in it.

You need an admin token in TOKEN. The quickstart shows how to get one.

  1. List the rules:

    Terminal window
    curl http://localhost:3001/api/admin/waf/rules \
    -H "Authorization: Bearer $TOKEN"

    Each rule has an id such as SQLI-001, a name, a category, a severity, an action (block or log), the targets it reads and whether it is enabled.

  2. Send a request that looks like SQL injection:

    Terminal window
    curl -i "http://localhost:3002/api/v1/content/post?q=1%20UNION%20SELECT%20password%20FROM%20users"
    HTTP/1.1 403 Forbidden
    X-Waf-Block: true
    Content-Type: application/json
    {"code": "waf_blocked", "error": "request blocked by WAF", "matches": 1, "score": 5}
  3. Find it in the violation log:

    Terminal window
    curl "http://localhost:3001/api/admin/waf/violations?category=sqli&limit=1" \
    -H "Authorization: Bearer $TOKEN"
    {
    "data": [
    {
    "id": 9,
    "timestamp": "2026-10-01T11:02:19Z",
    "rule_id": "SQLI-001",
    "rule_name": "UNION SELECT injection",
    "category": "sqli",
    "severity": "critical",
    "action": "block",
    "target": "query_string",
    "param": "query_string",
    "value": "q=1 UNION SELECT password FROM users",
    "path": "/api/v1/content/post",
    "method": "GET",
    "client_ip": "203.0.113.7",
    "tenant_id": "default",
    "user_agent": "curl/8.5.0"
    }
    ],
    "limit": 1,
    "offset": 0,
    "total_count": 1
    }
  4. Read the summary:

    Terminal window
    curl http://localhost:3001/api/admin/waf/stats \
    -H "Authorization: Bearer $TOKEN"

    The answer holds total, by_category, by_severity, top_rules, last_24h, total_rules and enabled_rules.

Set a rule to log only, or turn it off:

Terminal window
curl -X PUT http://localhost:3001/api/admin/waf/rules/SQLI-003 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"action": "log"}'

A change to a built-in rule is saved as an override for your tenant.

Terminal window
curl -X POST http://localhost:3001/api/admin/waf/rules \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "CUSTOM-001",
"name": "Block the legacy export path",
"description": "The old export endpoint was removed",
"category": "custom",
"severity": "high",
"action": "block",
"pattern": "(?i)/legacy-export",
"targets": ["path"],
"enabled": true
}'

The answer is 201 with the rule. A custom rule applies to requests of the tenant that created it, and a tenant can hold up to 500. A custom rule whose id matches a built-in one replaces it for the tenant.

FieldValues
idRequired. Use an id of your own, because a second rule with the same id is not refused.
categorysqli, xss, path_traversal, tenant_injection, command_injection, ssrf or custom
severitycritical, high, medium or low. Any other value scores 0.
actionblock or log
patternA Go regular expression
targetsAny of path, query_string, body, headers and cookie

A pattern that does not compile is saved but never matches, so test it before you rely on it.

When a rule matches traffic it should not, exempt that rule on a path:

Terminal window
curl -X POST http://localhost:3001/api/admin/waf/false-positives \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"rule_id": "XSS-002", "path_glob": "/api/v1/forms/*", "param_name": "*", "reason": "Support form accepts HTML"}'

In path_glob, * matches any run of characters, slashes included. param_name is compared with the param of a match: * matches any, a header or cookie name matches that header or cookie, and for the path, query string and body the param is the target itself (path, query_string or body). Delete an exception with DELETE /api/admin/waf/false-positives/{id}.

DELETE /api/admin/waf/violations?older_than_hours=720 deletes old entries and answers {"pruned": <count>}. Without a valid value it deletes entries older than 30 days. The list takes rule_id, category, severity, since, and limit (default 50, at most 500).

Settings apply to the whole instance, so only a super admin changes them:

Terminal window
curl -X PUT http://localhost:3001/api/admin/waf/config \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"anomaly_threshold": 8, "max_inspect_body_bytes": 131072}'
SettingWhat it doesDefault
anomaly_thresholdA request whose score is above this is blocked. 0 becomes 5, and a negative value is refused.5
max_inspect_body_bytesHow much of a body is inspected. 0 takes the default, -1 inspects whole bodies.131072
max_violation_body_lengthStored and returned. A logged value is cut to 200 characters whatever it says.500

Inspecting whole bodies costs CPU on requests that need no credential. Raise the limit with care.

Firewall routes
MethodPathRolePurpose
GET/api/admin/waf/rulesadminList rules
POST/api/admin/waf/rulesadminAdd a custom rule. 201
GET/api/admin/waf/rules/{id}adminRead a rule
PUT/api/admin/waf/rules/{id}adminChange a rule's action or enabled
GET/api/admin/waf/violationsadminList violations, paginated
DELETE/api/admin/waf/violationsadminDelete old violations
GET/api/admin/waf/false-positivesadminList exceptions
POST/api/admin/waf/false-positivesadminAdd an exception
DELETE/api/admin/waf/false-positives/{id}adminRemove an exception. 204
GET/api/admin/waf/configadminRead the settings
PUT/api/admin/waf/configsuper_adminChange the settings
GET/api/admin/waf/statsadminViolation statistics
StatusMessageCause
403request blocked by WAFThe firewall blocked the request.
404The requested endpoint does not exist.The instance started without a license that carries waf.
402payment_requiredThe license stopped carrying waf while the instance was running.
Every other error
StatusMessageCause
400rule.id is requiredA custom rule had no id.
409maximum custom rules (500) reachedThe tenant has 500 custom rules.
404rule not foundNo rule with that id.
400rule_id is requiredAn exception named no rule.
403only a super admin may scope a false positive outside its own tenantAn admin set another tenant on an exception.