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.
| Row status | Time | Action | Severity | Host | Client IP | Request | Rule ID | Actions |
|---|---|---|---|---|---|---|---|---|
| Blocked | Critical | app.example.com | 45.147.230.14NL | POST/login | 942100 | |||
| Blocked | Critical | app.example.com | 45.147.230.14NL | GET/search?q=%3Cscript%3E | 941100 | |||
| Blocked | Warning | grafana.example.com | 185.220.101.77DE | GET/.env | 930120 | |||
| Blocked | Warning | grafana.example.com | 185.220.101.77DE | GET/wp-admin/setup-config.php | 930130 | |||
| Detected | Notice | wiki.example.com | 10.0.4.18 | POST/api/pages/42 | 941160 | |||
| Blocked | Critical | app.example.com | 103.152.220.9SG | POST/api/import | 932130 | |||
| Detected | Notice | wiki.example.com | 10.0.4.18 | POST/api/pages/17 | 941160 |
Block, or only detect
Section titled “Block, or only detect”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.
The rule set
Section titled “The rule set”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.
When a rule is wrong
Section titled “When a rule is wrong”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.
Presets
Section titled “Presets”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.
CRS plugins
Section titled “CRS plugins”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.
Registries
Section titled “Registries”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,SESSIONand 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,SecMarkerand theSecRuleRemoveById,SecRuleRemoveByTagandSecRuleUpdateTargetBy*exclusions, orctl: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.
Custom directives
Section titled “Custom directives”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.
Before anything is saved
Section titled “Before anything is saved”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.
The event log
Section titled “The event log”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.