The playbook, the README and the status skill knew five statuses; the cycle check knew a sixth, 'fixed', and not 'wontfix'. Eleven issues sat in the sixth for weeks with their fixes shipped, one step short of closed. They are resolved; the check refuses the word from now on and accepts the one the playbook allows.
112 lines
5.8 KiB
Markdown
112 lines
5.8 KiB
Markdown
---
|
|
status: resolved
|
|
opened: 2026-09-01
|
|
located-in: [mesh-control]
|
|
fixed-by: mesh-control 122680b
|
|
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.
|
|
|
|
## Fixed
|
|
|
|
Both halves had the same cause: **the mesh knew something and did not say it.**
|
|
|
|
**Who a consumer is, said once.** `ConsumerIdentity(node, module)` is one derivation, sent to the
|
|
provider in its grant and to the consumer in its binding — so the two agree by construction rather
|
|
than by two conventions that happened to match on the day they were written. The provisioners now
|
|
use the name they are given and **refuse to invent one** if the mesh says nothing: falling back to
|
|
a name of their own would create a login the consumer could never guess, and everything would
|
|
report success. They also refuse a name that does not carry the mesh's prefix, because that prefix
|
|
is how withdrawal finds what it made.
|
|
|
|
**Bound values reach the file that needs them.** `${bound:provision:key}` is the symmetric twin of
|
|
the sealed placeholder and simpler: these values are not secret, so the control plane fills them
|
|
in before sending, and the host gains no field and learns no format. `at`, `as` and `from` are
|
|
true of any provision; every other key comes from what the provider said it *serves*, so the
|
|
control plane still learns nothing about what a `postgres-database` is.
|
|
|
|
Keycloak and Gitea now produce complete connection strings — asserted from the manifests on disk,
|
|
checking that every part is filled, that no placeholder survives as a value, and that the password
|
|
is still a hole only the host can close.
|
|
|
|
### Also found, and separate
|
|
|
|
The lab run that was meant to prove this failed in a way that looked like the fix being wrong: a
|
|
rotation test could not authenticate against a real database. The cause was that the suite
|
|
rebuilt the control plane's image and not the provisioner's, so a run with an image built that
|
|
minute used a provisioner built the day before. Fixed in `mesh-lab`; it is
|
|
[`005`](../005-pipeline-test-harness-unbuildable/00-report.md)'s family — a rebuild covering
|
|
most of what a run uses is worse than one covering none, because the run that follows it is
|
|
believed.
|