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
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>
90 lines
4.3 KiB
Markdown
90 lines
4.3 KiB
Markdown
# 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.
|