Skip to content

Commit f2ce587

Browse files
committed
feat(vmm): select TEE simulator per instance
Signed-off-by: Kevin Wang <wy721@qq.com>
1 parent 717cc3a commit f2ce587

11 files changed

Lines changed: 179 additions & 31 deletions

File tree

docs/development-without-tee.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ qemu_path = "/usr/bin/qemu-system-x86_64"
8989
mode = "user"
9090

9191
[cvm.tee_simulator]
92-
platform = "dstack-tdx"
9392
# Development credential only. Use a different random 32-byte hex seed for
9493
# each isolated test environment; never use it for production secrets.
9594
mock_attestation_seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
@@ -99,9 +98,11 @@ collateral_base_url = "http://10.0.2.2:18088"
9998
exe = "/home/USER/.local/bin/supervisor"
10099
```
101100

102-
The VMM writes these settings to the guest's development-only
103-
`.tee-simulator.json`. The guest starts the simulator only when that file is
104-
present in the host share.
101+
These node-local settings provide development credentials and collateral URLs;
102+
they do not enable simulation globally. Each deployment selects its simulated
103+
platform with `--simulated-tee`. The VMM then writes an instance-specific
104+
`.tee-simulator.json`, and the guest starts the simulator only when that file
105+
is present in the host share.
105106

106107
Start the VMM from a stable working directory because its default API socket is
107108
relative to that directory:
@@ -153,15 +154,16 @@ dstack compose \
153154
--output app-compose.json
154155
```
155156

156-
Deploy with the development image, no KMS, and no TEE:
157+
Deploy with the development image, no KMS, and an instance-specific simulated
158+
TEE platform:
157159

158160
```bash
159161
dstack deploy \
160162
--name swtpm-persistence \
161163
--image dstack-dev-<version> \
162164
--compose app-compose.json \
163165
--vcpu 2 --memory 3G --disk 10G \
164-
--no-tee
166+
--simulated-tee dstack-tdx
165167
```
166168

167169
Successful output contains a VM ID. `dstack info VM_ID` should eventually show

dstack/vmm/rpc/proto/vmm_rpc.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ message VmConfiguration {
119119
// Per-VM networking overrides. If set, wins over singular networking.
120120
repeated NetworkingConfig networks = 19;
121121
reserved 20;
122+
// Development-only TEE ABI to simulate for this VM. When set, the VMM
123+
// launches a non-TEE VM and provides an instance-specific simulator config.
124+
optional string simulated_tee = 21;
122125
}
123126

124127
// Per-VM networking configuration.

dstack/vmm/src/app.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ pub struct Manifest {
110110
pub gateway_urls: Vec<String>,
111111
#[serde(default)]
112112
pub no_tee: bool,
113+
#[serde(default, skip_serializing_if = "Option::is_none")]
114+
pub simulated_tee: Option<dstack_types::TeeVariant>,
113115
#[serde(default, skip_serializing_if = "Vec::is_empty")]
114116
pub networks: Vec<Networking>,
115117
#[serde(default)]
@@ -1108,7 +1110,8 @@ impl App {
11081110
)?;
11091111
fs::write(shared_dir.join(SYS_CONFIG), &sys_config_str)
11101112
.context("Failed to write vm config")?;
1111-
sync_tee_simulator_config(&shared_dir, cfg.cvm.tee_simulator.as_ref(), &sys_config_str)?;
1113+
let simulator_config = simulator_config_for_manifest(&self.config.cvm, &manifest)?;
1114+
sync_tee_simulator_config(&shared_dir, simulator_config.as_ref(), &sys_config_str)?;
11121115
Ok(())
11131116
}
11141117

@@ -1244,7 +1247,22 @@ fn rotate_serial_log(work_dir: &VmWorkDir, max_bytes: u64) {
12441247
}
12451248
}
12461249

