Skip to content

Commit cffdf2c

Browse files
committed
docs: add no-TEE swtpm development guide
1 parent f0d0e06 commit cffdf2c

2 files changed

Lines changed: 300 additions & 0 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ Setting up dstack on your own hardware? Start with the [self-hosted quick onboar
7373

7474
Building or customizing the guest OS itself? Follow the [guest-OS build guide](./docs/building-guest-os.md).
7575

76+
Developing without TEE hardware? Use a development image with
77+
[no-TEE mode and swtpm](./docs/development-without-tee.md).
78+
7679
## Architecture
7780

7881
![Architecture](./docs/assets/arch.png)

docs/development-without-tee.md

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
# Develop with dstack without TEE hardware
2+
3+
Development guest images can run dstack's normal guest setup on a KVM machine
4+
that has no TDX or SEV-SNP support. The VMM starts the guest with `no_tee`, the
5+
guest supplies the TDX ABI through `dstack-tee-simulator`, and `swtpm` provides
6+
persistent TPM-backed application keys. This is suitable for development and
7+
integration testing, not for production workloads or secrets.
8+
9+
This guide covers both a normal KVM host and a nested libvirt lab. The nested
10+
path was tested with a 16 GiB Ubuntu 24.04 VM, QEMU 8.2.2, swtpm 0.7.3, and a
11+
`dstack-dev-0.6.0` image built from commit `4d014d3b`.
12+
13+
## What this mode tests
14+
15+
The development simulator provides configfs-tsm reports, RTMR extension files,
16+
and a CCEL event log. Guest preparation, measurements, application key setup,
17+
encrypted persistent storage, the guest agent, and Docker Compose therefore
18+
use their normal code paths.
19+
20+
It does not provide hardware isolation or a valid hardware-signed quote. The
21+
host controls QEMU and swtpm, and a production verifier or KMS must reject the
22+
simulated quote.
23+
24+
## Build the development image on the host
25+
26+
TEE hardware is not needed for the build. Follow the prerequisites in
27+
[Build the dstack guest OS](building-guest-os.md), then run this from the
28+
repository root:
29+
30+
```bash
31+
make os-deps
32+
cd os/yocto/repro-build
33+
RELEASE_FLAVORS="prod dev" ./repro-build.sh -n
34+
```
35+
36+
The artifact used below is
37+
`os/yocto/repro-build/dist/dstack-dev-<version>.tar.gz`. Confirm that it really
38+
is a development image:
39+
40+
```bash
41+
mkdir -p /tmp/dstack-dev-image
42+
tar -xzf os/yocto/repro-build/dist/dstack-dev-*.tar.gz \
43+
-C /tmp/dstack-dev-image
44+
jq '{version, git_revision, is_dev}' \
45+
/tmp/dstack-dev-image/dstack-dev-*/metadata.json
46+
```
47+
48+
Expected output includes `"is_dev": true`. The VMM rejects `--no-tee` for a
49+
production image.
50+
51+
## Optional: create a nested libvirt host
52+
53+
Skip this section when the development machine already exposes `/dev/kvm`.
54+
The outer host must enable VT-x or AMD-V and load KVM with nested virtualization
55+
enabled.
56+
57+
Create an Ubuntu VM with host CPU passthrough. This example assumes an Ubuntu
58+
cloud image and cloud-init seed already exist:
59+
60+
```bash
61+
virt-install \
62+
--name dstack-no-tee-lab \
63+
--memory 16384 \
64+
--vcpus 8 \
65+
--cpu host-passthrough,cache.mode=passthrough \
66+
--disk path=/var/lib/libvirt/images/dstack-no-tee-lab.qcow2,bus=virtio \
67+
--disk path=/var/lib/libvirt/images/dstack-no-tee-lab-seed.img,device=cdrom \
68+
--os-variant ubuntu24.04 \
69+
--network network=default,model=virtio \
70+
--graphics none \
71+
--import --noautoconsole
72+
```
73+
74+
Add a virtio-vsock device. The dstack guest uses vsock to obtain its launch
75+
configuration from the VMM:
76+
77+
```bash
78+
cat >/tmp/dstack-vsock.xml <<'EOF'
79+
<vsock model="virtio"><cid auto="yes"/></vsock>
80+
EOF
81+
virsh attach-device dstack-no-tee-lab /tmp/dstack-vsock.xml --live --config
82+
```
83+
84+
Inside the Ubuntu VM, install the host dependencies and verify nested KVM:
85+
86+
```bash
87+
sudo apt-get update
88+
sudo apt-get install -y \
89+
qemu-system-x86 libvirt-daemon-system libvirt-clients swtpm swtpm-tools
90+
sudo usermod -aG kvm,libvirt "$USER"
91+
92+
test -r /dev/kvm && test -w /dev/kvm
93+
qemu-system-x86_64 --version
94+
swtpm --version
95+
lscpu | grep -E 'Virtualization|Hypervisor'
96+
```
97+
98+
Log out and back in after changing groups. A tested nested host reported both
99+
`Virtualization: VT-x` and `Hypervisor vendor: KVM`.
100+
101+
Copy the image built on the outer host into the nested host rather than
102+
rebuilding it there:
103+
104+
```bash
105+
scp os/yocto/repro-build/dist/dstack-dev-*.tar.gz ubuntu@LAB_HOST:/tmp/
106+
ssh ubuntu@LAB_HOST 'mkdir -p ~/.dstack-vmm/image && \
107+
tar -xzf /tmp/dstack-dev-*.tar.gz -C ~/.dstack-vmm/image'
108+
```
109+
110+
## Install and configure the VMM
111+
112+
Build the VMM and supervisor from the same checkout as the image:
113+
114+
```bash
115+
cargo build --manifest-path dstack/Cargo.toml --release \
116+
-p dstack-vmm -p supervisor
117+
```
118+
119+
Install both binaries and the CLI on the machine that will run QEMU. You can
120+
also run them directly from the checkout during development:
121+
122+
```bash
123+
install -Dm755 dstack/target/release/dstack-vmm ~/.local/bin/dstack-vmm
124+
install -Dm755 dstack/target/release/supervisor ~/.local/bin/supervisor
125+
install -Dm755 dstack/vmm/src/vmm-cli.py ~/.local/bin/dstack
126+
cp dstack/vmm/vmm.toml ~/.dstack-vmm/vmm.toml
127+
```
128+
129+
Set these values in `~/.dstack-vmm/vmm.toml`:
130+
131+
```toml
132+
[image]
133+
path = "/home/USER/.dstack-vmm/image"
134+
135+
[cvm]
136+
qemu_path = "/usr/bin/qemu-system-x86_64"
137+
138+
[cvm.networking]
139+
mode = "user"
140+
141+
[supervisor]
142+
exe = "/home/USER/.local/bin/supervisor"
143+
```
144+
145+
On bare metal, keep the default host API address `vsock:2`. An outer libvirt VM
146+
has its own non-2 local CID, so the nested setup must listen on any local CID:
147+
148+
```toml
149+
[host_api]
150+
address = "vsock:4294967295"
151+
port = 10000
152+
```
153+
154+
Start the VMM from a stable working directory because its default API socket is
155+
relative to that directory:
156+
157+
```bash
158+
mkdir -p ~/.dstack-vmm/run
159+
cd ~/.dstack-vmm/run
160+
dstack-vmm -c ../vmm.toml
161+
```
162+
163+
In another terminal, check that the development image is visible:
164+
165+
```bash
166+
cd ~/.dstack-vmm/run
167+
dstack lsimage --json | jq '.[] | {name, version, is_dev}'
168+
```
169+
170+
## Launch with no TEE and swtpm
171+
172+
Create a small stateful workload:
173+
174+
```yaml
175+
# docker-compose.yml
176+
services:
177+
state-test:
178+
image: alpine:3.20
179+
command:
180+
- sh
181+
- -c
182+
- |
183+
date -Iseconds >> /data/boots
184+
touch /data/persistent-marker
185+
sleep infinity
186+
volumes:
187+
- state-data:/data
188+
volumes:
189+
state-data: {}
190+
```
191+
192+
Convert it to app-compose JSON and select the TPM key provider:
193+
194+
```bash
195+
dstack compose \
196+
--name swtpm-persistence \
197+
--docker-compose docker-compose.yml \
198+
--key-provider tpm \
199+
--public-logs \
200+
--public-sysinfo \
201+
--output app-compose.json
202+
```
203+
204+
Deploy with the development image, no KMS, and no TEE:
205+
206+
```bash
207+
dstack deploy \
208+
--name swtpm-persistence \
209+
--image dstack-dev-<version> \
210+
--compose app-compose.json \
211+
--vcpu 2 --memory 3G --disk 10G \
212+
--no-tee
213+
```
214+
215+
Successful output contains a VM ID. `dstack info VM_ID` should eventually show
216+
`Boot Progress: done`. The QEMU command line should contain the swtpm frontend
217+
and no TDX guest object:
218+
219+
```bash
220+
ps aux | grep '[q]emu-system' | grep -E -- '-tpmdev|tpm-tis'
221+
ps aux | grep '[s]wtpm socket'
222+
```
223+
224+
The VMM stores both durable components below the VM work directory:
225+
226+
```text
227+
~/.dstack-vmm/vm/VM_ID/hda.img
228+
~/.dstack-vmm/vm/VM_ID/swtpm/tpm2-00.permall
229+
```
230+
231+
Do not delete either path if the VM must restart with the same state.
232+
233+
## Verify encrypted storage and restart persistence
234+
235+
First check the guest preparation log:
236+
237+
```bash
238+
dstack logs VM_ID -n 300 | grep -E \
239+
'Generating app keys from TPM|Filesystem options|LUKS2 header|Device mapper'
240+
```
241+
242+
A successful first boot includes output like:
243+
244+
```text
245+
Generating app keys from TPM
246+
Filesystem options: encryption=true, filesystem=Zfs
247+
Mounting tmpfs for in-memory LUKS header
248+
Loading the LUKS2 header
249+
Device mapper /dev/mapper/dstack_data_disk is ready
250+
```
251+
252+
The LUKS2 header is kept in memory rather than written beside the ciphertext,
253+
so `cryptsetup luksDump hda.img` is not an applicable test. The log establishes
254+
that the guest opened `/dev/mapper/dstack_data_disk` and placed ZFS on that
255+
encrypted mapping rather than on the raw data partition.
256+
257+
Record the instance ID, then perform a graceful restart:
258+
259+
```bash
260+
dstack info VM_ID | grep 'Instance ID'
261+
dstack stop VM_ID
262+
dstack start VM_ID
263+
```
264+
265+
Wait for `Boot Progress: done`, then inspect the recent events and boot log:
266+
267+
```bash
268+
dstack info VM_ID
269+
dstack logs VM_ID -n 300 | grep -E \
270+
'mounting data disk|Loading the LUKS2 header|Device mapper|Container .* Started'
271+
```
272+
273+
The restart passes when all of the following hold:
274+
275+
1. the instance ID is unchanged;
276+
2. the event stream says `mounting data disk`, not `initializing data disk`;
277+
3. the LUKS mapping becomes ready without reformatting the disk;
278+
4. Docker starts the existing container and volume, and
279+
`/data/persistent-marker` and the earlier entries in `/data/boots` remain.
280+
281+
In the nested test, the same instance ID
282+
`cbcc237cdf1f8134434f374988e776c7ddea2e07` appeared before and after restart.
283+
The second boot reported `mounting data disk`, loaded the detached LUKS2
284+
header, opened `/dev/mapper/dstack_data_disk`, and started the existing
285+
`dstack-state-test-1` container.
286+
287+
## Common failures
288+
289+
`tpm key provider requested but swtpm is not installed` means `swtpm` is not on
290+
the VMM user's `PATH`. If QEMU reports that KVM is unavailable, confirm that
291+
the outer hypervisor uses host CPU passthrough and that `/dev/kvm` is writable
292+
inside the Ubuntu VM.
293+
294+
`Failed to bind host API: Cannot assign requested address` in a nested host
295+
usually means the VMM tried to bind `vsock:2`. Add a virtio-vsock device to the
296+
outer VM and use `vsock:4294967295` for the nested VMM. Do not make that change
297+
on a normal bare-metal host without a reason.

0 commit comments

Comments
 (0)