Rules Engine
Deterministic WAF-style rules engine — the fast path before heuristics and AI
Rules Engine
Sentry has a deterministic rules engine that runs before heuristics
and AI — the "fast path" of
Pipeline::process.
Inspired by Cloudflare's Custom Rules / WAF: each rule is a match +
action, evaluated in ascending priority order, with short-circuit.
Rules are the first line of defense (instant blocking of scanners, Tor,
VPN/proxy, countries) and also the source of allowlists (trusted
IPs/ASNs that bypass all scoring).
Evaluation semantics
The
RuleSet
keeps rules sorted by priority (lower = evaluated first) and
evaluate_with
returns the first match. What happens next depends on the
RuleAction:
| Action | Effect |
|---|---|
Allow | Short-circuit: bypasses scoring and AI entirely (allowlist). |
Block / Challenge / RateLimit | Short-circuit: executes containment and ends the pipeline. |
Log | Annotates the event (RuleHit, weight 0) and continues into heuristics + AI. |
Tag | Same as Log: annotates and lets it flow. |
Order at a glance:
Normalized event
└─ Rules Engine (by priority; first match wins)
├─ Allow → Allow + bypass scoring/AI
├─ Block/Challenge/RateLimit → immediate containment (short-circuit)
├─ Log/Tag → annotate and continue
└─ no match → heuristics → reputation → routes
→ scorer → policy → deciderA Block/Challenge/RateLimit short-circuit emits a
RuleHit
signal with a fixed weight per verdict (Block 100, Challenge 50, RateLimit
30) and still respects the never-ban guard ([real_ip] trusted_ips) and the
cap_shadow
flag — shadow-listed IPs are analyzed but never banned. Console verbosity
for short-circuited events is controlled by the rule's log_level
(silent suppresses the line; the event is still stored and dispatched).
Core types
Defined in
crates/sentry-core/src/rules.rs.
Rule — a single rule:
pub struct Rule {
pub id: RuleId,
pub name: String,
pub priority: i32,
pub enabled: bool,
pub match_: RuleMatch,
pub action: RuleAction,
pub ttl: Option<Duration>,
pub source: RuleSource,
pub tags: Vec<String>,
pub log_level: Option<RuleLogLevel>,
}ttlexpires dynamic rules (temporary blocks; the Cloudflare reaper uses the samesentry:<ts>:<ttl>stamp format).log_levelcontrols the console line verbosity (silent,info,warn,error).- Config rule ids are a stable ASCII slug of the name
(
allow_internal_monitoring).
RuleAction — Allow | Block | Challenge | RateLimit | Log | Tag.
RuleSource — where the rule came from:
| Variant | Origin |
|---|---|
Config | [[rules.custom]] in sentry.toml. |
Db | Created via CLI/dashboard, persisted in Postgres. |
CloudflareSync | Mirror of live Cloudflare edge rules (startup reconcile). |
Feed | Generated by the reputation feed/dataset importer (feed:<name>). |
AutoLearned | TTL auto-block derived from confirmed incidents. |
DefaultPack | Built-in default pack. |
RuleMatch — the condition (composable All/Any/Not tree).
The full reference for conditions and the textual syntax lives on the
Match DSL page.
RuleSet — a compiled set sorted by priority, with
replace_by_prefix
for atomic replacement of rule groups (used by dataset hot-reload).
SharedRuleSet — alias Arc<RwLock<RuleSet>>
(definition):
the daemon shares one handle across pipeline, inline edge and CLI; the
hot-reload task swaps the inner set with write() and evaluation reads it
with read().
Rule sources
- Config (
sentry.toml) —[[rules.custom]]compiled byrules_from_config: an invalid DSL expression fails only that rule (the error names it), not the whole config. - Postgres (
rulestable) — dynamic rules created via CLI (sentry rules add/allow/block) or the dashboard. - Default packs — see Default Rule Packs.
- Reputation feeds and datasets — synthetic
feed:<name>rules (priority 50) built byfeed_rule(IP, when the feed declares anaction) anddataset_rule(user_agent/path/ja3). - Cloudflare sync — startup reconcile adopts live edge rules as a
local mirror (
CloudflareSync). - Auto-learned — repeat-offender escalation persisted in
ip_state.
Hot-reload
sentry_rules_changed— therules_hot_reloadtaskLISTENs on the Postgres channel, reloads the ruleset from the database and swaps theArc<RwLock<RuleSet>>without a restart. The CLI and dashboard emit theNOTIFY.sentry_datasets_changed— thedatasets_hot_reloadtask rebuilds thedataset:-prefixed rules viareplace_by_prefix(atomic, all-or-nothing) and refreshes the prefilter literals.- Postgres is the source of truth: a node that starts later loads the ruleset from the database at startup.
Performance caches (F5)
- Process-lifetime regex cache
(
REGEX_CACHE) and IP-spec cache (IP_CACHE): patterns compile once (the old code compiled a regex per rule per event — ~3.4 ms/event). Invalid patterns are cached asNoneso broken rules don't re-parse on every event. - Aho-Corasick prefilter — heuristics only run a family's regex when
one of its literals appears in the text
(
PREFILTER, anArcSwap);user_agent/pathdatasets join that gate viareload_dataset_lists, alongsideSENSITIVE_PATH_REandBAD_CRAWLER_RE. - Decode-once — the URL-decoded path is computed once per evaluation
(
EvalCtx) and shared by everypathcondition; rules can't be bypassed by encoding (%2e%2e%2f=../,+= space —url_decode).
Config packing
Default packs are declared like this:
[[rules.pack]]
name = "vpn_proxy"
mode = "shadow" # shadow | enforce | off
[[rules.pack]]
name = "country_blocklist"
mode = "enforce"
[rules.pack.params]
countries = ["RU", "CN"]Each pack becomes a pair of keys in the mode map and each param is
flattened as <pack>__<param> (e.g. country_blocklist__countries)
before reaching the builder —
wiring in the daemon.
This flattening (F7.6) fixed the bug where pack params never reached the
builder. Only strings and arrays of strings are rendered. The params read
today are country_blocklist__countries and host_allowlist__domains
(pack_list_param).
Rule management CLI
sentry rules list # list active rules
sentry rules show <id>
sentry rules add --name "block admin from RU" \
--match 'country=RU AND path=/admin/*' --action block --priority 10
sentry rules allow <ip> [--ttl 24h] [--note "monitoring agent"]
sentry rules block <ip> [--ttl 24h] [--note "scan"]
sentry rules enable <id> | disable <id> | delete <id>
sentry rules packs # default pack state
sentry rules test --path /admin --method GET --ip 1.2.3.4 --ua "sqlmap/1.0"sentry rules test evaluates the match expression against a hypothetical
event — use it to validate rules before promoting them to enforce.
Per-IP allow/block actions write to Postgres and emit NOTIFY, so
running nodes hot-reload without a restart.
Next steps
- Match DSL — full condition syntax.
- Default Rule Packs — the current pack catalog.
- Shadow vs Enforce — safe rollout and verdict policy.