Files
hq/02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md
T

8.6 KiB

topic, status, date, deciders, reconstructed
topic status date deciders reconstructed
what runs on it accepted 2026-09-05 jochen false

49. A consumer's identity is bounded by the tightest backend that must accept it

Context

The mesh says who a consumer is, once, and hands the same name to the provider (to create) and the consumer (to present), so the two ends agree by construction rather than by two conventions (the principle behind ConsumerIdentity, 04-ISSUES/023). The name is mesh_<node>_<module>, cleaned to lower-case letters, digits and underscore.

Proving the provider contract per backend (ADR 0048) turned up 04-ISSUES/034: redis and postgres create that name verbatim, but minio refuses it — an S3 access key is capped at 20 characters, and mesh_anchor_bucketuser is 22. The provisioner then retries for ever, per consumer, and the consumer holding that same too-long name could never present it either.

Two things about the existing derivation decide most of this:

  • The charset is already right. [^a-z0-9_] is deliberately conservative, and its own comment says it reaches "a PostgreSQL role, a MinIO access key, an LDAP uid and a Keycloak client without quoting." That much is true.
  • The length is wrong. CheckIdentity refuses names over identityLimit = 63, commented as "the shortest identifier limit among the systems these names reach: PostgreSQL's". It is not the shortest — S3's 20 is shorter — so the guard that was meant to catch exactly this lets it through, and the failure lands at provision time as a silent retry instead of at assignment as a refusal.

So this is a small wrong constant with a real cost attached: whatever bound we set, mesh_ (5) plus a node name plus _ plus a module name has to fit inside it.

The options

A — Bound the identity by the true minimum, and refuse early. Lower identityLimit to the real shortest (20, S3's), so CheckIdentity refuses an over-long name at assignment with a clear message, the way it already refuses over-63 names. The derivation does not change; long names are simply rejected before anything is provisioned.

  • For: smallest change; keeps "the mesh says the identity once, verbatim" intact; the failure moves from a per-consumer provision-time retry to an up-front, legible refusal — which is what CheckIdentity exists to do.
  • Against: a hard budget. mesh_ + node + _ + module ≤ 20 means node + module ≤ 14 characters. anchor + bucketuser (16) is already over. It pushes the constraint onto how machines and modules are named, which is a real limitation on legible names.

B — Keep the readable name when it fits, compact it when it does not. Below the bound, the name is mesh_<node>_<module> as today; over it, the mesh substitutes a deterministic short form (e.g. mesh_ + a truncated hash of node+module) — still one derivation, so both ends still agree.

  • For: no naming constraint; short backends always satisfied; the common case stays legible.
  • Against: some identities become opaque, and a provisioner tracing "whose login is this" loses the answer for exactly the consumers that overflowed. The mesh now owns a fallback format and its collision properties (a truncated hash is not free of collisions at 15 characters).

C — Let each interface declare its identifier bounds, and derive within the tightest a consumer reaches. s3-bucket states identifier: { max: 20 }; postgres-database states 63; the mesh derives a name that fits the minimum bound across the providers a given consumer is granted.

  • For: the most precise — each provision gets exactly the room it has, and a database consumer keeps long legible names while an S3 consumer gets a short one; the constraint lives where the fact does (on the interface).
  • Against: the most work, and a consumer of two interfaces with different bounds must satisfy the smaller — so its name shortens for both, reintroducing B's opacity in a narrower case. It also means one consumer can hold different identities per provision, which the "said once" model currently forbids.

D — Let the provider generate a backend-valid identity and hand it back (rejected). minio mints its own access key and returns it to the consumer. This is the data-provision return path this era keeps meeting — but it directly contradicts 023 and ADR 0048: the identity would no longer be the mesh's single derivation the two ends share, it would be a value one side invents and the other must be told. Listed for completeness; not recommended.

E — A module (and a node) may declare a short slug; the identity is built from it. The identity becomes mesh_<node-slug|node-name>_<module-slug|module-name>: where a slug is declared it is used, otherwise the cleaned name. A slug is a deliberately short, operator-chosen identifier — kc for keycloak, wkstn for a workstation. It is optional: short names (anchor, redis) need none.

  • For: this is the escape hatch B wanted to be, without the opacity. The name stays legible — a provisioner can read mesh_wkstn_kc and know who is asking — because a person chose it, not a hash function. And it makes an early refusal palatable: if even the slug-built identity overflows, the refusal points at the slug, a field made for exactly this, rather than at the machine's name. Both ends still derive it from one declared thing, so they agree by construction.
  • Against: a new optional manifest field, and someone must pick the slug — but only for names that would otherwise overflow, and picking a short legible identifier is a better job than being handed a hash.

What implementing A revealed

A was tried first. At identityLimit = 20, the readable budget is mesh_ (5) + node + _ + module ≤ 20, i.e. node + module ≤ 14 characters — far tighter than it looked. The catalogue's own existing tests use workstation+keycloak (25), which compacts to mesh_dbbc02f8dde34d3; common mesh names (home-server, the-build-node, workstation) blow the budget with any module. So B's compact fallback would fire for the common case, not the rare overflow — which inverts A+B: most identities would be opaque hashes. A alone (hard refusal at 20) would refuse most realistic names. This is what moved the recommendation to E: the problem is not the limit, it is that the readable name is the wrong source when it is long, and a slug is a better source than either a hash or a ban.

Recommendation

E, over a per-consumer bound (start with the global minimum, 20). Build the identity from an optional slug, keep it when it fits, and refuse at assignment with "declare or shorten <module>'s slug" when it does not — no hash, no lost legibility, and the fix is a first-class field. Set the bound to the true minimum (20) now; it needs no per-interface machinery to unblock S3, and a module that consumes S3 simply declares a short slug. Graduate to C (per-interface bounds) later if it turns out that non-S3 consumers are paying for S3's limit often enough to mind — E and C compose: slugs are the mechanism, per-interface bounds refine where the ceiling sits. B is dropped: a declared slug is a strictly better escape hatch than an opaque hash. D stays rejected.

Consequences (of E)

  • A module manifest gains an optional slug; a node may carry one too. ConsumerIdentity prefers the slug over the cleaned name for each half. identityLimit becomes 20 (the true minimum), and CheckIdentity refuses at module add / assignment — now with a message naming the slug to set.
  • The common case stays legible; only names that overflow the budget need a slug, and what they get is a name a person chose, not a hash.
  • Existing modules/nodes whose names overflow declare a slug once — a migration cost paid as a clear refusal with an obvious remedy, not a silent hash or a silent truncation.
  • minio (04-ISSUES/034) is unblocked: an S3 consumer declares a short slug and its access key fits.
  • How it is checked: the minio grant e2e — a consumer whose (slugged) identity fits reaches its bucket with the credential the mesh delivered — plus unit tests that a slug is preferred, that an un-sluggable over-long identity is refused (naming the slug), and that two consumers never collide.

References

  • 04-ISSUES/034 — the observation.
  • ADR 0048 — a provider creates the credential the mesh minted; the identity it creates it under is the one this decision bounds.
  • mesh-control internal/catalogue/identity.go — ConsumerIdentity, identityUnusable, identityLimit, CheckIdentity — where the constant and the check live.