|
1 | | -# Tutorial 03: Connecting to TEE Nodes |
| 1 | +# Tutorial 03: TLS and Connectivity |
2 | 2 |
|
3 | | -Multiple ways to route traffic to your dstack applications. |
| 3 | +Self-signed TLS with attestation-bound certificates. |
4 | 4 |
|
5 | | -## Overview |
| 5 | +## Prerequisites |
6 | 6 |
|
7 | | -dstack apps run in isolated TEE environments without public IPs. This tutorial covers three approaches to connect: |
| 7 | +Complete [01-attestation](../01-attestation) first. This tutorial builds on attestation verification. |
8 | 8 |
|
9 | | -| Approach | Use Case | Client Setup | |
10 | | -|----------|----------|--------------| |
11 | | -| Gateway Domain | HTTP/HTTPS apps | None (browser works) | |
12 | | -| Stunnel | TCP protocols (games, SSH) | Install stunnel on client | |
13 | | -| Ngrok | Bypass gateway, public IP | Run ngrok inside TEE | |
| 9 | +## The Problem |
14 | 10 |
|
15 | | -## Approach 1: Gateway Domain (Default) |
| 11 | +TEE apps need TLS, but: |
| 12 | +- The dstack gateway terminates TLS → gateway sees plaintext |
| 13 | +- Let's Encrypt requires DNS control → adds complexity |
| 14 | +- Trusting a relay service (ngrok) to terminate TLS → breaks TEE integrity |
16 | 15 |
|
17 | | -Every deployed app gets a URL like: |
18 | | -``` |
19 | | -https://<app-id>-<port>.dstack-prod5.phala.network |
20 | | -``` |
| 16 | +## The Solution: Attestation-Bound Certificates |
21 | 17 |
|
22 | | -The gateway terminates TLS and routes traffic to your container's port. |
| 18 | +The TEE generates a self-signed certificate. The certificate fingerprint is included in the attestation. Clients verify: |
23 | 19 |
|
24 | 20 | ``` |
25 | | -┌──────────┐ HTTPS ┌─────────────┐ TCP ┌──────────┐ |
26 | | -│ Client │ ────────────── │ Gateway │ ──────────── │ TEE App │ |
27 | | -│ (browser)│ │ (port 443) │ │ (port X) │ |
28 | | -└──────────┘ └─────────────┘ └──────────┘ |
29 | | -``` |
30 | | - |
31 | | -**Pros:** Works out of the box, no client setup |
32 | | -**Cons:** HTTP/WebSocket only, gateway sees plaintext |
33 | | - |
34 | | -### Example |
35 | | - |
36 | | -Deploy any HTTP app: |
37 | | - |
38 | | -```yaml |
39 | | -services: |
40 | | - web: |
41 | | - image: nginx |
42 | | - ports: |
43 | | - - "80:80" |
| 21 | +1. Connect to TEE (ignore cert validation initially) |
| 22 | +2. Fetch attestation from /attestation endpoint |
| 23 | +3. Validate attestation (quote, measurements, etc.) |
| 24 | +4. Extract cert fingerprint from attestation |
| 25 | +5. Verify the TLS cert matches the attested fingerprint |
| 26 | +6. Now the connection is trusted end-to-end |
44 | 27 | ``` |
45 | 28 |
|
46 | | -After deploy, access at `https://<app-id>-80.dstack-prod5.phala.network` |
47 | | - |
48 | | -## Approach 2: Stunnel (TCP over TLS) |
| 29 | +This works regardless of how you reach the TEE - gateway, ngrok, direct IP, etc. |
49 | 30 |
|
50 | | -For protocols that aren't HTTP - games, SSH, databases, etc. The client runs stunnel to unwrap TLS locally. |
| 31 | +## Architecture |
51 | 32 |
|
52 | 33 | ``` |
53 | | -┌──────────┐ TCP ┌─────────┐ TLS ┌─────────────┐ TCP ┌──────────┐ |
54 | | -│ Game │ ─────── │ stunnel │ ──────── │ Gateway │ ───── │ TEE App │ |
55 | | -│ Client │ :25565 │ (local) │ │ (port 443) │ │ :25565 │ |
56 | | -└──────────┘ └─────────┘ └─────────────┘ └──────────┘ |
| 34 | +┌────────┐ ┌─────────┐ ┌─────────────────────────┐ |
| 35 | +│ Client │ ─ TLS ─ │ Relay │ ─ TLS ─ │ TEE │ |
| 36 | +│ │ │ (ngrok) │ │ ┌─────────────────────┐ │ |
| 37 | +│ │ │ │ │ │ Self-signed cert │ │ |
| 38 | +│ │ │ │ │ │ Attestation includes│ │ |
| 39 | +│ │ │ │ │ │ cert fingerprint │ │ |
| 40 | +│ │ │ │ │ └─────────────────────┘ │ |
| 41 | +└────────┘ └─────────┘ └─────────────────────────┘ |
| 42 | + relay only sees |
| 43 | + encrypted TLS traffic |
57 | 44 | ``` |
58 | 45 |
|
59 | | -**Pros:** Any TCP protocol works |
60 | | -**Cons:** Requires client-side setup |
| 46 | +## Oracle with Self-Signed TLS |
61 | 47 |
|
62 | | -### Minecraft Example |
| 48 | +Building on the oracle from [02-kms-and-signing](../02-kms-and-signing), we add: |
| 49 | +- Self-signed TLS certificate generated at startup |
| 50 | +- Certificate fingerprint included in `/attestation` response |
63 | 51 |
|
64 | | -Server side (in TEE): |
65 | 52 | ```yaml |
66 | 53 | services: |
67 | | - minecraft: |
68 | | - image: itzg/minecraft-server |
69 | | - environment: |
70 | | - - EULA=TRUE |
| 54 | + app: |
| 55 | + build: |
| 56 | + context: . |
| 57 | + dockerfile_inline: | |
| 58 | + FROM node:22-slim |
| 59 | + RUN apt-get update && apt-get install -y openssl |
| 60 | + WORKDIR /app |
| 61 | + RUN npm init -y && npm install @phala/dstack-sdk viem |
| 62 | + COPY index.mjs . |
| 63 | + CMD ["node", "index.mjs"] |
71 | 64 | ports: |
72 | | - - "25565:25565" |
| 65 | + - "8443:8443" |
| 66 | + volumes: |
| 67 | + - /var/run/dstack.sock:/var/run/dstack.sock |
73 | 68 | ``` |
74 | 69 |
|
75 | | -Client side - create `stunnel.conf`: |
76 | | -```ini |
77 | | -foreground = yes |
78 | | -pid = ./stunnel.pid |
| 70 | +## index.mjs |
79 | 71 |
|
80 | | -[minecraft] |
81 | | -client = yes |
82 | | -accept = 127.0.0.1:25565 |
83 | | -connect = <app-id>-25565.dstack-prod5.phala.network:443 |
84 | | -``` |
| 72 | +See [index.mjs](index.mjs) for the full implementation. Key points: |
85 | 73 |
|
86 | | -Run stunnel and connect your Minecraft client to `localhost:25565`: |
87 | | -```bash |
88 | | -stunnel stunnel.conf |
89 | | -``` |
| 74 | +```javascript |
| 75 | +// Generate self-signed cert at startup |
| 76 | +execSync(`openssl req -x509 -newkey rsa:2048 ... -subj "/CN=tee-oracle"`) |
90 | 77 |
|
91 | | -### SSH Example |
| 78 | +// Hash the DER-encoded certificate (matches what TLS clients see) |
| 79 | +const certDer = Buffer.from(pemToDer(certPem), 'base64') |
| 80 | +const certFingerprint = createHash("sha256").update(certDer).digest("hex") |
92 | 81 |
|
93 | | -Using socat instead of stunnel (simpler one-liner): |
94 | | -```bash |
95 | | -socat TCP-LISTEN:2222,fork,reuseaddr \ |
96 | | - OPENSSL:<app-id>-22.dstack-prod5.phala.network:443 |
| 82 | +// Include fingerprint in attestation |
| 83 | +app.get("/attestation", async (req, res) => { |
| 84 | + const reportData = Buffer.from(certFingerprint, "hex") |
| 85 | + const quote = await client.getQuote(reportData) |
| 86 | + res.json({ certFingerprint, quote: quote.quote.toString("hex"), ... }) |
| 87 | +}) |
97 | 88 | ``` |
98 | 89 |
|
99 | | -Or configure `~/.ssh/config`: |
100 | | -``` |
101 | | -Host tee-box |
102 | | - ProxyCommand openssl s_client -quiet -connect <app-id>-22.dstack-prod5.phala.network:443 |
103 | | - User root |
104 | | -``` |
| 90 | +## Verification Script |
105 | 91 |
|
106 | | -Then: `ssh tee-box` |
| 92 | +See [verify_tls.py](verify_tls.py) for the full implementation. The script: |
107 | 93 |
|
108 | | -> **macOS note:** System openssl is LibreSSL. Install OpenSSL via homebrew: `brew install openssl` |
| 94 | +1. Connects to the endpoint and extracts the TLS certificate fingerprint |
| 95 | +2. Fetches `/attestation` (ignoring cert validation initially) |
| 96 | +3. Verifies the certificate fingerprint matches what's in the attestation |
| 97 | +4. Verifies the attestation quote itself |
109 | 98 |
|
110 | | -## Approach 3: Ngrok Reverse Proxy |
111 | | - |
112 | | -Run ngrok inside the TEE to get a public URL that bypasses the dstack gateway entirely. Useful when: |
113 | | -- You need a non-TLS public endpoint |
114 | | -- You want to control your own domain |
115 | | -- You're building a reverse tunnel |
116 | | - |
117 | | -``` |
118 | | -┌──────────┐ ┌─────────────┐ tunnel ┌──────────┐ |
119 | | -│ Client │ ───────── │ ngrok edge │ ────────── │ TEE App │ |
120 | | -│ │ public │ (ngrok.io) │ │ + ngrok │ |
121 | | -└──────────┘ URL └─────────────┘ └──────────┘ |
122 | 99 | ``` |
| 100 | +$ python3 verify_tls.py https://localhost:8443 |
123 | 101 |
|
124 | | -**Pros:** Public IP, custom domains, bypasses gateway |
125 | | -**Cons:** Trusts ngrok infrastructure, requires account |
| 102 | +Verifying: https://localhost:8443 |
126 | 103 |
|
127 | | -### Example |
| 104 | +1. Connecting and getting certificate... |
| 105 | + Certificate fingerprint: 789b0a77f2ad4b17... |
| 106 | +2. Fetching attestation... |
| 107 | + Attested fingerprint: 789b0a77f2ad4b17... |
| 108 | +3. Verifying certificate matches attestation... |
| 109 | + Certificate fingerprint matches attestation |
| 110 | +4. Verifying attestation... |
| 111 | + Quote present (full verification requires trust-center) |
128 | 112 |
|
129 | | -```yaml |
130 | | -services: |
131 | | - app: |
132 | | - image: nginx |
133 | | - ports: |
134 | | - - "80:80" |
135 | | - |
136 | | - ngrok: |
137 | | - image: ngrok/ngrok:alpine |
138 | | - environment: |
139 | | - - NGROK_AUTHTOKEN=${NGROK_AUTHTOKEN} |
140 | | - command: http app:80 |
141 | | - depends_on: |
142 | | - - app |
| 113 | +============================================================ |
| 114 | +SUCCESS: TLS certificate is bound to TEE attestation |
| 115 | +The connection is end-to-end secure regardless of relay. |
143 | 116 | ``` |
144 | 117 |
|
145 | | -Deploy with your ngrok auth token: |
| 118 | +## Testing Locally |
| 119 | + |
146 | 120 | ```bash |
147 | | -NGROK_AUTHTOKEN=<your-token> phala deploy ... |
| 121 | +# Terminal 1: Start simulator |
| 122 | +phala simulator start |
| 123 | + |
| 124 | +# Terminal 2: Run oracle with TLS |
| 125 | +docker compose build |
| 126 | +docker compose run --rm -p 8443:8443 \ |
| 127 | + -v ~/.phala-cloud/simulator/0.5.3/dstack.sock:/var/run/dstack.sock app |
| 128 | + |
| 129 | +# Terminal 3: Verify |
| 130 | +python3 verify_tls.py https://localhost:8443 |
148 | 131 | ``` |
149 | 132 |
|
150 | | -The ngrok container logs will show your public URL. |
| 133 | +## Connectivity Options |
151 | 134 |
|
152 | | -### Alternative: Cloudflare Tunnel |
| 135 | +The verification works regardless of how you reach the TEE - the attestation binds the certificate. |
153 | 136 |
|
154 | | -Similar concept using Cloudflare: |
155 | | -```yaml |
156 | | -services: |
157 | | - app: |
158 | | - image: nginx |
159 | | - ports: |
160 | | - - "80:80" |
| 137 | +| Method | TLS Termination | Trust Path | |
| 138 | +|--------|-----------------|------------| |
| 139 | +| Direct (localhost) | Your App TEE | Single TEE | |
| 140 | +| Self-signed + attestation | Your App TEE | Single TEE | |
| 141 | +| dstack gateway | Gateway TEE | Gateway TEE → Your App TEE | |
| 142 | +| dstack-ingress | Your App TEE | Single TEE (with Let's Encrypt) | |
| 143 | +| ngrok TCP tunnel | Your App TEE | Single TEE (ngrok is transport only) | |
161 | 144 |
|
162 | | - tunnel: |
163 | | - image: cloudflare/cloudflared:latest |
164 | | - command: tunnel --no-autoupdate run --token ${CF_TUNNEL_TOKEN} |
165 | | - depends_on: |
166 | | - - app |
167 | | -``` |
| 145 | +### For Production |
| 146 | + |
| 147 | +**dstack-ingress** handles Let's Encrypt inside the TEE using DNS-01 challenges (no inbound port 80 needed). See [dstack-ingress](../../custom-domain/dstack-ingress) for setup. |
| 148 | + |
| 149 | +### About the dstack Gateway |
168 | 150 |
|
169 | | -## Trust Considerations |
| 151 | +The dstack gateway is itself a TEE application - it's a dstack docker compose running in its own enclave. TLS termination happens inside the gateway's TEE, not in untrusted infrastructure. |
170 | 152 |
|
171 | | -| Approach | Who sees plaintext? | Who can intercept? | |
172 | | -|----------|--------------------|--------------------| |
173 | | -| Gateway | dstack gateway | Gateway operator | |
174 | | -| Stunnel | dstack gateway | Gateway operator | |
175 | | -| Ngrok | ngrok servers | Ngrok | |
176 | | -| CF Tunnel | Cloudflare | Cloudflare | |
| 153 | +**Audit surface:** When using the gateway, verifying the gateway's attestation becomes part of your trust assumptions. The gateway code is open source and scheduled for security audit. |
177 | 154 |
|
178 | | -For end-to-end encryption where only the TEE sees plaintext, you need: |
179 | | -- TLS termination inside the TEE (see [05-hardening-https](../05-hardening-https)) |
180 | | -- Custom domain with cert inside TEE (see `dstack-ingress`) |
| 155 | +For maximum isolation (single TEE in the trust path), use the self-signed + attestation approach from this tutorial or dstack-ingress. |
181 | 156 |
|
182 | 157 | ## Files |
183 | 158 |
|
184 | 159 | ``` |
185 | 160 | 03-gateway-and-tls/ |
186 | | -├── docker-compose.yaml # TCP echo server for stunnel testing |
187 | | -├── docker-compose-ngrok.yaml # Ngrok reverse proxy example |
188 | | -├── stunnel-client.conf # Client-side stunnel config template |
| 161 | +├── docker-compose.yaml # Oracle with self-signed TLS |
| 162 | +├── index.mjs # HTTPS server with attestation |
| 163 | +├── verify_tls.py # Verification script |
189 | 164 | └── README.md |
190 | 165 | ``` |
191 | 166 |
|
192 | 167 | ## Next Steps |
193 | 168 |
|
194 | | -- [04-onchain-oracle](../04-onchain-oracle): On-chain verification with AppAuth |
195 | | -- [05-hardening-https](../05-hardening-https): TLS inside TEE with OCSP/CT verification |
| 169 | +- [04-onchain-oracle](../04-onchain-oracle): AppAuth contracts and on-chain verification |
| 170 | +- [05-hardening-https](../05-hardening-https): Let's Encrypt with dstack-ingress |
196 | 171 |
|
197 | | -## References |
| 172 | +## Key Insight |
198 | 173 |
|
199 | | -- [tcp-port-forwarding](../../tcp-port-forwarding): More socat examples |
200 | | -- [ssh-over-gateway](../../ssh-over-gateway): SSH server setup |
201 | | -- [minecraft](../../minecraft): Full minecraft example |
| 174 | +**The certificate doesn't need to be signed by a CA.** The attestation IS the trust anchor. By binding the certificate fingerprint to a valid TEE attestation, we prove the certificate was generated inside the TEE. This pattern works for any self-signed credential. |
0 commit comments