TURN Server Configuration & Auth

TURN relays carry the media that direct peer-to-peer paths cannot. When symmetric NAT, carrier-grade NAT, or a corporate firewall blocks every host and srflx candidate pair, the relay is the only transport that still connects the call β€” and a misconfigured relay fails silently, surfacing only as a failed ICE state minutes into a session. This guide is part of the WebRTC Protocol Stack & Signaling Servers guide, and it walks through standing up an authenticated, production-grade TURN relay end to end: listener and port configuration, time-bound HMAC credentials, secure delivery to the browser, and the verification commands that prove the relay actually allocates before you ship it.

The goal is a relay that authenticates every Allocate request against a rotating shared secret, advertises a routable public address, listens on both UDP and TLS, and bounds per-user bandwidth so a single abusive client cannot exhaust the node. Two focused references extend this guide: Configuring Coturn for Production TURN Relay for the full turnserver.conf and OS-level tuning, and Time-Limited TURN Credentials with HMAC for the exact credential-signing math.

TURN allocation and auth flow A browser fetches an ephemeral HMAC credential from the app backend, then sends an Allocate request to the TURN server on port 3478 or 5349, which validates the credential and reserves a relay port in the 49152 to 65535 range. Browser RTCPeerConnection ICE agent App backend signs credential HMAC-SHA1 TURN server coturn validates secret fetch Listener ports 3478 STUN/TURN UDP 5349 TURNS / TLS 443 TCP fallback relay 49152-65535 Allocate + username:credential -> 3478 / 5349 200 OK + relayed transport address media relayed Credential = expiry:userId + base64( HMAC-SHA1( username, static-auth-secret ) )
TURN allocation and authentication: the browser fetches a signed credential, sends an Allocate to ports 3478/5349, and the relay reserves a port in 49152–65535.

Step 1 β€” Provision listeners, ports, and public IP

