Skip to main content

ADR-0016: Vendor control-plane service (activation, entitlement signing, renewal, fleet health)

Status: Accepted · 2026-07-15

Context

ADR-0015 established the two-plane separation and implemented the deployment side of the Control contract (local entitlement verification, offline activation, allowlisted telemetry preview), explicitly leaving the vendor-hosted Control service — the holder of the signing private key — out of scope. That gap meant online activation/renewal against control.findmydata.io was documented but not runnable: a deployment could only paste a pre-signed token. This ADR covers building that vendor service (packages/server/src/vendor/, run via bun run --cwd packages/server control) and closing the loop with a deployment-side online client.

Decisions

1. A separate deployable with the mirror-image trust properties

The vendor service is its own process with its own entrypoint (src/control-main.ts), its own port (FMD_CONTROL_PORT, default 8720), and its own isolated database (FMD_CONTROL_DB_PATH, schema created inline in vendor/store.ts — never the customer migration set). It holds what the deployment must not: the entitlement signing private key and the pseudonymous commercial registry (accounts, plans, activation codes, deployments, the entitlements it has signed, fleet heartbeats). The customer product holds only the public verification key and never runs this code path.

2. Content boundary is preserved by construction

The only customer-adjacent data the service accepts is the strict fleet-health heartbeat, validated against the SAME .strict() FleetTelemetrySchema the deployment uses (controlplane/telemetry.ts) before anything is stored — an off-allowlist field (a name, path, excerpt, raw count) is rejected, never persisted. Activation/renewal carry only an activation code, a tenant id, an optional deployment pseudonym, and entitlement tokens. No content, evidence, identities, or prompts cross the boundary.

3. Signing keys and admin credential come only from the environment

vendor/config.ts resolves keys by reference (an env var name pointing at the secret), never from source. Exactly one signer is configured: an Ed25519 private key (FMD_CONTROL_SIGNING_PRIVATE_KEY_REF, production) or an HMAC secret (FMD_CONTROL_SIGNING_HMAC_REF, dev/test). Production refuses HMAC signing, an ephemeral key, both-keys-set, or a missing admin credential — the boot guard throws rather than start in an unsafe posture. Development generates an ephemeral Ed25519 keypair and logs the public key so a local deployment can be pointed at it.

4. Activation binds a deployment to an account plan and signs an entitlement

POST /control/v1/activate validates the activation code (existence, not revoked, not expired, within its activation limit, tenant binding if set), confirms the account is active and within its deployment ceiling, allocates a pseudonymous deployment id, and returns a freshly signed entitlement built from the account plan (edition, features, capacity, term, offline policy). Plans are edition presets (vendor/plans.ts) with optional per-field overrides, so a bespoke plan needs no code change.

5. Renewal re-issues with an extended term and supersedes the prior token

POST /control/v1/renew verifies a previously-issued token (any temporal state — renewal is usually needed because it expired), confirms the deployment/account are in good standing, and issues a fresh entitlement carrying supersedes = <old id>. The superseded entitlement is revoked so a replaced token cannot be renewed again (replay resistance). Renewal reads the current account plan, so plan changes take effect on renewal.

6. The deployment never trusts a Control response blindly

The deployment-side online client (controlplane/control-client.ts) fetches a token, but activateOnline/renewOnline (controlplane/licensing.ts) then verify that token locally against the deployment’s own trusted public key and re-bind it to the requesting tenant before storing it. A spoofed or misrouted Control response therefore cannot inject a license (covered by the “untrusted-key” end-to-end test). Online renewal falls back to a local re-verify if Control is unreachable, so a transient outage does not break a still-valid deployment inside its grace window.

7. Admin surface is credential-gated and minimal

Account creation, activation-code minting, and account suspension require a Bearer admin credential (constant-time compared). When no admin key is configured the admin routes return 503 (disabled) rather than run unauthenticated. Activation-code plaintext is returned once and stored only as a salted SHA-256 hash.

Consequences

  • Online activation/renewal against control.findmydata.io is now real and tested end-to-end (deployment ↔ live vendor service), superseding the “gated / out of scope” note in ADR-0015 §2.
  • The vendor service and the customer product share the entitlement + telemetry contracts (one keyring shape, one strict schema) but nothing else — proven by the round-trip tests in vendor/vendor.test.ts (30 tests).
  • Out of scope here (future work): the human Account portal (account.findmydata.io), self-service checkout/billing, key rotation across multiple concurrent signing keys, and a hardened KMS/HSM binding for the production signing key (the reference contract is env/vault by design).

Verification

vendor/vendor.test.ts — activation (verifiable token, single-use/metered codes, tenant binding, suspension, capacity ceiling, pseudonym anti-hijack), renewal (extend + supersede + revoke-old, unknown/garbage token), telemetry (strict allowlist rejection, unknown-deployment flagging), status transitions, the full HTTP surface (incl. admin auth 401/503), config guards (production key rules), and two live end-to-end flows (online activate+renew, and untrusted-key rejection). All server tests pass (237) with typecheck and lint clean; the service boots and serves a full activation flow via bun run --cwd packages/server control.