Skip to content

Commit 3d88c7b

Browse files
committed
Add host-side compose TDX E2E suite
1 parent 29e3563 commit 3d88c7b

15 files changed

Lines changed: 1748 additions & 11 deletions

File tree

dstack/gateway/dstack-app/bootstrap-cluster.sh

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ echo "Waiting for gateway admin API at $ADMIN_ADDR..."
3333
max_retries=60
3434
retry=0
3535
while [ $retry -lt $max_retries ]; do
36-
if curl -sf "${AUTH_HEADER[@]}" "http://$ADMIN_ADDR/prpc/Status" >/dev/null 2>&1; then
36+
if curl -sf "${AUTH_HEADER[@]}" "http://$ADMIN_ADDR/prpc/Admin.Status" >/dev/null 2>&1; then
3737
break
3838
fi
3939
retry=$((retry + 1))
@@ -48,29 +48,45 @@ fi
4848

4949
echo "Admin API ready, bootstrapping configuration..."
5050

51-
# Set ACME URL
52-
if [ "$ACME_STAGING" = "yes" ]; then
51+
# Set ACME URL. ACME_URL is useful for local E2E suites (for example Pebble).
52+
if [ -n "${ACME_URL:-}" ]; then
53+
:
54+
elif [ "$ACME_STAGING" = "yes" ]; then
5355
ACME_URL="https://acme-staging-v02.api.letsencrypt.org/directory"
5456
else
5557
ACME_URL="https://acme-v02.api.letsencrypt.org/directory"
5658
fi
5759

5860
echo "Setting certbot config (ACME URL: $ACME_URL)..."
59-
curl -sf -X POST "${AUTH_HEADER[@]}" "http://$ADMIN_ADDR/prpc/SetCertbotConfig" \
61+
curl -sf -X POST "${AUTH_HEADER[@]}" "http://$ADMIN_ADDR/prpc/Admin.SetCertbotConfig" \
6062
-H "Content-Type: application/json" \
6163
-d '{"acme_url":"'"$ACME_URL"'","renew_interval_secs":3600,"renew_before_expiration_secs":864000,"renew_timeout_secs":300}' >/dev/null \
6264
&& echo " Certbot config set" || echo " WARN: failed to set certbot config"
6365

6466
# Create DNS credential if CF_API_TOKEN is provided and no credentials exist yet
6567
if [ -n "$CF_API_TOKEN" ]; then
66-
existing=$(curl -sf "${AUTH_HEADER[@]}" "http://$ADMIN_ADDR/prpc/ListDnsCredentials" 2>/dev/null)
68+
existing=$(curl -sf "${AUTH_HEADER[@]}" "http://$ADMIN_ADDR/prpc/Admin.ListDnsCredentials" 2>/dev/null)
6769
cred_count=$(echo "$existing" | jq -r '.credentials | length' 2>/dev/null || echo "0")
6870

6971
if [ "$cred_count" = "0" ]; then
7072
echo "Creating default DNS credential..."
71-
curl -sf -X POST "${AUTH_HEADER[@]}" "http://$ADMIN_ADDR/prpc/CreateDnsCredential" \
73+
dns_payload=$(jq -cn \
74+
--arg token "$CF_API_TOKEN" \
75+
--arg api_url "${CF_API_URL:-}" \
76+
--argjson ttl "${DNS_TXT_TTL:-60}" \
77+
--argjson max_wait "${MAX_DNS_WAIT:-300}" \
78+
'{
79+
name: "cloudflare",
80+
provider_type: "cloudflare",
81+
cf_api_token: $token,
82+
set_as_default: true,
83+
dns_txt_ttl: $ttl,
84+
max_dns_wait: $max_wait
85+
}
86+
+ (if $api_url == "" then {} else {cf_api_url: $api_url} end)')
87+
curl -sf -X POST "${AUTH_HEADER[@]}" "http://$ADMIN_ADDR/prpc/Admin.CreateDnsCredential" \
7288
-H "Content-Type: application/json" \
73-
-d '{"name":"cloudflare","provider_type":"cloudflare","cf_api_token":"'"$CF_API_TOKEN"'","set_as_default":true}' >/dev/null \
89+
-d "$dns_payload" >/dev/null \
7490
&& echo " DNS credential created" || echo " WARN: failed to create DNS credential"
7591
else
7692
echo " DNS credentials already exist ($cred_count), skipping"
@@ -81,12 +97,12 @@ fi
8197

