diff --git a/02-DECISIONS/0169-the-firewall-seat-serves-its-verbs.md b/02-DECISIONS/0169-the-firewall-seat-serves-its-verbs.md new file mode 100644 index 0000000..073eeff --- /dev/null +++ b/02-DECISIONS/0169-the-firewall-seat-serves-its-verbs.md @@ -0,0 +1,84 @@ +--- +topic: the mesh +status: accepted +date: 2026-10-02 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md +--- + +# 169. The firewall seat serves its verbs, and a foreign rule set is removed through one of them + +## Context + +[ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md) made the mesh say truthfully +what filters a converged machine, and left the removal of what it did not write to the operator's +hand. The first time that hand was needed — two machines, five rule sets a predecessor and a +retired front end had left — there was no mesh way to lend it: the packet filter is a seat +([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)), a seat's +holder serves its verbs ([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), +[ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)), and the +firewall seat declared none. The only remaining path was a shell on the machine, which is the path +the mesh exists to replace, and which the operator's own tooling rightly refused to an agent. + +A seat's verbs are the contract every holder implements, whatever filter it speaks. What a person +asks a machine's packet filter is the same whether nftables, a front end or a legacy filter answers: +what are the rules, reload the mesh's own, remove this thing the mesh did not write. What differs by +filter is the holder's own business and may be its own tools beside the seat's. + +## Decision + +**1. The `node-packet-filter` seat serves three verbs**, and a module that claims it serves all +three or is refused the claim, as with every seat: + +- `rules` — the packet filter as the machine enforces it now: the nftables ruleset, and the legacy + filter's listings where that tool exists; narrowed to one table or chain when asked. Read-only. +- `reload` — load the mesh's own filter again from the file the mesh writes, and answer with the + mesh's table as loaded. The holder's own act on the holder's own rules. +- `remove` — remove one rule set the mesh did not write, named exactly as the host reports it under + ADR 0168 (`chain HAL-MESH-ONLY (iptables-legacy)`, `table ip6 filter, chain DOCKER-USER`), and + answer with what was done. It refuses the mesh's own tables, the container runtime's own chains, + a built-in chain other than the runtime's user chain, and any chain of a found firewall that is + in force. The runtime's user chain is emptied back to its one return; another chain loses the + jumps into it, is flushed and deleted; a table of the machine's own is deleted whole. Each is an + operator's act, by name, on one thing the mesh reported — never a flush, never a rule the mesh + itself marked. + +**2. A holder may serve its own tools beside the seat's.** The nftables module keeps its reading of +the mesh's table as its own tool, and a holder speaking a filter with specifics of its own may add +tools for them; the seat's three are what every holder owes. + +**3. A container may ask for a capability.** Serving `remove` and `reload` needs the machine's +network namespace and the right to change its packet filter; a holder's runtime declares +`capabilities: ["NET_ADMIN"]` on its container and runs on the machine's network. The host grants +exactly the capabilities declared, names them in the container's spec so a change recreates it, and +refuses a name that is not a capability's. A privileged container stays undeclarable. + +**4. ADR 0168's "by hand" is read as "by the operator, through the seat".** Removing what the mesh +reports as *other* is still the operator's act and is still never the mesh's own doing; the verb is +how the act reaches the machine, recorded on the bus like every other, instead of a shell. + +## Consequences + +- The seat's row gains the three verbs; a mesh that already runs widens its row at the next + controller start. The nftables module claims them and gains a runtime — a tool server with the + packet filter's tools in its image, on the machine's network, with `NET_ADMIN`. +- The host's container vocabulary grows by `capabilities`; an older host refuses a declaration that + carries it, so the host rolls before the module. +- The two machines of this mesh that ADR 0168 found not filtered by the mesh alone are cleaned + through `remove`, and read *the mesh alone* afterwards; `status` returns to well without a hand on + either machine. + +## How this is checked + +| Rule | Checked by | +|---|---| +| The seat declares the three verbs; a claim that serves fewer is refused by name | the catalogue's seat tests | +| `remove` refuses the mesh's tables, the runtime's chains, a built-in chain and an active front end's chains, and removes a user chain with its jumps, empties the user chain, deletes an own table | the module's tests over a fake command runner, with the shapes the host reported live | +| A container's capabilities reach the runtime and its spec; an unknown name is refused | host tests | +| Live | `node-packet-filter.remove@` on the home server and the control node; `node show` reads *the mesh alone* on both; `status` is well | + +## References + +- [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) +- [Design 33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md), [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index ee87d97..6da6144 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -180,6 +180,7 @@ python3 00-META/checks/index.py fail if stale - **0167** — [A membership carries what its module receives, and who the mesh is](0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md) - **0168** — [A converged machine is filtered by the mesh alone, and the host says what else refuses](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md) - **0169** — [A machine joins through the tunnel, and the bus is never public](0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md) +- **0169** — [The firewall seat serves its verbs, and a foreign rule set is removed through one of them](0169-the-firewall-seat-serves-its-verbs.md) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/08-connectivity.md b/03-DESIGN/01-to-be/08-connectivity.md index 21fb8a9..4a15fb6 100644 --- a/03-DESIGN/01-to-be/08-connectivity.md +++ b/03-DESIGN/01-to-be/08-connectivity.md @@ -9,6 +9,7 @@ code: - mesh-host internal/apply (the service that reflects a rule set) updated: 2026-10-02 decisions: + - 02-DECISIONS/0169-the-firewall-seat-serves-its-verbs.md - 02-DECISIONS/0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md - 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md - 02-DECISIONS/0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md @@ -766,6 +767,11 @@ tests over a fixture report check the recording, the preview's fates, the status predicate. Live: the home server's record names the predecessor's chain as *other* and `status` names the machine until the chain is removed by hand. +*2026-10-02, [ADR 0169](../../02-DECISIONS/0169-the-firewall-seat-serves-its-verbs.md):* removing what +the host reports as *other* is reached through the packet filter seat's `remove` verb, an operator's act +by name on the bus; the seat also serves `rules` and `reload`, and its holder's runtime declares the +`NET_ADMIN` capability on the machine's network. See design 33. + ## 5 — Certificates **Two authorities, kept separate on purpose.** diff --git a/03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md b/03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md index f04437e..493b7c7 100644 --- a/03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md +++ b/03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md @@ -2,8 +2,9 @@ layer: to-be status: implemented code: [mesh-controller, mesh-tools] -updated: 2026-10-01 +updated: 2026-10-02 decisions: + - 02-DECISIONS/0169-the-firewall-seat-serves-its-verbs.md - 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md - 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md - 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md @@ -169,6 +170,18 @@ either way. What stays as designed and not built: which verbs any *other* seat serves, and §3 for module-declared seats' schemas beyond the names their manifests already list. +## The firewall seat's verbs, 2026-10-02 + +[ADR 0169](../../02-DECISIONS/0169-the-firewall-seat-serves-its-verbs.md). The first node-scoped seat +to carry verbs: `node-packet-filter` serves `rules` (the filter as the machine enforces it, nftables +and legacy), `reload` (the mesh's own filter from its file) and `remove` (one rule set the mesh did +not write, named as the host reports it under ADR 0168; refusing the mesh's tables, the runtime's +own chains, a built-in chain and an active found firewall's). Every holder serves all three; the +nftables module does so from a runtime on the machine's network with `NET_ADMIN`, which is the first +container to declare a capability. Removing a predecessor's rule set is an operator's act reached +through the seat, recorded on the bus, instead of a shell on the machine. *How it is checked:* ADR +0169's table. + ## What this does not settle - Which verbs each seat should serve. That is a decision per seat, and the reason to do it slowly: a