Customizing Detection Rules
EzyShield's detection rules ship embedded in the binary — every install runs the full, current ruleset with zero files on disk, and every ezyshield update delivers the latest rule tuning automatically.
To adjust a rule (or add your own) you don't fork that base: you drop a file in /etc/ezyshield/rules.d/.
How drop-ins work
- Every
*.yamlfile inrules.d/is loaded in lexical order (10-wordpress.yamlbefore50-local.yaml). - Entries merge over the built-in rules by
name: a drop-in entry with the same name as a built-in rule replaces it; a new name adds a rule. Later files win over earlier ones. - Everything you do not override keeps riding binary updates — you tune one threshold, and the other rules stay current forever.
- Overriding a built-in rule logs a WARN at startup (deliberately loud: a drop-in that weakens a shipped protective rule should be visible).
- An invalid drop-in stops the daemon from starting (fail-closed) — a typo can never silently degrade detection. After editing, restart and check:
sudo systemctl restart ezyshield && sudo systemctl status ezyshield.
Example: raise the wp-login threshold
# /etc/ezyshield/rules.d/50-local.yaml
rules:
- name: http_wp_probe # same name as the built-in => override
description: "WordPress login probe (site-tuned)"
kinds: [http_request]
field: path
contains: wp-login
window: 60s
threshold: 10 # built-in default is 3
score: 80
category: scannerExample: add your own rule
# /etc/ezyshield/rules.d/60-admin-panel.yaml
rules:
- name: local_admin_probe # new name => added alongside the built-ins
description: "Probing our internal admin path"
kinds: [http_request]
field: path
contains: /internal-admin
window: 60s
threshold: 3
score: 85
category: scannerThe rule schema (fields, matchers, windows) is documented in Getting Started §6; the full current ruleset ships as /etc/ezyshield/rules.yaml.example for reference.
Test a rule before enabling it
Writing a rule blind — enable it and wait — is how false positives ship. rule test dry-evaluates one rule against the event history the daemon already stores, with zero side effects (no strikes, no bans, no audit writes; the daemon doesn't even need to be running):
$ ezyshield rule test ssh_bruteforce_daily --since 7d
Rule test: ssh_bruteforce_daily
kinds: ssh_fail, ssh_invalid_user | window: 24h0m0s | threshold: 5 | ...
Would have fired : 12 time(s)
Unique IPs : 4
Allowlisted hits : 1 — this rule would fire on protected addresses; tune it before enablingThe argument is a loaded rule's name (built-in or drop-in) or a path to a standalone YAML file — so you can test rules.d/60-admin-panel.yamlbefore copying it into place. The rule goes through the same fail-closed validation as the daemon's loader first; an invalid rule is a clear error and a non-zero exit.
The allowlisted hits line is the false-positive early warning: it counts would-be detections on addresses covered by your allowlist/admin_cidrs or the runtime allowlist. A rule that fires on your own ranges needs tuning, not enabling. --json gives the full result for scripting.
Honest limitation: evaluation uses the stored hourly aggregates, so granularity is bounded by 1-hour buckets and by retention — and only kinds referenced by long-window (>1h) rules are persisted at all. Field-level matchers (field/value/contains) can't be applied to counts, so such rules are reported as a kind-level upper bound, loudly marked.
WordPress installs
When ezyshield init detects WordPress containers it writes a fully-commented tuning template to rules.d/10-wordpress.yaml. The WordPress rules are built in and already active — the template exists so the most commonly tuned rules are one uncomment away. Re-running init never overwrites your edits.
Legacy: rules_path (deprecated)
Setting rules_path in config.yaml replaces the built-in rules with your file entirely — no merge, and rules.d/ is ignored. That freezes the install out of all upstream rule tuning (updates change the binary, never your file), so the daemon logs a warning at startup when it is set. Prefer drop-ins; to migrate, move only your actual changes into a rules.d/50-local.yaml and remove rules_path from config.yaml.
Safety boundary
Rules — built-in or drop-in — only ever suggest verdicts. The allowlist and anti-lockout checks run downstream in the decision engine on every target, regardless of which rule fired. No rule can allowlist, unban, or bypass those guarantees.