Let one provider carry a per-target credential #16

Open
opened 2026-09-01 19:47:05 +01:00 by vnprc-agent · 0 comments

Let a provider authenticate with a different bearer on each machine that uses it, so a deployment can give every target its own credential for one shared endpoint instead of copying one bearer to all of them.

Primary goals:

  • Per-target bearers for one provider — a provider keeps a single identity, endpoint and model list while each target machine decrypts a bearer only it holds.
  • Independent revocation — withdrawing one machine's access leaves the other targets untouched, with no re-keying of a shared ciphertext.
  • Upstream attribution and limits — an endpoint that identifies callers by bearer can attribute requests and budget rate per machine, which is impossible while every machine presents the same one.
  • No duplicate catalogue entries — the workaround available today is one provider ID per machine, which multiplies otherwise identical provider records and names each after a machine rather than the service.

Current state

A provider ID may belong to exactly one credential. lib/pi-credential-schema.nix:79 collects every provider across the registry and :95 throws providers referenced by multiple credentials on any repeat, so two credentials cannot both serve one provider. A credential's targets list then fans that single bearer out to every machine named, and lib/pi-credential-recipients.nix:60-61 encrypts one ciphertext per token to the union of those targets' host keys. The result is deliberate and correct for a shared service account; it has no expression for a per-machine one.

The global provider-to-credential map at lib/pi-credential-contract.nix:169-173 is where the single-credential assumption is baked in: it is a flat provider -> credential attrset with no target dimension.

The per-machine projection is already closer to the target shape than that map is. projectionFor at :192-216 selects the credentials naming a target and derives that machine's providers from them, so a projection is keyed by target already and each machine only ever learns about its own credential. A per-target credential mostly changes what the projection selects, not its shape.

Suggested direction

Give targets a per-target credential rather than making a provider name repeatable. The registry entry shape at lib/pi-credential-schema.nix:5 is the natural place to decide this, and the decision governs the size of the change: extending an existing field keeps the migration mechanical, while a new field means every consumer's exact-field check has to move together.

Whatever shape wins, three things follow: the uniqueness rule at :95 narrows from "a provider appears once" to "a provider appears once per target"; the recipient derivation encrypts each machine's ciphertext to that machine alone rather than to the union; and the global providerCredentials export either gains a target dimension or stops being derivable and its consumers move to the projection.

The recipient change is the one with a real failure mode. Today every target of a credential can decrypt the same file, and a bug that widens a recipient set is invisible at evaluation time. Whatever lands should have a check that a per-target ciphertext lists exactly one machine's host keys beside the hypervisor's.

Scope

In scope: the registry shape, the uniqueness rule, the projection, the recipient derivation, and the fixtures and checks covering them.

Out of scope, tracked separately: the operator tool that writes registries and ciphertext needs a way to add a target with its own bearer, tracked as allod/nexus issue #36, which cannot be designed before this contract settles. Consumers that vendor lib/pi-credential-schema.nix or lib/pi-credential-recipients.nix to reach them without a flake input must re-vendor after this lands; that is deployment work, not framework work, but the change should not silently assume a single copy exists.

Migrating an existing shared credential to per-target ones is a deployment action requiring the bearers in hand, so this issue owes a documented migration path, not a migration.

Let a provider authenticate with a different bearer on each machine that uses it, so a deployment can give every target its own credential for one shared endpoint instead of copying one bearer to all of them. Primary goals: - **Per-target bearers for one provider** — a provider keeps a single identity, endpoint and model list while each target machine decrypts a bearer only it holds. - **Independent revocation** — withdrawing one machine's access leaves the other targets untouched, with no re-keying of a shared ciphertext. - **Upstream attribution and limits** — an endpoint that identifies callers by bearer can attribute requests and budget rate per machine, which is impossible while every machine presents the same one. - **No duplicate catalogue entries** — the workaround available today is one provider ID per machine, which multiplies otherwise identical provider records and names each after a machine rather than the service. ### Current state A provider ID may belong to exactly one credential. `lib/pi-credential-schema.nix:79` collects every provider across the registry and `:95` throws `providers referenced by multiple credentials` on any repeat, so two credentials cannot both serve one provider. A credential's `targets` list then fans that single bearer out to every machine named, and `lib/pi-credential-recipients.nix:60-61` encrypts one ciphertext per token to the union of those targets' host keys. The result is deliberate and correct for a shared service account; it has no expression for a per-machine one. The global provider-to-credential map at `lib/pi-credential-contract.nix:169-173` is where the single-credential assumption is baked in: it is a flat `provider -> credential` attrset with no target dimension. The per-machine projection is already closer to the target shape than that map is. `projectionFor` at `:192-216` selects the credentials naming a target and derives that machine's providers from them, so a projection is keyed by target already and each machine only ever learns about its own credential. A per-target credential mostly changes what the projection selects, not its shape. ### Suggested direction Give `targets` a per-target credential rather than making a provider name repeatable. The registry entry shape at `lib/pi-credential-schema.nix:5` is the natural place to decide this, and the decision governs the size of the change: extending an existing field keeps the migration mechanical, while a new field means every consumer's exact-field check has to move together. Whatever shape wins, three things follow: the uniqueness rule at `:95` narrows from "a provider appears once" to "a provider appears once per target"; the recipient derivation encrypts each machine's ciphertext to that machine alone rather than to the union; and the global `providerCredentials` export either gains a target dimension or stops being derivable and its consumers move to the projection. The recipient change is the one with a real failure mode. Today every target of a credential can decrypt the same file, and a bug that widens a recipient set is invisible at evaluation time. Whatever lands should have a check that a per-target ciphertext lists exactly one machine's host keys beside the hypervisor's. ### Scope In scope: the registry shape, the uniqueness rule, the projection, the recipient derivation, and the fixtures and checks covering them. Out of scope, tracked separately: the operator tool that writes registries and ciphertext needs a way to add a target with its own bearer, tracked as `allod/nexus` issue #36, which cannot be designed before this contract settles. Consumers that vendor `lib/pi-credential-schema.nix` or `lib/pi-credential-recipients.nix` to reach them without a flake input must re-vendor after this lands; that is deployment work, not framework work, but the change should not silently assume a single copy exists. Migrating an existing shared credential to per-target ones is a deployment action requiring the bearers in hand, so this issue owes a documented migration path, not a migration.
Sign in to join this conversation.
No description provided.