Skip to content

WAF

The web application firewall is Coraza with the OWASP Core Rule Set. Turn it on globally in WAF → Settings, then override per proxy host where a particular application needs something different.

The WAF card on a proxy hostToggle it off to see what the host inherits, or open Rule presets to pick another.DemoNothing you change here is saved
Web Application FirewallInspect and block malicious requests via Coraza / OWASP CRS
Covers SQLi, XSS, LFI, RCE and hundreds of other attack patterns
Request Body LimitsCoraza buffers request bodies for inspection and rejects anything larger than its limit - 12.5 MiB with the OWASP CRS loaded. Raise it for hosts that receive large uploads. Leave blank to inherit.
MiB
MiB
Over-limit actionUses the action set on the WAF settings page.
Excluded Rule IDsRules listed here are disabled via SecRuleRemoveById
942100
Added to the presets the global WAF settings select.
Added to the plugins the global WAF settings select. Plugins load only with the OWASP CRS.
ModSecurity SecLang syntax. Appended after OWASP CRS if enabled.
Tab indents; press Escape then Tab to leave the fieldSecLang
Quick Templates
The WAF event logFilter it, then pick a row - the rule that fired opens beside the list.DemoNothing you change here is saved
7Total Events
5Blocked
3Critical
3Unique Hosts
6Rule IDs Triggered
FirewallEnabled
5Blocked
7Total Events
3Critical
3Unique Hosts
6Rule IDs Triggered
Row statusTimeActionSeverityHostClient IPRequestRule IDActions
BlockedCriticalapp.example.com
45.147.230.14NL
POST/login
942100
BlockedCriticalapp.example.com
45.147.230.14NL
GET/search?q=%3Cscript%3E
941100
BlockedWarninggrafana.example.com
185.220.101.77DE
GET/.env
930120
BlockedWarninggrafana.example.com
185.220.101.77DE
GET/wp-admin/setup-config.php
930130
DetectedNoticewiki.example.com
10.0.4.18
POST/api/pages/42
941160
BlockedCriticalapp.example.com
103.152.220.9SG
POST/api/import
932130
DetectedNoticewiki.example.com
10.0.4.18
POST/api/pages/17
941160

On, matching requests are rejected with 403. The global settings also take a detection-only mode through the API - waf.mode set to DetectionOnly - in which matching requests are logged and allowed through. Detect first is the usual order: it tells you what the rules would have done to your traffic before they start doing it.

OWASP CRS covers SQL injection, cross-site scripting, local file inclusion, remote code execution and more. It is enabled by default when the WAF is on.

A WebSocket upgrade is inspected like any other request, so claiming to be one does not get a request past the rules. Once the connection is upgraded, the messages on it are not inspected.

Real applications trip generic rules. Two ways out, both from the event detail panel beside the list:

  • Suppress Globally - the rule stops firing everywhere. The Suppressed Rules tab lists these, and takes a rule id typed in by hand.
  • Suppress for the host - it keeps protecting everything else. A host’s suppressions are also edited in its WAF card.

An application that trips the CRS usually needs the same handful of exclusions everywhere it runs. A preset is that set of rules, written once under a name in WAF → Presets and then selected wherever it applies:

  • Globally, in WAF → Settings, for every host with the WAF on.
  • Per host, in the host’s WAF card. In merge mode the host’s presets are added to the global ones; in override mode only the host’s apply.

Presets load after the CRS setup and before the CRS rules - where a CRS plugin’s exclusions go - so a runtime exclusion such as ctl:ruleRemoveById takes effect before the rule it removes runs.

Editing a preset applies the change to every host that selects it. A preset cannot be deleted while anything still selects it; the error names what does.

The CRS plugin registry lists plugins for the Core Rule Set: rule exclusions for WordPress, Nextcloud, Drupal, phpMyAdmin and other applications, and extra detections. WAF → Plugins lists the registry with whether each plugin is official or third party, how far it has been tested and its license, and installs one with a click.

Installing fetches the plugin’s latest release from its GitHub repository - or the newest commit, for a plugin that has never tagged one - and stores its files, so what runs is exactly what was installed until you update it. A newer release shows beside the plugin’s version once the registries have been checked, or straight away with Check for updates.

Once installed, a plugin is selected like a preset: globally in WAF → Settings, or per host in the host’s WAF card, merged or overridden the same way. Plugins tune the CRS rules, so they load only where the CRS does, in the order CRS itself uses: every plugin’s configuration, then its -before rules, the CRS rules, then its -after rules.

Click an installed plugin to open it: its files are listed in that order, and the -config file - the one that turns parts of a plugin off or sets its variables - can be edited there. The edit is kept when the plugin updates; restoring the upstream text goes back to the plugin’s own. The other files are shown as the release shipped them.

The gear on the Plugin Registry card lists the registries plugins come from. The OWASP one is there to start with; add your own - any https:// URL serving a registry.json in the same format - or remove it. Plugins from every registry share one table, with a column naming the registry each came from.

