Files
hq/04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md
T
jschoubben c86adbe3cc 022 — a credential belongs to a node, so a second consumer refuses
Found while checking whether the module vocabulary covers real use
cases. A node running three modules that all want a database cannot be
planned at all:

  anchor has 3 modules asking for "postgres-database" and they would
  share one credential: gitea, keycloak, umami

The refusal is right about what it says and wrong about what it implies.
They would share one credential, and sharing is worse than refusing —
but the arrangement being refused is the ordinary one, and the node this
mesh exists to take over runs eight modules against one database server.

The cause is the key: a credential is keyed by provision, consumer node
and provider node, so `consumer` is a machine. The provisioner inherits
it and names the role `mesh_<node>`. The refusal is not a check that
caught something; it is the only honest thing that function can do with
a key that cannot tell two consumers apart.

It is the same mistake as 021 with a different face. There the machine
was treated as a trust boundary; here it is treated as an identity, as
though "who is asking" is answered by naming a host. Two modules on one
node are as separate as two on different nodes.

Worth stating plainly: without the refusal, gitea's login would have
opened keycloak's database, and nothing would have said so — from the
provisioner's side it created exactly what it was asked to create.

Not a local fix. It crosses the control plane, the grant file naming and
every provisioner that names something after a consumer.
2026-09-01 02:28:39 +02:00

3.8 KiB

status, opened, located-in, fixed-by, amended-design
status opened located-in fixed-by amended-design
located 2026-09-01
mesh-control

022 — A credential belongs to a node and a provision, so a second consumer refuses

Symptom

A node running more than one module that wants the same provision cannot be planned at all:

anchor has 3 modules asking for "postgres-database" and they would share one
credential: gitea, keycloak, umami

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 eight modules against one database server.

Where it comes from

A credential is keyed by (provision, consumer node, provider node):

func (i *Inventory) SecretFor(ctx context.Context, name, consumer, provider string) (Secret, error)

consumer is a node. Everything downstream inherits that granularity: Grant.Consumer is a node, the grant's file is named after a node, and the provisioner names the role it creates after one — role := mark + c.Node.

So the refusal in ContributionsTo is not a check that found a problem. It is the only honest thing that function can do, given a key that cannot tell two consumers apart.

Why it was not noticed

Every scenario so far had one consumer per node. That is the natural shape of a small test — a consumer here, a provider there — and it is the shape of every lab scenario written to date. A node with two modules wanting a database is not an edge case discovered by fuzzing; it is what a real machine looks like, and nothing had modelled a real machine yet.

The refusal also reads as deliberate. It names the modules, explains the consequence, and refuses rather than picking — the house rule everywhere else. It looks like a decision. It is a limit.

Why the granularity is wrong

The same argument that closed 021. There, the machine was treated as a trust boundary and containers made that untrue. Here, the machine is treated as an identity — as though "who is asking" is answered by naming a host.

Two modules on one node are as separate as two on different nodes. They run as different containers, on different networks, with different data. A key that cannot distinguish them means the mesh cannot express the thing it is for.

It also silently weakens what the provisioner does. mesh_<node> is one role. Had the refusal not been there, gitea's login would have opened keycloak's database — and nothing anywhere would have said so, because from the provisioner's side it created exactly what it was asked to create.

What a fix has to keep

  • The refusal, where it is still right. Two modules wanting one provision must not silently share a credential. After a fix they do not share one, so there is nothing to refuse — but a genuine collision must still refuse rather than pick.
  • A credential per consuming module, sealed to the node that holds it. Both facts are needed: the module is who it is for, the node is what it is sealed to.
  • The provisioner names what it creates after the module, so a login is traceable to the thing using it, and so withdrawing one consumer does not remove another's.
  • Withdrawal still works. A module unassigned must lose its login while the others keep theirs — which is precisely what one role per node cannot do.
  • Existing single-consumer nodes keep working, since that is every scenario that exists.

Scope

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.