131 lines
8.6 KiB
Markdown
131 lines
8.6 KiB
Markdown
---
|
|
topic: what runs on it
|
|
status: accepted
|
|
date: 2026-09-05
|
|
deciders: jochen
|
|
reconstructed: 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](../04-ISSUES/034-mesh-login-exceeds-s3-access-key-limit/00-report.md) — 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.
|