Skip to content

Commit 067d551

Browse files
committed
Merge remote-tracking branch 'origin/master' into codex/external-attestation-root-ca
# Conflicts: # dstack/Cargo.lock # dstack/kms/Cargo.toml # dstack/test-scripts/snp-e2e-smoke.sh
2 parents 3ebb6b6 + 6944db8 commit 067d551

63 files changed

Lines changed: 2093 additions & 712 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
- shared API authentication (`dstack-api-auth`) protecting the full VMM HTTP/pRPC/UI surface and unifying Gateway/KMS admin auth: bearer/`X-Admin-Token`/HTTP Basic/bcrypt htpasswd, constant-time verification (#796)
12+
813
## [0.5.5] - 2025-10-20
914

1015
### Added

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/deployment.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ reuse = true
7474
image_path = "./images"
7575
run_path = "./run/vm"
7676

77+
[auth]
78+
enabled = true
79+
# generate with: openssl rand -hex 32
80+
tokens = ["<paste output of: openssl rand -hex 32>"]
81+
7782
[cvm]
7883
kms_urls = []
7984
gateway_urls = []
@@ -93,6 +98,10 @@ address = "vsock:2"
9398
port = 10000
9499
```
95100

101+
> **VMM API authentication (required for non-localhost binds).** Since [PR #796](https://github.com/Dstack-TEE/dstack/pull/796), the `[auth]` token guards the *entire* VMM surface — creating and stopping CVMs, the web UI, and all pRPC calls — not just `/logs`. Because the example above binds `tcp:0.0.0.0:9080` (reachable off the host), you MUST enable `[auth]`; otherwise the whole control API is exposed unauthenticated. Auth is fail-closed: with `enabled = true` and no usable credential, requests are rejected.
102+
>
103+
> Clients authenticate with `Authorization: Bearer <token>` (or the `X-Admin-Token: <token>` header). Instead of inline `tokens`, you can point to an Apache bcrypt htpasswd file with `htpasswd_file = "/etc/dstack/admin.htpasswd"` (create it with `htpasswd -B -c /etc/dstack/admin.htpasswd admin`). Only bcrypt (`-B`) entries are accepted.
104+
96105
Download guest images from [dstack guest-OS releases](https://github.com/Dstack-TEE/dstack/releases) and extract to `./images/`.
97106

98107
> For reproducible builds and verification, see the [Security Model](./security/security-model.md).
@@ -113,6 +122,8 @@ Production KMS requires:
113122

114123
#### Auth Server Options
115124

125+
> **Note:** the boot-authorization webhook below (auth-simple / auth-eth) is a *different* mechanism from the KMS admin-API authentication. The webhook allowlists which CVMs may boot and receive keys; the admin API (`[core.admin]` in `kms.toml`) guards operator RPCs. See [dstack-kms admin authentication](../dstack/kms/README.md#admin-api-authentication).
126+
116127
| Server | Use Case | Configuration |
117128
|--------|----------|---------------|
118129
| [auth-simple](../dstack/kms/auth-simple/) | Config-file-based whitelisting | JSON config file |
@@ -428,6 +439,8 @@ curl http://<new-kms>:9203/finish
428439
>
429440
> If you skip this, `Onboard.Onboard` or later trusted RPCs will fail with KMS authorization errors.
430441
442+
> **Admin authentication.** Onboarding itself is gated by attestation and the authorization backend above — not by a token. Separately, the KMS *admin* RPCs (for example `ClearImageCache`) are served on a dedicated `[core.admin]` listener behind the shared HTTP authenticator, just like the VMM and gateway: set `[core.admin] enabled = true` with an `auth_token` (or the `DSTACK_KMS_ADMIN_TOKEN` / `ADMIN_API_TOKEN` env vars), and clients send `Authorization: Bearer <token>` or `X-Admin-Token`. Enabled with neither `auth_token` nor `htpasswd_file` (and `insecure_no_auth = false`) fails closed — the KMS refuses to start. See [dstack-kms admin authentication](../dstack/kms/README.md#admin-api-authentication).
443+
431444
---
432445

433446
## Deploying Apps

docs/development-without-tee.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
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+
## What this mode tests
10+
11+
The development simulator provides configfs-tsm reports, RTMR extension files,
12+
and a CCEL event log. Guest preparation, measurements, application key setup,
13+
encrypted persistent storage, the guest agent, and Docker Compose therefore
14+
use their normal code paths.
15+
16+
It does not provide hardware isolation or a valid hardware-signed quote. The
17+
host controls QEMU and swtpm, and a production verifier or KMS must reject the
18+
simulated quote.
19+
20+
## Build the development image on the host
21+
22+
TEE hardware is not needed for the build. Follow the prerequisites in
23+
[Build the dstack guest OS](building-guest-os.md), then run this from the
24+
repository root:
25+
26+
```bash
27+
make os-deps
28+
cd os/yocto/repro-build
29+
RELEASE_FLAVORS="dev" ./repro-build.sh -n
30+
```
31+
32+
The artifact used below is
33+
`os/yocto/repro-build/dist/dstack-dev-<version>.tar.gz`. Confirm that it really
34+
is a development image:
35+
36+
```bash
37+
mkdir -p ~/.dstack-vmm/image
38+
tar -xzf os/yocto/repro-build/dist/dstack-dev-*.tar.gz \
39+
-C ~/.dstack-vmm/image
40+
jq '{version, git_revision, is_dev}' \
41+
~/.dstack-vmm/image/dstack-dev-*/metadata.json
42+
```
43+
44+
Expected output includes `"is_dev": true`. Use a development image for this
45+
workflow because production images do not include `dstack-tee-simulator`.
46+
47+
## Install and configure the VMM
48+
49+
Install QEMU and swtpm on the development host. On Ubuntu:
50+
51+
```bash
52+
sudo apt-get update
53+
sudo apt-get install -y qemu-system-x86 swtpm swtpm-tools jq
54+
test -r /dev/kvm && test -w /dev/kvm
55+
```
56+
57+
Add the current user to the `kvm` group and log in again if the `/dev/kvm`
58+
check fails because of its permissions.
59+
60+
Build the VMM and supervisor from the same checkout as the image:
61+
62+
```bash
63+
cargo build --manifest-path dstack/Cargo.toml --release \
64+
-p dstack-vmm -p supervisor
65+
```
66+
67+
Install both binaries and the CLI on the machine that will run QEMU. You can
68+
also run them directly from the checkout during development:
69+
70+
```bash
71+
mkdir -p ~/.dstack-vmm
72+
install -Dm755 dstack/target/release/dstack-vmm ~/.local/bin/dstack-vmm
73+
install -Dm755 dstack/target/release/supervisor ~/.local/bin/supervisor
74+
install -Dm755 dstack/vmm/src/vmm-cli.py ~/.local/bin/dstack
75+
mkdir -p ~/.dstack-vmm
76+
cp dstack/vmm/vmm.toml ~/.dstack-vmm/vmm.toml
77+
```
78+
79+
Set these values in `~/.dstack-vmm/vmm.toml`:
80+
81+
```toml
82+
[image]
83+
path = "/home/USER/.dstack-vmm/image"
84+
85+
[cvm]
86+
qemu_path = "/usr/bin/qemu-system-x86_64"
87+
88+
[cvm.networking]
89+
mode = "user"
90+
91+
[supervisor]
92+
exe = "/home/USER/.local/bin/supervisor"
93+
```
94+
95+
Start the VMM from a stable working directory because its default API socket is
96+
relative to that directory:
97+
98+
```bash
99+
mkdir -p ~/.dstack-vmm/run
100+
cd ~/.dstack-vmm/run
101+
dstack-vmm -c ../vmm.toml
102+
```
103+
104+
In another terminal, check that the development image is visible:
105+
106+
```bash
107+
cd ~/.dstack-vmm/run
108+
dstack lsimage --json | jq '.[] | {name, version, is_dev}'
109+
```
110+
111+
## Launch with no TEE and swtpm
112+
113+
Create a small stateful workload:
114+
115+
```yaml
116+
# docker-compose.yml
117+
services:
118+
state-test:
119+
image: alpine:3.20
120+
command:
121+
- sh
122+
- -c
123+
- |
124+
date -Iseconds >> /data/boots
125+
touch /data/persistent-marker
126+
sleep infinity
127+
volumes:
128+
- state-data:/data
129+
volumes:
130+
state-data: {}
131+
```
132+
133+
Convert it to app-compose JSON and select the TPM key provider:
134+
135+
```bash
136+
dstack compose \
137+
--name swtpm-persistence \
138+
--docker-compose docker-compose.yml \
139+
--key-provider tpm \
140+
--public-logs \
141+
--public-sysinfo \
142+
--output app-compose.json
143+
```
144+
145+
Deploy with the development image, no KMS, and no TEE:
146+
147+
```bash
148+
dstack deploy \
149+
--name swtpm-persistence \
150+
--image dstack-dev-<version> \
151+
--compose app-compose.json \
152+
--vcpu 2 --memory 3G --disk 10G \
153+
--no-tee
154+
```
155+
156+
Successful output contains a VM ID. `dstack info VM_ID` should eventually show
157+
`Boot Progress: done`. The QEMU command line should contain the swtpm frontend
158+
and no TDX guest object:
159+
160+
```bash
161+
ps aux | grep '[q]emu-system' | grep -E -- '-tpmdev|tpm-tis'
162+
ps aux | grep '[s]wtpm socket'
163+
```
164+
165+
The VMM stores both durable components below the VM work directory:
166+
167+
```text
168+
~/.dstack-vmm/vm/VM_ID/hda.img
169+
~/.dstack-vmm/vm/VM_ID/swtpm/tpm2-00.permall
170+
```
171+
172+
Do not delete either path if the VM must restart with the same state.
173+
174+
## Verify encrypted storage and restart persistence
175+
176+
First check the guest preparation log:
177+
178+
```bash
179+
dstack logs VM_ID -n 300 | grep -E \
180+
'Generating app keys from TPM|Filesystem options|LUKS2 header|Device mapper'
181+
```
182+
183+
A successful first boot includes output like:
184+
185+
```text
186+
Generating app keys from TPM
187+
Filesystem options: encryption=true, filesystem=Zfs
188+
Mounting tmpfs for in-memory LUKS header
189+
Loading the LUKS2 header
190+
Device mapper /dev/mapper/dstack_data_disk is ready
191+
```
192+
193+
Record the instance ID, then perform a graceful restart:
194+
195+
```bash
196+
dstack info VM_ID | grep 'Instance ID'
197+
dstack stop VM_ID
198+
dstack start VM_ID
199+
```
200+
201+
Wait for `Boot Progress: done`, then inspect the recent events and boot log:
202+
203+
```bash
204+
dstack info VM_ID
205+
dstack logs VM_ID -n 300 | grep -E \
206+
'mounting data disk|Loading the LUKS2 header|Device mapper|Container .* Started'
207+
```
208+
209+
The restart passes when all of the following hold:
210+
211+
1. the instance ID is unchanged;
212+
2. the event stream says `mounting data disk`, not `initializing data disk`;
213+
3. the LUKS mapping becomes ready without reformatting the disk;
214+
4. Docker starts the existing container and volume, and
215+
`/data/persistent-marker` and the earlier entries in `/data/boots` remain.
216+
217+
## Common failures
218+
219+
`tpm key provider requested but swtpm is not installed` means `swtpm` is not on
220+
the VMM user's `PATH`. If QEMU reports that KVM is unavailable, confirm that
221+
hardware virtualization is enabled and that `/dev/kvm` is writable by the VMM
222+
user.

docs/dstack-gateway.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,28 @@ Open `vmm.toml` and adjust dstack-gateway configuration in the `gateway` section
6363

6464
- `base_domain`: Same as `base_domain` from `gateway.toml`'s `core.proxy` section
6565
- `port`: Same as `listen_port` from `gateway.toml`'s `core.proxy` section
66+
67+
## Admin API authentication
68+
69+
The gateway exposes a separate admin API (used for sync, WireGuard peer management, and other operator RPCs). Configure it in the `core.admin` section of `gateway.toml`:
70+
71+
```toml
72+
[core.admin]
73+
enabled = true
74+
address = "0.0.0.0:9016"
75+
# generate with: openssl rand -hex 32
76+
admin_token = "<paste output of: openssl rand -hex 32>"
77+
# alternatively, an Apache bcrypt htpasswd file (htpasswd -B -c admin.htpasswd admin)
78+
# htpasswd_file = "/etc/dstack/gateway-admin.htpasswd"
79+
insecure_no_auth = false
80+
```
81+
82+
- `enabled`: enable the admin API server.
83+
- `address`: bind address/port for the admin API.
84+
- `admin_token`: shared admin token. It can also be supplied via the environment variables `DSTACK_GATEWAY_ADMIN_TOKEN` or `ADMIN_API_TOKEN` instead of the config file.
85+
- `htpasswd_file`: path to an Apache bcrypt htpasswd file (create with `htpasswd -B -c admin.htpasswd admin`); only bcrypt entries are accepted. Can be used instead of, or alongside, `admin_token`.
86+
- `insecure_no_auth`: development-only escape hatch that disables admin authentication. Never enable it on a network-reachable admin interface.
87+
88+
The admin server is fail-closed: if it is enabled with no `admin_token` and no `htpasswd_file`, and `insecure_no_auth` is `false`, it refuses to start rather than exposing an unauthenticated admin API.
89+
90+
Clients authenticate by sending `Authorization: Bearer <token>` or the `X-Admin-Token: <token>` header.

docs/security/security-best-practices.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ The KMS TLS listener may keep `rpc.tls.mutual.mandatory = false` because bootstr
9393

9494
App key release and KMS key handover still require verified caller attestation from the RA-TLS client certificate. Certificate signing verifies the CSR signature and embedded attestation before signing.
9595

96+
## Management/admin API authentication
97+
98+
The VMM, gateway, and KMS management surfaces must have authentication enabled in production:
99+
100+
- VMM: set `[auth] enabled = true` with `tokens` (or `htpasswd_file`) — this guards the entire VMM HTTP/pRPC/UI surface. Never bind to a non-localhost address without it. Clients send `Authorization: Bearer <token>` or `X-Admin-Token`.
101+
- Gateway: set `[core.admin] admin_token` (or `htpasswd_file`) and keep `insecure_no_auth = false`. Clients send `Authorization: Bearer <token>` or `X-Admin-Token`.
102+
- KMS: enable `[core.admin]` with an `auth_token` (or `htpasswd_file`); the admin RPCs are served on a dedicated listener and clients send `Authorization: Bearer <token>` or `X-Admin-Token`. Enabled with no credential denies all admin RPCs (fail-closed).
103+
104+
All three share the same HTTP authenticator: bcrypt-only htpasswd (via `htpasswd -B`), constant-time token comparison, and fail-closed behavior.
105+
96106
## Keep private material owner-only
97107

98108
Secret-bearing files should be owner-only (`0600`) wherever possible, including app keys, decrypted env files, KMS root keys, gateway WireGuard/TLS keys, and ACME credentials. Preserve restrictive permissions when copying volumes, backing up `/etc/kms/certs`, or moving gateway and certbot state between hosts. Public issue [#606](https://github.com/Dstack-TEE/dstack/issues/606) tracks the remaining low-cost hardening work in dstack-managed file writes.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Test `dstackup image pull` with a local release API
2+
3+
`--release-api-base-url` accepts plain HTTP URLs, including localhost. A small
4+
GitHub Releases API overlay is included for local testing. Locally published
5+
releases shadow GitHub; API requests that are not present locally are relayed to
6+
`https://api.github.com`.
7+
8+
Start it:
9+
10+
```bash
11+
./tools/mock-github-releases.py --port 8000
12+
```
13+
14+
Publish a release and copy a local tarball into the mock asset store. The mock
15+
calculates and publishes its SHA-256 digest automatically:
16+
17+
```bash
18+
curl -fsS -X POST \
19+
http://localhost:8000/__admin/repos/Dstack-TEE/dstack/releases \
20+
-H 'content-type: application/json' \
21+
-d '{
22+
"tag_name": "guest-os-v99.0.0",
23+
"assets": [{"local_path": "/tmp/dstack-99.0.0.tar.gz"}]
24+
}'
25+
```
26+
27+
Pull the locally published latest release:
28+
29+
```bash
30+
sudo dstackup image pull \
31+
--release-api-base-url http://localhost:8000/repos \
32+
--force
33+
```
34+
35+
A release can instead reference an already hosted asset by supplying `name`,
36+
`browser_download_url`, and optionally `digest` in the asset object. State is
37+
in memory and resets when the mock exits; copied assets remain in `--asset-dir`.
38+
GitHub API rate limits still apply to relayed requests. Set an `Authorization`
39+
header on a direct request to the mock when testing authenticated relays.

0 commit comments

Comments
 (0)