Configuring Coturn for Production TURN Relay
This deep-dive turns a default coturn install into a hardened production relay: a correct turnserver.conf, public-IP mapping, HMAC authentication, resource limits, and the exact commands to verify an allocation. It is part of the TURN Server Configuration & Auth section under the broader WebRTC Protocol Stack & Signaling Servers guide. The specific decision here is which directives are non-negotiable for a relay that authenticates real users and survives symmetric NAT β and which defaults will silently break media if you leave them in place.
Context & Trade-offs
A relay only matters when ICE has exhausted host and srflx candidates. By then the call has already spent time gathering, so an additional 10β15 seconds of slow allocation pushes Chromeβs ICE agent toward a failed state before the relay pair is even tried. Two configuration choices dominate reliability. First, external-ip: on every cloud instance the kernel sees only the private RFC 1918 address, so coturn must be told the public address explicitly or it advertises an unroutable relay candidate. Second, the relay port range: media flows through min-portβmax-port (conventionally 49152β65535), not through the 3478 control port, so a firewall that opens only 3478 lets allocation succeed and then drops every media packet.
Transport coverage is the other axis. UDP on 3478 is the fast path, but corporate networks routinely block UDP and restrict outbound to 80/443. no-tcp-relay saves resources on a relay that only serves consumer traffic, but it must be removed the moment you need to serve users behind DPI proxies β those clients require TCP, ideally TLS on 443 via turns://. Keep relay nodes in the same regions as your STUN Server Deployment Strategies endpoints so allocation latency stays low and clients prefer the cheaper srflx path before falling back to relay.
Minimal Runnable Implementation
Disable every protocol you do not need, bind explicitly, and map the public address. This baseline authenticates with the REST-API HMAC model rather than a per-user database.
# /etc/turnserver.conf β production baseline
listening-ip=0.0.0.0 # bind all interfaces; coturn selects per request
listening-port=3478 # STUN + plain TURN (UDP/TCP)
tls-listening-port=5349 # turns:// over TLS
external-ip=203.0.113.10/10.0.1.5 # PUBLIC_IP/PRIVATE_IP β public first, non-negotiable
realm=turn.yourdomain.com # auth realm advertised in the 401 challenge
server-name=turn.yourdomain.com
min-port=49152 # first relay media port β open the whole range in the firewall
max-port=65535 # last relay media port
no-multicast-peers # block relays to multicast addresses (abuse vector)
no-tcp-relay # drop TCP relay allocations β REMOVE if UDP-blocked clients exist
lt-cred-mech # enable long-term credential mechanism (required for HMAC)
use-auth-secret # REST-API shared-secret model, no per-user DB rows
static-auth-secret=<YOUR_32_BYTE_SECRET> # HMAC key; supports multiple lines for rotation
stale-nonce=600 # rotate nonce every 600s to block handshake replay
fingerprint # add FINGERPRINT attribute; strict WebRTC clients require it
max-bps=5000000 # per-allocation cap β 5 Mbps ceiling on relayed throughput
user-quota=10 # max simultaneous allocations per credential
total-quota=1000 # max simultaneous allocations on the node
max-allocate-lifetime=3600 # purge stale allocations after 1 hour to reclaim ports
log-file=/var/log/turnserver/turn.log
verbose
no-tcp-relay disables TCP-based relay allocations; remove that single line when clients on UDP-blocking firewalls need to reach you. Enabling static-auth-secret without lt-cred-mech is a silent no-op β the HMAC mechanism never engages and every authenticated Allocate is rejected. The credential format your backend must produce is username = ${expiry}:${userId} with the credential being the base64 HMAC-SHA1 of that username; the signing code and TTL guidance live in Time-Limited TURN Credentials with HMAC.
Reproduction Steps & Debugging Log Patterns
- Apply the config and restart the daemon:
systemctl restart coturn. Confirm it bound the listeners withss -lunp | grep turnserverβ you should see UDP/TCP on 3478 and TLS on 5349. - Mint a test credential from your backend (or inline with
openssl) and drive a real allocation throughturnutils_uclient:
# Real Allocate + 10 relayed messages using an HMAC credential
turnutils_uclient \
-u "1780000000:alice" \
-w "$(printf '%s' '1780000000:alice' \
| openssl dgst -sha1 -hmac "$TURN_SECRET" -binary | base64)" \
-y -m 10 turn.yourdomain.com
- Tail the daemon log and watch for the allocation line:
journalctl -u coturn -f \
| grep -E "relayed address .* (allocated|not allocated)"
- Expected success:
INFO: session <id>: relayed address 203.0.113.10:51234 allocated, with the IP matching the public half ofexternal-ipand the port inside 49152β65535. A401/403in the log means the HMAC did not match β check the secret and theexpiry:userIdusername format.ERROR: β¦ not allocatedmeans a firewall is blocking the relay range orexternal-ipis wrong. - Confirm media actually flows:
turnutils_uclientprints sent/received message counts; a successful relay shows all 10 messages echoed. From a browser,pc.getStats()should report a succeeded candidate pair withlocalCandidateType === 'relay'.
Common Implementation Mistakes
- Omitting
external-ipbehind cloud NAT β coturn returns the private instance IP as the relay address, unreachable from public peers, and the call fails silently behind symmetric NAT. - Inverting the
external-ipformat β the order isPUBLIC_IP/PRIVATE_IP, public first; reversing it advertises the wrong address. static-auth-secretwithoutlt-cred-mechβ HMAC credentials require the long-term mechanism active, or every authenticated allocation is rejected.- Blocking the relay port range β opening only 3478 lets allocation succeed but drops all media; open
min-portβmax-portfor both UDP and TCP. - Forgetting
fingerprintβ disables message integrity and breaks strict WebRTC clients that validate TURN message authenticity.
FAQ
Why does Coturn allocate relay addresses but WebRTC peers still fail to connect?
Almost always a misconfigured external-ip: ICE is advertising the private RFC 1918 address instead of the public one. Verify the allocated relay IP in the log matches your public interface, and confirm UDP and TCP are open across the entire min-portβmax-port range in your security group.
Can I run Coturn without a signalling server? No. Coturn only handles media relay allocation; it does not exchange SDP. You still need a separate signalling path β see WebSocket Signaling Implementation β to deliver offers, answers, and the ephemeral credentials.
How do I rotate static-auth-secret without dropping active sessions?
Append the new secret as an additional static-auth-secret line, run systemctl reload coturn, and wait for the longest outstanding credential TTL to expire before removing the old line. Both secrets validate during the overlap, so no active allocation is interrupted.
Related: return to TURN Server Configuration & Auth, pair this with Time-Limited TURN Credentials with HMAC, and co-locate nodes with your STUN Server Deployment Strategies.