From 10ca9b29d9fb894f2330681a0178066260129554 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 24 Aug 2026 08:09:45 -0700 Subject: [PATCH 1/2] docs(security): describe both external API surfaces in cvm-boundaries The public-listener section predated the v0/v1 split: it showed one Worker service with Health bolted onto the frozen method table by a footnote, and said nothing about how the two surfaces bound what an anonymous caller can cost or learn. Split the method list into the frozen surface and the v1 surface, state the invariant that makes the listener safe to expose (no key material, no caller-chosen signing or attestation -- that lives on the internal socket, which is not a CVM boundary), and extend the cost/exposure bullets to the v1 methods: cached identity behind Info, the retry throttle, the deliberate absence of a v1 attestation-on-demand entry point, and the public_tcbinfo reach difference between the two Info methods. Point at guest-api-v1.md as the normative reference and at agent_rpc_v1.proto alongside the frozen proto. --- docs/security/cvm-boundaries.md | 67 ++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/docs/security/cvm-boundaries.md b/docs/security/cvm-boundaries.md index 7f3abfd72..7a9b8f38a 100644 --- a/docs/security/cvm-boundaries.md +++ b/docs/security/cvm-boundaries.md @@ -180,33 +180,66 @@ Full specification: [host_api.proto](../../dstack/host-api/proto/host_api.proto) The dstack-guest-agent runs an HTTP server on port 8090 inside the CVM. This port is publicly accessible, allowing external clients to view basic CVM information. -| Service | Purpose | -|---------|--------| -| Worker | Provides public-facing app information | - -**Available Methods:** +Since dstack 0.6.0 the listener serves two API surfaces, selected by URL path +alone: the frozen v0.5.11 `Worker` service at `/prpc` (equivalently +`/prpc/v0`), and the versioned `dstack.guest.v1` `Worker` service at +`/prpc/v1`. The frozen surface is closed and never changes again; new +capability arrives only on v1. [guest-api-v1.md](../guest-api-v1.md) is the +normative specification of the v1 surface, including its status-code and +version-probing rules. + +Neither surface returns key material, and no caller chooses what gets signed +or attested. That boundary, not the method list, is what makes this listener +safe to expose: key material and caller-chosen attestation live only on the +internal Unix socket (`/var/run/dstack.sock`), which is not a CVM boundary — +it is reachable only by the application itself. + +**Frozen `Worker` (`/prpc`, alias `/prpc/v0`):** | Method | Description | Return Type | |--------|-------------|------------| | Info | Get application information | AppInfo | | Version | Get guest agent version | WorkerVersion | | GetAttestationForAppKey | Attest the key the agent derives for the app | GetQuoteResponse | -| Health | Report whether the application is serving (`/prpc/v1` only) | HealthResponse | + +**v1 `Worker` (`/prpc/v1`):** + +| Method | Description | Return Type | +|--------|-------------|------------| +| Info | Get application identity and configuration | InfoResponse | +| Version | Get guest agent version | VersionResponse | +| Health | Report whether the application is serving | HealthResponse | Everything on this listener is unauthenticated, so each method is bounded in what it costs and in what it says: -- `Health` answers from a cache the agent refreshes on its own timer, so a call - costs a lock and a clone however many callers there are. It reveals whether - the app opted into health gating, its current verdict, and — when the app - declared a `health_status_file` — the path it named and which parsing rule - failed. That last part is a narrow oracle for whether a path exists and what - shape its first two lines have; the path itself is already public, since it is - measured into the compose hash. The file's *contents* are never quoted back. - Container names and statuses were already public through the dashboard below. -- `GetAttestationForAppKey` generates a fresh platform attestation per call and - is by far the most expensive method here. +- `Health` (v1) answers from a cache the agent refreshes on its own timer, so a + call costs a lock and a clone however many callers there are. It reveals + whether the app opted into health gating, its current verdict, and — when the + app declared a `health_status_file` — the path it named and which parsing + rule failed. That last part is a narrow oracle for whether a path exists and + what shape its first two lines have; the path itself is already public, since + it is measured into the compose hash. The file's *contents* are never quoted + back. Container names and statuses were already public through the dashboard + below. +- `GetAttestationForAppKey` (frozen) generates a fresh platform attestation per + call and is by far the most expensive method here. It has no v1 counterpart + on purpose: a v1 application attests its own key through the internal socket + (`/v1/GetKey`, then `/v1/Attest`) and serves the result itself, so the public + listener never gained a second attestation-on-demand entry point. +- The frozen `Info` decodes identity out of a boot attestation per call, which + costs a hardware quote under the agent's global quote lock. The v1 `Info` + serves the same identity from a cache decoded once at startup, so an + anonymous caller cannot drive quote generation through it; if the boot-time + decode failed, retries are throttled to one attempt per interval. +- Both `Info` methods honour the app's `public_tcbinfo` choice, with different + reach. The frozen one blanks `tcb_info` and `vm_config` but always serves + `key_provider_info`. The v1 one blanks `app_compose`, `vm_config`, and + `key_provider_info`, and carries no measurement registers or event log at + all — those are attestation data and belong to the internal `Attest`, where + a quote vouches for them. Identity and the measurement hashes are always + visible on both surfaces. The service also provides a web dashboard at the root URL (`/`) showing basic CVM information. View the dashboard template [here](../../dstack/guest-agent/templates/dashboard.html). -Full specification: [agent_rpc.proto](../../dstack/guest-agent/rpc/proto/agent_rpc.proto) +Full specifications: [agent_rpc.proto](../../dstack/guest-agent/rpc/proto/agent_rpc.proto) for the frozen surface, [agent_rpc_v1.proto](../../dstack/guest-agent/rpc/proto/agent_rpc_v1.proto) for v1. From 50b5a215c13c4c8a14aa9aa1a7ae7b4c880b3a5c Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 24 Aug 2026 19:08:39 -0700 Subject: [PATCH 2/2] docs(security): drop the stale cost superlative from the public listener Review of #1119 caught the section contradicting itself. One bullet called `GetAttestationForAppKey` "by far the most expensive method here"; the next bullet, added by the same commit, says the frozen `Info` also generates a hardware quote per call and replays the event log under the same global lock. The superlative predates that second bullet and is now false. It also misdirects: a reader deciding what to rate-limit follows it to the method that needs an algorithm argument, not to the one clients actually poll. Name both quote-generating methods, then say plainly which of the two is worth bounding first. Two smaller corrections in the same section: - The v1 `Info` row promised "identity and configuration" while the bullets below explain that the external surface blanks the configuration documents unless the app set `public_tcbinfo`. The row now carries the condition. - "the internal socket ... is reachable only by the application itself" is true of the platform, not of every deployment: the socket is bind-mounted per the app's own compose, so an app that proxies it has moved the boundary. Say so, rather than leaving an absolute claim in a security document. --- docs/security/cvm-boundaries.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/security/cvm-boundaries.md b/docs/security/cvm-boundaries.md index 7a9b8f38a..867578d9f 100644 --- a/docs/security/cvm-boundaries.md +++ b/docs/security/cvm-boundaries.md @@ -192,7 +192,9 @@ Neither surface returns key material, and no caller chooses what gets signed or attested. That boundary, not the method list, is what makes this listener safe to expose: key material and caller-chosen attestation live only on the internal Unix socket (`/var/run/dstack.sock`), which is not a CVM boundary — -it is reachable only by the application itself. +it is reachable only by the application itself. An application that re-exports +that socket has moved the boundary itself, and everything behind it moves with +it. **Frozen `Worker` (`/prpc`, alias `/prpc/v0`):** @@ -206,7 +208,7 @@ it is reachable only by the application itself. | Method | Description | Return Type | |--------|-------------|------------| -| Info | Get application identity and configuration | InfoResponse | +| Info | Get application identity, plus configuration when `public_tcbinfo` is set | InfoResponse | | Version | Get guest agent version | VersionResponse | | Health | Report whether the application is serving | HealthResponse | @@ -223,7 +225,8 @@ what it costs and in what it says: back. Container names and statuses were already public through the dashboard below. - `GetAttestationForAppKey` (frozen) generates a fresh platform attestation per - call and is by far the most expensive method here. It has no v1 counterpart + call. With the frozen `Info` below, it is one of the two methods here that + let an anonymous caller drive quote generation. It has no v1 counterpart on purpose: a v1 application attests its own key through the internal socket (`/v1/GetKey`, then `/v1/Attest`) and serves the result itself, so the public listener never gained a second attestation-on-demand entry point. @@ -231,7 +234,10 @@ what it costs and in what it says: costs a hardware quote under the agent's global quote lock. The v1 `Info` serves the same identity from a cache decoded once at startup, so an anonymous caller cannot drive quote generation through it; if the boot-time - decode failed, retries are throttled to one attempt per interval. + decode failed, retries are throttled to one attempt per interval. Of the two + quote-generating methods, the frozen `Info` is the one worth rate-limiting + first: it is what clients actually poll, and it replays the event log on top + of the quote. - Both `Info` methods honour the app's `public_tcbinfo` choice, with different reach. The frozen one blanks `tcb_info` and `vm_config` but always serves `key_provider_info`. The v1 one blanks `app_compose`, `vm_config`, and