Skip to content

Commit b9d968d

Browse files
author
ChrisWorkBot
committed
fix: complete sev-snp smoke proxy path
1 parent 45c7777 commit b9d968d

13 files changed

Lines changed: 224 additions & 36 deletions

File tree

basefiles/dstack-guest-agent.service

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ WatchdogSec=30s
1515
StandardOutput=journal+console
1616
StandardError=journal+console
1717
Environment=RUST_LOG=warn
18+
EnvironmentFile=-/run/dstack/environment
1819

1920
[Install]
2021
WantedBy=multi-user.target

basefiles/dstack-prepare.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,14 @@ if [ -f "/sys/class/block/${device_name}/partition" ]; then
274274
fi
275275
fi
276276

277+
AMD_KDS_PROXY_URL="$(tr ' ' '\n' </proc/cmdline | sed -n 's/^dstack.amd_kds_proxy_url=//p' | tail -n1)"
278+
if [ -n "$AMD_KDS_PROXY_URL" ]; then
279+
log "Using AMD KDS proxy URL from kernel cmdline"
280+
export DSTACK_AMD_KDS_PROXY_URL="$AMD_KDS_PROXY_URL"
281+
mkdir -p /run/dstack
282+
printf 'DSTACK_AMD_KDS_PROXY_URL=%s\n' "$AMD_KDS_PROXY_URL" >/run/dstack/environment
283+
fi
284+
277285
dstack-util setup --work-dir $WORK_DIR --device "$DATA_DEVICE" --mount-point $DATA_MNT
278286

279287
log "Mounting container runtime dirs to persistent storage"

docs/amd-sev-snp-review-readiness.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,14 @@ platform=amd-sev-snp
133133
image_kernel=Linux 6.18.24-dstack with CONFIG_AMD_MEM_ENCRYPT=y, CONFIG_SEV_GUEST=y, CONFIG_TSM_REPORTS=y
134134
kms_guest=booted SNP Linux/userspace and started dstack-kms
135135
kms_marker=SNP_KMS_CONTAINER_STARTED / KMS runtime ready
136-
app_guest=booted SNP Linux/userspace and reached dstack-prepare.sh
137-
app_key_boundary=GetTempCaCert/GetAppKey request reached
138-
strict_tcb_probe=app request reached strict KMS, but final policy decision was blocked before TCB evaluation by AMD KDS collateral throttling
139-
success_probe=app request reached permissive lab KMS, but final GetAppKey success marker was blocked by the same AMD KDS collateral throttling
140-
failure_error=amd sev-snp KDS collateral unavailable while fetching VCEK/cert-chain collateral; Genoa VCEK returned HTTP 429
136+
kds_proxy=enabled for smoke via DSTACK_AMD_KDS_PROXY_URL=https://cors.litgateway.com/
137+
strict_tcb_probe=denied_as_expected with tcb_status is not allowed
138+
success_probe=GetTempCaCert HTTP 200; GetAppKey HTTP 200; SignCert HTTP 200; app container started
139+
smoke_result=SNP E2E smoke success
141140
no_secret_material_logged=true
142141
```
143142

144-
This means the PR has live SNP report proof, live golden-vector measurement proof, release-gate unit/integration coverage, and hardware smoke proof through dstack-managed SNP KMS boot plus app guest key-request boundary. The fresh-box smoke now reaches Linux/userspace, `SNP_KMS_CONTAINER_STARTED`, `GetTempCaCert`, and the app `GetAppKey` request when using a coherent **SNP** `meta-dstack` image. The remaining blocker for completed strict-denial and success markers in the latest run was external AMD KDS throttling while fetching VCEK/cert-chain collateral for the app quote (`HTTP 429` for the Genoa VCEK request), not guest boot, KMS startup, or release-policy wiring. Host/KMS binaries must match PR #703, guest-side `dstack-util`/`dstack-attest` must include the PR cert-chain/KDS fallback, and the Yocto image must be built with `MACHINE = "sev-snp"` so the guest kernel includes AMD memory-encryption/SNP support. A coherent PR image built with the default `tdx` machine produced a `6.18.24-dstack` kernel with `# CONFIG_AMD_MEM_ENCRYPT is not set`; controlled QEMU tests showed that kernel resets immediately after OVMF loads kernel/initrd, while SNP-capable kernels boot the same QEMU/OVMF path to Linux/SNP markers.
143+
This means the PR has live SNP report proof, live golden-vector measurement proof, release-gate unit/integration coverage, and hardware smoke proof through dstack-managed SNP KMS boot, strict TCB denial, app guest key release, and app container startup. The fresh-box smoke now reaches Linux/userspace, `SNP_KMS_CONTAINER_STARTED`, `GetTempCaCert`, `GetAppKey`, `SignCert`, and app container startup when using a coherent **SNP** `meta-dstack` image. During the smoke, AMD KDS throttling was worked around by explicitly routing AMD KDS collateral fetches through `DSTACK_AMD_KDS_PROXY_URL`; the proxy value is carried in the measured guest cmdline for smoke runs and mirrored in KMS measurement recomputation to avoid measurement drift. Host/KMS binaries must match PR #703, guest-side `dstack-util`/`dstack-attest` must include the PR cert-chain/KDS fallback, and the Yocto image must be built with `MACHINE = "sev-snp"` so the guest kernel includes AMD memory-encryption/SNP support. A coherent PR image built with the default `tdx` machine produced a `6.18.24-dstack` kernel with `# CONFIG_AMD_MEM_ENCRYPT is not set`; controlled QEMU tests showed that kernel resets immediately after OVMF loads kernel/initrd, while SNP-capable kernels boot the same QEMU/OVMF path to Linux/SNP markers.
145144

