fix(web): honor X-Forwarded-Proto in uvicorn so request.url is https behind a reverse proxy (#515)

* 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 <noreply@anthropic.com>

* simplify run_web.sh comment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: prabhat pankaj <prabhatiitbhu@gmail.com>
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
prabhatlepton 2026-07-16 10:26:45 +05:30 committed by GitHub
parent ef9c276e44
commit 58cc9c8b1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 18 additions and 0 deletions

View file

@ -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).

View file

@ -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