Files
hq/02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md
T

9.8 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
the mesh accepted 2026-10-02 jochen false 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md

179. The intrusion seat serves its verbs, a container may log to the journal, and every door declares its jail

Context

Read on the control node on 2026-10-02, the day the machines were confirmed filtered by the mesh alone (ADR 0168): the intrusion prevention watched one door. Its two jails read the ssh daemon's journal and its own log, banned five failures in ten minutes for ten minutes, and in a day had seen twelve thousand failed logins from three hundred addresses and banned none of the busiest, which paced themselves at one try every ten minutes. The mail submission port took a hundred and sixty password guesses in the same day from thirty-eight addresses with no jail reading it at all; the forge and the public proxy had no jail either, and the proxy logged nothing a jail could read. Nobody could see the jails without a shell: the module's three tools existed in code and were served by nothing, and the seat it holds declared no verbs.

Three things were missing and they are three shapes the mesh already has. The packet filter's seat serves verbs every holder owes (ADR 0170); the intrusion seat serves none. A module's listens compose into the machine's filter, and to-be 31 says a module's jails compose into the machine's intrusion prevention the same way — the controller composes them, and no module declares one. And a jail reads a log; a container's output goes to a file of the runtime's own under a path that changes when the container is recreated, which is why no jail could read the mail front end, the forge or the proxy, however they logged.

Decision

1. The node-intrusion-prevention seat serves four verbs, and a module that claims it serves all four or is refused the claim, as with every seat:

  • status — every jail with what it watches, how many addresses it is counting failures against and holding now, and the totals since it started; one jail's detail when named. Read-only.
  • banned — every address banned now, with the jail holding it, when it was banned and when the ban ends. Read-only.
  • ban — ban one address in one jail now, for that jail's ban time. An operator's act on the live ban list, which the mesh composes the rules for and never writes itself.
  • unban — let one address go, from one jail or from every jail.

A holder may serve its own tools beside these; the fail2ban module reads one jail's effective settings as its own.

2. A container may log to the journal. logging: journald on a container has the host run it with the journal as its log driver; the journal keeps the container's name on every line, and docker logs keeps working. Where a container logs is part of its spec, so moving it recreates the container, and the only place besides the runtime's own file is the journal: a machine's intrusion prevention reads the journal already, for the ssh daemon, and a container that logs there is read the same way, by the container's name, whatever the container is called by the runtime this time.

3. A module with a door declares its jail, and the holder composes them. What to-be 31 designed is now the rule: a module whose service authenticates from outside — the mail front end, the forge, the public proxy — declares in its manifest what a failed attempt looks like in its log and how to ban on it, naming no node and no path; the module that holds the intrusion seat declares where the composed jails and filters land, and the mesh writes them on every machine that runs both. A machine not running the module has no such jail. The holder restarts its daemon on the composed file.

4. The base is strict, and the mesh's own range is never banned. Three failures in a day ban for a day, on every jail unless the jail says otherwise; banned twice in two weeks, by any jail, is banned for four. The attackers this mesh sees pace themselves under any ten-minute window; a day's window counts them. A person who mistypes three times from one address is out for a day from that address, and never from a machine of the mesh, whose range stays in the never-banned list the module has carried since ADR 0112. The operator chose this knowing it.

5. The proxy says a refused name in its log. A request for a name this mesh does not serve, from outside, is what a scanner does; the proxy already logged a certificate refused for such a name, and now logs the plain request too, with the asking address last, as its own jail's filter expects it.

Consequences

  • The seat's row gains four verbs; a mesh that already runs widens its row at the next controller start. The fail2ban module claims them and gains a runtime — a tool server whose image carries the fail2ban client, with the daemon's socket shared in from the machine, and nothing else of the machine. The daemon stays the machine's; what runs in the container is only the client.
  • That runtime is the shape the catalogue has today, and it is on its way out. ADR 0175, accepted the same day as this record, replaces a tool container per module with one tool runtime per node on the host side, taking each module's tools as a bundle. Nothing here depends on the container: the verbs, the client that speaks to the daemon over its socket, and the jails are the same code under either. This module converts with the packet filter's, whose runtime that record names, and the socket it needs becomes the node runtime's to reach rather than a mount of its own.
  • The host's container vocabulary grows by logging; an older host refuses a declaration that carries it, so the host rolls before the modules. Three containers are recreated once, when their modules are pushed with the field: the mail front end, the forge and the proxy — each a moment's outage.
  • The fail2ban module declares where jails compose (jailing) and the directory the filters go in; the mail, forge and proxy modules each declare one jail reading the journal by their container's name. The composed jail file is the one resource the daemon restarts on when a module arrives or leaves a machine.
  • The two base jails and the composed ones take the day's window; the ssh jail's ten minutes are gone. An address banned on the first day of this record stays banned for the day.
  • The module's three old tools, served by nothing, are replaced by the seat's four verbs and one own tool; fail2ban_status as a name is gone.

How this is checked

Rule Checked by
The seat declares the four verbs; a claim that serves fewer is refused by name the catalogue's seat tests
status, banned, ban and unban read and steer the daemon through its client, with the shapes fail2ban 1.1.0 printed live; a non-address and a non-name are refused before anything runs the module's tests over a fake command runner
A container's logging reaches the runtime's arguments and its spec; a place other than the journal is refused host tests
A module's jails compose into the holder's file and a filter per jail, and the file is written empty when none is declared the controller's composition tests (to-be 31)
The proxy logs a refused name with the address last the proxy's tests
A jail's pattern names <HOST> once per shape, since two is a duplicate capture group and costs the machine every ban the catalogue's manifest tests
Live done 2026-10-02: status and banned answered on both servers through the console; the proxy's jail counted seven refusals on the home server; a documentation address banned in the ssh jail came back with its end time and was released

Built and proven live, 2026-10-02

All five rules are in the mesh. The host carries logging; the controller's seat row carries the four verbs and the proxy says a refused name in its log; the fail2ban module holds the seat from a runtime with the daemon's socket shared in, composes the jails, and the mail front end, the forge and the proxy each declare one. Through the console on the control node: status listed five jails with what each watches, banned listed the nine the long jail holds, and a documentation address banned in the ssh jail came back with its ban's end time and was released again. On the home server the proxy's jail had counted seven refusals within minutes of starting.

One fault, found by the machine and not by a test. The proxy's pattern matched two shapes of refusal in one expression and so named <HOST> twice. fail2ban expands that placeholder into a named capture group; two of them is a duplicate group name, and the daemon refuses its whole configuration and exits — both servers kept no bans at all for about ten minutes, every jail and not the one at fault. The pattern is now one per shape. A manifest check refuses the mistake at merge time, naming what it would cost, which is the only reason this record can claim the rule rather than the instance.

References