diff --git a/03-DESIGN/01-to-be/16-module-coverage.md b/03-DESIGN/01-to-be/16-module-coverage.md index 12b794f..703d60f 100644 --- a/03-DESIGN/01-to-be/16-module-coverage.md +++ b/03-DESIGN/01-to-be/16-module-coverage.md @@ -104,6 +104,28 @@ what kind it is. Small, self-contained, and only interesting once something pres Modules published to a registry and consumed as libraries. This is a *build* output the mesh does not deliver to a node, so it may not belong here at all. +## What checking the coverage found + +Two faults, both surfaced by asking what a *real* node looks like rather than what a test does. +Neither is about the vocabulary; both are about the machinery under it. + +**A credential belonged to a machine, not to a module** +([`022`](../../04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md), +fixed). A node running three services against one database could not be planned at all — and on +the consumer's side did not refuse, it just gave two of the three no credential. Every scenario +written to date had one consumer per node, which is the natural shape of a small test and not the +shape of a machine. + +**A consumer still cannot build a connection string** +([`023`](../../04-ISSUES/023-a-consumer-cannot-build-a-connection-string/00-report.md), open). It +is given its own password in whatever shape its configuration needs, and the host, port and user +name are still out of reach: the user name is invented by the provisioner and recorded nowhere, +and the bound values sit in a JSON document that an application reading `KEY=value` cannot use. + +The asymmetry is worth stating, because it is backwards. **The secret is the hard case** — the +mesh must not be able to read it — and the secret is the part that now arrives. The host and port +are ordinary facts held in the clear, and they are the ones stuck. + ## What the survey found that is not about coverage **Declaration and reality had drifted in the system being replaced.** Several live provisions are diff --git a/04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md b/04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md index 78d970b..f62a987 100644 --- a/04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md +++ b/04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md @@ -1,8 +1,8 @@ --- -status: located +status: fixed opened: 2026-09-01 located-in: [mesh-control] -fixed-by: +fixed-by: mesh-control 0af3ea1 amended-design: --- @@ -17,6 +17,20 @@ anchor has 3 modules asking for "postgres-database" and they would share one credential: gitea, keycloak, umami ``` +**And that is only the loud half.** The consuming node does not refuse at all. Three modules +wanting one database produce **one** need: + +``` +modules=3 needs=1 + name=postgres-database from=anchor for=gitea +``` + +So the first module gets a credential, the other two get no file at all, and each starts and fails +to authenticate with nothing anywhere saying why — the shape of +[`021`](../021-a-consumer-on-the-providers-machine-is-given-no-credential/00-report.md), on a +different axis. The refusal that reads like a decision is on the provider; the silence is on the +consumer. + The refusal is correct about what it says. They *would* share one credential, and sharing one is worse than refusing — a login that opens three databases is not three credentials. But the arrangement being refused is the ordinary one. **The node this mesh exists to take over runs @@ -79,3 +93,21 @@ said so, because from the provisioner's side it created exactly what it was aske This crosses the control plane, the grant file naming, and every provisioner that names something after `Consumer`. It is not a local fix, and it is the last thing between the current state and a node that looks like a real one. + +## Fixed + +Needs fan out per consuming module in one place, after the resolution walk. The credential's key +gains the consuming module, the grant file is named after both halves of the consumer, needs are +matched by provision *and* module, and the provisioners name the role and the access key after the +module rather than the machine. The refusal is gone because there is nothing left to refuse. + +Existing credentials are discarded rather than backfilled: they cannot say which module they were +for, and one is remade and delivered to both ends on the next push, so it costs one rotation. + +A guard was added for PostgreSQL's 63-byte identifier limit, which truncates with a notice rather +than an error — two consumers whose role names agree that far would otherwise become one login, +which is this same fault at a length nobody would think to test. + +What it did **not** fix is [`023`](../023-a-consumer-cannot-build-a-connection-string/00-report.md): +a consumer now receives its own password and still cannot build a connection string, because the +user name is the provisioner's invention and the bound values cannot reach a configuration file. diff --git a/04-ISSUES/023-a-consumer-cannot-build-a-connection-string/00-report.md b/04-ISSUES/023-a-consumer-cannot-build-a-connection-string/00-report.md new file mode 100644 index 0000000..0bbb280 --- /dev/null +++ b/04-ISSUES/023-a-consumer-cannot-build-a-connection-string/00-report.md @@ -0,0 +1,79 @@ +--- +status: located +opened: 2026-09-01 +located-in: [mesh-control] +fixed-by: +amended-design: +--- + +# 023 — A consumer is given every part of a connection except the two it cannot invent + +## Symptom + +A module that requires a database is now given its password in whatever shape its configuration +needs ([`022`](../022-one-credential-per-node-per-provision-not-per-module/00-report.md) and the +sealed-placeholder work). It still cannot connect, because a password is not a connection. + +What it is given is a **binding**, as JSON: + +``` +provision postgres-database +from the node providing it +at that node's address on the private network +serves what the provider said a consumer must know — the port +``` + +What it needs, to write `KC_DB_URL` or `GITEA__database__USER`, is the host, the port, the +database name and **the user name**. Two of those are missing, for two different reasons. + +## The user name is nobody's to say + +The provisioner invents it — `mesh__` — and nothing else in the mesh knows that +string. The control plane does not record it, the binding does not carry it, and the consumer +cannot derive it without hard-coding another module's naming convention. + +So the one identifier a consumer must present in order to authenticate is the one thing no part +of the mesh will tell it. It works today only because nothing has yet had to write a connection +string; every proof so far stopped at "the credential arrived". + +## The values cannot reach the file that needs them + +The binding is a JSON document. The consumers are containers reading `KEY=value`, or a program +reading a YAML file, or one reading an attribute inside a different JSON document. A sealed secret +can now be placed inside any of those — the module writes the file with a hole in it and the host +fills the hole on the machine. **The bound values have no such route**, so the half of the +connection that is not secret is the half that cannot be delivered. + +This asymmetry is backwards. The secret is the hard case, because the mesh must not be able to +read it. The host and port are ordinary facts the mesh knows in the clear, and they are the ones +stuck in a document nothing can read. + +## Why it was not noticed + +Every provider so far has been reached by a **provisioner**, a program written for the job, which +reads the JSON because it was built to. The first consumers to need a plain configuration file +were the first real applications. The binding was designed for the program and then handed to the +application. + +There is also a stale comment saying a binding *"carries no credential: the mesh has no way to +issue one yet"*. That stopped being true when [`021`](../021-a-consumer-on-the-providers-machine-is-given-no-credential/00-report.md) +was fixed. + +## What a fix has to settle + +- **Who names the role.** Either the mesh records what the provisioner will create, or the + consumer contributes the name it wants and the provisioner uses it. The second is more in + keeping with the rest — a consumer already contributes the database name it wants — and it + removes an invented convention rather than documenting one. +- **How a bound value reaches a file.** The symmetric answer to the sealed placeholder, and + simpler: these values are not secret, so the control plane can put them in before sending and + the host learns nothing new. +- **That it stays name-agnostic.** The control plane must not learn what a `postgres-database` + is. What the keys mean is agreed by the requirement's name + ([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)), so + whatever is added has to work for a bucket and a mail relay without being told about either. + +## Blocked on this + +Keycloak, Gitea, Mailu and MinIO all have manifests that parse and resolve, and none of them can +start. This is what stands between the module set and a running one.