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

@@ -6,12 +6,17 @@ package main
import (
"context"
"crypto/ed25519"
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"flag"
"log"
"net/http"
"strconv"
"sync"
"time"
@@ -23,16 +28,40 @@ import (
func main() {
bind := flag.String("bind", "0.0.0.0:17339", "address to listen on")
turnSecret := flag.String("turn-secret", "", "coturn use-auth-secret shared secret; enables GET /turn-credentials")
flag.Parse()
a := newAnchor()
http.HandleFunc("/ws", a.handleWS)
if *turnSecret != "" {
http.HandleFunc("/turn-credentials", turnCredentialsHandler(*turnSecret))
log.Printf("anchor: /turn-credentials enabled")
}
log.Printf("anchor: listening on %s", *bind)
if err := http.ListenAndServe(*bind, nil); err != nil {
log.Fatalf("anchor: %v", err)
}
}
// turnCredentialsHandler mints short-lived coturn use-auth-secret credentials
// server-side, so the shared secret never reaches the browser. Mirrors the
// scheme in internal/netmgr.Manager.turnICEServers (daemon mode).
func turnCredentialsHandler(secret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
expiry := strconv.FormatInt(time.Now().Add(time.Hour).Unix(), 10)
mac := hmac.New(sha1.New, []byte(secret))
mac.Write([]byte(expiry))
credential := base64.StdEncoding.EncodeToString(mac.Sum(nil))
json.NewEncoder(w).Encode(struct {
Username string `json:"username"`
Credential string `json:"credential"`
TTL int `json:"ttl"`
}{Username: expiry, Credential: credential, TTL: 3600})
}
}
// ── Anchor ────────────────────────────────────────────────────────────────────
type client struct {