# TURN for office screen sharing This directory is an **operator-reviewed template**, not an installer. Nothing here changes DNS, Caddy, systemd, UFW or Oracle Cloud. Those are public routing changes and require explicit operator approval on the deployment host. Tera's browser peers normally connect directly. Coturn is the fallback for symmetric NATs and restrictive networks; it relays encrypted WebRTC packets and never receives application signaling, screen URLs or recordings. The signaling service remains the separate loopback API described in `server/README.md`. > **Mandatory public-enablement gate:** do not expose the TURN listeners or set > `TERA_ICE_URLS` / `TERA_TURN_SHARED_SECRET` in production until the deployed > ICE endpoint requires an active, unexpired screen-signaling grant for the exact > office/screen binding and authenticates that grant to the same signed-in > subject. The current endpoint checks only global member authentication, which > would let any signed-in member mint a general-purpose Internet relay credential. > It is intentionally not approved for broad public TURN enablement. The production host audited on 2026-08-11 has private VNIC `10.0.0.2`, public IPv4 `170.9.14.61`, and an existing `turn.lumbridgecorp.com` A record. Reconfirm all three immediately before deployment. Do not copy those addresses into a different installation merely because they appear here. ## 1. Review the network boundary The intended public listeners are: | Transport | Port | Purpose | | --- | ---: | --- | | UDP | 3478 | primary STUN/TURN listener | | TCP | 3478 | TURN fallback | | TCP/TLS | 5349 | TURN/TLS fallback | | UDP | 52000–53023 | dedicated relay allocations | Open the same ingress in **both** UFW and the instance's OCI security list or NSG. The OCI console is authoritative; a host firewall rule does not prove the cloud edge permits traffic. Relay UDP must accept arbitrary Internet peers, not only the browser that created an allocation. Outbound traffic remains allowed. Example commands to review, not paste blindly: ```bash dig +short A turn.lumbridgecorp.com ip -br address curl -4 https://api.ipify.org sudo ufw status verbose # Only after the operator approves these public ingress changes: sudo ufw allow 3478/udp comment 'Tera TURN UDP' sudo ufw allow 3478/tcp comment 'Tera TURN TCP' sudo ufw allow 5349/tcp comment 'Tera TURN TLS' sudo ufw allow 52000:53023/udp comment 'Tera TURN relay' ``` Add equivalent stateful OCI ingress rules. Do not open UDP 5349: this template disables DTLS. Do not reuse UDP 50000–50200; stopped LiveKit deployments on the audited host reserve that range and could otherwise collide if restarted. The public/private NAT must preserve relay port numbers. This is why the config uses `external-ip=PUBLIC/PRIVATE`; a relay candidate advertising `10.0.0.2` or a different public port is a failed deployment. ## 2. Install and render the config Prefer Ubuntu's native coturn package and unit. Docker adds no isolation benefit to a service that needs a large host UDP range, and bridged port translation is an extra failure mode. Inspect the package before enabling it: ```bash apt-cache policy coturn systemctl cat coturn.service getent passwd turnserver getent group turnserver ``` Some distro packages also ship `/etc/default/coturn` with an explicit enable switch. Inspect it and the unit's conditions; set `TURNSERVER_ENABLED=1` only as part of the reviewed enablement, never by assuming a successful `systemctl` command means the daemon actually bound its sockets. Generate one 256-bit hexadecimal secret without putting its value in shell history: ```bash umask 077 TURN_SECRET="$(openssl rand -hex 32)" test "${#TURN_SECRET}" -eq 64 ``` Render `turnserver.conf.example` to a root-owned `/etc/turnserver.conf`, replacing the two IP placeholders and the secret placeholder. The installed file should be `root:turnserver` mode `0640`. Put the **same raw secret** in the Tera API's root-owned environment file: ```ini TERA_TURN_SHARED_SECRET= TERA_ICE_URLS=stun:turn.lumbridgecorp.com:3478,turn:turn.lumbridgecorp.com:3478?transport=udp,turn:turn.lumbridgecorp.com:3478?transport=tcp,turns:turn.lumbridgecorp.com:5349?transport=tcp TERA_TURN_CREDENTIAL_TTL=300 ``` The audited service reads `/etc/tera-api.env`; the repository unit reads `/etc/tera/tera.env`. Use the path its installed unit actually declares. Never commit either rendered file, print the secret, put it in a URL, or pass it as a process argument. Clear the shell variable after both files are installed: ```bash unset TURN_SECRET ``` The current authenticated `POST /api/v1/media/ice` endpoint validates only the signed-in caller and returns a five-minute username/password generated with coturn's REST scheme. That is insufficient authorization for a public relay. Before enabling the environment above, the deployed request must also carry an active screen-signaling credential and exact binding in its POST body. The server must validate its token hash, subject, role, binding, lease and revocation state, then rate-limit issuance by both subject and trusted client IP. The presenter or viewer must join signaling before requesting ICE configuration. GET/query-string credentials remain forbidden. An accepted response uses: - username: `:` - credential: Base64 HMAC-SHA1 of that username using `TERA_TURN_SHARED_SECRET` - response: `Cache-Control: private, no-store` The shared secret never leaves the server. Expiry prevents new allocations and refreshes; it cannot instantly terminate an allocation that already exists. Install `tera-coturn-preflight` as `/usr/local/libexec/tera-coturn-preflight` mode `0755`, and install the reviewed systemd drop-in only after confirming the distro unit name. The preflight deliberately refuses unresolved placeholders, short/non-hex secrets, and missing certificates. ## 3. Issue and renew the TLS certificate Do not point coturn into Caddy's private certificate store. It is owned by the Caddy account, and Caddy renewal does not provide coturn a reliable reload hook. 1. Create `/var/www/turn-acme` root-owned and readable by Caddy. 2. Review `turn-acme.Caddyfile.example`, replace its private IP, import it, run `caddy validate`, and reload Caddy. 3. Verify a test file under `/.well-known/acme-challenge/` is reachable over public port 80. 4. Use Certbot's webroot mode for `turn.lumbridgecorp.com`. 5. Install `certbot-deploy-hook` under `/etc/letsencrypt/renewal-hooks/deploy/` mode `0755`. 6. Run the hook once with `RENEWED_LINEAGE` set to the issued lineage, then verify ownership and certificate names without printing the private key. Example issuance, after review: ```bash sudo certbot certonly --webroot -w /var/www/turn-acme \ -d turn.lumbridgecorp.com sudo env RENEWED_LINEAGE=/etc/letsencrypt/live/turn.lumbridgecorp.com \ /etc/letsencrypt/renewal-hooks/deploy/certbot-deploy-hook sudo openssl x509 -in /etc/coturn/certs/turn.fullchain.pem \ -noout -subject -issuer -dates -ext subjectAltName ``` `try-reload-or-restart` may restart coturn when the package unit has no reload action, interrupting allocations. Confirm the installed unit's behavior and schedule renewal accordingly. Test renewal with `certbot renew --dry-run` before calling the certificate path production-ready. ## 4. Validate before enabling First prove the mandatory application gate: a signed-in member with no active signaling grant, and a caller using a stopped, revoked, expired, wrong-subject or wrong-binding grant, must all receive no TURN credential. Only then configure the shared secret/ICE URLs and approve public firewall changes. Keep the TURN ports closed and both Tera TURN environment values unset if any case fails. Run the repository preflight against a staged rendered config, then use coturn's installed version/config inspection facilities. Do not start the daemon merely to discover an unresolved placeholder on a public interface. ```bash sudo TURN_CONFIG=/etc/turnserver.conf \ /usr/local/libexec/tera-coturn-preflight turnserver --version sudo systemctl daemon-reload sudo systemctl start coturn.service # operator-approved change sudo systemctl --no-pager --full status coturn.service sudo journalctl -u coturn.service --since -10m --no-pager sudo ss -lntup | grep -E ':3478|:5349' ``` From a machine outside OCI: ```bash dig +short A turn.lumbridgecorp.com openssl s_client -connect turn.lumbridgecorp.com:5349 \ -servername turn.lumbridgecorp.com