--- status: resolved opened: 2026-09-28 located-in: [mesh-catalog modules/fail2ban] fixed-by: mesh-catalog — the intrusion-prevention module bans through an action it ships itself, already in use on every machine, instead of naming a firewall front-end two of them do not have. The instance is closed; the class in "What is still true" is not. amended-design: --- # 136 — A module may name a program the machine does not have, and everything reports success ## What was observed Two machines were given the intrusion-prevention module on 2026-09-28. Both refused to start it: ``` ERROR Failed during configuration: Have not found any log file for 'recidive' jail. ERROR Async configuration of server failed fail2ban.service: Main process exited, code=exited, status=255/EXCEPTION ``` The jail that bans whoever keeps coming back reads the service's *own* log, and the service checks every jail's log file while it configures itself — before it has created that log. The module declared the jail and shipped the rotation for that log, and never declared the log. On the two machines where it had run for years the file was simply there, so nothing had ever noticed. That failure was loud. Fixing it uncovered a second one in the same module that is not. The module's defaults named `ufw` as the way to ban an address. Two of these four machines have no `ufw` — they filter with nftables — and nothing checks that until an address is banned. Asked to ban a documentation address on such a machine, the service accepted the instruction, counted it, ran the command, and wrote this to a log nobody reads: ``` ERROR ... -- stderr: '/bin/sh: line 5: ufw: command not found' ERROR ... -- returned 127 ERROR Failed to execute ban jail 'sshd' action 'ufw' ... Error banning 192.0.2.99 ``` No rule existed afterwards. Throughout, the unit was `active`, the module was applied, and the machine's report said so. **A machine had been added to the mesh's intrusion prevention, reported as protected, and was banning nobody.** ## Why it matters beyond this instance **The two faults are the same mistake with opposite symptoms.** Both are the module assuming something about the machine — a file that happens to exist, a program that happens to be installed. One stopped the service, which anybody notices. The other left it running and empty, which nobody does. A mesh that only catches the loud one is a mesh whose coverage is unknown. **"The unit is running" was taken for "the module is doing its job".** That is the only health a service resource has. It is the right answer for most modules and it is silent for any module whose work happens later, on an event — a ban, a renewal, a backup, a notification. The report cannot distinguish "protecting this machine" from "installed and inert". **And it is exactly the naming rule, one level down.** [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) says a definition names no node, no mesh and no host path, because the same definition has to raise a different mesh. `ufw` is not a node name, but it is the same class of assumption: a value the module cannot know, true on some machines and false on others, written as though it were a constant. The module already knew how to do better a few lines away — the mesh's own address range is named there as something the machine fills in. ## What was done The module declares the log its own jail reads, created once and never touched again, since what grows in it is the service's and the rotation the module already ships is what keeps it small. And it bans through the action it ships itself, which every machine here can run, which was already in use by the other jail on all four, and which covers a container's published port as well as the host's own. All four machines now run it, with both jails, and a ban lands on each — verified by banning and unbanning a documentation address on every one. ## What is still true **Nothing would have caught either fault before it shipped.** The control plane reads a manifest, not a machine; `ufw` and `/var/log/…` are strings in a file it has no way to evaluate. The host could in principle be asked whether a declared program exists, but no resource says "this file names a command that must be there", so there is nothing to check. **Two machines' bans from before this are stale rules in the old front-end**, which the service no longer knows about and will never lift. They reject two addresses for ever. Harmless, and a reminder that changing how a module enforces something leaves what it already enforced behind. ## Open questions - What does a service resource's health mean for a module whose work is event-driven? A unit being active is the weakest claim available, and four of this mesh's modules are of that kind. - Should a declaration be able to say that a resource depends on a program, so the machine can refuse what it cannot carry out rather than reporting success? - Where should the packet filter a module bans through come from — the module's own choice, as now, or the seat that owns the machine's filtering?