Each registry is re-read on the interval set there (daily by default, 0 for only when you click Check now), and every plugin’s latest release is checked the way an install would check it. One that cannot run here is greyed out and marked Unsupported, with the reason on hover. A release is checked once: later checks only look for a newer one.

Some plugins cannot run here, and are refused by the check and at install:

  • A rule that reads a file - a Lua script (@inspectFile), a data file (@pmFromFile, @ipMatchFromFile) or a GeoIP database (@geoLookup). Caddy runs on your agents and only has the rule set compiled into it.
  • A rule Coraza cannot compile: one that keeps per-client state in ModSecurity’s persistent collections (IP, SESSION and the like), or one with unbalanced quotes. Caddy would refuse the whole config, for every host.
  • A rule id outside the range the registry gave the plugin. That range is what keeps a plugin from redefining a CRS rule, which would stop Caddy loading its config.
  • A rule id range that overlaps a plugin already installed - checked at install, since registries allocate ranges independently and two of them can clash.
  • A directive other than SecRule, SecAction, SecMarker and the SecRuleRemoveById, SecRuleRemoveByTag and SecRuleUpdateTargetBy* exclusions, or ctl:ruleEngine.

An install, an update and a configuration edit also go through the checks in Before anything is saved, Caddy’s own included. A plugin that gets past all of them can still, rarely, be one Coraza refuses - one seeded from a backup, say, or checked while no agent could run Caddy - and Coraza building the WAF is part of Caddy loading its config, so one such plugin would stop every host’s config from loading. When that happens the plugin is found and switched off, and the config loads without it: hosts that select it keep the rest of their WAF. The plugin is marked Disabled in its row, the audit log records it, and opening the plugin shows what happened with a Retry button. Updating it or editing its configuration tries it again too.

The controller needs outbound HTTPS to api.github.com, raw.githubusercontent.com and any registry you add. GitHub allows 60 unauthenticated API requests an hour: an install takes two or three, and a first check of the OWASP registry most of them. A GitHub token in the registry settings - one with no scopes - raises that to 5,000. It is stored encrypted and sent only to api.github.com.

Each host and the global settings also take directives of their own, loaded after the CRS rules. Presets and custom directives accept the same SecLang: SecRule, SecAction, SecMarker and SecDefaultAction, plus the request body limit directives. A rule can span several lines with a trailing backslash:

SecRule REQUEST_FILENAME "@rx \A/api/(?:[^.]|\.[^.])*\.?\z" \
"id:9001,phase:1,pass,nolog,ctl:ruleRemoveById=942100"

Match a path on REQUEST_FILENAME, the decoded path, and refuse .. in it: the raw REQUEST_URI of /api/../admin starts with /api/, and an upstream that resolves dot-segments serves /admin. The quick templates do both.

Anything that would switch the engine off or rewrite rules - SecRuleEngine, ctl:ruleEngine, SecRuleRemoveById and the other SecRule* mutations, or Include - is refused when you save, with the offending lines named, and so are setenv and the operators that read the container’s files or run a program (@inspectFile, @pmFromFile, @ipMatchFromFile, @validateSchema). The exception is a data file of the embedded CRS, such as @pmFromFile @owasp_crs/unix-shell.data, while the CRS is loaded. To stop a rule from firing, suppress it instead.

A stored line that a newer release refuses does not block saving anything else, but it is left out of the config, and the WAF page lists it until you rewrite or remove it.

Coraza compiles every WAF while Caddy loads its config, so one rule it refuses would stop every host’s config from loading, not just the one it was written for. Directives are checked twice before they are stored - custom directives, presets and a CRS plugin’s files alike.

  • As you type. The editor reads the text the way Coraza does and marks each line it would refuse: an unknown variable, operator, action or transformation, an action missing its value or given one it does not take, a malformed rule, a repeated rule id, a disruptive action on a chained rule, or a regular expression using lookaround, backreferences or anything else Go’s RE2 engine does not support. An error blocks the save. A warning - a rule with no id, or one in the range the CRS reserves - does not.
  • On save. The agent has its own Caddy validate the WAFs the change produces - the edited one, and every host that merges or selects it - without loading them. This is what catches the rest: a rule id the CRS already uses, rules from the global settings and a host colliding once merged, or a pattern Go refuses to compile. The error says which WAF was refused and quotes Coraza.

The second check runs in a short-lived container from the Caddy image, with no network, and takes a second or two. It needs a paired agent whose Caddy container exists; without one the save goes ahead on the first check alone.

Every blocked and detected request is recorded with its rule, severity and classification. Type to search the host, client IP, URI and rule message, or filter exactly on Host, Client IP, Rule ID, Action or Severity - the filters combine with each other and with the time range. That log is how suppression decisions get made - the drawer for an event links straight to suppressing the rule that produced it.