From 58cc9c8b1cc52e71bf84c2b846fe523b6e8df71b Mon Sep 17 00:00:00 2001 From: prabhatlepton Date: Thu, 16 Jul 2026 10:26:45 +0530 Subject: [PATCH] fix(web): honor X-Forwarded-Proto in uvicorn so request.url is https behind a reverse proxy (#515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(web): honor X-Forwarded-Proto in uvicorn so request.url is https behind a reverse proxy ## Problem When Dograh runs behind a TLS-terminating reverse proxy (Cloudflare → Traefik in Kubernetes, nginx in the docker-compose install), the inside of the cluster/host is plain HTTP. Uvicorn defaults to trusting `scope["scheme"]` from the socket, so `request.url.scheme` reads `http` even though the client dialed `https`. That breaks any code path that hashes or echoes the request URL back to the caller. Concrete symptom seen in production: **Vobiz inbound webhook signatures fail with "signature validation failed for vobiz"** because Vobiz computes HMAC over the URL it dialed (`https://.../inbound/run`) while Dograh recomputes it as `http://...`. Log excerpt from the failing call: ``` WARNING | provider.py | Vobiz webhook signature mismatch. Expected: daOpAZPm..., Got: 1+eW/RxE... WARNING | telephony.py | /inbound/run: signature validation failed for vobiz ``` Twilio, Plivo and any other provider that signs over the callback URL have the same failure mode when Dograh is deployed behind a proxy. ## Fix Start uvicorn with `--proxy-headers --forwarded-allow-ips="*"` in `scripts/run_web.sh`. Uvicorn rewrites `scope["scheme"]` and client address from `X-Forwarded-Proto` / `X-Forwarded-For` when the request originates from a trusted upstream — Traefik and Cloudflare set both correctly, so `request.url.scheme == "https"` inside the app once again and provider signature checks pass. Verified end-to-end on a production k3s install (Traefik + Cloudflare edge → dograh-web pod) — after the change, the very next Vobiz inbound webhook validated successfully and the call connected past the previous 11-second signature-failure hangup. * address review: let operators narrow FORWARDED_ALLOW_IPS Both bot reviewers on #515 flagged `--forwarded-allow-ips="*"` as a defence-in-depth concern: if uvicorn is directly reachable from an untrusted network (bypassing the proxy), any client can spoof `X-Forwarded-Proto` / `X-Forwarded-For`, and uvicorn will rewrite `request.client` / `request.url` from those attacker-controlled headers. Fix: consume `FORWARDED_ALLOW_IPS` from the environment (uvicorn already recognizes this env var; see `deploy/hostinger/docker-compose.yaml:179` for the existing precedent). Default stays `"*"` so the behavior of the original fix is preserved for the standard docker-compose / helm layouts where the app pod is only reachable via the proxy Service. Operators who terminate uvicorn on a host that's also reachable directly can narrow it to the proxy CIDR: FORWARDED_ALLOW_IPS="10.42.0.0/16" ./scripts/run_web.sh * address review: declare FORWARDED_ALLOW_IPS in the helm chart, not the script uvicorn already enables proxy-header handling by default and falls back to the FORWARDED_ALLOW_IPS env var when --forwarded-allow-ips is absent, so the CLI flags were redundant and the script-level "*" default hid a security-relevant trust decision away from operators. Drop the flags, keep run_web.sh deployment-agnostic, and declare the env var where the other deployment config lives — web.forwardedAllowIps in values.yaml (default "*", narrowable to a proxy CIDR) — mirroring how docker-compose already sets it on the api service. Co-Authored-By: Claude Fable 5 * simplify run_web.sh comment Co-Authored-By: Claude Fable 5 --------- Co-authored-by: prabhat pankaj Co-authored-by: Abhishek Kumar Co-authored-by: Claude Fable 5 --- deploy/helm/dograh/templates/web-deployment.yaml | 4 ++++ deploy/helm/dograh/values.yaml | 10 ++++++++++ scripts/run_web.sh | 4 ++++ 3 files changed, 18 insertions(+) diff --git a/deploy/helm/dograh/templates/web-deployment.yaml b/deploy/helm/dograh/templates/web-deployment.yaml index 5dcaf18c..3afb09c1 100644 --- a/deploy/helm/dograh/templates/web-deployment.yaml +++ b/deploy/helm/dograh/templates/web-deployment.yaml @@ -61,6 +61,10 @@ spec: env: - name: WEB_PORT value: {{ .Values.web.port | quote }} + # Trust proxy headers from these peers so request.url reflects the + # original https scheme — see web.forwardedAllowIps in values.yaml. + - name: FORWARDED_ALLOW_IPS + value: {{ .Values.web.forwardedAllowIps | quote }} {{- include "dograh.dbEnv" . | nindent 12 }} # Distinct probes: readiness flips fast (drain), liveness is # slower (process aliveness). diff --git a/deploy/helm/dograh/values.yaml b/deploy/helm/dograh/values.yaml index 47d08fb8..ef68600a 100644 --- a/deploy/helm/dograh/values.yaml +++ b/deploy/helm/dograh/values.yaml @@ -158,6 +158,16 @@ web: replicaCount: 2 port: 8000 + # Peers uvicorn trusts X-Forwarded-Proto / X-Forwarded-For from (exported + # as FORWARDED_ALLOW_IPS). The ingress proxy reaches the pod from a + # pod-network IP — not loopback — so uvicorn's 127.0.0.1 default ignores + # the headers, request.url reads back as http://, and telephony providers + # that sign their webhook URL (Vobiz, Twilio, Plivo) fail signature + # validation. "*" trusts every peer, which is fine while only the ingress + # can reach the pod; narrow to your proxy/pod CIDR (e.g. "10.42.0.0/16") + # if the pod is reachable from untrusted networks. + forwardedAllowIps: "*" + # Long-lived signaling WebSockets keep per-connection state in process # memory (api/routes/webrtc_signaling.py). A naive pod restart drops every # in-flight call. The two settings below give the gateway time to stop diff --git a/scripts/run_web.sh b/scripts/run_web.sh index 913eb73a..31571e21 100755 --- a/scripts/run_web.sh +++ b/scripts/run_web.sh @@ -10,5 +10,9 @@ fi PORT="${WEB_PORT:-8000}" +# uvicorn trusts X-Forwarded-Proto / X-Forwarded-For only from peers listed +# in the FORWARDED_ALLOW_IPS env var (default 127.0.0.1). Behind a reverse +# proxy it must be set (compose: api service env, helm: web.forwardedAllowIps) +# or request.url stays http:// and URL-signed webhook validation fails. cd "$BASE_DIR" exec uvicorn api.app:app --host 0.0.0.0 --port "$PORT" --workers 1