The warden
Demerzel’s agent only ever reads: it opens no port, changes nothing on your server and takes no orders. Blocking an attacker is a change, so it is not the agent’s job. It belongs to a separate, optional component: the warden.
The warden is a small root daemon with no network access. It reads the decisions the agent’s detector writes, checks each one against a local policy that only you (root) can write, and applies the ones that pass as entries, with a timeout, in its own nftables table, inet demerzel. It touches no other firewall rule. Without the warden installed, Demerzel detects and explains attacks exactly as before, and blocks nothing.
Install — it blocks nothing yet
The warden needs the agent’s service and nftables. Only nftables’ kernel side is really required, and every current distribution has it, even where your firewall is iptables (on Debian, iptables is iptables-nft): the warden’s table lives beside your rules without touching them. If the nft command is missing, the installer adds it and leaves nftables.service off — keep it off on an iptables host, its default configuration flushes every rule at boot.
curl -fsSL https://get.demerzel.cloud/warden.sh | sudo sh
sudo demerzel-warden status
It starts in shadow mode: every decision is checked and recorded, and nothing is blocked. Watch what it would have done for as long as you like.
Turn it on
sudo demerzel-warden enable
This enforces one rule: Signature S1, a request for a secret file no website ever serves (/.env, /.git/, /.aws/, a config backup, phpunit’s eval-stdin.php). It is the only rule allowed to enforce from day one. Every other rule must first be replayed on your own server’s logs, at its exact current definition, with no legitimate visitor caught; a rule whose definition changed since its replay drops back to shadow until it is cleared again.
Clear another rule
A rule other than S1 is cleared by a replay of its current definition over your own logs. Run one as the agent’s user, over at least a day (two weeks is better), then enable the rule. --to defaults to 24 hours after --from, so a longer range needs it (a replay covers at most 31 days):
sudo -u demerzel demerzel-agent replay --from 2026-09-15 --to 2026-09-29
sudo demerzel-warden enable rate-dynamic
The rules enable <rule> accepts:
| Rule | Fires on | Action |
|---|---|---|
S1-secret-probe | a request for a secret file no site serves (/.env, /.git/, /.aws/, a config backup, phpunit’s eval-stdin.php) — needs no replay | ban |
S2-cms-probe | 3 failed probes (status 400 or more) in 10 minutes for software the server does not run: WordPress login, admin, wp-includes or wp-json on a non-WordPress site, phpinfo, Spring actuator, xmlrpc.php, phpMyAdmin. A path the server serves or redirects never counts, and /wp-content/ is not watched: a site migrated off WordPress keeps answering its old media URLs | ban |
rate-dynamic | more than 300 dynamic requests in 2 minutes from one address, across all vhosts (static assets do not count) | throttle |
spread-vhosts | more than 8 different sites of the server hit within 10 minutes | ban |
error-ratio | more than 60 % of at least 50 requests in 10 minutes ending in a 4xx — a fuzzer | throttle |
ssh-bruteforce | failed SSH logins on 10 connections within 10 minutes | ban |
The replay prints, for each rule, how many addresses it would have caught and how many of them look like real visitors (they loaded the pages’ images, styles and scripts, as a browser does). It leaves its verdict in /etc/demerzel/replays/, where the warden reads it. enable refuses the rule if the newest replay long enough to judge — 24 hours and 1,000 requests at least — caught a single visitor, or if the agent now runs a different version of the rule than the replay did. Otherwise it pins the rule to that exact definition in the policy.
What the rule caught in shadow during the last 15 minutes is not lost: once it is enabled, the warden gives those decisions a second look and applies the ones that now pass — at most half the per-minute ban cap at once, so enabling never trips the cap. What the kill switch held back is not revived when you lift it.
SSH password guessers work the same way, from agent v0.22: let the agent read sshd’s messages (grant-ssh-logs), replay a day or more, then sudo demerzel-warden enable ssh-bruteforce. A ban drops every packet from the address, SSH included. Your own address is never banned while you are logged in over SSH, nor for 24 hours afterwards, and the agent does not count an address that logged in successfully.
To lift a block by hand, and forget the address’s previous offences:
sudo demerzel-warden unban 203.0.113.7
When a rule gets it wrong
A replay is a snapshot, and traffic changes. A rule a replay cleared is watched after it enforces, and goes back to shadow on its own when it errs:
- you say so:
sudo demerzel-warden unban 203.0.113.7 --false-positivelifts the block and demotes the rule that made it; - it catches a visitor: the agent saw the address load pages with their images, styles and scripts, as a browser does, and never ask for a secret file. The warden blocks nothing and demotes the rule. Signature S1 is exempt: no visitor asks for
/.env.
The rule’s blocks are lifted, demerzel-warden status shows it DEMOTED with the reason, and Demerzel raises an Action on the server. Once you understand what it caught, replay again and enable it: only a replay made after the demotion clears it.
To stop:
sudo demerzel-warden disable rate-dynamic # stop enforcing one rule; the others keep enforcing
sudo demerzel-warden disable # enforce nothing; every block is lifted
sudo demerzel-warden kill-switch on # stop everything now, whatever the policy says
One attacker, one ban, every server
With fleet decisions on, an address one of your servers bans is banned on all the others too, until the same time, and you can lift a ban everywhere at once. An owner or admin turns them on in Settings → Fleet decisions, which creates your organization’s signing key and shows the command to run, as root, on each server that should take them (warden v0.25 and later):
sudo demerzel-warden trust <your organization's key>
Every fleet decision is signed with that key. The agent fetches them — it still opens no port, and it only copies what it fetched into a directory the warden reads — and the warden checks the signature before anything else, then applies the decision within its own policy: your allowlists, your operators, the kill switch and every cap still win. A decision signed with any other key, or altered on the way, is refused. sudo demerzel-warden untrust stops taking them and lifts what they blocked.
- Fleet memory. A ban a server’s warden applied on its own evidence reaches your other servers within about a minute, and ends when it ends there.
- Unban everywhere. An address’s page in Demerzel has an Unban on every server button, for members who are not read-only. Say why; the reason is kept with the unban, with your name, and in the activity log. Tick false positive and each server also demotes the rule that banned the address there. An unban creates no allow: new evidence can ban the address again.
- Never ban the operator. When an owner or admin signs in to Demerzel, the public address they signed in from is protected on every server for a day, as an SSH login protects its address on one.
What it will never do
- Block you. Any address logged in to the server over SSH is protected, and stays protected for 24 hours after it logs out. A ban already in force on it is lifted the moment it logs in. “Logged in” means authenticated: the warden reads systemd-logind’s sessions, which exist only once sshd has let someone in — a bot that merely connects to port 22 to guess passwords gets no protection. On a host without systemd-logind,
demerzel-warden statussays it cannot see logins; put your addresses in the policy’sallowlist. - Block the server itself, loopback, or Cloudflare. A Cloudflare address is shared by every visitor behind it: blocking it would cut them all off.
- Block anything on your allowlist, and adding an entry lifts the bans it covers at once. Give each entry a note, so that in a year you still know who it is:
sudo demerzel-warden allow 198.51.100.4 --note "Acme's integration server"(anddisallowto remove it). The server’s page in Demerzel lists them with their notes. - Block forever. A first offence is banned for an hour, then a day, a week and four weeks for repeat offences. Bans expire in the kernel even if the warden stops. A stopped warden removes its table.
- Block for the past. A decision is about what an address is doing now: the agent turns only firings from the last five minutes into decisions, and the warden refuses any decision more than 15 minutes old. Catching up on a backlog of logs reports everything to Demerzel and blocks nothing.
- Block a range. Decisions from the local detector target single addresses only.
- Run away. More than 30 new blocks in a minute (by default) freezes enforcement: existing blocks run out, new ones are refused, until you run
sudo demerzel-warden resume. - Cut off a big part of your traffic on a rule’s word. An address that carried more than 10 % of the server’s successful requests over the last hour (by default, from v0.25) is not blocked, whatever the rule: an office behind one address, a mobile carrier’s NAT, a client’s integration. Demerzel raises an Action instead, and you decide:
allowit with a note, or block it yourself. Setmax_traffic_sharein the policy to change the threshold, or to0to turn this off. - Reach the network or run commands. Its service can open no IP socket at all; the only program it runs is
nft. Closing a banned address’s connections is a request to the kernel over netlink, like the firewall changes themselves.
A ban also closes the connections the address already holds. A firewall rule only stops an address’s new packets: the connections it opened before stay open, and your web server keeps a worker waiting on each until its own timeout — 300 seconds on a stock Debian Apache. An address banned while it holds hundreds of connections would take every worker with it, and your sites would go down while the server is fine. So the warden closes them in the kernel the moment the ban lands, the way ss -K does, and the journal records how many (connections-closed). Lowering the web server’s timeout is still worth doing: see Security checklist.
A throttle does not cut an address off: it limits new connections to your web ports (2 a second by default), which is what slows an aggressive crawler without blocking a heavy but legitimate client.
The local policy
/etc/demerzel-warden/warden.json is owned by root, in a directory only root can write, and cannot be written by the agent or by Demerzel. (Before v0.20.2 it lived in /etc/demerzel/, the agent’s own directory; reinstalling the warden moves it.) Neither can raise a cap in it. A field you leave out keeps its default; an unknown field is refused, so a typo cannot silently drop a cap.
{
"enforce": true,
"rules": { "S1-secret-probe": "*" },
"allow": ["203.0.113.0/24", { "address": "198.51.100.4", "note": "a client's integration server" }],
"throttle_rate_per_second": 2,
"operator_grace": "24h",
"ban_ladder": ["1h", "24h", "168h", "672h"],
"max_ttl": "672h",
"max_bans_per_minute": 30,
"max_set_size": 10000,
"max_traffic_share": 0.1
}
The warden picks up a change within a second. If an edit breaks the file, the last good policy stays in force and demerzel-warden status says what is wrong.
Every action is evidence
The warden records every decision and what came of it — applied, extended, refused and why, shadowed, expired, lifted by an allow — in /var/lib/demerzel-warden/journal.jsonl. The agent can read it but never write it, and reports it to Demerzel: the server’s Security tab shows the warden’s mode and what it did lately, an address’s dossier shows every block it got, and an incident shows what was actually blocked while it happened.
Demerzel raises an Action when the warden cannot do what you enabled it to: blocking frozen by a cap (critical), the warden not running, its policy file broken, or the agent unable to hand it decisions. It raises one too when a rule is demoted, and when the warden spared an address too big a part of your traffic to block without you.