SentrySentry
Rules Engine

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:

ActionEffect
AllowShort-circuit: bypasses scoring and AI entirely (allowlist).
Block / Challenge / RateLimitShort-circuit: executes containment and ends the pipeline.
LogAnnotates the event (RuleHit, weight 0) and continues into heuristics + AI.
TagSame 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 → decider

A 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>,
}
  • ttl expires dynamic rules (temporary blocks; the Cloudflare reaper uses the same sentry:<ts>:<ttl> stamp format).
  • log_level controls 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:

VariantOrigin
Config[[rules.custom]] in sentry.toml.
DbCreated via CLI/dashboard, persisted in Postgres.
CloudflareSyncMirror of live Cloudflare edge rules (startup reconcile).
FeedGenerated by the reputation feed/dataset importer (feed:<name>).
AutoLearnedTTL auto-block derived from confirmed incidents.
DefaultPackBuilt-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

  1. Config (sentry.toml) — [[rules.custom]] compiled by rules_from_config: an invalid DSL expression fails only that rule (the error names it), not the whole config.
  2. Postgres (rules table) — dynamic rules created via CLI (sentry rules add/allow/block) or the dashboard.
  3. Default packs — see Default Rule Packs.
  4. Reputation feeds and datasets — synthetic feed:<name> rules (priority 50) built by feed_rule (IP, when the feed declares an action) and dataset_rule (user_agent/path/ja3).
  5. Cloudflare sync — startup reconcile adopts live edge rules as a local mirror (CloudflareSync).
  6. Auto-learned — repeat-offender escalation persisted in ip_state.

Hot-reload

  • sentry_rules_changed — the rules_hot_reload task LISTENs on the Postgres channel, reloads the ruleset from the database and swaps the Arc<RwLock<RuleSet>> without a restart. The CLI and dashboard emit the NOTIFY.
  • sentry_datasets_changed — the datasets_hot_reload task rebuilds the dataset:-prefixed rules via replace_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 as None so 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, an ArcSwap); user_agent/path datasets join that gate via reload_dataset_lists, alongside SENSITIVE_PATH_RE and BAD_CRAWLER_RE.
  • Decode-once — the URL-decoded path is computed once per evaluation (EvalCtx) and shared by every path condition; 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

On this page