sentry — user guide

Centrally-managed fail2ban + WAF + filtering DNS, with a hub/agent architecture.

← back to the console

Contents
  1. Architecture
  2. Install the hub (Docker + Postgres)
  3. Enroll agents
  4. Sign in & MFA
  5. Your profile (password, TOTP, passkeys, sessions)
  6. How traffic reaches your app (interception & per-domain routing)
  7. DNS setup (zone file / A records)
  8. Streams: mail & other non-HTTP services (L4)
  9. Config tabs: WAF, DNS, Log Jails, Firewall
  10. Pushing config to agents (global vs per-agent)
  11. Fleet-wide global blocklist
  12. Users & roles
  13. Settings: WebAuthn, OIDC (SSO), LLM assist
  14. Reading the console

1. 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.

2. Install the hub (Docker + Postgres)

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.

3. Enroll agents

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.

The agent container mounts /var/log read-only so the log-watch (fail2ban) jails can read host logs.

4. Sign in & MFA

The console login supports three methods:

A machine/API token (hub.token) always acts as admin for automation.

5. Your profile

The Profile tab manages your own account:

How traffic reaches your app (interception & per-domain routing)

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:

What happens per request:

  1. The client connects to the agent's listen socket (because DNS/your edge sends it there — see the next section).
  2. The agent reads the HTTP request and its Host: header, percent-decodes the path/query, and runs the request against the global WAF rules plus any per-domain overlay for that host.
  3. If a rule matches → 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.

The proxy preserves method, headers, and body, strips hop-by-hop headers, and streams the upstream response (including large downloads) without buffering it whole. Request bodies up to 2 MiB are inspected; larger bodies pass through un-inspected.

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.

DNS setup (zone file / A records)

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.

Streams: mail & other non-HTTP services (L4)

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"

6. Config tabs

TabWhat you edit
WAF RulesRegex rules (name, on/off, target: path/query/body/header/user-agent/method, pattern) + auto-block criteria (hits, window, rate-limit, ban-vs-403).
DNSProtocol/port monitor toggles, per-type thresholds, tunneling toggle, allowed client CIDRs, allow/block domain lists.
Log Jailsfail2ban jails: name, log paths, filter regex (with an (?P<ip>…) group), findtime, maxretry, bantime.
FirewallWhitelist CIDRs (never banned) + default ban TTL.
✨ On the Log Jails editor, the sparkle button suggests a regex from a sample log line — if LLM assist is enabled in Settings.

7. Pushing config to agents

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).

8. Fleet-wide global blocklist

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).

9. Users & roles

Admins manage users on the Users tab. Roles:

RoleCan
viewerread everything
operator+ push blocks & config, edit the global blocklist
admin+ manage users & settings

10. Settings

Secrets (OIDC client secret, LLM key, TOTP encryption key) can be provided via env vars and are never required to live in the database.

11. Reading the console

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.