ADR-0016: Vendor control-plane service (activation, entitlement signing, renewal, fleet health)
Status: Accepted · 2026-07-15Context
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 againstcontrol.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.iois 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.