A TURN relay needs three things reachable from the open internet: a control port for Allocate/Refresh messaging, a TLS port for clients behind deep-packet-inspection proxies, and a contiguous block of relay ports for the actual media. The control port is 3478 (shared with STUN), the TLS port is 5349 (turns://), and the relay range is conventionally 49152–65535 β€” the IANA ephemeral range. Many production deployments also bind TLS on 443 so the relay is indistinguishable from ordinary HTTPS to a filtering proxy.

The single most common deployment failure is a missing or inverted external-ip. On any cloud instance the OS sees only the private RFC 1918 address; coturn must be told the public address explicitly or it will advertise an unroutable relay candidate and every call behind symmetric NAT will fail. The format is PUBLIC_IP/PRIVATE_IP β€” public first.

# /etc/turnserver.conf β€” listeners and addressing
listening-ip=0.0.0.0          # bind all interfaces; coturn picks per-request
listening-port=3478           # STUN + plain TURN (UDP and TCP)
tls-listening-port=5349       # turns:// over TLS
external-ip=203.0.113.10/10.0.1.5   # PUBLIC/PRIVATE β€” public address first
realm=turn.example.com        # authentication realm advertised to clients
server-name=turn.example.com
min-port=49152                # start of the relay media port range
max-port=65535                # end of the relay range β€” open BOTH in the firewall
fingerprint                   # add FINGERPRINT to messages (strict clients require it)

Open the relay range in your security group as well as 3478/5349/443. Media flows through min-port–max-port, not through 3478 β€” a firewall that allows only 3478 lets allocation succeed yet drops every media packet, producing a connection that negotiates and then goes silent. The full annotated config, including no-tcp-relay trade-offs and kernel tuning, lives in Configuring Coturn for Production TURN Relay.

Step 2 β€” Enable HMAC authentication and a rotating secret

Never ship static long-term usernames and passwords. A static credential embedded in a client bundle is scraped within hours and replayed to mine free relay bandwidth. The production model is the TURN REST API scheme (draft-uberti-behave-turn-rest): the server holds a single static-auth-secret, and your backend mints short-lived credentials by signing a timestamped username with HMAC-SHA1. The relay recomputes the same HMAC at allocation time, so no per-user state is stored and expired credentials are rejected automatically.

Enable the long-term credential mechanism and register the shared secret. stale-nonce forces nonce rotation and blocks replay of a captured handshake.

# /etc/turnserver.conf β€” authentication
lt-cred-mech                  # enable long-term credential mechanism (required for HMAC)
use-auth-secret               # use the REST-API shared-secret model, not per-user DB rows
static-auth-secret=BASE64_32_BYTE_SECRET   # the HMAC key; rotate via env injection
stale-nonce=600               # force a fresh nonce every 600s to defeat replay

The credential the browser presents is username = ${expiryUnixTimestamp}:${userId} and credential = base64(HMAC_SHA1(username, static-auth-secret)). Keep the TTL between 1 and 24 hours: shorter windows shrink the blast radius of a leaked token but force re-fetches on long calls and ICE restarts. coturn supports multiple static-auth-secret lines simultaneously, which is how you rotate with zero downtime β€” append the new secret, reload, and remove the old one after the longest outstanding TTL expires. The exact signing code, padding rules, and the failure log lines are covered in Time-Limited TURN Credentials with HMAC.

Step 3 β€” Deliver credentials and build the ICE server array

Credentials must reach the browser over an authenticated, encrypted channel before RTCPeerConnection is constructed β€” typically the same channel you already use for SDP, covered in WebSocket Signaling Implementation. Fetch them just-in-time from a backend endpoint that performs the HMAC signing server-side; the static secret must never touch the client.

Construct the iceServers array with both a turn: URL on 3478 and a turns: TLS URL, so the ICE agent can fall back to TLS-over-TCP when UDP is blocked. Pair the relay with a STUN Server Deployment Strategies endpoint so cheap srflx paths are tried before the relay is ever allocated.

// Fetch ephemeral credentials, then build the peer connection
const res   = await fetch('/api/turn-credentials', { credentials: 'include' });
const creds = await res.json();              // { username, credential, ttl }

const iceServers = [
  { urls: 'stun:stun.example.com:3478' },    // try srflx first β€” no relay bandwidth cost
  {
    urls: 'turn:turn.example.com:3478?transport=udp',  // primary relay over UDP
    username: creds.username,
    credential: creds.credential
  },
  {
    urls: 'turns:turn.example.com:5349?transport=tcp', // TLS fallback for DPI proxies
    username: creds.username,
    credential: creds.credential
  }
];

const pc = new RTCPeerConnection({
  iceServers,
  iceTransportPolicy: 'all'   // set 'relay' only to force relay-only paths for testing
});

Cache the credential object for the duration of ttl and reuse it across ICE restarts; re-fetching on every restartIce() adds avoidable signalling round-trips. To force the relay path during development and confirm it works in isolation, set iceTransportPolicy: 'relay' β€” this suppresses host and srflx candidates so only relay pairs remain.

Step 4 β€” Verification

Prove allocation works from the server before trusting it in production. coturn ships turnutils_uclient, which performs a real Allocate and relays test packets using a credential you supply.

# Mint a credential with your backend, then drive a real allocation through the relay
turnutils_uclient \
  -u "1780000000:alice" \
  -w "$(printf '%s' '1780000000:alice' \
        | openssl dgst -sha1 -hmac "$TURN_SECRET" -binary | base64)" \
  -y -m 10 turn.example.com    # -y verbose, -m 10 send 10 relayed messages

A healthy run logs allocate sent, allocate response received, and a relayed address in the 49152–65535 range. On the server, tail the log and confirm the matching allocation line.

# Watch coturn confirm or reject the allocation in real time
journalctl -u coturn -f \
  | grep -E "relayed address .* (allocated|not allocated)"
# success:  INFO: session ...: relayed address 203.0.113.10:51234 allocated
# failure:  ERROR: session ...: relayed address ... not allocated

Cross-check that the relayed IP equals the public half of your external-ip. From the browser side, poll pc.getStats() and confirm the selected candidate pair has localCandidateType === 'relay' with non-zero bytesSent/bytesReceived β€” that is the only definitive proof the relay is carrying media rather than just answering allocations.

Edge Cases & Browser Quirks

Common Implementation Mistakes

FAQ

When should I use static long-term credentials instead of ephemeral HMAC ones? Only for isolated internal test rigs where the relay is not internet-exposed. Any production relay must use the REST-API HMAC model (use-auth-secret + static-auth-secret) with a TTL between 1 and 24 hours, because static credentials are trivially scraped from client traffic and replayed.

How do I confirm a call is actually using the relay rather than a direct path? On the client, poll pc.getStats() for the succeeded candidate pair and check localCandidateType === 'relay' with rising bytesSent. On the server, grep coturn logs for relayed address … allocated and track active session counts.

Why does the relay work on home networks but fail on corporate ones? Corporate firewalls commonly block all UDP and restrict outbound to 80/443. Without a turns://…?transport=tcp listener on 443, those users have no reachable transport. Bind TLS on 443 and validate with turnutils_uclient over TLS before rollout.

Can I rotate the shared secret without dropping live calls? Yes. coturn accepts multiple static-auth-secret entries. Add the new secret, systemctl reload coturn, let outstanding credentials expire by their TTL, then remove the old secret β€” no active allocation is interrupted.

Related: this guide sits under WebRTC Protocol Stack & Signaling Servers; pair it with Configuring Coturn for Production TURN Relay, Time-Limited TURN Credentials with HMAC, STUN Server Deployment Strategies, and ICE Candidate Gathering & Filtering.