WAF
Overview
The WAF uses Coraza SPOA via HAProxy’s SPOE filter mechanism. It provides OWASP CRS protection, custom SecRules, conditional exceptions, rule version snapshots, and remote rule set management — all managed from the coreX Platform UI.
Rule Set Types
coreX Platform supports three types of WAF rule sets:
CRS (OWASP CRS)
The OWASP Core Rule Set is bundled in the coraza-spoa image. CRS provides broad protection against common web attacks (SQL injection, XSS, RCE, LFI/RFI, etc.) using anomaly scoring.
Custom
Custom rule sets contain only sec_rules — no includes, no CRS. Use these for application-specific rules that don’t fit the CRS model.
Remote
Remote rule sets download .conf files from a URL with SHA256 verification and optional auto-update on a configurable interval. This is useful for subscribing to third-party rule feeds.
WAF Rules
WAF rules are created per listener (and optionally per backend). Each rule specifies:
| Property | Description |
|---|---|
| Name | Display name (unique) |
| Listener | Which listener to protect |
| Backend | Optional backend scoping (rule only applies to one backend) |
| Rule set | CRS, Custom, or Remote |
| Engine | On, DetectionOnly, or Off |
| Paranoia level | CRS paranoia level (1–4) |
| Inbound anomaly threshold | Score at which inbound requests are blocked |
| Outbound anomaly threshold | Score at which outbound responses are blocked |
| Action | What to do when the threshold is exceeded |
| Status code | Custom HTTP status code for blocked responses |
| Path pattern | Optional path prefix/regex scope (e.g. /api/) |
| HTTP methods | Optional comma-separated method scope (e.g. GET,POST) |
| Content types | Optional content-type scope (e.g. application/json) |
| Fail open | When enabled, allow the request if the SPOA is unreachable (default: fail closed) |
| Rate-based rule | Enable per-rule rate limiting (see below) |
| Custom SecRules | Free-text Coraza SecRules appended to the generated config |
| Enabled | Toggle without deleting |
Actions
| Action | Description |
|---|---|
| block | Return 403 (or the custom status code) |
| allow | Permit the request (skip remaining WAF rules) |
| log | Log but allow |
| redirect | Redirect the client to a configured URL |
| challenge | Present a CAPTCHA challenge (see CAPTCHA) |
Exceptions
WAF exceptions let you selectively disable or modify specific CRS rules for specific requests — the standard mechanism for handling false positives without lowering the global anomaly threshold or disabling the WAF entirely.
Exception Actions
Each exception has one of four actions:
| Action | Description |
|---|---|
| remove | Skip the matched rule entirely for matching requests (SecRuleRemoveById) |
| allow | Skip the rule only for a specific variable/zone (e.g. exclude ARGS:search from rule 942100) — uses ctl:ruleRemoveTargetById so the rule still runs on other variables |
| comment | Disable the rule by ID with a comment marker |
| update | Change the rule’s action for matching requests (e.g. force pass instead of deny) and optionally exclude a target variable |
Rule Selection
An exception targets one or more rules by any combination of:
| Field | Description |
|---|---|
| Rule ID | Coraza rule ID (e.g. 942100). Multi-value — one exception can target multiple rule IDs. |
| Rule Tag | Coraza rule tag (e.g. attack-sqli). Multi-value. |
| Rule Message | Match by rule message substring. Multi-value. |
The suggestion catalog (rule IDs, tags, messages, and zones) is loaded from the backend when the exception editor opens, so the multi-value inputs offer autocomplete suggestions drawn from the active rule set.
Variable Exclusion (allow action only)
When the action is allow, you can scope the exclusion to a specific variable so the rule still runs on everything else:
| Field | Description |
|---|---|
| Zone | Coraza zone: ARGS, REQUEST_HEADERS, REQUEST_COOKIES, ARGS_NAMES, etc. (multi-value) |
| Variable | The specific variable name within the zone (e.g. search, password) |
| Matcher | equals, contains, regex, or startsWith — how the variable value must match |
| Value | The value to match against the variable (only required when a matcher is set) |
Condition (optional)
A condition gates the entire exception on a request attribute, so the exception only applies when the condition matches. Without a condition the exception is unconditional.
| Field | Description |
|---|---|
| Condition variable | The request attribute to test (e.g. REQUEST_URI, REQUEST_METHOD, REMOTE_ADDR) |
| Condition operator | equals, contains, startsWith, regex, gt, lt |
| Condition value | The value to compare against (e.g. /api/search for REQUEST_URI startsWith) |
Live Preview
The exception editor shows a live preview of the Coraza directives the exception will generate (e.g. SecRuleRemoveById 942100 or a chained SecRule with ctl:ruleRemoveTargetById). The preview updates as you edit, so you can verify the exception does what you intend before saving.
Creating an Exception from a WAF Log Event
The fastest way to create an exception is from a logged false positive:
- Navigate to Security > WAF > Logs
- Find the false-positive event (use the search box to filter by rule ID, URI, or message)
- Click the row to expand it
- Click Create exception — this opens the exception editor pre-filled with:
- Rule ID from the log event
- Condition variable =
REQUEST_URI, operator =startsWith, value = the event’s request URI - Name =
exception-<rule_id>
- Adjust the condition (e.g. narrow the path prefix) and save
- Apply changes
The log event already contains the exact rule ID and the URI that triggered the false positive, so the pre-filled exception is correct by construction. You only need to decide how broad the path condition should be — keep it narrow to avoid masking real attacks on other endpoints.
WAF Rule Scoping
An exception can be scoped to a specific WAF rule (selected in the WAF Rule dropdown) or left Global so it applies across all WAF rules on all listeners.
Rule Version Snapshots
coreX Platform can save and restore the full configuration of a single WAF rule. This is useful for:
- Auditing changes over time
- Rolling back a bad configuration change
- Comparing before/after states
Auto-pruning keeps the N most recent snapshots per rule (configurable).
Per-Rule Rate Limiting
Each WAF rule can optionally rate-limit clients that trigger it, using a sliding-window counter backed by HAProxy stick-tables. This lets you progressively block or challenge abusive clients that repeatedly trigger WAF rules without affecting well-behaved clients.
Enable Rate-based rule in the WAF rule editor (only available for the block and challenge actions). When enabled, the rule tracks exceedances per client and applies the rate action once the threshold is crossed.
| Field | Description |
|---|---|
| Rate events | Number of WAF trigger events allowed within the window (default 100) |
| Window (seconds) | Sliding window length in seconds (default 60) |
| Rate key | What to count per: src (client IP, default), user_id (tracks X-User-ID or a custom header), header (tracks a custom header), path (tracks the request path) |
| Rate header | Header name when rate_key is user_id (defaults to X-User-ID) or header |
| Rate action | block (return 403) or challenge (present a CAPTCHA) when the rate is exceeded |
| Block duration (seconds) | Tarpit duration after first exceedance. 0 = sliding-window only (the client is blocked only while their rate stays above the threshold). >0 = the client is blocked for that many seconds after the first exceedance, even if their rate drops. |
How the sliding window works
- The counter uses HAProxy’s
gpc0_rate({window}s)/gpc1_rate({window}s)stick-table stores, which are true sliding-window rates (not lifetime counters). A client is blocked only while their rolling event count exceeds the threshold. srckeys use the IP-type stick table onsc0(shared with listener rate limiting).user_id,header, andpathkeys use a separate string-type stick table backend (waf_rate_{listener_name}) tracked onsc1.- When Block duration is greater than 0, a separate
block_table_{listener_name}stick table onsc2records the first-exceedance timestamp so the block persists for the configured duration even after the rolling rate drops below the threshold.
WAF per-rule rate limiting counts WAF trigger events (requests that matched a WAF rule), not all requests. Use listener rate limiting to cap total request volume per client, and WAF per-rule rate limiting to progressively block clients that keep tripping the WAF. The two features use different stick-table counters (sc0/sc1/sc2) and can be combined on the same listener.
CRS Downloader
The CRS downloader fetches official OWASP CRS releases from GitHub. Features:
- Download specific CRS versions
- Rollback to previous versions
- View release notes
- Automatic update checks
Each CRS version is stored separately. You can switch between versions without re-downloading. The active version is the one referenced by WAF rules that use the CRS rule set type.
Remote Rule Set Downloader
For remote rule sets:
- Download
.conffiles from a URL - Verify SHA256 checksum
- Auto-update on a configurable interval
- Manual refresh button for immediate updates
WAF Logs
The WAF Logs page shows expandable rows with full Coraza event details:
- Rule ID, message, severity
- Client IP, URI, method
- Matched data / variables
- Action taken
- Timestamp
Inline search lets you filter by any field.
WAF Metrics
WAF events are sampled into the database for dashboard breakdowns:
- Breakdowns: action, rule ID, severity, message
- Stacked bar chart over time
- Totals table with counts per category
- Configurable time range
Blocking Behavior
SecRuleEngine On must be set after all includes. If it appears before includes, the engine may not pick up the included rules correctly. coreX Platform handles this automatically in generated configs.
Do not duplicate SecDefaultAction. CRS defines its own default action. Adding a conflicting default action can cause unexpected behavior. coreX Platform validates this before apply.
Custom Error Pages
WAF can serve custom error pages for 403 (blocked) and 500 (internal error) responses. Template variables available:
| Variable | Description |
|---|---|
{{ request_id }} | HAProxy unique request ID |
{{ waf_unique_id }} | Coraza transaction ID |
Step-by-Step: Enable Per-Rule Rate Limiting
- Navigate to Security > WAF and edit an existing WAF rule (or create one)
- Set Action to block or challenge (rate limiting is only available for these actions)
- Enable Rate-based rule
- Rate events:
10(allow 10 WAF triggers per window) - Window (seconds):
60 - Rate key:
src(count per client IP) - Rate action: block (return 403 when exceeded) or challenge (present a CAPTCHA)
- Block duration (seconds):
300(tarpit the client for 5 minutes after the first exceedance) - Save and Apply
Now a client that triggers the WAF rule more than 10 times in 60 seconds is blocked (or challenged) for all subsequent requests that match the rule, for 5 minutes.
Step-by-Step: Enable WAF with CRS
- Navigate to Security > WAF
- If no CRS is downloaded, click Download CRS (latest version)
- Click Add WAF Rule
- Name:
protect-web - Select your listener
- Rule set: CRS
- Anomaly threshold:
5(start conservative) - Action: block
- Click Save
- Click Apply Changes
When deploying WAF for the first time, set the action to log and monitor the WAF Logs page for false positives. Once you’re confident, switch to block. Add exceptions for any legitimate traffic that triggers rules.
Step-by-Step: Add an Exception
- Navigate to Security > WAF > Exceptions
- Click Add Exception
- Name:
skip-sqli-on-search - Action: remove (skip the rule entirely) or allow (skip only a specific variable)
- Rule ID:
942100(SQL injection detection) — use the autocomplete suggestions - (Optional) Condition variable:
REQUEST_URI, operator:startsWith, value:/api/search - (Optional, allow action only) Zone:
ARGS, Variable:search— exclude only thesearchparameter - Review the live preview of the generated Coraza directives
- Save and Apply
Step-by-Step: Create an Exception from a Log Event
- Navigate to Security > WAF > Logs
- Search for the false-positive event (e.g. filter by rule ID)
- Click the row to expand it
- Click Create exception
- The exception editor opens pre-filled with the rule ID and a
REQUEST_URI startsWith <event-uri>condition - Narrow the condition if needed (e.g. shorten the path prefix)
- Review the live preview and save
- Apply changes
Verification
-
Test with a malicious request:
curl -k "https://localhost/?id=1' OR '1'='1"Expect a 403 (or log entry if in log-only mode).
-
Check WAF Logs:
- Navigate to Observability > WAF Logs
- Verify the event appears with rule ID and details
-
Check WAF Metrics:
- Navigate to Observability > Metrics > WAF
- Verify events appear in the breakdown chart
-
Test with a benign request:
curl -k https://localhost/Expect a normal 200 response.
Next Steps
- Security Rules — Layer access control on top of WAF
- CAPTCHA — Configure challenge providers for WAF challenge actions
- Metrics & Logging — Monitor WAF events and metrics