ADR 0100 after re-review: the bus and registry stay reachable for enrolment; the mesh guards the store in a table that only refuses; a machine in use defined; the flip refuses while a found container is held; held containers and returning to adopted spelled out

This commit is contained in:
2026-09-22 16:32:37 +02:00
parent 02c40bcab4
commit f3152d827f
7 changed files with 112 additions and 74 deletions
@@ -76,9 +76,10 @@ authoritative, and every declaration it sends says whether the node is adopted a
have been taken on it. A node stays adopted until the operator converges it. An adopted node is
said to be adopted wherever the mesh reports a node's state.
**A converged genesis refuses a machine in use.** Raised without saying adopted on a machine with
an active firewall or services listening that are not the mesh's, genesis refuses and names what
it found — a forgotten flag must not close a working machine.
**A converged genesis refuses a machine in use.** A machine is in use when a container is running
on it or a port is listening that is neither ssh nor one of the operating system's own services.
Raised without saying adopted on such a machine, genesis refuses and names what it found — a
forgotten flag must not close a working machine.
**Before a node is adopted, its predecessor's control is stopped by the operator** — the daemons
that write its configuration. Its services keep running on what they have.
@@ -92,23 +93,34 @@ file and a found container as they are, records the file's original content befo
happens to it, and reports each as held. Assigning a module on an adopted node prepares it: what
the module declares that is not found is created; what is found is held. **Taking a module** on a
node is its cutover — the operator's act, done when that module's data has moved — and from then
on the module's resources converge on that node like any other. A held file that changes while
held is reported as changed by something else, not reverted: that is how a predecessor still
writing is caught. A held file whose module is unassigned is left where it is, never removed.
on the module's resources converge on that node like any other. What is held is never removed,
even when its module is unassigned, and a held file or container that changes while held — a file
rewritten, a container stopped or replaced — is reported as changed by something else, not reverted
or restarted: that is how a predecessor still writing is caught.
**The firewall found on the machine stays in force.** The mesh loads no table of its own on an
adopted node — neither genesis's base ruleset nor the filter module's derived one. What the mesh
needs is declared as **openings**: a resource that says a port is reachable, from where, on the
incoming path or the forwarded path — a published container port is forwarded. The host converges
an opening through the found firewall in that firewall's own terms, marks it as the mesh's, and
removes only what it marked; it re-checks each opening on every reconcile, so a reload or a reboot
of the found firewall does not lose it. An opening is a state, not a command, which is what lets it
travel over the link. The host reports which firewall it found, and a machine with a kind no host
speaks is refused adoption rather than adopted with nothing protecting the mesh's ports.
**The firewall found on the machine stays in force.** The mesh loads no table on an adopted node
that drops by default or that accepts — neither genesis's base ruleset nor the filter module's
derived one. What the mesh needs reachable is declared as **openings**: a resource that says a port
is reachable, from where, on the incoming path or the forwarded path — a published container port is
forwarded. The controller derives them from the same inputs as the filter: the `listens` of the
modules assigned there, the private network's hub port, and the foundation's ports. The host
converges an opening through the found firewall in that firewall's own terms, marks it as the
mesh's, and removes only what it marked; it re-checks each opening on every reconcile, so a reload
or a reboot of the found firewall does not lose it for longer than one reconcile. An opening is a
state, not a command, which is what lets it travel over the link. The host reports which firewall
it found. A machine with no firewall needs no openings; a machine with a kind no host speaks is
refused adoption.
**The mesh protects its own ports itself.** On an adopted node its foundation's ports get openings
from the private network and marked refusals from anywhere else, on the forwarded path — so the
store is unreachable from outside whether or not the found firewall filters forwarded traffic.
**The mesh guards its own ports itself, in a table of its own that only refuses.** It accepts by
default and holds nothing but refusals, so it cannot close anything the machine serves — a drop in
any chain is final and an accept in it would change nothing — and it is the mesh's, so the found
firewall reloading does not touch it. It refuses the store's port and the broker's management port
from anywhere but the private network, matched on the port the packet was sent to, before the
container runtime redirects it. The bus and the registry stay reachable from anywhere, as a node
enrols over the bus and pulls from the registry before it has a private-network address
([ADR 0088](0088-the-foundation-filters-before-anything-listens.md)); so does the private network's
hub port. The store is unreachable from outside whatever the found firewall does, and on a machine
with none.
**The foundation's ports are the node's.** Every port the foundation binds is an input to genesis,
checked free before anything is raised, refused with the name of what holds it. The ports given
@@ -118,13 +130,16 @@ filter, the base ruleset, the private network's endpoint, and the addresses cons
The private network's address range must not overlap a tunnel the predecessor still runs; genesis
checks that too.
**Converging a node is one act, previewed.** The preview lists what is reachable on the machine now
— every listening socket and every published container port — and for each whether an assigned
module declares it or it will close. The flip then takes every module not yet taken, loads the
mesh's derived filter, and retires the found firewall by disabling it, never by flushing: the
container runtime's rules and the found firewall's own configuration stay on disk. Returning a
converged node to adopted re-enables the firewall it retired. A node converges when its migration
is done; the mesh is migrated when every node has converged.
**Converging a node is one act, previewed.** It refuses while an assigned module still holds a
found container: each service is taken on its own, when its data has moved, never by the flip. The
preview lists what is reachable on the machine now — every listening socket and every published
container port — and for each whether an assigned module declares it or it will close, and every
module the flip will take, with what each will replace. The flip then takes those modules, loads the
mesh's derived filter in place of its refusal-only table, and retires the found firewall by
disabling it, never by flushing: the container runtime's rules and the found firewall's own
configuration stay on disk. Returning a converged node to adopted unloads the derived filter,
restores the refusal-only table and enables the found firewall again; what was taken stays taken. A
node converges when its migration is done; the mesh is migrated when every node has converged.
**The order is the operator's:** the control-node first, adopted, its modules assigned and taken
one at a time; then each other machine, adopted, migrated, converged in turn. The predecessor's
@@ -144,7 +159,8 @@ open).
What got harder:
- **The mesh must speak a firewall it did not install**, on both the incoming and the forwarded
path. One kind is found on the machines measured; another is refused until a host speaks it.
path. One kind is found on the machines measured; another is refused until a host speaks it. The
mesh's own guard does not depend on it: that table is the mesh's.
- **The host gains a guard it did not have** — keep what you found — and its report must say
which files and containers it holds, or an adopted node reads as converged.
- **The declaration gains a node's mode and its taken modules**, and a resource, the opening.
@@ -154,7 +170,7 @@ What got harder:
nodes are adopted, so one left behind is visible.
- **This narrows [ADR 0088](0088-the-foundation-filters-before-anything-listens.md) for adopted
nodes**: the base ruleset is not loaded on a node raised adopted, and its duty — the store never
reachable from outside — passes to the mesh's own openings and refusals on the forwarded path.
reachable from outside — passes to a table of the mesh's that only refuses.
## How it is checked
@@ -163,29 +179,33 @@ port and denying the rest, a service container listening on that port under a na
module also uses, a file at a path that module declares, a stand-in for the predecessor's control
that would rewrite that file, and a container holding the registry's port. Then:
- **Genesis converged** on it refuses and names the firewall and the listener.
- **Genesis converged** on it refuses and names the running container and the listener.
- **Genesis adopted, with the registry's port held**, refuses and names the holder; with another
port given, the foundation comes up — and adopting the foundation as modules leaves it on that
port.
- **Nothing that serves changed**: the service is reachable from a second machine, the file is byte
for byte what it was, and the found firewall's rules differ only by rules marked as the mesh's.
- **The store is unreachable from outside** — probed from a machine off the private network — and
reachable over it.
- **The store is unreachable from outside** — probed from a machine off the private network, and
again after the found firewall is reloaded — and reachable over it; the bus is reachable from a
machine that has not yet enrolled.
- **The mesh works through the found firewall, and keeps working after it is reloaded and after the
machine reboots**: the second machine enrols, and the openings are there again.
- **A predecessor still writing is caught**: with the stand-in left running, the held file's change
is reported and not reverted.
- **Assigning prepares, taking cuts over**: the module assigned holds the found container and file;
taken, it replaces them and its port is opened.
- **Converging previews, then changes**: the preview names the service's port and a published port
no firewall rule mentions; after the flip the mesh's derived filter is loaded, the found firewall
is disabled with its configuration still on disk, the declared port is open and the undeclared one
closed. Returned to adopted, the found firewall is enabled again.
- **Converging previews, then changes**: it refuses while the service's module holds its found
container; once that module is taken, the preview names the service's port and a published port no
firewall rule mentions, and the modules it will take; after the flip the mesh's derived filter is
loaded, the found firewall is disabled with its configuration still on disk, the declared port is
open and the undeclared one closed. Returned to adopted, the found firewall is enabled again and
the derived filter is gone.
Unit tests hold the host to keeping a found file and container on an adopted node, converging them
once taken, never removing a held file, and reporting a held file that changed; genesis to refusing
a held port and a converged raise on a machine in use; the controller to carrying the mode and the
taken modules in every declaration.
once taken, never removing what it holds, and reporting a held file or container that changed;
genesis to refusing a held port and a converged raise on a machine in use; the controller to
carrying the mode and the taken modules in every declaration, deriving the openings, and refusing
a flip while a found container is held.
## References