mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-25 12:01:04 +02:00
* 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>
99 lines
4 KiB
YAML
99 lines
4 KiB
YAML
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: {{ include "dograh.web.fullname" . }}
|
|
namespace: {{ .Release.Namespace }}
|
|
labels:
|
|
{{- include "dograh.labels" . | nindent 4 }}
|
|
app.kubernetes.io/component: web
|
|
spec:
|
|
{{- /* Omit spec.replicas only when the HPA object will actually render — matches
|
|
the gate in templates/web-hpa.yaml. If HPA is enabled but both metric
|
|
targets are null the HPA is suppressed, so we must keep the static
|
|
replica count here to avoid a Deployment with no owner. */ -}}
|
|
{{- if not (and .Values.autoscaling.web.enabled (or .Values.autoscaling.web.targetCPUUtilizationPercentage .Values.autoscaling.web.targetMemoryUtilizationPercentage)) }}
|
|
replicas: {{ .Values.web.replicaCount }}
|
|
{{- end }}
|
|
selector:
|
|
matchLabels:
|
|
{{- include "dograh.selectorLabels" . | nindent 6 }}
|
|
app.kubernetes.io/component: web
|
|
strategy:
|
|
type: RollingUpdate
|
|
rollingUpdate:
|
|
maxSurge: 1
|
|
maxUnavailable: 0
|
|
template:
|
|
metadata:
|
|
labels:
|
|
{{- include "dograh.selectorLabels" . | nindent 8 }}
|
|
app.kubernetes.io/component: web
|
|
annotations:
|
|
# Roll pods when the ConfigMap changes (e.g. `helm upgrade --set
|
|
# config.enableSignup=false`). envFrom values are otherwise only read
|
|
# at pod startup, so a ConfigMap-only upgrade would leave running pods
|
|
# on the stale value until an unrelated restart.
|
|
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
|
{{- with .Values.web.podAnnotations }}
|
|
{{- toYaml . | nindent 8 }}
|
|
{{- end }}
|
|
spec:
|
|
serviceAccountName: {{ include "dograh.serviceAccountName" . }}
|
|
{{- with .Values.imagePullSecrets }}
|
|
imagePullSecrets:
|
|
{{- toYaml . | nindent 8 }}
|
|
{{- end }}
|
|
# Long-lived signaling WebSockets keep state in-process; honor the
|
|
# configured drain window so in-flight calls survive a rolling
|
|
# update. See README "Decisions log".
|
|
terminationGracePeriodSeconds: {{ .Values.web.terminationGracePeriodSeconds }}
|
|
containers:
|
|
- name: web
|
|
image: {{ include "dograh.image" . }}
|
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
command: ["./scripts/run_web.sh"]
|
|
ports:
|
|
- name: http
|
|
containerPort: {{ .Values.web.port }}
|
|
protocol: TCP
|
|
envFrom:
|
|
{{- include "dograh.backendEnvFrom" . | nindent 12 }}
|
|
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).
|
|
livenessProbe:
|
|
{{- toYaml .Values.web.livenessProbe | nindent 12 }}
|
|
readinessProbe:
|
|
{{- toYaml .Values.web.readinessProbe | nindent 12 }}
|
|
lifecycle:
|
|
preStop:
|
|
# Sleep so the gateway / load balancer observes the pod
|
|
# NotReady and stops sending new connections before SIGTERM
|
|
# propagates to uvicorn.
|
|
exec:
|
|
command: ["sh", "-c", "sleep {{ .Values.web.preStopSleepSeconds }}"]
|
|
resources:
|
|
{{- toYaml .Values.web.resources | nindent 12 }}
|
|
{{- with .Values.web.nodeSelector }}
|
|
nodeSelector:
|
|
{{- toYaml . | nindent 8 }}
|
|
{{- end }}
|
|
{{- with .Values.web.tolerations }}
|
|
tolerations:
|
|
{{- toYaml . | nindent 8 }}
|
|
{{- end }}
|
|
{{- with .Values.web.affinity }}
|
|
affinity:
|
|
{{- toYaml . | nindent 8 }}
|
|
{{- end }}
|
|
{{- with .Values.web.topologySpreadConstraints }}
|
|
topologySpreadConstraints:
|
|
{{- toYaml . | nindent 8 }}
|
|
{{- end }}
|