Design 25 §4: the server verifies no client certificate, and writes only accounts

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.
This commit is contained in:
2026-09-27 02:51:40 +02:00
parent 8c91ba1cfa
commit a9b8f41570
2 changed files with 53 additions and 13 deletions
+27 -2
View File
@@ -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
+26 -11
View File
@@ -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.