Skip to content

Policies and rules

A policy is a set of rules, a mode, and two thresholds. Sites point at a policy; several sites can share one.

A site with no policy is a plain reverse proxy — forwarded and recorded, but not inspected. The Sites page says so, since a site nobody meant to leave uninspected is worth noticing.

Creating one

Security Policy → Create Policy sets up everything a policy holds, in one pass:

  • Name, mode and thresholds — see Modes and Scoring.
  • Rule sets and rules, chosen from the catalogue.
  • Country rules — the mode and the countries, as on the policy's own page.
  • IP lists — addresses to block or allow, one per line, # starting a comment. A line that is not an address is named back to you rather than quietly dropped.
  • Published lists — each off unless you choose Challenge or Block.

Start from an existing policy copies another policy's rules, installed sets, exclusions, IP entries, published-list choices and country rules into the new one; anything you choose on the form is applied on top. The two are independent afterwards — this is the way to say "like the websites policy, but for this application". An exclusion naming a custom rule is not copied, since that rule belongs to the policy it was written in; the message says how many were left.

Exclusions are otherwise added later, from the traffic that shows you need them — see False positives.

The policy's own page

Security Policy → the policy's settings carries everything it holds, in the same tabs the create page uses:

Tab What is there
Policy Mode and thresholds, and the Smart Protect checkbox. The heading names the sites the policy applies on
Rules What it holds and the catalogue to change it — tick to add a rule or a whole set, untick to switch one off
Countries The country mode and list
IP Lists Its own blocked and allowed addresses, its published lists, and what reaches it from All policies
Exclusions Rules it does not run, and the form to add one

The IP Lists and Exclusions tabs are the same panels as the pages of those names, so anything added from either place shows up in both. Each tab saves on its own: saving the mode does not touch the country rules, and the other way round.

Modes

Mode Behaviour
Off No inspection at all
DetectionOnly Rules run and matches are recorded; nothing is blocked or challenged. The default for a new policy
On Enforced — matching traffic is blocked or challenged

DetectionOnly is how you find out what a policy would do to your traffic before it does it. Traffic Monitor marks what would have happened — WOULD BLOCK, WOULD CHALLENGE — so the mode is not silent.

Scoring

Each matching rule adds its score. When the total reaches the policy's score threshold — 10 by default — the request is blocked with 403. Some rules block outright regardless of score.

If a challenge threshold is set, a score that reaches it but stays under the block threshold gets a CAPTCHA instead of a hard block. Set it to 0 to disable score-based challenges.

What a blocked visitor sees

A page saying the request was blocked by the site's security rules, and a reference: the time and the visitor's address. Nothing about the rule, the score or the threshold — told to the person sending the request, those are directions for getting under the threshold next time. All of it is on the request's row in Traffic Monitor, which the reference is enough to find when someone writes in to say they were blocked by mistake.

An address blocked by Smart Protect, an IP list or a country rule gets the same page; Smart Protect's says the block is temporary.

A request that matched something but stayed under both thresholds is recorded as SCORED, with its score and the rules that produced it. It is not a block and not a clean request, and it is usually the first sign of both a real probe and a rule that is about to become a false positive.

Rule sets

Rules arrive as sets, versioned and published to a signed channel.

Basic sets are bundled with every build and installed by default. They assume nothing about the application behind the proxy. They are built from OWASP CRS and rebuilt from each CRS release; each rule's description names the CRS rule it came from:

Band Set
913 OWASP CRS: scanner and automated tool detection
920 OWASP CRS: protocol enforcement
930 OWASP CRS: local file inclusion
931 OWASP CRS: remote file inclusion
932 OWASP CRS: remote code execution
933 OWASP CRS: PHP injection
941 OWASP CRS: cross-site scripting
942 OWASP CRS: SQL injection

Only the CRS rules EasyWAF can read faithfully are taken, and each is checked against CRS's own regression tests in this engine before it is published — so a basic set holds fewer rules than CRS's band of the same name. CRS's scores are doubled, so a policy's default threshold of 10 blocks what CRS's threshold of 5 does.

Optional sets are published but never bundled. They only make sense in front of the thing they name — WordPress, Apache httpd, Java and Tomcat, protocol attacks, and credential and secret file exposure. A set for a stack you do not run cannot detect anything, and can only match something legitimate.