146145
### Fresh SNP host / image requirements
147146

@@ -156,7 +155,7 @@ Practical implication for reviewers/testers on a fresh box:
156155

157156
1. Install/use an AMDSEV QEMU 10.x build and the matching SNP-capable OVMF.
158157
2. Build the PR binaries with `cargo build --release -p dstack-vmm -p supervisor -p dstack-kms`.
159-
3. Run `test-scripts/snp-e2e-smoke.sh` unchanged and first confirm it reaches `SNP_KMS_CONTAINER_STARTED` and the app guest key-request boundary.
158+
3. Run `test-scripts/snp-e2e-smoke.sh` unchanged and first confirm it reaches `SNP_KMS_CONTAINER_STARTED`; if AMD KDS throttles the lab host, set `DSTACK_SNP_SMOKE_KDS_PROXY_URL` to a trusted AMD-KDS passthrough/cache endpoint and rerun.
160159
4. For full `SNP_APP_CONTAINER_STARTED` / `GetAppKey` success, use or publish a coherent `meta-dstack` guest image whose kernel, modules, initramfs, rootfs, verity metadata, and guest userspace include the same PR #703 `dstack-util`/`dstack-attest` SNP cert-chain/KDS fallback code. The reproducible path is to build `meta-dstack` with its `dstack` submodule checked out to this PR branch, for example:
161160

162161
```bash
@@ -177,7 +176,7 @@ Practical implication for reviewers/testers on a fresh box:
177176
Do not try to inject only a replacement `dstack-util` into the stock image; that experiment changed the initramfs/measurement enough to regress boot.
178177
5. Only after the baseline smoke reaches the app success marker should testers swap the simple app workload for Chipotle.
179178

180-
If the smoke stops after `EFI stub: Loaded initrd ...` with `cpus are not resettable`, use a host/image/kernel that is known to boot dstack under SNP before debugging app-level behavior. If it reaches `Requesting app keys from KMS` and fails with the cert-chain error above, rebuild/use a coherent PR guest image rather than changing KMS release policy.
179+
If the smoke stops after `EFI stub: Loaded initrd ...` with `cpus are not resettable`, use a host/image/kernel that is known to boot dstack under SNP before debugging app-level behavior. If it reaches `Requesting app keys from KMS` and fails with AMD KDS `HTTP 429`, use the smoke proxy hook above; if it fails with missing cert-chain/collateral without KDS proxy evidence, rebuild/use a coherent PR guest image rather than changing KMS release policy.
181180

182181
## Validation commands
183182

dstack-attest/src/amd_sev_snp.rs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -283,12 +283,15 @@ fn fetch_amd_kds_collateral_for_product(
283283
.context("amd sev-snp chip_id too short")?,
284284
);
285285
let vcek_url = amd_kds_vcek_url(product, &chip_id, report.reported_tcb.into());
286+
let vcek_request_url = amd_kds_request_url(&vcek_url);
286287
let vcek = reqwest::blocking::Client::new()
287-
.get(&vcek_url)
288+
.get(&vcek_request_url)
288289
.send()
289-
.with_context(|| format!("failed to request amd sev-snp vcek from {vcek_url}"))?
290+
.with_context(|| format!("failed to request amd sev-snp vcek from {vcek_request_url}"))?
290291
.error_for_status()
291-
.with_context(|| format!("amd sev-snp vcek request failed for {vcek_url}"))?
292+
.with_context(|| {
293+
format!("amd sev-snp vcek request failed for {vcek_url} via {vcek_request_url}")
294+
})?
292295
.bytes()
293296
.context("failed to read amd sev-snp vcek response")?
294297
.to_vec();
@@ -304,17 +307,25 @@ fn fetch_amd_kds_collateral_for_product(
304307

305308
fn fetch_amd_kds_ca_chain(product: &str) -> Result<(CertBytes, CertBytes)> {
306309
let url = format!("https://kdsintf.amd.com/vcek/v1/{product}/cert_chain");
310+
let request_url = amd_kds_request_url(&url);
307311
let chain = reqwest::blocking::Client::new()
308-
.get(&url)
312+
.get(&request_url)
309313
.send()
310-
.with_context(|| format!("failed to request amd sev-snp cert_chain from {url}"))?
314+
.with_context(|| format!("failed to request amd sev-snp cert_chain from {request_url}"))?
311315
.error_for_status()
312-
.with_context(|| format!("amd sev-snp cert_chain request failed for {url}"))?
316+
.with_context(|| format!("amd sev-snp cert_chain request failed for {request_url}"))?
313317
.bytes()
314318
.context("failed to read amd sev-snp cert_chain response")?;
315319
extract_ark_ask_from_amd_kds_cert_chain(&chain)
316320
}
317321

322+
fn amd_kds_request_url(amd_url: &str) -> String {
323+
match std::env::var("DSTACK_AMD_KDS_PROXY_URL") {
324+
Ok(proxy) if !proxy.trim().is_empty() => format!("{}{}", proxy.trim(), amd_url),
325+
_ => amd_url.to_string(),
326+
}
327+
}
328+
318329
fn amd_kds_vcek_url(product: &str, chip_id: &[u8; 64], tcb: AmdSnpTcbVersion) -> String {
319330
format!(
320331
"https://kdsintf.amd.com/vcek/v1/{}/{}?blSPL={}&teeSPL={}&snpSPL={}&ucodeSPL={}",
@@ -571,6 +582,25 @@ mod tests {
571582
);
572583
}
573584

585+
#[test]
586+
fn amd_kds_proxy_url_wraps_amd_urls_when_configured() {
587+
const ENV_KEY: &str = "DSTACK_AMD_KDS_PROXY_URL";
588+
let old = std::env::var(ENV_KEY).ok();
589+
std::env::set_var(ENV_KEY, "https://cors.litgateway.com/");
590+
591+
let wrapped = amd_kds_request_url("https://kdsintf.amd.com/vcek/v1/Genoa/cert_chain");
592+
593+
assert_eq!(
594+
wrapped,
595+
"https://cors.litgateway.com/https://kdsintf.amd.com/vcek/v1/Genoa/cert_chain"
596+
);
597+
if let Some(old) = old {
598+
std::env::set_var(ENV_KEY, old);
599+
} else {
600+
std::env::remove_var(ENV_KEY);
601+
}
602+
}
603+
574604
#[test]
575605
fn amd_kds_cert_chain_extracts_ask_pem_and_ark_pem() {
576606
let chain = b"-----BEGIN CERTIFICATE-----\nASK\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nARK\n-----END CERTIFICATE-----\n";

kms/src/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ pub(crate) struct SevSnpMeasureConfig {
3838
///
3939
/// Optional when callers provide OVMF section metadata with the request.
4040
pub ovmf_path: Option<String>,
41+
/// Optional diagnostic/cache proxy used for AMD KDS collateral requests.
42+
///
43+
/// Empty by default. When set, the KMS process exports the same proxy env
44+
/// used by dstack-attest before any attestation verification happens.
45+
#[serde(default)]
46+
pub amd_kds_proxy_url: Option<String>,
4147
/// SNP guest features bitmask used at launch. Defaults to SNP with kernel
4248
/// hashes enabled.
4349
#[serde(default = "default_guest_features")]

kms/src/main.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ fn record_attestation_metrics(req: &rocket::Request<'_>, res: &rocket::Response<
105105
.record_attestation_request(res.status().code >= 400);
106106
}
107107

108+
fn configure_amd_kds_proxy_from_config(config: &KmsConfig) {
109+
let Some(proxy_url) = config
110+
.sev_snp
111+
.as_ref()
112+
.and_then(|sev_snp| sev_snp.amd_kds_proxy_url.as_deref())
113+
.map(str::trim)
114+
.filter(|proxy_url| !proxy_url.is_empty())
115+
else {
116+
return;
117+
};
118+
std::env::set_var("DSTACK_AMD_KDS_PROXY_URL", proxy_url);
119+
info!("AMD SEV-SNP KDS proxy configured");
120+
}
121+
108122
#[rocket::main]
109123
async fn main() -> Result<()> {
110124
{
@@ -116,6 +130,7 @@ async fn main() -> Result<()> {
116130

117131
let figment = config::load_config_figment(args.config.as_deref());
118132
let config: KmsConfig = figment.focus("core").extract()?;
133+
configure_amd_kds_proxy_from_config(&config);
119134

120135
if config.onboard.enabled && !config.keys_exists() {
121136
info!("Onboarding");
@@ -137,6 +152,10 @@ async fn main() -> Result<()> {
137152
}
138153

139154
let pccs_url = config.pccs_url.clone();
155+
let amd_kds_proxy_url = config
156+
.sev_snp
157+
.as_ref()
158+
.and_then(|sev_snp| sev_snp.amd_kds_proxy_url.clone());
140159
let metrics_enabled = config.metrics.enabled;
141160
let state = main_service::KmsState::new(config).context("Failed to initialize KMS state")?;
142161
let figment = figment
@@ -164,7 +183,7 @@ async fn main() -> Result<()> {
164183
.mount("/", rocket::routes![metrics]);
165184
}
166185

167-
let verifier = QuoteVerifier::new(pccs_url);
186+
let verifier = QuoteVerifier::new_with_amd_kds_proxy(pccs_url, amd_kds_proxy_url);
168187
rocket = rocket.manage(verifier);
169188

170189
rocket

kms/src/main_service.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,7 @@ mod tests {
614614
fn sev_snp_config() -> SevSnpMeasureConfig {
615615
SevSnpMeasureConfig {
616616
ovmf_path: None,
617+
amd_kds_proxy_url: None,
617618
guest_features: 1,
618619
}
619620
}

kms/src/main_service/amd_attest.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,13 +1070,15 @@ mod tests {
10701070
fn config() -> SevSnpMeasureConfig {
10711071
SevSnpMeasureConfig {
10721072
ovmf_path: None,
1073+
amd_kds_proxy_url: None,
10731074
guest_features: 1,
10741075
}
10751076
}
10761077

10771078
fn config_with_path(path: &str) -> SevSnpMeasureConfig {
10781079
SevSnpMeasureConfig {
10791080
ovmf_path: Some(path.to_string()),
1081+
amd_kds_proxy_url: None,
10801082
guest_features: 1,
10811083
}
10821084
}
@@ -1803,6 +1805,7 @@ mod tests {
18031805
let err = validate_amd_snp_measurement_binding(
18041806
Some(&SevSnpMeasureConfig {
18051807
ovmf_path: None,
1808+
amd_kds_proxy_url: None,
18061809
guest_features: 0,
18071810
}),
18081811
&verified,

kms/src/onboard_service.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ mod tests {
205205
fn sev_snp_config() -> SevSnpMeasureConfig {
206206
SevSnpMeasureConfig {
207207
ovmf_path: None,
208+
amd_kds_proxy_url: None,
208209
guest_features: 1,
209210
}
210211
}

ra-rpc/src/rocket_helper.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ fn unix_peer_cred(stream: &UnixStream) -> Option<UnixPeerCred> {
184184
#[derive(Debug, Clone)]
185185
pub struct QuoteVerifier {
186186
pccs_url: Option<String>,
187+
amd_kds_proxy_url: Option<String>,
187188
}
188189

189190
pub mod deps {
@@ -316,7 +317,25 @@ impl<'r> FromRequest<'r> for &'r QuoteVerifier {
316317

317318
impl QuoteVerifier {
318319
pub fn new(pccs_url: Option<String>) -> Self {
319-
Self { pccs_url }
320+
Self::new_with_amd_kds_proxy(pccs_url, None)
321+
}
322+
323+
pub fn new_with_amd_kds_proxy(
324+
pccs_url: Option<String>,
325+
amd_kds_proxy_url: Option<String>,
326+
) -> Self {
327+
Self {
328+
pccs_url,
329+
amd_kds_proxy_url: amd_kds_proxy_url
330+
.map(|url| url.trim().to_string())
331+
.filter(|url| !url.is_empty()),
332+
}
333+
}
334+
335+
fn configure_amd_kds_proxy_for_request(&self) {
336+
if let Some(proxy_url) = &self.amd_kds_proxy_url {
337+
std::env::set_var("DSTACK_AMD_KDS_PROXY_URL", proxy_url);
338+
}
320339
}
321340
}
322341

@@ -440,6 +459,21 @@ mod tests {
440459
use rocket::tokio;
441460
use std::time::{SystemTime, UNIX_EPOCH};
442461

462+
#[test]
463+
fn quote_verifier_carries_trimmed_amd_kds_proxy_url() {
464+
let verifier = QuoteVerifier::new_with_amd_kds_proxy(
465+
None,
466+
Some(" https://cors.litgateway.com/ ".to_string()),
467+
);
468+
assert_eq!(
469+
verifier.amd_kds_proxy_url.as_deref(),
470+
Some("https://cors.litgateway.com/")
471+
);
472+
473+
let verifier = QuoteVerifier::new_with_amd_kds_proxy(None, Some(" ".to_string()));
474+
assert!(verifier.amd_kds_proxy_url.is_none());
475+
}
476+
443477
#[test]
444478
fn custom_unix_endpoint_maps_to_remote_endpoint() {
445479
let endpoint = Endpoint::new(UnixPeerEndpoint {
@@ -533,6 +567,7 @@ pub async fn handle_prpc_impl<S, Call: RpcCall<S>>(
533567
.flatten();
534568
let attestation = match (request.quote_verifier, attestation) {
535569
(Some(quote_verifier), Some(attestation)) => {
570+
quote_verifier.configure_amd_kds_proxy_for_request();
536571
let pubkey = request
537572
.certificate
538573
.context("certificate is missing")?

0 commit comments

Comments
 (0)