A node's intrusion filter should be composed from its assigned modules, like its firewall (the Filtering mechanism): a service module (postgres, mssql, mailu) declares its jail in its manifest (filter + stanza, no node/path per ADR 0112), and the mesh writes the jails of a node's modules into the fail2ban holder's jail.d. The base (sshd, recidive, ignoreip=mesh-range) stays the fail2ban module's. Records the model after novox's HAL per-module jails were lost as dangling symlinks; the ignoreip is now safe on disk, the service jails need this to be restored.
3.9 KiB
layer, status, code, updated, decisions
| layer | status | code | updated | decisions | |
|---|---|---|---|---|---|
| to-be | proposed | 2026-09-27 |
|
31 — A module declares its fail2ban jail, and the mesh composes them per node
A node's intrusion filter should be composed from the modules it runs, the same way its firewall
is. The mesh already derives a node's nftables ruleset from every assigned module's listens and
guards (the Filtering mechanism). fail2ban is the same shape and is not modelled: a module that
runs an authenticating service — postgres, mssql, mailu — has a jail (a filter that reads its log
and a jail stanza that bans on it), and which jails a node's fail2ban runs should be exactly the
jails of the modules assigned to that node.
The predecessor did this with per-module files: postgres shipped postgres-auth.conf, mssql
shipped mssql-auth.conf, mailu shipped mailu.conf, and the node's fail2ban read whichever were
present. When HAL retired on novox those became dangling symlinks — fail2ban ran the jails only from
memory, and a restart would have dropped them. The base was salvaged (the fail2ban module now ships
sshd, recidive, and the ignoreip that spares the mesh's own range), but the service jails
are gone, because no nox module declares one yet.
The shape
- A module declares its jail in its manifest, naming no node and no path (ADR 0112): the filter
(the failregex, or a stock filter it uses) and the jail stanza (port, logpath, maxretry, bantime).
The
postgresmodule says what a postgres brute-force looks like and how to ban it; it does not say on which machine, because it does not know. - The mesh composes them per node. For each node, the jails of its assigned modules are gathered
and written into the fail2ban holder's
jail.d/(and filters intofilter.d/), exactly aslistens/guardsare gathered into the node's firewall. So a node running postgres gets the postgres jail; a node not running it does not. Thenode-intrusion-preventionholder receives them the way a provider receives its consumers' contributions. - The base stays the fail2ban module's:
sshd,recidive, and theignoreipnaming${machine:mesh-range}so a tunnel peer is never banned.
Why this, and not the module writing the file itself
A module could declare a file resource at /etc/fail2ban/jail.d/<x>.conf directly. Rejected: the
path is the fail2ban holder's to own (one module owns jail.d, as one module owns the firewall
table), the jail's logpath and defaults want the mesh's composition (the ignoreip, the ban action
the node uses), and two modules writing into one directory is the collision the seat/holder model
exists to prevent. The module declares what its jail is; the holder's composition decides how it
lands — the same split as listens (the module says the port; the mesh says the rule).
Why now
fail2ban on novox currently runs the service jails from memory only; the next restart drops them
(the ignoreip is safe on disk, so the mesh-partition risk is closed, but postgres/mssql/mailu
auth-banning would be lost). This is the mechanism that restores them properly, and it is needed as
each of those modules migrates to the other nodes — ace running postgres should get the postgres
jail, composed from the postgres module's manifest, without anyone editing a node.
References
- ADR 0112 — a module
names no node or path; its jail is declared the same way its
listensare - mesh-controller
internal/catalogue/adoption.go(Filtering— the firewall composition this mirrors),internal/catalogue/manifest.go(Listens/Guards, the fields a jail field sits beside) - mesh-catalog
modules/fail2ban(the base: sshd, recidive, ignoreip); the service modules (postgres,mssql,mailu) that will declare jails