8298
# Add ZT-Domain if SRV_DOMAIN is provided and domain doesn't exist yet
8399
if [ -n "$SRV_DOMAIN" ]; then
84-
existing=$(curl -sf "${AUTH_HEADER[@]}" "http://$ADMIN_ADDR/prpc/ListZtDomains" 2>/dev/null)
100+
existing=$(curl -sf "${AUTH_HEADER[@]}" "http://$ADMIN_ADDR/prpc/Admin.ListZtDomains" 2>/dev/null)
85101
has_domain=$(echo "$existing" | jq -r '.domains[]? | select(.domain=="'"$SRV_DOMAIN"'") | .domain' 2>/dev/null)
86102

87103
if [ -z "$has_domain" ]; then
88104
echo "Adding ZT-Domain: $SRV_DOMAIN..."
89-
curl -sf -X POST "${AUTH_HEADER[@]}" "http://$ADMIN_ADDR/prpc/AddZtDomain" \
105+
curl -sf -X POST "${AUTH_HEADER[@]}" "http://$ADMIN_ADDR/prpc/Admin.AddZtDomain" \
90106
-H "Content-Type: application/json" \
91107
-d '{"domain":"'"$SRV_DOMAIN"'","port":443,"priority":100}' >/dev/null \
92108
&& echo " ZT-Domain added" || echo " WARN: failed to add ZT-Domain"

dstack/gateway/dstack-app/deploy-to-vmm.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ NODE_ID=1
6464
6565
# Whether to use ACME staging (yes/no)
6666
ACME_STAGING=no
67+
# Optional ACME directory override for local E2E (for example Pebble).
68+
# ACME_URL=http://10.0.2.2:14000/dir
69+
# Optional Cloudflare API override for local E2E mock.
70+
# CF_API_URL=http://10.0.2.2:18080/client/v4
71+
# DNS_TXT_TTL=60
72+
# MAX_DNS_WAIT=300
6773
6874
# Networking mode: bridge or user (default: user)
6975
# NET_MODE=bridge

dstack/key-provider-build/Dockerfile.key-provider

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,20 @@ RUN git clone https://github.com/MoeMahhouk/gramine-sealing-key-provider && \
5959
WORKDIR /gramine-sealing-key-provider
6060
COPY Cargo.lock .
6161

62-
# Build key provider, generate keys, and build manifest
62+
# Build key provider, generate keys, and build manifest.
63+
#
64+
# Do not use `gramine-sgx-gen-private-key` here. On some hosts the Python
65+
# cryptography/OpenSSL stack in the Gramine image fails while generating the
66+
# RSA key with a generic "Unknown OpenSSL error". Generate the exact same kind
67+
# of Gramine signing key directly with OpenSSL instead:
68+
# - 3072-bit RSA
69+
# - public exponent 3
70+
# - traditional PEM accepted by gramine-sgx-sign
6371
RUN make target/release/gramine-sealing-key-provider && \
64-
gramine-sgx-gen-private-key && \
72+
mkdir -p /root/.config/gramine && \
73+
chmod 0700 /root/.config /root/.config/gramine && \
74+
openssl genrsa -traditional -3 -out /root/.config/gramine/enclave-key.pem 3072 && \
75+
chmod 0600 /root/.config/gramine/enclave-key.pem && \
6576
make RUST_LOG=info
6677

