Files
rutster/docs/deploy/reverse-proxies.md
Aaron D. Lee 6c8c754cb1 docs(deploy): reverse-proxies.md — BYO nginx/HAProxy/Traefik tuned-timeout configs (slice-G)
The universal 60s idle-timeout footgun stated up front; the five-point
contract any proxy must honor (incl. RUTSTER_TRUSTED_PROXIES + honest
X-Forwarded-* for signature validation); complete snippets: nginx
(explicitly not-recommended, snippet-only support), HAProxy (timeout
tunnel in defaults per #2280; hard-stop-after warning), Traefik (pin the
exact version — #10601 / v2.11.2 / #11405 WS-breaking patch history).

Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-05 22:56:34 -04:00

140 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Bring your own reverse proxy — tuned-timeout configs
Supported: disable the `caddy` service in the compose stack
([quickstart-docker.md](quickstart-docker.md#t2--modular-compose-stack)); the engine keeps its
plaintext `:8080` listener. Everything below terminates TLS in *your* proxy and forwards to
the engine.
## The universal 60-second footgun
**nginx, HAProxy, and Traefik all default their idle/read timeouts to ~60180 seconds — every
one of them will kill a quiet, perfectly healthy call WebSocket.** A Media Streams WS can
legitimately be near-silent in one direction for hours, and neither Twilio nor Telnyx sends
protocol-level keepalives. The engine ships app-level WS pings (`RUTSTER_WS_PING_SECS`,
default 20) as belt-and-braces, but your proxy timeouts must still exceed the maximum call
duration. Every snippet below does that.
## The contract any proxy must honor
1. **Publicly-trusted cert**, auto-renewed — no self-signed path exists
([certificates.md](certificates.md)).
2. **WS upgrade with no frame buffering and no connection-lifetime cap** — 20 ms audio frames,
calls up to 24 h.
3. **Honest `X-Forwarded-Proto` and `X-Forwarded-Host`** (including on the WS upgrade
request): `X-Twilio-Signature` is HMAC over the URL as Twilio saw it
([Twilio's SSL-termination guidance](https://www.twilio.com/en-us/blog/developers/tutorials/building-blocks/handle-ssl-termination-twilio-node-js-helper-library)).
4. **Engine-side trust**: set `RUTSTER_TRUSTED_PROXIES` to your proxy's source IP/CIDR —
forwarded headers from unlisted sources are ignored, and signature validation will fail.
5. **TLS 1.2+1.3, mainstream ECDHE — never 1.3-only** (Twilio's 1.3 client support is
undocumented).
## nginx (works if you insist — not a recommended edge)
Why not recommended: no native DNS-01/wildcard support, and cert renewal requires a reload
whose old workers linger against hours-long WS connections (research basis: [TLS brief §4](../superpowers/specs/2026-07-05-tls-edge-decision-brief.md)). rutster ships this snippet and
nothing more for nginx.
```nginx
map $http_upgrade $connection_upgrade { default upgrade; '' close; }
server {
listen 443 ssl;
server_name pbx.example.com;
ssl_certificate /etc/letsencrypt/live/pbx.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/pbx.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; # never 1.3-only
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_buffering off; # 20 ms frames must not be coalesced
proxy_read_timeout 86400s; # the 60s default is the footgun
proxy_send_timeout 86400s;
}
}
```
## HAProxy (3.2 LTS or newer)
The best WS mental model of the classic family: after the upgrade, only `timeout tunnel`
governs the connection — **but only if you set it**; put it in `defaults` or it silently
doesn't apply to the paths you think it does (see
[haproxy#2280](https://github.com/haproxy/haproxy/issues/2280)). Two extra notes: `acme.sh`
can hot-load renewed certs through the stats socket with zero reload; and `hard-stop-after`
kills calls that are still draining — leave it unset or above your drain deadline.
```haproxy
defaults
mode http
timeout connect 5s
timeout client 75s
timeout server 75s
timeout tunnel 24h # governs upgraded WS; unset = the 60s-class footgun
frontend fe_rutster
bind :443 ssl crt /etc/haproxy/certs/ ssl-min-ver TLSv1.2
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Host %[req.hdr(Host)]
default_backend be_rutster
backend be_rutster
server fob 127.0.0.1:8080
```
## Traefik v3 — PIN THE EXACT VERSION
Traefik has the best built-in DNS-01/wildcard of the classic family **and a track record of
breaking WebSockets in patch releases**:
[#10601](https://github.com/traefik/traefik/issues/10601), the v2.11.2 timeout-behavior flip,
and [#11405](https://github.com/traefik/traefik/issues/11405). **Pin the exact image tag and
never auto-update the edge.**
```yaml
# traefik.yml (static config)
entryPoints:
websecure:
address: ":443"
transport:
respondingTimeouts:
readTimeout: 0 # 0 disables; the 60s-class default kills quiet WS
idleTimeout: 0
writeTimeout: 0
certificatesResolvers:
le:
acme:
email: you@example.com
storage: /data/acme.json # persist this volume — certificates.md
dnsChallenge:
provider: cloudflare
```
```yaml
# dynamic config
http:
routers:
rutster:
rule: "Host(`pbx.example.com`)"
entryPoints: [websecure]
service: rutster
tls: { certResolver: le }
services:
rutster:
loadBalancer:
servers:
- url: "http://127.0.0.1:8080"
```
Traefik forwards `X-Forwarded-Proto/Host` by default — you still must list its address in
`RUTSTER_TRUSTED_PROXIES`.
## First call
Any of the above in front of the engine, then the same two first-call paths as
[quickstart-docker.md](quickstart-docker.md): browser at `https://pbx.example.com/`, Twilio
webhook at `https://pbx.example.com/v1/trunk/webhook`.