Exploit probes is the optional set that suits almost any site. It names the exact requests scanners send to every address for one known hole each — PHPUnit's eval-stdin.php, Laravel's debug console, a router's firmware endpoint — and blocks each one. A refusal is what Smart Protect counts, so with both on a scanner is refused outright a few requests into its list. The one place not to install it is in front of a device it names: a site that is a router's own interface.

Web shells matches the request an attacker makes after getting in: for a shell by its known name (c99, r57, WSO, b374k), for a script where only data belongs — under /.well-known/ or an uploads directory — and in the dialect of the shell itself, whatever its file has been renamed to. It suits any site that runs PHP, ASP.NET or JSP. An application that runs its own scripts from an uploads directory needs an exclusion on that path.

IIS and ASP.NET is the counterpart of the Apache set, for sites served by Microsoft's web server: web.config and its backups, trace.axd and elmah.axd, short-file-name enumeration, ::$DATA, .NET deserialization gadgets and SharePoint's ToolShell.

Bad bots refuses automated clients by what they call themselves, in four rules to take or leave separately: SEO and marketing-data crawlers such as Ahrefs and Semrush (untick this one if you use those tools), site copiers such as HTTrack, address harvesters, and internet scanners such as Nuclei, Censys and Shodan. Like the AI sets below, the first three are a policy rather than a protection, and none of it stops a bot that pretends to be a browser.

Node.js and NoSQL is for an application on Node.js or a document database such as MongoDB. It refuses a query operator where a password or a token was expected — password[$ne]=x, which a bracket-parsing framework hands to the database as "any password but x" — along with $where, prototype pollution through a key named __proto__, and the constructor tricks that reach child_process from a template. Operators on ordinary fields are left alone: Strapi, Feathers and similar frameworks use ?price[$gt]=10 as their query language. On a site where people post JavaScript, expect to add an exclusion for the path that takes it.

AI crawlers and AI assistants and agents are two optional sets that are a policy rather than a protection — nothing they match is an attack.

Set Rule What it refuses What refusing it costs
AI crawlers Model training crawlers GPTBot, ClaudeBot, Common Crawl, Bytespider and others collecting content to train on Nothing a visitor would notice
AI crawlers Search and answer indexers OAI-SearchBot, PerplexityBot and others indexing for AI search The site drops out of those products' answers. Untick the rule to stay in
AI assistants and agents Assistants fetching for a user ChatGPT-User, Claude-User, Perplexity-User — a page fetched because a person asked A reader: somebody was waiting for that answer
AI assistants and agents Agents acting for a user Agents that browse on someone's behalf The same

They are separate sets so that ticking AI crawlers does not also turn away people who ask an assistant about you. Both match what a crawler calls itself in its User-Agent, so they stop the ones that announce themselves — the large operators do — and do nothing about one pretending to be a browser.

The ET Open sets are four optional sets converted from Emerging Threats' ET Open ruleset and rebuilt from each new ET release. They need EasyWAF 1.3.0:

Set Rules What it catches
ET Open: web application exploits about 1,200 Exploits of named applications and devices — WordPress plugins, CMSs, routers, VPN and file-transfer appliances — mostly by CVE
ET Open: exploits about 780 Exploit attempts over HTTP against servers and the software behind them
ET Open: web server attacks about 140 Attacks on the server itself and on generic server-side code
ET Open: scanners about 50 Vulnerability scanners and reconnaissance tools, by what they send

Each rule keeps its ET signature id as its number, so a Traffic Monitor row can be looked up in ET's own documentation. Only ET's rules from 2015 on are taken, and only those for requests to a server; a rule that fires on any of the ordinary requests the channel is checked against is left out before it is published. Together the four cost about 77 µs per request. They overlap the basic sets in places — an injection both recognise scores in both — so a policy holding them may want a higher threshold.

Take a whole set by ticking its heading: it is recorded as a set and offered an update when the channel publishes a newer version. Open the heading and tick rules individually to take only those — single rules are copies, and nothing will ever correct them.

A heading is the set, and it follows the rules under it. Tick every rule of a set and the heading ticks itself, so you get the set rather than copies of all of it. Untick one rule and the heading lets go, because a set with a rule taken out of it is not the set. Unticking a heading switches its whole set off.

