From a9b8f415700009bfeb1c9f35b18fea2109b7d225 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 27 Sep 2026 02:51:40 +0200 Subject: [PATCH] =?UTF-8?q?Design=2025=20=C2=A74:=20the=20server=20verifie?= =?UTF-8?q?s=20no=20client=20certificate,=20and=20writes=20only=20accounts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two corrections of fact, both found by building the module's image and connecting to it as a host would. The first composed configuration said `verify: true`, which makes the server demand a *client* certificate — and nothing in the mesh presents one. A host pins this server's exact certificate and authenticates with the password the mesh minted, and so does a module's runtime. Every connection in the mesh would have died at the TLS handshake before any password was looked at, with an error that reads as a fault in the client. TLS is still required; verify only decides whether client certificates are checked. Mutual TLS is a later question and would need machinery the mesh does not have — a certificate per module per node. And §4 read as though the controller wrote the whole file. It writes the user list and nothing else: ports, TLS paths and a store directory belong to the container the module raises. The two files share one directory of necessity, because an absolute include path is resolved relative to the including file's own directory. The decision stands in both cases — accounts are composed, not called for, and passwords are minted and sealed. What changed is what the file says and who writes which half. --- 03-DESIGN/01-to-be/25-the-bus-on-nats.md | 29 ++++++++++++++++-- 03-DESIGN/01-to-be/28-building-the-bus.md | 37 ++++++++++++++++------- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/03-DESIGN/01-to-be/25-the-bus-on-nats.md b/03-DESIGN/01-to-be/25-the-bus-on-nats.md index 1052976..4338f1a 100644 --- a/03-DESIGN/01-to-be/25-the-bus-on-nats.md +++ b/03-DESIGN/01-to-be/25-the-bus-on-nats.md @@ -7,7 +7,7 @@ code: - mesh-tools src/broker-amqp.ts (to be replaced) - mesh-catalog modules/nats (to be written) - mesh-sdk src (the protocol's NATS binding, step 3) -updated: 2026-09-26 +updated: 2026-09-27 decisions: - 02-DECISIONS/0106-the-bus-is-nats.md - 02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md @@ -213,7 +213,32 @@ expresses this exactly, per subject, and better than a vhost could: - **A person's user** (§7) is a module-shaped user with permissions on the tool subjects it may invoke, issued and revoked by the controller like any account. -**Accounts are configuration, not API calls.** The controller composes the server's user list and +**The server does not verify client certificates, and TLS is still required.** *Revision, +2026-09-27, found by building the module's image and connecting to it as a host would.* The first +composed configuration said `verify: true`, which makes the server demand a **client** certificate — +and nothing in the mesh presents one. A host pins this server's exact certificate and authenticates +with the password the mesh minted ([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)), +and a module's runtime does the same. With it on, every connection in the mesh dies at the TLS +handshake before any password is looked at, and the error — "client didn't provide a certificate" — +reads as a fault in the client rather than in the bus's configuration. The `tls` block is what makes +TLS required; `verify` only decides whether client certificates are checked. What is given up is a +second factor the mesh has no machinery to issue or rotate — a certificate per module per node — and +what is kept is stronger than a name check in both directions: an exact pin outward, a password +scoped per user inward. **Mutual TLS is a later question and would need that machinery first.** + +**The mesh composes the accounts; the module composes its server.** *Revision, 2026-09-27, while +building the composition.* An earlier reading of the paragraph below had the controller writing the +whole file. It writes only the user list. A server's ports, its TLS paths and its store directory +are properties of the container the module raises — they live in its image and its mounts and change +when it does — so the module declares its own configuration and `include`s the mesh's half. A +controller that wrote the whole file would have to be kept in step with a Dockerfile it never sees, +and a module could not change its own image without the mesh agreeing. Asking for the user list is +not enough to receive it: the file holds every user's password hash, so the claim on `mesh-broker` is +what authorises it. And the two files share one directory of necessity — an absolute include path is +resolved relative to the including file's own directory, so a server given one from elsewhere looks +for it underneath that directory and refuses to start. + +**Accounts are configuration, not API calls.** The controller composes the mesh's user list and its permissions into a file the host declares. **How that file reaches the running server is §5's, not this one's** — revision, first review: an earlier draft said "reloads" and cited a precedent that does not apply to a container (see §5). No management API, no credential travelling through a diff --git a/03-DESIGN/01-to-be/28-building-the-bus.md b/03-DESIGN/01-to-be/28-building-the-bus.md index 1421c79..d698032 100644 --- a/03-DESIGN/01-to-be/28-building-the-bus.md +++ b/03-DESIGN/01-to-be/28-building-the-bus.md @@ -178,23 +178,38 @@ paper is wrong until there is a second mesh to find out. **Out, and what each needs.** - *Delivery.* Resolution already prepends `file` resources whose content comes from the - rendering — a certificate does exactly this, and refuses when a module asks for one and none - was issued. The composed configuration is the same shape, and the open question is what the - module *declares* in order to receive it, which is design 29's ground rather than this - document's: a field naming where it wants the file, or nothing at all because the module - holding `mesh-broker` is the one that gets it. The server's own values — ports, TLS paths, - store directory — are constants of the module's own resources today and would have to be read - from one place rather than two. + **Delivery is in, and it settled what a module declares.** The mesh writes the *accounts* and + the module owns its *server*. The alternative was a manifest field enumerating ports, TLS + paths and a store directory so the controller could write a whole configuration — wrong, + because those are properties of the container the module raises and the controller would have + to be kept in step with a Dockerfile it never sees. So a module declares its own configuration + as a file resource and `bus-users` names where the mesh's half goes beside it; **asking is not + enough to receive it**, because that file holds every user's password hash, so the claim on + `mesh-broker` is what authorises it. - *Minting, and it is transport-coupled.* A password is minted at enrolment and at assignment, + Two things a running server changed. **An absolute include path is resolved relative to the + including file's directory** — `include /etc/nats/accounts.conf` from another directory makes + the server look for it *under* that directory and refuse to start — so both files share one. + And **`verify: true` was refusing every connection in the mesh**: it makes the server demand a + *client* certificate, and nothing in the mesh presents one — a host pins this server's exact + certificate and authenticates with the password the mesh minted ([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md), + design 25 §4). Every connection would have died at the TLS handshake before any password was + looked at, with an error that reads as a fault in the client. Removed; TLS is still required, + because the block is what requires it and `verify` only decides whether client certificates + are checked. **Design 25 §4 should say this**, and says nothing about it today. + + Also collected here: task 1.2's payoff, end to end against the module's own image — the user + list rewritten, the module noticing and reloading the server itself with no signal from + outside, and the connection the mesh already had still working afterwards. + + *Still out — minting, and it is transport-coupled.* A password is minted at enrolment and at assignment, and an enrolment reply carries exactly one. **A node on the old bus must not be handed a credential for the new one**, so which bus a node is joining has to be a fact the controller holds before it can mint for both — the same switch the host's `Transport` is, from the other end. - *Asserting on start.* The streams, the controller's consumers and every node's are defined - and idempotent; nothing calls them from a start path yet. + *Still out — asserting on start.* The streams, the controller's consumers and every node's + are defined and idempotent; nothing calls them from a start path yet. **People are not in the list**, deliberately: the account model is built and `operator issue` is not (4.4), so there is nobody to derive. Left empty rather than guessed at.