Skip to content

Commit f7e1eda

Browse files
committed
improve 03 connectivity example
1 parent 16648d5 commit f7e1eda

7 files changed

Lines changed: 267 additions & 183 deletions

File tree

Lines changed: 115 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,201 +1,174 @@
1-
# Tutorial 03: Connecting to TEE Nodes
1+
# Tutorial 03: TLS and Connectivity
22

3-
Multiple ways to route traffic to your dstack applications.
3+
Self-signed TLS with attestation-bound certificates.
44

5-
## Overview
5+
## Prerequisites
66

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.
88

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
1410

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
1615

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
2117

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:
2319

2420
```
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
4427
```
4528

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.
4930

50-
For protocols that aren't HTTP - games, SSH, databases, etc. The client runs stunnel to unwrap TLS locally.
31+
## Architecture
5132

5233
```
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
5744
```
5845

59-
**Pros:** Any TCP protocol works
60-
**Cons:** Requires client-side setup
46+
## Oracle with Self-Signed TLS
6147

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
6351

64-
Server side (in TEE):
6552
```yaml
6653
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"]
7164
ports:
72-
- "25565:25565"
65+
- "8443:8443"
66+
volumes:
67+
- /var/run/dstack.sock:/var/run/dstack.sock
7368
```
7469
75-
Client side - create `stunnel.conf`:
76-
```ini
77-
foreground = yes
78-
pid = ./stunnel.pid
70+
## index.mjs
7971
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:
8573
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"`)
9077

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")
9281

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+
})
9788
```
9889

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
10591

106-
Then: `ssh tee-box`
92+
See [verify_tls.py](verify_tls.py) for the full implementation. The script:
10793

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
10998

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 └─────────────┘ └──────────┘
12299
```
100+
$ python3 verify_tls.py https://localhost:8443
123101
124-
**Pros:** Public IP, custom domains, bypasses gateway
125-
**Cons:** Trusts ngrok infrastructure, requires account
102+
Verifying: https://localhost:8443
126103
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)
128112
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.
143116
```
144117

145-
Deploy with your ngrok auth token:
118+
## Testing Locally
119+
146120
```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
148131
```
149132

150-
The ngrok container logs will show your public URL.
133+
## Connectivity Options
151134

152-
### Alternative: Cloudflare Tunnel
135+
The verification works regardless of how you reach the TEE - the attestation binds the certificate.
153136

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) |
161144

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
168150

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.
170152

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.
177154

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.
181156

182157
## Files
183158

184159
```
185160
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
189164
└── README.md
190165
```
191166

192167
## Next Steps
193168

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
196171

197-
## References
172+
## Key Insight
198173

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.

tutorial/03-gateway-and-tls/docker-compose-ngrok.yaml

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
11
services:
2-
tcp-echo:
2+
app:
33
build:
44
context: .
55
dockerfile_inline: |
6-
FROM python:3.12-slim
7-
RUN cat > /server.py <<'EOF'
8-
import socket
9-
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10-
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
11-
s.bind(('0.0.0.0', 8080))
12-
s.listen()
13-
print('TCP echo server on :8080')
14-
while True:
15-
conn, addr = s.accept()
16-
print(f'Connected: {addr}')
17-
conn.sendall(b'ECHO> ')
18-
while data := conn.recv(1024):
19-
conn.sendall(data)
20-
EOF
21-
CMD ["python", "/server.py"]
6+
FROM node:22-slim
7+
RUN apt-get update && apt-get install -y openssl
8+
WORKDIR /app
9+
RUN npm init -y && npm install @phala/dstack-sdk viem
10+
COPY index.mjs .
11+
CMD ["node", "index.mjs"]
2212
ports:
23-
- "8080:8080"
13+
- "8443:8443"
14+
volumes:
15+
- /var/run/dstack.sock:/var/run/dstack.sock

0 commit comments

Comments
 (0)