The tick beside a rule says whether that rule applies here. Untick one the policy holds and it is switched off: the rule stays in the policy, matches nothing, and stays off through every later update of its set — which is how one rule of a set goes without the set. Tick it again to switch it back on. Nothing happens until you press Apply to this policy, and the heading says how many rules are going off.

Delete, beside the rule, removes it outright instead. That is not the same thing: installing or updating its set puts a deleted rule back, so switching it off is what keeps it out for good.

Updates

EasyWAF checks the channel on start and every six hours. The Policy Manager shows which policies are behind — per policy, because a set is imported into a policy: one may hold version 2 while another already has 3.

Nothing is applied on its own. Applying is a button somebody presses, because a bad rule applied automatically is an outage across every site using that policy.

When you apply one:

  • The manifest's signature is checked before anything is read from it, and each set is checked against the SHA-256 the signed manifest gives for it. A channel serving something else is refused, and nothing reaches the database.
  • Rules already installed are overwritten in place, keeping each rule's enabled state. A rule you disabled because it blocked your traffic stays disabled through the update.
  • The version it replaced is kept. If the update refuses traffic it should not, put back vN on the Rule Sets page restores those rules as they were — including any the newer version removed — and deletes anything it added. One step back, and your clones and your switched-off rules are untouched.
  • A rule the new version no longer has is removed from the policy. Left in place it would go on scoring beside its replacement. Rules you cloned are your own and are never touched.

Under Settings → Updates you can turn the check off — an appliance with no outbound access should not keep trying — or point it at a different channel. Pointing it elsewhere relaxes nothing: the signature and hash checks are the same wherever the manifest came from, and the trusted key ships with EasyWAF.

What a rule reads

A rule's zone says which part of the request its pattern is matched against. There are two kinds.

Whole text. Each of these is one block of text, matched as a whole:

Zone The text
URL path and query string together
ARGS the query string
BODY the body, inflated if it arrived compressed, up to the inspection limit
HEADERS every header's value, joined
ANY all four

One value at a time. Each of these is matched against every value of one part of the request, on its own. So ^ and $ mean the start and end of that value: ^https?:// on ARG is an argument whose value begins with a URL, where on ARGS it would only be a query string that does.

