Centrally-managed fail2ban + WAF + filtering DNS, with a hub/agent architecture.
One hub (central manager) and one lightweight agent per protected host. Agents enforce locally (WAF reverse proxy, nftables firewall, connection-flood detect, fail2ban-style log-watch, optional filtering DNS) and report to the hub. The hub owns the managed policy (WAF rules, DNS ACLs, log jails, firewall whitelist, thresholds) and pushes it to agents on check-in. The hub does no local enforcement.
Hub state (users, MFA, config, agent roster, blocklists) is persisted in PostgreSQL 17 so it survives restarts and is easy to migrate.
cd deploy/hub export SENTRY_HUB_ADMIN_TOKEN=$(openssl rand -hex 24) # machine/API token (automation) export SENTRY_ENROLL_TOKEN=$(openssl rand -hex 24) # agent enrollment secret export SENTRY_ADMIN_USER=admin export SENTRY_ADMIN_PASSWORD='choose-a-strong-one' docker compose up -d --build # starts postgres:17 + the hub # open http://<host>:9090/ and sign in as the admin user
The compose file runs postgres:17 with a healthcheck and a named volume; the hub waits for it and applies its schema on first run. The DATABASE_URL is set for you. State persists across docker compose down/up.
On each protected host, run an agent pointed at the hub with the same enrollment token:
cd deploy/agent SENTRY_HUB_URL=http://<hub>:9090 SENTRY_ENROLL_TOKEN=<enroll> \ docker compose up -d --build # or on a systemd host: sudo ./install.sh --agent --hub http://<hub>:9090 --token <enroll>
The agent registers with a stable id (hostname + machine-id) and appears in the console Overview within a few seconds. Renaming or restarting never re-enrolls it.
/var/log read-only so the log-watch (fail2ban) jails can read host logs.The console login supports three methods:
A machine/API token (hub.token) always acts as admin for automation.
The Profile tab manages your own account:
An agent's WAF is a reverse proxy, so the agent must sit in the request path — it never sniffs traffic passively. There are two ways to place it:
0.0.0.0:8080, or :80/:443 behind your TLS terminator). Your app servers live behind it on the LAN. This is the Cloudflare-style placement: all inbound web traffic hits the agent first.127.0.0.1:8000). The agent forwards to it.What happens per request:
Host: header, percent-decodes the path/query, and runs the request against the global WAF rules plus any per-domain overlay for that host.403 (and, after N hits in the window, the source IP is banned in nftables). If clean → the agent looks up the upstream for that Host and proxies the request there, streaming the response straight back.Per-domain routing (this is the reverse-proxy part). On the Domains & Routing tab, each host maps to an upstream http://backend:port. One listener fronts many sites — the Host: header selects the backend, exactly like nginx server blocks or Pangolin resources. A host with no upstream falls back to the agent's default [waf].upstream. Because upstreams live in the pushed policy, you can add or re-route a site from the hub and every agent applies it on its next check-in — no restart.
Inbound TLS termination. The agent can terminate HTTPS itself (listen on :443). Set
tls_enabled=true and either acme_domains for automatic Let's Encrypt certificates
(issued and renewed via TLS-ALPN-01 on the same port — each domain must resolve to the agent) or a static
tls_cert_path/tls_key_path PEM pair. The WAF inspects the decrypted request exactly as it
does plain HTTP. Leave TLS off to keep running behind your own terminator. Every rule editor (WAF, jails, DNS) and
the Streams tab has a 🧪 test button so you can try a change before saving.
For clients to reach the agent, the public DNS name of each protected app must resolve to the agent's edge IP (not the app server's). Point the record at wherever the agent listens for that host.
BIND-style zone file (replace 203.0.113.10 with your agent's public IP):
; ── example.com zone ──────────────────────────────
$ORIGIN example.com.
$TTL 300
@ IN SOA ns1.example.com. hostmaster.example.com. (
2026072201 ; serial (bump on every edit)
3600 600 1209600 300 )
@ IN NS ns1.example.com.
; Point protected hosts at the sentry AGENT edge IP:
app IN A 203.0.113.10 ; app.example.com -> agent
api IN A 203.0.113.10 ; api.example.com -> agent (same edge, host-routed)
@ IN A 203.0.113.10 ; apex example.com -> agent
www IN CNAME example.com.
; Mail: MX names the host, its A record points at the agent running the [stream] proxy:
@ IN MX 10 mail.example.com.
mail IN A 203.0.113.10 ; SMTP/IMAP/POP proxied by the agent's stream routes
Registrar/hosted-DNS panel equivalent — one row per host:
Type Name Value TTL A app 203.0.113.10 300 A api 203.0.113.10 300 A @ 203.0.113.10 300 A mail 203.0.113.10 300 MX @ mail.example.com (priority 10)
Then add each host on the Domains & Routing tab with its real backend as the upstream. Keep TTL low (300s) while you cut over so you can repoint quickly. If you terminate TLS in front of the agent, the certificate must cover these hostnames; the agent itself speaks plain HTTP to the upstream.
The WAF only understands HTTP. Mail (SMTP/IMAP/POP), databases, and game servers speak raw TCP/UDP, so they are handled by the stream proxy (the Streams (L4) tab explains it in-console). Each route forwards a listen port to a backend host:port. It carries the same protection as everything else: banned IPs are dropped at L3 by nftables before they reach the listener, and an optional per-source connection-rate limit bans floods.
For TLS services, many names can share one port via SNI passthrough: the proxy peeks the TLS ClientHello (never terminating TLS or touching a key) and dials the backend matching the SNI server name. Stream routes are hub-managed per agent on the Streams (L4) tab — pick the agent, add routes, and Save; the agent rebinds its listeners on the next check-in (no restart). They can also be seeded from an agent's config.toml under [stream]:
[stream] enabled = true [[stream.routes]] # plain TCP: SMTP submission to a LAN mail server name = "smtp" listen = "0.0.0.0:25" proto = "tcp" upstream = "10.0.0.10:25" max_conns_per_min = 120 # 0 = unlimited; else ban a source over this rate [[stream.routes]] # TLS SNI passthrough: many mailboxes, one IMAPS port name = "imaps" listen = "0.0.0.0:993" proto = "tcp" upstream = "10.0.0.10:993" # fallback if no SNI matches [[stream.routes.sni_routes]] server_name = "mail.example.com" upstream = "10.0.0.10:993"
| Tab | What you edit |
|---|---|
| WAF Rules | Regex rules (name, on/off, target: path/query/body/header/user-agent/method, pattern) + auto-block criteria (hits, window, rate-limit, ban-vs-403). |
| DNS | Protocol/port monitor toggles, per-type thresholds, tunneling toggle, allowed client CIDRs, allow/block domain lists. |
| Log Jails | fail2ban jails: name, log paths, filter regex (with an (?P<ip>…) group), findtime, maxretry, bantime. |
| Firewall | Whitelist CIDRs (never banned) + default ban TTL. |
Every config tab has a Scope selector: Global (all agents) or a specific agent (a per-agent override). Edit, then Save config. Agents fetch and apply it live on their next check-in (typically ≤10s) — no restart. A per-agent override replaces global for that agent (badge: override).
The Global Blocklist tab lets an operator ban an IP (or CIDR) once at the hub; it's delivered to every agent and enforced in each agent's nftables. Remove it to un-block fleet-wide on the next check-in. Global bans show a red global badge in each agent's blocklist and can't be locally unblocked (remove them at the hub).
Admins manage users on the Users tab. Roles:
| Role | Can |
|---|---|
| viewer | read everything |
| operator | + push blocks & config, edit the global blocklist |
| admin | + manage users & settings |
The Overview shows fleet KPIs (agents online/offline, total blocked, WAF blocks, DNS sinkholes, detect events), the agents list (status, pillars, counters), and — on click — a per-agent detail with its live blocklist (per-row unblock), recent activity feed, and a manual-block form. Event tags: WAF, DNS, JAIL (log-watch), AUTO (detect/conn-flood), BLOCK/UNBLK (manual/global).
← back to the console · see also docs/USER_GUIDE.md and docs/PROTECTIONS_TEST_REPORT.md in the repository.