Intrusion blocking for Linux servers β fail2ban, a decade later.
EzyShield watches your server logs, detects attacking IPs, and bans them with escalating penalties: locally via nftables and at the edge via Cloudflare. A deterministic rule engine scores every event offline and always works; AI is consulted only for the ambiguous cases, so decisions stay cheap and the tool runs fully offline if you never configure a provider. It ships as a single static Go binary β no Python, no Java, no runtime to install.
Status: early release (v0.1). Everything described below is implemented, tested in CI, and running today on multiple Linux servers managed by the maintainer. Start in dry-run (the default), watch the decisions, and arm it once you trust them. Interfaces may still change before 1.0 β bug reports via issues are very welcome.
See it run: bash scripts/demo/demo.sh replays a full attack against a
throwaway dry-run instance β detection, the strike ladder escalating on the
repeat offense, and the ban receipt β in about 90 seconds, no root needed.
curl -sfL https://get.ezyshield.com | sudo sh # install (verifies SHA-256)
sudo ezyshield init # guided setup β installs & starts the service
ezyshield status # see what it *would* have banned (dry-run)
sudoedit /etc/ezyshield/policy.yaml # set `armed: true` when you trust it
sudo systemctl restart ezyshield # apply the new policyBefore v0.1.0 ships, every release is a release candidate β the install command above detects that and prints instructions instead of installing; see Install for the release-candidate command that works today. No flags will be needed the moment v0.1.0 ships.
That's the whole loop: init leaves the daemon running in dry-run; observe
first, arm only once the decisions look right.
| EzyShield | fail2ban | CrowdSec | SSHGuard | |
|---|---|---|---|---|
| Language / runtime | Go, single static binary | Python | Go | C |
| Setup | ezyshield init, dry-run by default |
jails + regex filters | agent + Local API + remediation components (bouncers) | small config + firewall backend |
| Strike escalation | built in: 5min β 1h β 24h β 7d β permanent, history kept forever | optional (bantime.increment, since 0.11) |
per-scenario durations via profiles; escalation via custom expressions | yes β block time doubles per repeat offense |
| Edge enforcement (CDN/WAF) | built in (Cloudflare) | via bundled actions (incl. Cloudflare) | yes β remediation components incl. Cloudflare | no β local firewall backends only |
| Shared threat intel | no β not built in today | report-to actions (AbuseIPDB, DShield); no community blocklist | yes β community blocklist + CTI; this is their core strength | no |
| Mandatory telemetry / account | none | none | signal sharing on by default (opt-out); console account optional | none |
| Anti-lockout guarantees | automatic β SSH peer + admin CIDRs allowlisted before every rule write | manual ignoreip |
manual whitelists | manual whitelisting |
| AI usage | optional, ambiguous cases only; rule engine needs zero AI | none | none | none |
fail2ban is battle-tested and great at what it does; CrowdSec's community blocklist is genuinely valuable and something EzyShield simply doesn't have; SSHGuard is admirably small and fast. EzyShield's bet is different: strike escalation, local and edge enforcement, and guardrails that make it hard to ban yourself β out of the box, from a single binary. You can even run EzyShield as the brain and keep fail2ban for enforcement.
Comparison verified against each project's docs as of July 2026 β fail2ban, CrowdSec, SSHGuard. Corrections welcome via issues.
flowchart TD
L["logs: SSH Β· Nginx Β· Apache Β· Caddy Β· Traefik Β· Postfix Β· Dovecot"] --> C["Collector (file tail / journald / docker)"]
C --> P["Parser β structured event"]
P --> A["Aggregator (per-IP windows)"]
A --> R["Rule engine β offline scoring, always runs"]
R -->|ambiguous only| AI["AI analyzer (optional)"]
AI --> D
R --> D["Decision engine β strikes + TTL escalation"]
D --> G1{{"allowlist always wins"}}
G1 --> G2{{"anti-lockout: SSH peer / CDN range"}}
G2 --> G3{{"dry-run by default"}}
G3 --> G4{{"ban rate limit"}}
G4 --> E["Enforcer β nftables (local) + Cloudflare / bunny.net / AWS WAF (edge)"]
G4 --> N["Notifier β Telegram / Email / Slack / Discord / webhook"]
style G1 fill:#f9e79f,stroke:#b7950b
style G2 fill:#f9e79f,stroke:#b7950b
style G3 fill:#f9e79f,stroke:#b7950b
style G4 fill:#f9e79f,stroke:#b7950b
The yellow diamonds are the safety gates every would-be ban must pass β no
rule, AI verdict, or feed can skip them. The whole path from parser to
decision is side-effect-free and tested against fixture logs. Firewall
changes only happen through a small privilege-separated helper
(ezyshield-enforcer) that holds CAP_NET_ADMIN and accepts a fixed,
minimal verb set β the main daemon can never run arbitrary firewall commands.
Want the narrated version? Anatomy of a Ban walks one SSH brute force through every stage, in dry-run and armed; the troubleshooting guide covers the common "why is nothing detected / why is nothing blocked" questions.
| Strike | Ban duration |
|---|---|
| 1 | 5 minutes |
| 2 | 1 hour |
| 3 | 24 hours |
| 4 | 7 days |
| 5 | permanent |
Strike history is kept forever in SQLite, so a repeat offender from last month still escalates today.
- Escalating bans β short first ban, permanent after repeated offences
- Local enforcement β nftables, via a privilege-separated enforcer helper
- Edge enforcement β push IP bans to a Cloudflare list, bunny.net pull-zone blocklists, and/or AWS WAFv2 IPSets for CloudFront/ALB (see the Cloudflare, bunny.net, and AWS WAF guides)
- SSH, Nginx, Apache, Caddy, Traefik, Postfix & Dovecot parsers with fuzz-tested, panic-safe parsing of hostile input β web and mail servers covered out of the box
- Deterministic rule engine β thresholds + scanner signatures; works with zero AI configured; detection quality is measured by a reproducible benchmark on a labeled corpus (currently 6/6 attacks detected, 0 false positives), regression-guarded in CI
- AI-assisted decisions (optional) β Anthropic, any OpenAI-compatible endpoint, or local Ollama, with provider failover, a token budget, and verdict caching
- Prompt-injection defense β log lines are treated as data, never instructions; AI output is schema-validated and clamped by policy (it can only suggest within limits)
- Anti-lockout β active SSH peer + admin CIDRs auto-allowlisted before any rule write; allowlist always wins
- Dry-run by default β nothing is enforced until you set
armed: true - Ban rate limit β
max_bans_per_minute(default 30) so a bad rule or poisoned feed can't ban the internet - Notifications β Telegram, Email (SMTP), Slack, Discord, generic webhook
- Audit trail β every action recorded in SQLite; JSON output for scripting
- Localhost-only dashboard β small web UI over 127.0.0.1 with status, active bans, allowlist, event log, live WebSocket updates and a strike timeline; CSRF-protected manual ban/unban/allow; access remotely via SSH tunnel or Cloudflare Tunnel (see docs and the remote-access guide)
- Scriptable β
--jsonon commands; unix-socket control, no TCP port ever
No telemetry, no phone-home, no account, no data sharing required for any
feature. The rule engine scores everything offline; the only outbound
connections are the ones you configure (edge enforcement, notifiers, AI
providers) or run yourself (ezyshield update). AI is opt-in, and when
enabled the provider never sees your logs: it receives only aggregated
counters per IP β event kinds, counts, and GeoIP/ASN metadata if you've
configured the MaxMind databases β never raw log lines.
CI gates enforce that secrets and hostile log content can't reach the
request (prompt-injection and
secret-leak tests).
The complete map β every outbound connection, its trigger and payload, and
the exact zero-outbound configuration β is in the
data-flow reference.
The local agent will never lose features to a paywall. The code is and stays open under AGPL-3.0. If a paid offering ever exists, it will be about coordination at scale β fleets, identity, compliance, support β never about the protection itself. If EzyShield defends one server well, that part stays free, forever. β Evert
# Debian / Ubuntu
curl -fsSL https://packages.ezyshield.com/ezyshield.asc | sudo gpg --dearmor -o /usr/share/keyrings/ezyshield.gpg
echo "deb [signed-by=/usr/share/keyrings/ezyshield.gpg] https://packages.ezyshield.com/apt testing main" | sudo tee /etc/apt/sources.list.d/ezyshield.list
sudo apt update && sudo apt install ezyshieldGPG-signed repositories with .deb and .rpm for amd64/arm64 β dnf setup and details in the install guide. Every release today is a release candidate, so this uses the testing suite; switch to stable once v0.1.0 ships. See the supported platforms matrix for the distros and architectures exercised by the end-to-end install test.
curl -sfL https://get.ezyshield.com | sudo EZYSHIELD_VERSION=v0.1.0-rc.N shCheck the releases page for the current tag. This is the install-script method that works today, before v0.1.0 ships.
curl -sfL https://get.ezyshield.com | sudo shFetches the latest release binaries (ezyshield and ezyshield-enforcer) and
verifies their SHA-256 checksums. Before v0.1.0 ships (every release today is a release candidate), this prints install instructions instead β see "Specific version" above.
See the install guide for all options (air-gapped mirrors, from source, upgrading).
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/Requires Go 1.26+ and Linux with nftables for local enforcement.
Then:
sudo ezyshield init # create config under /etc/ezyshield
sudo ezyshield doctor # validate config, permissions, and dependenciesNaming: the binary is
ezyshieldand behaves exactly asezy shieldwould in the widerezytool family βezyshield initβ‘ezy shield init.
# The daemon runs as a systemd service (installed and started by `init`)
sudo systemctl status ezyshield
# Inspect the running daemon
ezyshield status
# Manual ban / unban
sudo ezyshield ban 203.0.113.42
sudo ezyshield unban 203.0.113.42
# Permanently allow an IP or CIDR
sudo ezyshield allow 198.51.100.0/24
# See active bans / allowlist / recent events
ezyshield list
# Test a notification channel without waiting for a real event
sudo ezyshield test notifier telegram
# Coming from fail2ban? Generate an equivalent setup + migration report
sudo ezyshield migrate fail2ban| File | Purpose |
|---|---|
/etc/ezyshield/config.yaml |
Log sources, enforcement backends, AI providers, notifications |
/etc/ezyshield/policy.yaml |
Score thresholds, strike table, allowlists, rate limits |
/etc/ezyshield/rules.d/*.yaml |
Optional drop-in rule customizations β the built-in detection rules ship embedded in the binary and update with it; files here merge over them by name. (rules_path, whole-file replacement, is deprecated.) |
Secrets (API tokens, SMTP passwords) are never stored in YAML β reference
them as env:VARNAME or via systemd LoadCredential=. Inline secret values are
rejected when the config loads, and ezyshield doctor warns on bad file
permissions.
Minimal config.yaml:
data_dir: /var/lib/ezyshield
collectors:
- kind: journald
unit: sshd
- kind: file
path: /var/log/nginx/access.log
enforce:
nftables:
table: inet ezyshield
set: blocked
notify:
telegram:
bot_token: env:EZYSHIELD_TELEGRAM_BOT_TOKEN
chat_ids: ["-1001234567890"]Start in dry-run (armed: false in policy.yaml), watch what it would block,
then arm it and restart the daemon (sudo systemctl restart ezyshield β
policy changes are read at startup). The full setup walkthrough β collectors, AI, notifications, custom
rules β is in docs/content/en/getting-started/index.md.
Everything listed under Features is implemented, tested, and shipping in the current release. We are preparing the roadmap for the next versions β it will be published here. Ideas and requests are welcome in the issues.
EzyShield is a root-capable security daemon and is built accordingly: privilege separation for firewall writes, unix-socket control (no listening TCP port), a localhost-only dashboard, anti-lockout, action rate limiting, and secrets kept out of config and logs. Every change goes through a mandatory security review.
Found a vulnerability? Please follow SECURITY.md β do not open a public issue for security reports.
Contributions are welcome. Read CONTRIBUTING.md first; a CLA is required. Every PR ships code + tests + doc updates together, and CI (lint, tests, CodeQL, fuzz, security gates) must be green to merge.
EzyShield is free and open source, and always will be (AGPL-3.0). If it keeps your servers safer, consider sponsoring β it funds focused time to build this in the open, independently.
EzyShield is released under AGPL-3.0-only β see LICENSE β
with one deliberate exception: the public SDK package
pkg/sdk is Apache-2.0 (its own
LICENSE), so plugin and module authors can build
proprietary or differently-licensed integrations against the SDK types
without AGPL obligations. The boundary is documented in
pkg/sdk/README.md; every .go file carries an
SPDX-License-Identifier header stating which side it is on, enforced in
CI by scripts/spdx-gate.sh.