fix: stop leaking TURN secret to browser clients
All checks were successful
Build / Build & release (push) Successful in 13m40s

WASTE_CONFIG.turnSecret was shipped in plaintext config.js and used to
compute coturn HMAC credentials client-side in browser.ts. Anyone reading
the PWA's JS could read the secret and mint unlimited long-lived TURN
credentials, turning the relay into an open proxy.

The anchor now mints short-lived (1h) credentials server-side via a new
GET /turn-credentials endpoint (-turn-secret flag), mirroring what the
daemon already does. The browser fetches credentials instead of holding
the secret. Daemon mode was unaffected (already server-side).

Docs updated to drop turnSecret from config.js examples, document the
new nginx route, and instruct anyone with an old config.js to rotate
the coturn secret since it was previously exposed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-30 18:48:59 +02:00
parent bf4009558d
commit f425e0bb8e
5 changed files with 84 additions and 23 deletions

View File

@@ -134,7 +134,7 @@ This tells the browser where to connect for signaling. Without it the join form
### 3. Nginx Proxy Manager setup
Create one **Proxy Host** for your domain (e.g. `waste.example.com`) with TLS enabled. You need two locations:
Create one **Proxy Host** for your domain (e.g. `waste.example.com`) with TLS enabled. You need these locations:
**Location 1 — WebSocket signaling (`/ws`)**
- Location: `/ws`
@@ -142,6 +142,12 @@ Create one **Proxy Host** for your domain (e.g. `waste.example.com`) with TLS en
- Forward port: `8080`
- Enable: WebSockets Support
**Location 1b — TURN credentials (`/turn-credentials`, only if using TURN — see [step 4](#4-turn-relay-optional-fixes-mobile--cgnat))**
- Location: `/turn-credentials`
- Forward hostname/IP: `127.0.0.1`
- Forward port: `8080`
- Plain HTTP, no WebSockets toggle needed
**Location 2 — Web UI (catch-all)**
- Location: `/`
- Choose "Serve Static Files" (or point to a local HTTP server serving `/var/www/waste-web`)
@@ -162,6 +168,7 @@ Or use `serve-web.sh` which handles PID tracking and restart:
The key requirements:
- `/ws` → anchor process (WebSocket, keep-alive)
- `/turn-credentials` → anchor process (plain HTTP; only needed if using TURN)
- `/*` → static file server (SPA fallback: return `index.html` for unknown paths)
### 4. TURN relay (optional, fixes mobile / CGNAT)
@@ -200,21 +207,32 @@ systemctl enable coturn
systemctl start coturn
```
**Update `config.js`** to tell browsers about the TURN server:
**Start the anchor with the same secret**, so it can mint short-lived credentials on your behalf:
```bash
./waste-anchor -bind 0.0.0.0:8080 -turn-secret YOUR_SECRET_HERE
```
This enables `GET /turn-credentials` on the anchor, which returns a fresh `{username, credential}` pair (1-hour TTL) computed from the shared secret — the secret itself never leaves the server.
**Update `config.js`** to tell browsers about the TURN server (no secret here — only the public relay address):
```js
window.WASTE_CONFIG = {
signalURL: 'wss://your-domain.com/ws',
turnURL: 'turn:your-domain.com:3478',
turnSecret: 'YOUR_SECRET_HERE',
}
```
The `use-auth-secret` mode generates short-lived TURN credentials from the shared secret — no user database required. The relay only sees opaque DTLS-encrypted blobs.
> **Security note:** earlier versions of this doc had you put `turnSecret` directly in `config.js`. Don't — anyone reading the PWA's JS bundle could read it and mint unlimited, long-lived TURN credentials, turning your relay into an open proxy for anyone. The browser now calls the anchor's `/turn-credentials` endpoint instead and only ever sees a credential that expires in an hour. If you have an old `config.js` with `turnSecret` set, remove it and rotate the coturn secret (`static-auth-secret` in `turnserver.conf` and the anchor's `-turn-secret` flag) since the old one was exposed.
> The browser adapter reads `turnURL` and `turnSecret` from `WASTE_CONFIG` and adds the TURN server to the WebRTC `ICEServers` list automatically. If not configured, STUN-only is used (works for most desktop/home NAT situations).
The browser adapter calls `signalURL` with `/ws` swapped for `/turn-credentials` to find the anchor's endpoint by default; set `turnCredentialsURL` explicitly in `WASTE_CONFIG` if the anchor is reachable at a different path. If `turnURL` is set but the credentials endpoint is unreachable, the browser falls back to STUN-only.
**Daemon mode TURN:** pass `-turn-url turn:your-domain.com:3478 -turn-secret YOUR_SECRET_HERE` when starting the daemon. The same coturn `use-auth-secret` HMAC-SHA1 scheme is used — no extra config required beyond what you set up for browser mode.
You'll also need nginx to route the new path to the anchor, alongside `/ws` (see [step 3](#3-nginx-proxy-manager-setup)):
- `/turn-credentials` → anchor process (plain HTTP, no WebSocket upgrade needed)
**Daemon mode TURN:** pass `-turn-url turn:your-domain.com:3478 -turn-secret YOUR_SECRET_HERE` when starting the daemon. This is unaffected by the above — the daemon computes credentials itself server-side and never exposes the secret, same as the anchor now does for browser mode.
---