784e49239d
test / integration-docker (pull_request) Successful in 14s
test / unit (pull_request) Successful in 40s
lint / lint (push) Successful in 57s
test / integration-firecracker (pull_request) Successful in 3m51s
test / coverage (pull_request) Successful in 21s
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Successful in 12s
Address review on #482: drop the generic "credential boundary" framing in the PRD and docstrings and talk about the specific services — the orchestrator holds the control-plane key; the host controller (#468) gets a separate key the orchestrator never holds, so the orchestrator can't mint the credentials it uses to talk to the host controller that owns its lifecycle. Lead with that concrete win. No code behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
4.2 KiB
Markdown
89 lines
4.2 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 control plane never runs open where its data plane shares the host/VM.
|
|
|
|
## 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: it raises rather than hand back an empty key that would run the
|
|
server open where the data plane is co-located). `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.
|