1247-
fn sync_tee_simulator_config(
1250+
pub(crate) fn simulator_config_for_manifest(
1251+
cvm: &crate::config::CvmConfig,
1252+
manifest: &Manifest,
1253+
) -> Result<Option<dstack_types::TeeSimulatorConfig>> {
1254+
let Some(platform) = manifest.simulated_tee else {
1255+
return Ok(None);
1256+
};
1257+
let mut config = cvm
1258+
.tee_simulator
1259+
.clone()
1260+
.context("tee simulator credentials are not configured on this VMM")?;
1261+
config.platform = platform;
1262+
Ok(Some(config))
1263+
}
1264+
1265+
pub(crate) fn sync_tee_simulator_config(
12481266
shared_dir: &Path,
12491267
simulator_config: Option<&dstack_types::TeeSimulatorConfig>,
12501268
sys_config: &str,
@@ -1520,6 +1538,28 @@ mod tests {
15201538
Ok(())
15211539
}
15221540

1541+
#[test]
1542+
fn instance_platform_overrides_node_simulator_template() -> Result<()> {
1543+
let mut config = test_tdx_config()?;
1544+
config.cvm.tee_simulator = Some(dstack_types::TeeSimulatorConfig {
1545+
platform: dstack_types::TeeVariant::DstackTdx,
1546+
mock_attestation_seed: Some("11".repeat(32)),
1547+
..Default::default()
1548+
});
1549+
let mut manifest = test_manifest(2048);
1550+
assert!(simulator_config_for_manifest(&config.cvm, &manifest)?.is_none());
1551+
manifest.simulated_tee = Some(dstack_types::TeeVariant::DstackNitroEnclave);
1552+
1553+
let resolved = simulator_config_for_manifest(&config.cvm, &manifest)?
1554+
.context("simulator config should be enabled")?;
1555+
1556+
assert_eq!(
1557+
resolved.platform,
1558+
dstack_types::TeeVariant::DstackNitroEnclave
1559+
);
1560+
Ok(())
1561+
}
1562+
15231563
#[test]
15241564
fn gpu_config_has_gpus_only_when_resolved_gpu_list_is_non_empty() {
15251565
assert!(!GpuConfig::default().has_gpus());
@@ -1682,6 +1722,7 @@ mod tests {
16821722
kms_urls: vec![],
16831723
gateway_urls: vec![],
16841724
no_tee: false,
1725+
simulated_tee: None,
16851726
networks: vec![],
16861727
volumes: vec![],
16871728
}
@@ -1969,6 +2010,7 @@ mod tests {
19692010
kms_urls: vec![],
19702011
gateway_urls: vec![],
19712012
no_tee: false,
2013+
simulated_tee: None,
19722014
networks: vec![],
19732015
volumes: vec![],
19742016
};

dstack/vmm/src/app/qemu.rs

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ use crate::{
1919
};
2020
use anyhow::{bail, Context, Result};
2121
use bon::Builder;
22-
use dstack_types::{
23-
shared_filenames::HOST_SHARED_DISK_LABEL, KeyProviderKind, TeeSimulatorConfig, TeeVariant,
24-
};
22+
use dstack_types::{shared_filenames::HOST_SHARED_DISK_LABEL, KeyProviderKind, TeeVariant};
2523
use fs_err as fs;
2624
use nix::unistd::User;
2725
use serde::Serialize;
@@ -179,15 +177,16 @@ struct PreparedVolume {
179177
source: String,
180178
}
181179

182-
fn needs_qemu_swtpm(key_provider: KeyProviderKind, simulator: Option<&TeeSimulatorConfig>) -> bool {
183-
if !matches!(key_provider, KeyProviderKind::Tpm) {
184-
return false;
185-
}
186-
!matches!(
187-
simulator.map(|config| config.platform),
180+
fn simulated_platform_provides_tpm(platform: Option<TeeVariant>) -> bool {
181+
matches!(
182+
platform,
188183
Some(TeeVariant::DstackGcpTdx | TeeVariant::DstackAwsNitroTpm)
189184
)
190185
}
186+
187+
fn needs_qemu_swtpm(key_provider: KeyProviderKind, simulated_tee: Option<TeeVariant>) -> bool {
188+
matches!(key_provider, KeyProviderKind::Tpm) && !simulated_platform_provides_tpm(simulated_tee)
189+
}
191190
struct PreparedQemuLaunch {
192191
workdir: VmWorkDir,
193192
platform: CvmPlatform,
@@ -246,7 +245,7 @@ impl PreparedQemuLaunch {
246245
None
247246
};
248247
let (swtpm_socket, swtpm_path) =
249-
if needs_qemu_swtpm(app_compose.key_provider(), cfg.tee_simulator.as_ref()) {
248+
if needs_qemu_swtpm(app_compose.key_provider(), vm.manifest.simulated_tee) {
250249
let swtpm_path = which::which("swtpm")
251250
.context("tpm key provider requested but swtpm is not installed")?;
252251
let state_dir = workdir.swtpm_state_dir();
@@ -989,20 +988,19 @@ mod tests {
989988
use crate::app::image::{Image, ImageInfo};
990989
use crate::app::{GpuConfig, Manifest, PortMapping, VmVolume, VmWorkDir};
991990
use crate::config::{Config, CvmPlatform, Protocol, DEFAULT_CONFIG};
992-
use dstack_types::{KeyProviderKind, TeeSimulatorConfig, TeeVariant};
991+
use dstack_types::{KeyProviderKind, TeeVariant};
993992

994993
#[test]
995994
fn qemu_swtpm_is_omitted_when_simulator_provides_the_tpm() {
996995
for platform in [TeeVariant::DstackGcpTdx, TeeVariant::DstackAwsNitroTpm] {
997-
let simulator = TeeSimulatorConfig {
998-
platform,
999-
..Default::default()
1000-
};
1001-
assert!(!needs_qemu_swtpm(KeyProviderKind::Tpm, Some(&simulator)));
996+
assert!(!needs_qemu_swtpm(KeyProviderKind::Tpm, Some(platform)));
997+
assert!(!needs_qemu_swtpm(KeyProviderKind::Kms, Some(platform)));
1002998
}
1003999

1004-
let tdx = TeeSimulatorConfig::default();
1005-
assert!(needs_qemu_swtpm(KeyProviderKind::Tpm, Some(&tdx)));
1000+
assert!(needs_qemu_swtpm(
1001+
KeyProviderKind::Tpm,
1002+
Some(TeeVariant::DstackTdx)
1003+
));
10061004
assert!(needs_qemu_swtpm(KeyProviderKind::Tpm, None));
10071005
assert!(!needs_qemu_swtpm(KeyProviderKind::Kms, None));
10081006
}
@@ -1070,6 +1068,7 @@ mod tests {
10701068
kms_urls: vec![],
10711069
gateway_urls: vec![],
10721070
no_tee: true,
1071+
simulated_tee: None,
10731072
networks: vec![],
10741073
volumes: vec![VmVolume {
10751074
source: "/does-not-exist/volume.img".into(),

dstack/vmm/src/app/vm_info.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ impl VmInfo {
167167
gateway_urls: custom_gateway_urls.clone(),
168168
stopped,
169169
no_tee,
170+
simulated_tee: self
171+
.manifest
172+
.simulated_tee
173+
.map(|platform| platform.as_str().to_string()),
170174
networking: configured_networking,
171175
networks: configured_networks,
172176
})

dstack/vmm/src/config.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,8 @@ pub struct CvmConfig {
260260
/// The URL of the PCCS server
261261
#[serde(default)]
262262
pub pccs_url: String,
263-
/// Development-image simulator configuration copied into guest sys-config.
263+
/// Node-local credentials and collateral settings used when an individual
264+
/// VM requests a simulated TEE platform.
264265
#[serde(default)]
265266
pub tee_simulator: Option<dstack_types::TeeSimulatorConfig>,
266267
/// Optional NVIDIA OCSP/RIM cache passed to guests in sys-config.

dstack/vmm/src/main_service.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,16 @@ pub fn create_manifest_from_vm_config(
189189
dstack_types::validate_verity_volumes(&verity_volumes).map_err(anyhow::Error::msg)?;
190190
let volumes = resolve_volumes(&verity_volumes, cvm_config)?;
191191

192+
let simulated_tee = request
193+
.simulated_tee
194+
.as_deref()
195+
.map(str::parse)
196+
.transpose()
197+
.map_err(anyhow::Error::msg)?;
198+
if simulated_tee.is_some() && cvm_config.tee_simulator.is_none() {
199+
bail!("tee simulator credentials are not configured on this VMM");
200+
}
201+
192202
Ok(Manifest {
193203
id,
194204
name: request.name.clone(),
@@ -204,7 +214,8 @@ pub fn create_manifest_from_vm_config(
204214
gpus: Some(gpus),
205215
kms_urls: request.kms_urls.clone(),
206216
gateway_urls: request.gateway_urls.clone(),
207-
no_tee: request.no_tee,
217+
no_tee: request.no_tee || simulated_tee.is_some(),
218+
simulated_tee,
208219
networks: networks_from_vm_config(&request, cvm_config)?,
209220
volumes,
210221
})
@@ -941,6 +952,7 @@ mod tests {
941952
gateway_urls: vec![],
942953
stopped: false,
943954
no_tee: false,
955+
simulated_tee: None,
944956
networking: None,
945957
networks: vec![],
946958
}
@@ -954,6 +966,47 @@ mod tests {
954966
assert!(manifest.networks.is_empty());
955967
}
956968

969+
#[test]
970+
fn simulated_tee_is_selected_per_instance_and_implies_no_tee() {
971+
let mut request = test_vm_configuration();
972+
request.simulated_tee = Some("dstack-amd-sev-snp".into());
973+
let mut config = test_cvm_config();
974+
config.tee_simulator = Some(dstack_types::TeeSimulatorConfig {
975+
mock_attestation_seed: Some("11".repeat(32)),
976+
..Default::default()
977+
});
978+
979+
let manifest = create_manifest_from_vm_config(request, &config).unwrap();
980+
981+
assert_eq!(
982+
manifest.simulated_tee,
983+
Some(dstack_types::TeeVariant::DstackAmdSevSnp)
984+
);
985+
assert!(manifest.no_tee);
986+
}
987+
988+
#[test]
989+
fn simulated_tee_requires_node_credentials() {
990+
let mut request = test_vm_configuration();
991+
request.simulated_tee = Some("dstack-tdx".into());
992+
993+
let err = create_manifest_from_vm_config(request, &test_cvm_config()).unwrap_err();
994+
995+
assert!(err
996+
.to_string()
997+
.contains("tee simulator credentials are not configured"));
998+
}
999+
1000+
#[test]
1001+
fn invalid_simulated_tee_is_rejected() {
1002+
let mut request = test_vm_configuration();
1003+
request.simulated_tee = Some("not-a-platform".into());
1004+
1005+
let err = create_manifest_from_vm_config(request, &test_cvm_config()).unwrap_err();
1006+
1007+
assert!(err.to_string().contains("unsupported TEE variant"));
1008+
}
1009+
9571010
#[test]
9581011
fn volume_extraction_keeps_other_compose_fields_opaque() -> Result<()> {
9591012
assert!(extract_verity_volumes("not json")?.is_empty());

dstack/vmm/src/one_shot.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

5-
use crate::app::{make_sys_config, Image, VmConfig, VmWorkDir};
5+
use crate::app::{
6+
make_sys_config, simulator_config_for_manifest, sync_tee_simulator_config, Image, VmConfig,
7+
VmWorkDir,
8+
};
69
use crate::config::Config;
710
use crate::main_service;
811
use anyhow::{Context, Result};
@@ -265,7 +268,13 @@ Compose file content (first 200 chars):
265268
app_compose.requirements.as_ref(),
266269
)?;
267270
let sys_config_path = vm_work_dir.shared_dir().join(".sys-config.json");
268-
fs_err::write(&sys_config_path, sys_config_str).context("Failed to write sys config")?;
271+
fs_err::write(&sys_config_path, &sys_config_str).context("Failed to write sys config")?;
272+
let simulator_config = simulator_config_for_manifest(&config.cvm, &manifest)?;
273+
sync_tee_simulator_config(
274+
&vm_work_dir.shared_dir(),
275+
simulator_config.as_ref(),
276+
&sys_config_str,
277+
)?;
269278

270279
// Create vm-state.json with initial state
271280
vm_work_dir

dstack/vmm/src/vmm-cli.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,8 @@ def create_vm(self, args) -> None:
901901
"stopped": args.stopped,
902902
"no_tee": args.no_tee,
903903
}
904+
if args.simulated_tee:
905+
params["simulated_tee"] = args.simulated_tee
904906
if args.swap is not None:
905907
swap_bytes = max(0, int(round(args.swap)) * 1024 * 1024)
906908
if swap_bytes > 0:
@@ -1810,6 +1812,17 @@ def _patched_format_help():
18101812
help="Force-enable Intel TDX (default)",
18111813
)
18121814
deploy_parser.set_defaults(no_tee=False)
1815+
deploy_parser.add_argument(
1816+
"--simulated-tee",
1817+
choices=[
1818+
"dstack-tdx",
1819+
"dstack-gcp-tdx",
1820+
"dstack-amd-sev-snp",
1821+
"dstack-nitro-enclave",
1822+
"dstack-aws-nitro-tpm",
1823+
],
1824+
help="Simulate the selected TEE ABI for this VM (development images only)",
1825+
)
18131826
deploy_parser.add_argument(
18141827
"--net",
18151828
choices=["bridge", "user"],

0 commit comments

Comments
 (0)