Files
bot-bottle/docs/prds/prd-new-control-plane-auth-provisioning.md
T
didericis-claude e53104d5c1
tracker-policy-pr / check-pr (pull_request) Successful in 7s
test / integration-docker (pull_request) Successful in 15s
test / unit (pull_request) Successful in 39s
test / integration-firecracker (pull_request) Successful in 3m55s
test / coverage (pull_request) Successful in 22s
test / publish-infra (pull_request) Has been skipped
prd-number / assign-numbers (push) Failing after 21s
test / integration-docker (push) Successful in 22s
lint / lint (push) Successful in 56s
Update Quality Badges / update-badges (push) Successful in 53s
test / unit (push) Successful in 1m52s
test / integration-firecracker (push) Successful in 5m2s
test / coverage (push) Successful in 16s
test / publish-infra (push) Successful in 1m56s
refactor(orchestrator): fail closed unconditionally, drop topology opt-out
Remove the empty-key opt-out codex flagged: ControlPlaneProvisioning
no longer lets the orchestrator start without a signing key when a
backend declares an "isolated" topology. Host separation is not the
safety condition — the gateway (and any other caller) must reach the
control-plane listener for /resolve, so an open orchestrator would
still grant them full `cli`. The signing key is now mandatory for
every backend.

Drops the now-purposeless Topology/COLOCATED machinery (no backend
declared a non-default topology) and the contractual
test_orchestrator_key_allows_empty_when_isolated. Updates the PRD's
invariant 4 and lifecycle docstring accordingly. Docs/behaviour only
otherwise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 01:40:19 +00:00

4.3 KiB

PRD prd-new: Per-service signing keys for control-plane auth

  • Status: Draft
  • Author: claude
  • Created: 2026-07-26
  • Issue: #476

Summary

Provision control-plane signing keys per service, through one shared seam, so no service can mint another's credentials. Concretely: the orchestrator holds the control-plane key and mints the gateway's and CLI's tokens; the host controller (#468, next) gets a separate key the orchestrator never holds — so the orchestrator cannot forge the credentials it uses to talk to the host controller that starts and stops it. Landing this seam also retires the per-backend provisioning duplication that made PR #471 take three review rounds.

Problem

1. The orchestrator could forge host-controller credentials. The orchestrator's key signs roles {gateway, cli}. The tempting way to add the host controller (#468) is a third role, host, on that same key. But then the orchestrator — which holds the key — can mint host tokens, and the host controller, which owns the orchestrator's lifecycle, must not trust anything the orchestrator can mint. The two services need separate keys.

2. Every backend provisioned auth by hand. Each launcher (docker gateway/infra, macOS infra, firecracker infra) re-derived how to generate the signing key, scope it to the orchestrator, mint the gateway JWT, and keep the host key file canonical. All three PR #471 High-severity findings were this one integration bug in different launchers: the data plane got the full cli token; the firecracker control plane ran open; the firecracker guest clobbered the host key.

Goals / Success Criteria

  • The orchestrator and the host controller sign with different keys; neither can mint the other's tokens. (This PR provisions the orchestrator's key and leaves a drop-in seam for the host controller's.)
  • One shared provisioning seam every backend uses — a new backend or daemon implements it instead of rediscovering these four invariants:
    1. the signing key is host-canonical: a guest is handed it, never generates or overwrites it;
    2. only the orchestrator process gets the raw key; the gateway gets a pre-minted gateway token it can't rewrite into cli;
    3. the host CLI's cli token is minted from the same key, so it stays valid across co-running backends;
    4. the orchestrator never runs open — the signing key is mandatory, with no topology opt-out (a separate host does not stop a caller from reaching the control-plane listener, so it cannot make open mode safe).

Non-goals

  • The host controller itself (#468) — this only provisions the orchestrator's key and the seam #468 plugs into.
  • Rewriting the HMAC primitive: orchestrator_auth.mint/verify gain an optional roles= arg (default unchanged) so a key can carry a different role set; nothing else changes.
  • Network topology, the plane split (#469), or the server's open-mode fallback for tests.

Design

A TrustDomain is one service's signing material: its host-canonical key file, the roles that key may sign, and the env vars its key and a pre-minted token ride in. mint/verify are scoped to that domain's roles, so a token signed by one service's key neither carries nor verifies another service's role.

  • CONTROL_PLANE — the orchestrator's domain: key orchestrator-token, roles {gateway, cli}. The orchestrator process holds the key; the gateway holds only a minted gateway token; the host CLI mints its own cli token.
  • The host controller (#468) will add a second TrustDomain — its own key file and role(s) — that the orchestrator never holds.

ControlPlaneProvisioning is the seam the backends call. orchestrator_key() returns the raw key for the control-plane process (fail-closed for every backend: it raises rather than hand back an empty key that would run the server open). gateway_token() mints the gateway's token. Each backend applies these through its own transport — docker/macOS inject env vars, firecracker pushes over SSH — but none re-derives which key or role.

paths.host_signing_key(filename) generalizes host_orchestrator_token() so each domain names its own key file.

Open questions

None blocking. #468 adds its TrustDomain and a second ControlPlaneProvisioning-shaped consumer; renaming that class to something service-neutral is a cosmetic call to make then.