fix(nginx): preserve upstream X-Forwarded-Proto when behind another TLS proxy by ajayr · Pull Request #3793 · bytedance/deer-flow · GitHub
Skip to content

fix(nginx): preserve upstream X-Forwarded-Proto when behind another TLS proxy#3793

Open
ajayr wants to merge 1 commit into
bytedance:mainfrom
ajayr:fix/nginx-forwarded-proto
Open

fix(nginx): preserve upstream X-Forwarded-Proto when behind another TLS proxy#3793
ajayr wants to merge 1 commit into
bytedance:mainfrom
ajayr:fix/nginx-forwarded-proto

Conversation

@ajayr

@ajayr ajayr commented Jun 25, 2026

Copy link
Copy Markdown

Why

When DeerFlow's bundled nginx runs behind another TLS-terminating reverse proxy (e.g. Pangolin/Traefik, Cloudflare, Caddy) — a common way to expose a self-hosted DeerFlow publicly over HTTPS — login silently fails. The user submits valid credentials and is bounced straight back to the login screen with no session established.

Root cause: every location block hardcodes proxy_set_header X-Forwarded-Proto $scheme. The front proxy terminates TLS and reaches nginx over plain HTTP on the private hop, so $scheme is http, overwriting the https the front proxy already set. The Gateway then treats HTTPS browser traffic as HTTP, and:

  • CSRFMiddleware.is_allowed_auth_origin computes the expected origin as http://<host>, which no longer matches the browser's Origin: https://<host>, so the login POST is rejected with 403 "Cross-site auth request denied" (backend/app/gateway/csrf_middleware.py).
  • is_secure_request() returns false, so the access_token session cookie is set without Secure and without max-age (session-only).

What changed

nginx now preserves an upstream proxy's X-Forwarded-Proto instead of unconditionally overwriting it, via a map that falls back to $scheme when there is no upstream value:

map $http_x_forwarded_proto $forwarded_proto {
    default    $scheme;
    "~*^https" https;
}

All proxy_set_header X-Forwarded-Proto lines now use $forwarded_proto.

  • Standalone make dev / Docker (nginx is the TLS edge): no incoming X-Forwarded-Proto → falls back to $schemebehavior unchanged.
  • Behind another HTTPS proxy: the real https scheme reaches the Gateway → the login origin check passes and session cookies regain Secure + max-age.

Note: this trusts an incoming X-Forwarded-Proto, which is the intended behavior when nginx sits behind a trusted front proxy. Operators who expose nginx's port directly to untrusted clients should terminate TLS at nginx (the $scheme fallback covers that) or restrict the trusted-proxy source.

Surface area

  • Sandboxdocker/ or sandboxed execution (reverse-proxy config under docker/nginx/)
  • Default behavior change (no — standalone deployments are byte-for-byte unchanged via the $scheme fallback)

Bug fix verification

nginx config is not exercised by the unit/E2E suites, so verified manually against the running stack (requests reaching the Gateway through nginx):

# BEFORE — X-Forwarded-Proto clobbered to http:
$ curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:2026/api/v1/auth/login/local \
    -H 'Host: example.com' -H 'Origin: https://example.com' \
    -H 'Content-Type: application/x-www-form-urlencoded' --data 'username=x@x.com&password=wrong'
403            # "Cross-site auth request denied"

# AFTER — front proxy's X-Forwarded-Proto: https preserved:
$ curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:2026/api/v1/auth/login/local \
    -H 'Host: example.com' -H 'X-Forwarded-Proto: https' -H 'Origin: https://example.com' \
    -H 'Content-Type: application/x-www-form-urlencoded' --data 'username=x@x.com&password=wrong'
401            # origin check passes → reaches credential check; access_token cookie now has `Secure`

The standalone case (no X-Forwarded-Proto) returns the same result as before via the $scheme fallback.

Validation

  • nginx -t passes on the modified config.
  • Manual reproduction above (403 → 401 flip; Secure flag restored on session cookies).
  • No backend/frontend source touched; no frontend/ changes, so E2E is unaffected.

🤖 Generated with Claude Code

…LS proxy

When DeerFlow's nginx runs behind another TLS-terminating reverse proxy
(Pangolin/Traefik, Cloudflare, Caddy), every location block overwrote the
already-correct X-Forwarded-Proto with $scheme (= http on the private hop).
The Gateway then treated HTTPS browser traffic as HTTP: the auth-origin check
rejected the login POST with 403 "Cross-site auth request denied", and session
cookies lost the Secure flag and max-age.

Preserve an upstream X-Forwarded-Proto via a map that falls back to $scheme when
nginx is itself the TLS edge, so standalone `make dev` / Docker is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@CLAassistant

CLAassistant commented Jun 25, 2026

Copy link
Copy Markdown

@github-actions github-actions Bot added area:sandbox Sandboxed execution and docker/ risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/S PR changes 20-100 lines labels Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:sandbox Sandboxed execution and docker/ risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/S PR changes 20-100 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants