ADR 0054 (proposed) — a consumer's identity is bounded by the tightest backend

Sketches the options for issue 010: the mesh's identityLimit (63, postgres's) is not
the shortest among the backends the derived name reaches — S3's is 20 — so CheckIdentity
lets an over-long access key through and minio fails at provision time. Options: bound
by the true minimum and refuse at assignment (recommended, with a compact fallback held
in reserve), per-interface bounds, or a provider-generated identity (rejected — breaks
"the mesh says the identity once"). Links issue 010 to it.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-05 02:17:46 +02:00
parent 75887d0bc5
commit 89b65dd4c0
2 changed files with 108 additions and 0 deletions
@@ -0,0 +1,104 @@
---
status: proposed
date: 2026-09-05
deciders: jochen
reconstructed: false
---
# 54. 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 0053) turned up 04-ISSUES/010: 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 0053: 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.
## Recommendation
**A, with B held as the escape hatch.** Fixing the constant is what the code already says it meant
to do ("the shortest limit among the systems these names reach"), and moving the failure to an
honest assignment-time refusal is strictly better than a silent provision loop. Accept the naming
budget first; it is a forcing function toward short node/module names, which the mesh benefits from
elsewhere. If the ≤14 budget proves too tight in real use, add B's compact fallback for the overflow
case only — keeping the legible name for everyone it fits. Reach for C only if per-interface bounds
turn out to matter beyond this one 20-vs-63 gap; it is the right shape but more machinery than the
problem currently demands.
## Consequences (of A)
- `identityLimit` becomes 20; the comment stops claiming postgres is the shortest and names S3 as
the binding one. `CheckIdentity` refuses over-long names at `module add` / assignment, not at
provision.
- Existing node/module names longer than the budget must be shortened before they can consume a
provision — a migration cost paid once, surfaced as a clear refusal.
- minio (04-ISSUES/010) is unblocked without any provider-specific mapping, and the minio
per-backend e2e (skipped, pending this) can run.
- **How it is checked:** the minio grant e2e — a consumer with a name inside the budget reaches its
bucket with the credential the mesh delivered — and a unit test that `CheckIdentity` refuses a
name whose derived identity exceeds 20.
## References
- [04-ISSUES/010](../04-ISSUES/010-mesh-login-exceeds-s3-access-key-limit/00-report.md) — the
observation.
- ADR 0053 — 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.
@@ -61,3 +61,7 @@ and those are not always the same string.
- Do redis/postgres actually want the long name, or did it only survive because they are
permissive? If nothing needs it long, the cheap fix is to cap it.
- Does this fold into the same decision as the data-provision return path, or is it separate?
The options are sketched in **ADR 0054 (proposed)** — bound the identity by the true minimum and
refuse early (recommended), a compact fallback for overflow, per-interface bounds, or a
provider-generated identity (rejected). Awaiting ratification.