ADR 0186: a ban list never holds a neighbour, and the mesh's own bans are its own wherever they hang; design 31

This commit is contained in:
2026-10-02 18:42:16 +02:00
parent a9f91fdd0c
commit d4a2f99ab5
3 changed files with 93 additions and 0 deletions
@@ -0,0 +1,81 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md
---
# 186. A ban list never holds a neighbour, and the mesh's own bans are its own wherever they hang
## Context
[ADR 0179](0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md) gave the
public proxy a jail. Within the hour the home server's ban list held `192.168.1.1` — the house's own
router. The router reflects local traffic, so every client in the building reaches that machine as
the gateway's address; one local request for a name the mesh does not serve, three times in a day,
and the whole house is refused by the machine it was asking. The jails inherited an `ignoreip` of
the loopback and the mesh's own range, which was right when the only jail read the ssh daemon and
the only clients were the mesh's; a jail on a public front door sees the neighbours too.
The same jail broke the other half of [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md).
The home server began reading *NOT the mesh alone: 1 rule set the mesh did not write refuses traffic
here*, and the rule set named was the mesh's own ban chain, written by the mesh's own intrusion
prevention minutes earlier. The host's reader of the legacy filter required every path into a chain
of refusals to come from a built-in chain whose policy accepts, before it would call that chain a
ban. On that machine the chain hangs off the container runtime's user chain as well as the input
chain, and the runtime had set the forward policy to DROP — so the mesh reported its own work as a
foreigner's, on the one machine where the group's exit condition was supposed to hold.
Both faults are one mistake in two places: a rule written about the public internet, applied to
everything that arrives.
## Decision
**1. A ban list never holds a neighbour.** The jails the mesh composes never ban a source on a
private range — the mesh's own range, which was already named rather than written
([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)), and every address space
reserved for private use beside it, in both families. A machine behind a router that reflects local
traffic sees its whole building as one address; a ban there is a self-inflicted outage, and the
sources worth banning are not on those ranges in the first place.
**2. The mesh's own bans are its own wherever they hang.** A chain of refusals is a ban list when
every refusal names the sources it refuses and the chain accepts nothing — the rule the host already
applied to the packet filter's own tables, now applied to the legacy filter too, and nothing more.
The policy of the chains that jump into it says nothing about what it is: that policy is already
classified where it belongs, as the container runtime's, and requiring it here counted it twice.
**3. A chain that accepts anything is still not a ban.** That is what keeps a predecessor's
allow-these-and-drop-the-rest chain classified as something an operator must look at, which is the
distinction [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md) exists to draw.
## Consequences
- The composed jails gain the private ranges in their never-ban list. An address already banned
stays banned until it is released; the house's router was released by hand the moment it was found.
- The home server reads *the mesh alone* again, which is group 7's exit condition and was false for
about an hour.
- A machine whose apply fails for an unrelated reason does not revisit its found firewall's record
at all — the step runs only after a clean apply ([ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)).
The home server's record therefore still reads *retired by the mesh* although the front end is
uninstalled, and will correct itself once that machine's own stuck module is fixed. It is a stale
record, not a wrong machine.
- The record number the front end's removal was given moved under it: another session took 0175
while that record was in review, and it is now
[ADR 0180](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md). The citations
the host and the control plane print were pointing at an unrelated record and are corrected here.
## How this is checked
| Rule | Checked by |
|---|---|
| A private source is never banned | the module's jail configuration, read back by `fail2ban.fail2ban_settings` on a machine |
| The mesh's own ban chain reads as a ban behind a dropping forward policy | a host test over the home server's own captured rule set |
| A chain that accepts anything is not a ban | a host test |
| Live | the home server reads *the mesh alone*; no ban held on either machine is a private address |
## References
- [ADR 0179](0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md), [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0180](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md)
- [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md), [Design 31 — A module declares its fail2ban jail](../03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md)
+1
View File
@@ -186,6 +186,7 @@ python3 00-META/checks/index.py fail if stale
- **0180** — [The found front end is uninstalled once a machine is converged](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md)
- **0184** — [A service the mesh asked to run is still running a moment later](0184-a-service-the-mesh-asked-to-run-is-still-running-a-moment-later.md)
- **0185** — [A control plane behind its seat's row serves what it can](0185-a-control-plane-behind-its-seats-row-serves-what-it-can.md)
- **0186** — [A ban list never holds a neighbour, and the mesh's own bans are its own wherever they hang](0186-a-ban-list-never-holds-a-neighbour.md)
### Its tiers, from the bottom up
@@ -9,6 +9,7 @@ updated: 2026-10-02
decisions:
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md
- 02-DECISIONS/0186-a-ban-list-never-holds-a-neighbour.md
---
# 31 — A module declares its fail2ban jail, and the mesh composes them per node
@@ -92,3 +93,13 @@ the daemon's, and both are read through the console.
*How it is checked:* ADR 0179's table.
## What the first jails taught, 2026-10-02
[ADR 0186](../../02-DECISIONS/0186-a-ban-list-never-holds-a-neighbour.md). Within an hour of the
first public jail the home server had banned the house's own router: the router reflects local
traffic, so every client in the building arrives as the gateway's address. The never-ban list now
holds every private range as well as the mesh's own. And the mesh read its own ban chain as a
foreign rule set on that machine, because the chain hangs off the container runtime's user chain and
that machine's forward policy is the runtime's DROP — the reader now calls a chain of source-named
refusals a ban wherever it hangs, as it already did for the packet filter's own tables.