6778
RUN gramine-sgx-sigstruct-view --output-format json gramine-sealing-key-provider.sig
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# Directory containing unpacked dstack guest images: <store>/<image-name>/{bzImage,initramfs...,rootfs...,digest.txt}
5+
# In meta-dstack this is usually ../../../build/images from this directory, or an absolute path.
6+
DSTACK_E2E_IMAGE_STORE=../../../build/images
7+
DSTACK_E2E_IMAGE_NAME=dstack-0.6.0
8+
DSTACK_E2E_PLATFORM=tdx
9+
10+
# App image used inside the only CVM launched by this suite.
11+
DSTACK_E2E_APP_IMAGE=nginx:alpine
12+
13+
# Mock ACME/DNS + externally visible test domain for Gateway SNI.
14+
DSTACK_E2E_BASE_DOMAIN=e2e.test
15+
DSTACK_E2E_PEBBLE_IMAGE=kvin/pebble:latest
16+
17+
# Host ports. Defaults are chosen to avoid the common dstackup ports.
18+
DSTACK_E2E_VMM_PORT=29080
19+
DSTACK_E2E_AUTH_PORT=28011
20+
DSTACK_E2E_KMS_HOST_PORT=28082
21+
DSTACK_E2E_GATEWAY_RPC_HOST_PORT=28000
22+
DSTACK_E2E_GATEWAY_ADMIN_HOST_PORT=28001
23+
DSTACK_E2E_GATEWAY_PROXY_HOST_PORT=28443
24+
DSTACK_E2E_GATEWAY_WG_HOST_PORT=28120
25+
DSTACK_E2E_KEY_PROVIDER_PORT=13443
26+
DSTACK_E2E_HOST_API_PORT=20011
27+
DSTACK_E2E_MOCK_CF_HTTP_PORT=38080
28+
DSTACK_E2E_PEBBLE_HTTP_PORT=34000
29+
DSTACK_E2E_PEBBLE_MGMT_PORT=35000
30+
31+
# Dev-compose gateway policy. "any" matches the tdxlab host-side gateway mode:
32+
# app CVMs skip gateway app-id pinning but still use KMS authorization for apps.
33+
DSTACK_E2E_GATEWAY_APP_ID=any
34+
DSTACK_E2E_GATEWAY_WG_INTERFACE=wg-ds-e2e
35+
DSTACK_E2E_GATEWAY_WG_IP=10.8.0.1/16
36+
DSTACK_E2E_GATEWAY_WG_RESERVED_NET=10.8.0.1/32
37+
DSTACK_E2E_GATEWAY_WG_CLIENT_RANGE=10.8.0.0/18
38+
39+
# VMM/QEMU knobs.
40+
DSTACK_E2E_CID_START=15000
41+
DSTACK_E2E_QGS_PORT=4050
42+
DSTACK_E2E_QEMU_PATH=/usr/bin/qemu-system-x86_64
43+
44+
# SGX QCNL config used by AESMD. On hosts with a local PCCS, point this at the
45+
# host config, e.g. /etc/sgx_default_qcnl.conf.
46+
DSTACK_E2E_QCNL_CONF=../../dstack/key-provider-build/sgx_default_qcnl.conf
47+
48+
# Runner behaviour.
49+
DSTACK_E2E_CLEAN_START=true
50+
DSTACK_E2E_CLEANUP_AFTER=false
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
.env
5+
/state/*
6+
!/state/.gitkeep
7+
__pycache__/
8+
*.pyc
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<!--
2+
SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
3+
SPDX-License-Identifier: Apache-2.0
4+
-->
5+
6+
# dstack full-stack compose E2E suite
7+
8+
This directory contains a Docker Compose based test suite that mirrors the
9+
`tdxlab`/`dstack-k8s deploy/compose` shape: host-side services run as normal
10+
processes/containers, and QEMU is used only for the real app CVM under test.
11+
12+
```text
13+
docker compose
14+
├─ mock-cf-dns-api # tiny Cloudflare API + TXT DNS mock
15+
├─ pebble # ACME test server (HTTP-capable custom Pebble image)
16+
├─ aesmd + local-keyprovider
17+
├─ dstack-auth # KMS auth webhook backed by auth-allowlist.json
18+
├─ dstack-kms # host-side dev KMS, not a CVM
19+
├─ dstack-gateway # host-side dev Gateway, not a CVM
20+
├─ dstack-vmm # launches real TDX/SNP app CVMs via QEMU
21+
└─ runner
22+
├─ configures Gateway certbot to use mock CF + Pebble
23+
├─ deploys nginx test app CVM with kms_enabled=true + gateway_enabled=true
24+
└─ verifies HTTPS Gateway -> app routing
25+
```
26+
27+
It is intended for a **real TDX/SGX host** (or SEV-SNP host after setting the
28+
platform/image knobs). It is not a pure unit-test environment; QEMU still starts
29+
a confidential VM for the app.
30+
31+
## Prerequisites
32+
33+
1. From this directory, build the host binaries from the dstack checkout:
34+
35+
```bash
36+
cargo build --release \
37+
--manifest-path ../../dstack/Cargo.toml \
38+
-p dstack-cli -p dstack-auth -p dstack-vmm -p dstack-kms \
39+
-p dstack-gateway -p supervisor
40+
```
41+
42+
2. Make sure the host has the required devices/services:
43+
44+
- `/dev/kvm`
45+
- Intel TDX support and QGS for TDX (`DSTACK_E2E_QGS_PORT`, default `4050`)
46+
- `/dev/sgx_enclave` and `/dev/sgx_provision` for the local key provider
47+
- an SGX QCNL config that works on the host. By default the suite uses
48+
`../../dstack/key-provider-build/sgx_default_qcnl.conf`; on hosts with a local
49+
PCCS, set `DSTACK_E2E_QCNL_CONF=/etc/sgx_default_qcnl.conf`.
50+
- `/dev/vhost-vsock` for guest↔host vsock services
51+
- a TDX-capable `qemu-system-x86_64` available inside the runtime container.
52+
The default runtime image follows `../dstack-k8s/deploy/compose` and
53+
installs QEMU from `ppa:kobuk-team/tdx-release`.
54+
- WireGuard kernel support on the host. The Gateway service runs privileged
55+
with host networking and creates `DSTACK_E2E_GATEWAY_WG_INTERFACE`.
56+
57+
3. Provide an unpacked guest image store. From `meta-dstack` this is usually
58+
`../../../build/images`; otherwise copy `.env.example` and set:
59+
60+
```bash
61+
cp .env.example .env
62+
$EDITOR .env
63+
```
64+
65+
No Gateway image publishing is needed in this architecture: `dstack-gateway` and
66+
`dstack-kms` are run from the local `target/release/` binaries on the host side.
67+
68+
## Run
69+
70+
From this directory, run:
71+
72+
```bash
73+
DOCKER_BUILDKIT=0 docker compose --env-file .env -f compose.yml up --build --abort-on-container-exit runner
74+
```
75+
76+
For iterative debugging, keep the stack running:
77+
78+
```bash
79+
DOCKER_BUILDKIT=0 docker compose --env-file .env -f compose.yml up --build
80+
```
81+
82+
The runner leaves the app VM running by default for inspection. Set
83+
`DSTACK_E2E_CLEANUP_AFTER=true` to remove suite VMs at the end. Stale VMs whose
84+
names start with `DSTACK_E2E_NAME_PREFIX` are removed at the start when
85+
`DSTACK_E2E_CLEAN_START=true`.
86+
87+
## What is verified
88+
89+
The runner checks:
90+
91+
1. Host-side KMS is reachable over TLS.
92+
2. Host-side Gateway Admin API accepts configuration.
93+
3. Gateway obtains a wildcard certificate for `*.DSTACK_E2E_BASE_DOMAIN` from
94+
Pebble using the mock Cloudflare DNS API.
95+
4. VMM API becomes reachable.
96+
5. A real nginx app CVM boots with `kms_enabled=true` and `gateway_enabled=true`.
97+
6. `curl -k` through the Gateway SNI route returns the nginx welcome page.
98+
99+
Artifacts are written under `state/work/`.
100+
101+
## Notes / flexibility gaps
102+
103+
- This is a dev compose KMS/Gateway setup. KMS and Gateway key material is
104+
generated under `state/`; the allowlist returns `gatewayAppId = "any"`, which
105+
matches the host-side tdxlab-style deployment and avoids requiring Gateway to
106+
be a CVM.
107+
- `dstackup install` is systemd-oriented; this suite renders `vmm.toml`,
108+
`kms.toml`, `gateway.toml`, and `auth-allowlist.json` itself.
109+
- Running VMM in a container depends on the container image having a QEMU build
110+
that supports the target TEE platform. The runtime Dockerfile uses Intel's
111+
kobuk-team TDX PPA for QEMU, matching the dstack-k8s compose deployment.
112+
- Host API is vsock-only by design. The suite therefore uses privileged host
113+
networking and a separate `DSTACK_E2E_HOST_API_PORT` to avoid conflicts with
114+
other VMM instances.

0 commit comments

Comments
 (0)