Reconcile: adopt initialization's consolidated HQ as canonical, re-home this session's new work #24
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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_<node>_<module>` — 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.
|
||||
Reference in New Issue
Block a user