From bff32e3370526d2f30e4b57d5cabdef0844c2ea8 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 28 Sep 2026 20:50:35 +0200 Subject: [PATCH] Issue 136: a module may name a program the machine does not have, and everything reports success --- .../00-report.md | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 04-ISSUES/136-a-module-may-name-a-program-the-machine-does-not-have/00-report.md diff --git a/04-ISSUES/136-a-module-may-name-a-program-the-machine-does-not-have/00-report.md b/04-ISSUES/136-a-module-may-name-a-program-the-machine-does-not-have/00-report.md new file mode 100644 index 0000000..1d4638d --- /dev/null +++ b/04-ISSUES/136-a-module-may-name-a-program-the-machine-does-not-have/00-report.md @@ -0,0 +1,92 @@ +--- +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? -- 2.54.0