Skip to content

Commit 39bf843

Browse files
committed
feat(vmm): pass a launch ticket to the guest without returning it
Signed-off-by: Kevin Wang <wy721@qq.com>
1 parent 9949c47 commit 39bf843

8 files changed

Lines changed: 62 additions & 0 deletions

File tree

‎dstack/vmm/rpc/proto/vmm_rpc.proto‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ message VmConfiguration {
151151
// it with the VM and returns it here; it never interprets it, passes it to
152152
// the guest, or measures it.
153153
map<string, string> annotations = 23;
154+
// Launch ticket the guest presents to the KMS, whose hash the app's
155+
// contract must allow (see dstack/kms/README.md#launch-tickets). The VMM
156+
// passes it to the guest's sys-config and never returns it.
157+
string launch_ticket = 25;
154158
}
155159

156160
// Per-VM networking configuration.
@@ -246,6 +250,8 @@ message UpdateVmRequest {
246250
// Optional update annotations, replacing them all.
247251
bool update_annotations = 21;
248252
map<string, string> annotations = 22;
253+
// Optional update launch ticket; an empty one removes it.
254+
optional string launch_ticket = 25;
249255
}
250256

251257
// Parameters for listing CVMs with pagination and filtering.

‎dstack/vmm/src/app.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ pub struct Manifest {
147147
/// Opaque metadata for host-side tooling; see `VmConfiguration.annotations`.
148148
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
149149
pub annotations: BTreeMap<String, String>,
150+
/// Written to the guest's sys-config, never returned by the RPCs.
151+
#[serde(default, skip_serializing_if = "Option::is_none")]
152+
pub launch_ticket: Option<String>,
150153
}
151154

152155
impl Manifest {
@@ -2152,6 +2155,9 @@ pub(crate) fn make_sys_config(
21522155
if cfg.cvm.guest_agent_api != Default::default() {
21532156
sys_config["guest_agent_api"] = serde_json::to_value(&cfg.cvm.guest_agent_api)?;
21542157
}
2158+
if let Some(ticket) = &manifest.launch_ticket {
2159+
sys_config["launch_ticket"] = ticket.as_str().into();
2160+
}
21552161
// No attestation trust anchor is ever written here. Simulated deployments
21562162
// receive only the development seed through `.tee-simulator.json`; the
21572163
// in-guest simulator derives the matching roots itself and publishes them
@@ -3675,6 +3681,7 @@ mod tests {
36753681
volumes: vec![],
36763682
disk_prealloc: DiskPrealloc::Off,
36773683
annotations: Default::default(),
3684+
launch_ticket: None,
36783685
}
36793686
}
36803687

@@ -4052,6 +4059,7 @@ mod tests {
40524059
volumes: vec![],
40534060
disk_prealloc: DiskPrealloc::Off,
40544061
annotations: Default::default(),
4062+
launch_ticket: Some("t".repeat(32)),
40554063
};
40564064

40574065
let mr_config = MrConfigV3::new(
@@ -4113,6 +4121,12 @@ mod tests {
41134121
.as_str()
41144122
.context("vm_config must be a string")?,
41154123
)?;
4124+
// The ticket reaches the guest, but never vm_config, which attestations embed.
4125+
assert_eq!(sys_config["launch_ticket"], "t".repeat(32));
4126+
assert!(!sys_config["vm_config"]
4127+
.as_str()
4128+
.unwrap()
4129+
.contains(&"t".repeat(32)));
41164130
let measurement_document = vm_config["sev_snp_measurement"]
41174131
.as_str()
41184132
.context("sev_snp_measurement must be a string")?;

‎dstack/vmm/src/app/qemu.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,7 @@ mod tests {
14841484
}],
14851485
disk_prealloc: DiskPrealloc::Off,
14861486
annotations: Default::default(),
1487+
launch_ticket: None,
14871488
},
14881489
image: Image {
14891490
info: ImageInfo {

‎dstack/vmm/src/app/vm_info.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ impl VmInfo {
245245
networking: configured_networking,
246246
networks: configured_networks,
247247
annotations: self.manifest.annotations.clone().into_iter().collect(),
248+
// A secret the VMM passes to the guest and never returns.
249+
launch_ticket: String::new(),
248250
})
249251
},
250252
app_url: self

‎dstack/vmm/src/main_service.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,9 +392,20 @@ pub fn create_manifest_from_vm_config(
392392
volumes,
393393
disk_prealloc,
394394
annotations: annotations_from_proto(&request.annotations)?,
395+
launch_ticket: launch_ticket_from_proto(&request.launch_ticket)?,
395396
})
396397
}
397398

