EzyShield Quick Start
Get EzyShield running on your server in under 5 minutes.
1. Requirements
| Requirement | Minimum version |
|---|---|
| Linux | kernel 4.x+ |
| nftables | 0.9+ |
2. Installation
One-line install (recommended)
curl -sfL https://get.ezyshield.com | sudo shThe installer is package-first: on apt/dnf hosts it sets up the EzyShield package repository and installs the signed ezyshield package (upgrades then arrive via the package manager). On other hosts it falls back to downloading the latest ezyshield and ezyshield-enforcer binaries, verifying their checksum's cosign signature when cosign is installed (plain SHA-256 otherwise).
To install a specific version:
curl -sfL https://get.ezyshield.com | sudo EZYSHIELD_VERSION=v0.1.2 shBuild from source
Requires Go 1.26+.
git clone https://github.com/evertramos/ezy-shield.git
cd ezy-shield
go build -o ezyshield ./cmd/ezyshield
go build -o ezyshield-enforcer ./cmd/ezyshield-enforcer
sudo mv ezyshield ezyshield-enforcer /usr/local/bin/Verify
ezyshield version3. Initial setup
ezyshield init
Runs the interactive setup wizard: detects the environment, writes config files, installs systemd units (only when the OS package doesn't already provide them in /usr/lib/systemd/system/), and starts EzyShield in dry-run mode.
sudo ezyshield initThis creates:
/etc/ezyshield/config.yaml/etc/ezyshield/policy.yaml/etc/ezyshield/rules.d/(drop-in rule customizations; WordPress installs also get a commented tuning template10-wordpress.yaml)/etc/ezyshield/.env(AI API key, mode 0600)/var/lib/ezyshield/(runtime data, SQLite)/etc/systemd/system/ezyshield.serviceandezyshield-enforcer.service— source/script installs only: on a package install the packaged units are used and nothing is written to/etc/systemd/system/
Tip: If config files already exist,
ezyshield initexits immediately listing the conflicting paths. Remove them and re-run to regenerate.
AI provider API key
When you enable AI analysis, the wizard prompts for your API key. The key is stored in /etc/ezyshield/.env (mode 0600) — never in config files or logs.
Supported providers:
| Provider | Env var |
|---|---|
anthropic | ANTHROPIC_API_KEY |
openai | OPENAI_API_KEY |
ollama | (no key needed) |
Use --yes for non-interactive mode (writes a placeholder you edit later).
ezyshield doctor
Validates configuration and checks dependencies:
sudo ezyshield doctorExpected output:
[PASS] config.yaml: exists
[PASS] config.yaml: parses
[PASS] policy.yaml: exists
[PASS] policy.yaml: parses
[PASS] nft: binary present
[PASS] journald: readable
[PASS] enforcer: socket connectivity4. Configuration — config.yaml
Main file at /etc/ezyshield/config.yaml.
Collectors (log sources)
collectors:
- kind: journald
unit: sshd
- kind: file
path: /var/log/nginx/access.logAvailable types:
journald— requiresunitfield (systemd service name)file— requirespathfield (log file path)
Enforce (local enforcement)
enforce:
nftables:
table: inet ezyshield
set: blockedThe privileged helper (ezyshield-enforcer) handles all firewall writes via a unix socket. The daemon re-syncs the full ban set to the enforcer whenever the daemon restarts, so blocks survive daemon restarts. Restarting only the ezyshield-enforcer helper does not trigger that re-sync on its own — the ban set catches up on the next periodic ban-expiry tick, or the next daemon restart.
AI (optional)
ai:
provider: anthropic
model: claude-haiku-4-5-20251001
api_key: env:ANTHROPIC_API_KEY
ambiguous_band: [30, 69]
token_budget_daily: 500000Important: Secrets must use
env:VAR_NAMEreferences. Inline values are rejected at config load time.
5. Configuration — policy.yaml
File at /etc/ezyshield/policy.yaml. Controls blocking behavior.
armed (operating mode)
armed: false # dry-run (default) — no real blocking
# armed: true # enable only after validating with 'ezyshield doctor'Allowlist
IPs and CIDRs that are never blocked:
allowlist:
- 192.168.1.0/24
- 10.0.0.1
admin_cidrs:
- 203.0.113.50/32 # your SSH access IPStrike table (ban escalation)
strikes:
- ttl: 5m # strike 1 — 5 minutes
- ttl: 1h # strike 2 — 1 hour
- ttl: 24h # strike 3 — 24 hours
- ttl: 168h # strike 4 — 7 days
- ttl: 0 # strike 5 — permanentEach strike represents an attack episode, not a single request. While an IP is already banned, new detections are suppressed until the ban expires.
Thresholds
ban_threshold: 70 # score ≥ 70 → apply strike
observe_threshold: 40 # score 40–69 → log/notify, no ban
max_bans_per_minute: 30 # safety: pause enforcement if exceeded6. Custom rules — rules.d drop-ins
The detection rules are embedded in the binary and update with it. To tune or add rules, drop a *.yaml file in /etc/ezyshield/rules.d/ — entries merge over the built-in rules by name and survive updates. Full guide: Customizing Detection Rules.
Rule structure
rules:
- name: ssh_bruteforce
description: "Repeated SSH authentication failures"
kinds: [ssh_fail, ssh_invalid_user]
window: 60s
threshold: 5
score: 85
category: bruteforceFields
| Field | Description |
|---|---|
name | Unique rule identifier |
description | Human-readable description |
kinds | Event types that activate the rule |
window | Time window for counting |
threshold | Occurrences to trigger |
score | Assigned score (0–100) |
category | Category (bruteforce, scanner, etc.) |
field | Event field to filter (optional) |
value | Exact field value (optional) |
contains | Substring match (optional) |
contains_any | Any-of substring match (optional) |
field and a matcher (value, contains, or contains_any — mutually exclusive) only work as a pair: a rule that sets one without the other is rejected at load time. Without the pairing check, a matcher alone would count every event of the listed kinds, and a field alone would never fire.
Example: block API scanners
- name: api_scanner
description: "Scan of non-existent API endpoints"
kinds: [http_request]
field: status
value: "404"
window: 30s
threshold: 15
score: 75
category: scannerNote: A drop-in only touches the rules it names — everything else keeps riding binary updates. An invalid drop-in stops the daemon from starting (fail-closed). The legacy
rules_path(whole-file replacement) is deprecated.
7. Notifications
Since issue #290,
ezyshield initoffers this step during setup — both interactively (the Notifications section) and via the non-interactive answers file (see the automation guide). The instructions below remain the post-install path for adding or changing a channel later (ezyshield config notifier <name>, or config.yaml by hand).
Telegram
- Create a bot via @BotFather and get the token.
- Add the bot to your group/channel and get the
chat_id. - Configure in
config.yaml:
notify:
rate_limit_per_minute: 5
dedup_window_sec: 600
telegram:
bot_token: env:EZYSHIELD_TELEGRAM_BOT_TOKEN
chat_ids:
- "-1001234567890"
severity: [] # empty = all; or: [warn, critical]Email (SMTP)
email:
from: ezyshield@yourdomain.com
to:
- admin@yourdomain.com
host: smtp.yourdomain.com
port: 587
username: ezyshield@yourdomain.com
password: env:EZYSHIELD_SMTP_PASSWORD
tls: starttls # starttls | tls | none
severity: []8. Test notifications
Validate delivery without waiting for a real event:
sudo ezyshield test notifier telegram
sudo ezyshield test notifier email9. Run the daemon
sudo ezyshield runWhile armed: false, EzyShield runs in dry-run: it processes everything, records strikes and simulated bans so escalation mirrors production exactly (ADR-0009), and logs what would be blocked — without ever touching the firewall.
As a systemd service
sudo systemctl enable --now ezyshield-enforcer
sudo systemctl enable --now ezyshieldChecklist before arming
- ✅
ezyshield doctor— no errors - ✅
allowlistincludes your access IPs - ✅
admin_cidrsincludes your SSH IP - ✅ Notifications tested with
test notifier - ✅ Ran in dry-run, reviewed the logs
- ⬜ Run
sudo ezyshield arm --for 1h(pre-flight + auto-revert window), thensudo ezyshield arm --keeponce you're confident
Learn more
- Anatomy of a Ban — one SSH brute force walked through every pipeline stage (with the safety-gate diagram), in dry-run and armed.
- Troubleshooting — "nothing is detected", "ban recorded but not blocked", socket permissions, journald matching — each with the doctor check that diagnoses it.