Zone Each value is
PATH the path, without the query
ARG an argument's value — from the query string, and from a body sent as a form or as JSON
ARG:name the value of each argument called name
ARG_NAME an argument's name
HEADER a header's value
HEADER:Name the value of the header Name, such as HEADER:User-Agent
HEADER_NAME a header's name
COOKIE a cookie's value
COOKIE:name the value of the cookie name
COOKIE_NAME a cookie's name
METHOD the request method
XML an XML body's text, as XPath's /* reads it
XML_ATTR each attribute's value in an XML body
FILE_NAME each uploaded file's name in a multipart/form-data body
HEADER_BLOCK the headers as one block of lines, Name: value and a line break (\r\n) each
REQUEST_LINE the request line, GET /path?query HTTP/1.1, with the path and query as they arrived
PROTOCOL the protocol: HTTP/1.0, HTTP/1.1, HTTP/2.0 or HTTP/3.0

Names are compared without regard to case. In a JSON body every leaf is an argument named by its own key, so ARG:password finds {"user": {"password": "…"}} as well as password=… in a form; a multipart/form-data upload's fields are arguments too. An XML body is read by XML and XML_ATTR: a DTD is read, no external entity is fetched, and entity expansion large enough to be a billion-laughs attack is refused.

HEADER_BLOCK is for a rule about how headers sit together — their order, one following another, a value running onto the next line. Its headers are in the order their names first arrived, a repeated header's values together after its first, and each name is written as HTTP/1.1 clients write it, User-Agent, whatever case it came in. HTTP/2 and HTTP/3 have no request line on the wire; theirs is written as HTTP/1.1 would have sent it, ending HTTP/2.0 or HTTP/3.0.

A body that says how it is written and cannot be read that way is read whole. JSON or XML cut off at the inspection limit or malformed, and a multipart body without its boundary, are each one value to the rules that would have read them parsed, so a broken body cannot hide what it carries.

Several zones are joined with | — ARG|COOKIE — and the rule matches once, however many of them its pattern is found in.

Every value is matched in the same forms as before: as it arrived, percent-decoded once and twice, and with the HTML, JavaScript and CSS escapes undone where it carries them.

A zone EasyWAF does not recognise is refused when a rule is saved. A rule set written for a newer EasyWAF, using a zone this one does not read, is shown on the policy's rule sets as needing an upgrade and cannot be applied.

Several conditions

A rule can have more than one condition, and matches only when every one holds. Each is a zone and a pattern of its own: the method is POST, and the path names /xmlrpc.php, and the body holds pingback.ping. A condition can also be negated: it holds when its pattern matches nothing in its zone, so "no Referer" is a condition, and a negated condition on a header the request does not have holds.

Conditions are checked in order and a rule stops at the first that does not hold, so it costs what its first condition costs on almost every request. A rule's page lists its conditions. They are not edited there: a rule with several conditions comes from a rule set, or from an imported configuration.

Customising a rule

Imported rules cannot be edited, because an update would overwrite the change without saying so. Use Clone: you get an ordinary custom rule that updates never touch, and its page tells you when the set it was forked from has moved on.

The original stays enabled unless you disable it, so a clone adds to what the policy enforces rather than replacing it.

False positives

When a rule blocks traffic that should have been allowed, you have three answers, from narrowest to widest.

Exclude it for the client that was blocked. From the Traffic Monitor row that shows the block, one click excludes that rule for the address that was refused. Under Security Policy → Rule Exclusions an exclusion can instead be narrowed to a path prefix, or to a CIDR block. The rule goes on protecting everyone else. A path prefix covers that path and everything below it, a whole segment at a time — /dav covers /dav and /dav/files, not /davx — and is compared with the path as your application receives it, after .. segments are resolved, so /dav/../admin is not under /dav.

Switch the rule off for the policy. Wider: the rule stops running for every request on every site using the policy. Untick it in the policy's Rules tab, or disable it under Manage rules; either way the off state survives rule updates.

Clone and tune it. Where the rule is right in principle and wrong in detail.

An exclusion belongs to the policy, like its rules, country rules and IP lists, so it applies on every site using that policy. Sites that face the same traffic share a policy and tend to trip the same false positives; a site that needs exceptions the others should not have needs a policy of its own. Every place an exclusion is made says how many sites it reaches before you confirm it.

Every rule that is not running anywhere — disabled or excluded — is listed on one page, so a temporary exclusion cannot quietly become permanent.

Those three all assume the rule is wrong. When it is the caller that is fine — a monitoring probe, a partner's integration, your own office — the answer is an allow list entry instead, which skips every check for that address rather than turning a rule off for everyone who trips it.

Country rules

Per policy: block listed countries, or allow only listed ones — and, under Security Policy → GeoLocation Rules, one rule for All policies, for a country nothing on this appliance should ever serve.

The two add up. A request refused by the policy's rule or by the every-policy one is refused; neither lets in a country the other refuses. The every-policy rule reaches policies that have no country rule of their own, including policies made later, and a policy's own page says what is already in force there. A site with no policy is not inspected and is not reached by it.

The way past a country rule for one client is its address on an allow list, which skips every check including this one.

In DetectionOnly a policy records what either rule would have done rather than doing it; with a policy Off, neither does anything. A mode with no countries listed does nothing either — an empty allow list is never read as "refuse everyone", so switching the mode on before typing the list cannot take a site offline between two clicks.

Lookups use a geolocation database compiled into the binary, so they are offline and there is nothing to download. Addresses with no country — private ranges, anything the database does not know — are never matched, so an allow list cannot lock out traffic from your own network.

The CAPTCHA challenge

The middle ground between allowing and blocking: suspicious-but-plausible traffic gets a self-hosted image CAPTCHA rather than a 403. No third-party service is involved.

Solving it sets a short-lived, HMAC-signed clearance cookie bound to the client address, and requests carrying a valid one skip the challenge for 30 minutes.

Each image can be answered once. A wrong answer brings a new image, not another try at the same one, and one address can have fifty waiting at a time — so a client cannot guess its way through, or ask for so many that other visitors are told to come back later.

Clearance is per client address

Behind a NAT — including hairpin NAT on your own network, where internal clients arrive as the router's address — one visitor solving a challenge clears it for everyone sharing that address. It is a reason to be careful with the challenge threshold on sites reached from inside a NAT, and a reason to get trusted proxies right when something sits in front.