399+
/// Refuse a ticket the KMS would refuse, so the mistake surfaces here rather
400+
/// than at boot. Empty means none.
401+
fn launch_ticket_from_proto(ticket: &str) -> Result<Option<String>> {
402+
if ticket.is_empty() {
403+
return Ok(None);
404+
}
405+
dstack_types::authorization::check_launch_ticket(ticket).map_err(anyhow::Error::msg)?;
406+
Ok(Some(ticket.to_string()))
407+
}
408+
398409
fn annotations_from_proto(
399410
annotations: &HashMap<String, String>,
400411
) -> Result<BTreeMap<String, String>> {
@@ -1185,6 +1196,9 @@ impl VmmRpc for RpcHandler {
11851196
if request.update_gateway_urls {
11861197
manifest.gateway_urls = request.gateway_urls.clone();
11871198
}
1199+
if let Some(ticket) = &request.launch_ticket {
1200+
manifest.launch_ticket = launch_ticket_from_proto(ticket)?;
1201+
}
11881202
if request.update_annotations {
11891203
manifest.annotations = annotations_from_proto(&request.annotations)?;
11901204
}
@@ -1572,6 +1586,7 @@ mod tests {
15721586
networks: vec![],
15731587
disk_prealloc: None,
15741588
annotations: Default::default(),
1589+
launch_ticket: String::new(),
15751590
}
15761591
}
15771592

‎dstack/vmm/ui/src/components/CreateVmDialog.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,11 @@ const CreateVmDialogComponent = {
197197
<textarea id="userConfig" v-model="form.user_config" placeholder="Optional user config placed at /dstack/.user-config in the CVM"></textarea>
198198
</div>
199199
200+
<div class="form-group full-width">
201+
<label for="launchTicket">Launch Ticket</label>
202+
<input id="launchTicket" type="password" autocomplete="off" v-model="form.launch_ticket" placeholder="Optional: presented to the KMS; its hash must be allowed by the app contract">
203+
</div>
204+
200205
<div class="form-group full-width">
201206
<label for="annotations">Annotations</label>
202207
<textarea id="annotations" v-model="form.annotations" placeholder="Optional KEY=VALUE per line, for host-side tooling; never passed to the CVM"></textarea>

‎dstack/vmm/ui/src/components/UpdateVmDialog.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,11 @@ const UpdateVmDialogComponent = {
259259
<textarea id="upgradeUserConfig" v-model="dialog.user_config" placeholder="Optional: User config to be put at /dstack/.user-config in the CVM"></textarea>
260260
</div>
261261
262+
<div class="form-group">
263+
<label for="upgradeLaunchTicket">Launch Ticket</label>
264+
<input id="upgradeLaunchTicket" type="password" autocomplete="off" v-model="dialog.launch_ticket" placeholder="Optional: replace the launch ticket; leave empty to keep it">
265+
</div>
266+
262267
<div class="dialog-footer">
263268
<button class="action-btn primary" @click="$emit('submit')">Update</button>
264269
<button class="action-btn" @click="$emit('close')">Cancel</button>

‎dstack/vmm/ui/src/composables/useVmManager.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ type VmFormState = {
192192
networks: NetworkFormEntry[];
193193
user_config: string;
194194
annotations: string;
195+
launch_ticket: string;
195196
kms_urls: string[];
196197
gateway_urls: string[];
197198
stopped: boolean;
@@ -223,6 +224,7 @@ type UpdateDialogState = {
223224
updateNetworking: boolean;
224225
networks: NetworkFormEntry[];
225226
user_config: string;
227+
launch_ticket: string;
226228
};
227229

228230
type CloneConfigDialogState = {
@@ -285,6 +287,7 @@ function createVmFormState(preLaunchScript: string): VmFormState {
285287
networks: [],
286288
user_config: '',
287289
annotations: '',
290+
launch_ticket: '',
288291
kms_urls: [],
289292
gateway_urls: [],
290293
stopped: false,
@@ -318,6 +321,7 @@ function createUpdateDialogState(): UpdateDialogState {
318321
updateNetworking: false,
319322
networks: [],
320323
user_config: '',
324+
launch_ticket: '',
321325
};
322326
}
323327

@@ -658,6 +662,7 @@ type CreateVmPayloadSource = {
658662
gateway_urls?: string[];
659663
stopped?: boolean;
660664
annotations?: Record<string, string>;
665+
launch_ticket?: string;
661666
};
662667

663668
function buildCreateVmPayload(source: CreateVmPayloadSource): VmmTypes.IVmConfiguration {
@@ -687,6 +692,7 @@ type CreateVmPayloadSource = {
687692
gateway_urls: source.gateway_urls?.filter((url) => url && url.trim().length) ?? [],
688693
stopped: !!source.stopped,
689694
annotations: source.annotations ?? {},
695+
launch_ticket: source.launch_ticket?.trim() || undefined,
690696
};
691697
}
692698

@@ -1202,6 +1208,7 @@ type CreateVmPayloadSource = {
12021208
updateNetworking: false,
12031209
networks: cloneNetworks(config),
12041210
user_config: config.user_config || '',
1211+
launch_ticket: '',
12051212
};
12061213
}
12071214

@@ -1300,6 +1307,7 @@ type CreateVmPayloadSource = {
13001307
gateway_urls: vmForm.value.gateway_urls,
13011308
stopped: vmForm.value.stopped,
13021309
annotations: parseAnnotations(vmForm.value.annotations),
1310+
launch_ticket: vmForm.value.launch_ticket,
13031311
});
13041312

13051313
await vmmRpc.createVm(payload);
@@ -1378,6 +1386,10 @@ type CreateVmPayloadSource = {
13781386
body.compose_file = composeNeedsUpdate ? await makeUpdateComposeFile() : undefined;
13791387
Object.assign(body, encryptedEnvPayload);
13801388
body.user_config = updated.user_config;
1389+
// The VMM never returns the ticket, so only a new one is sent.
1390+
if (updated.launch_ticket.trim()) {
1391+
body.launch_ticket = updated.launch_ticket.trim();
1392+
}
13811393
body.update_ports = true;
13821394
body.ports = normalizePorts(updated.ports);
13831395
body.gpus = updateDialog.value.updateGpuConfig ? configGpu(updated, true) : undefined;
@@ -1458,6 +1470,8 @@ type CreateVmPayloadSource = {
14581470
simulated_tee: config.simulated_tee || '',
14591471
user_config: config.user_config || '',
14601472
annotations: annotationsText(config.annotations),
1473+
// A clone is a new deployment and needs its own ticket.
1474+
launch_ticket: '',
14611475
stopped: !!config.stopped,
14621476
event_log_version: theVm.appCompose?.event_log_version || 1,
14631477
};

0 commit comments

Comments
 (0)