Django/Gunicorn HTTPS trust: forwarding-header guarantees and supported configuration
jak0w122
HOBBYOP

9 hours ago

We use Django 5.2 LTS and Gunicorn 26.2 on Railway Hobby, with exact hosts/origins, secure cookies and HTTPS redirects. Our intended environment contains only our web app, private recovery worker and PostgreSQL, with no web TCP proxy. We have paused synthetic validation.

Employee-confirmed support discussion explains why stable peer allowlisting is unavailable and private services must be trusted. Could a Railway engineer please confirm:

Is client-supplied X-Forwarded-Proto always removed and replaced with exactly one https value, including duplicate headers, case variants and comma-separated values over HTTP/1.1 and HTTP/2?

What supported Django/Gunicorn configuration should determine HTTPS in this controlled topology, including readiness probes, without assuming static proxy addresses?

Is there any supported listener restriction or authenticated edge-origin mechanism that avoids trusting every private caller? Please identify any other ingress paths we must account for.

Please link current guarantees/documentation and identify any limitations across restarts or redeployments. We have not relaxed proxy trust and are requesting guidance before a reviewed change.

Awaiting User Response

5 Replies

Railway
BOT

9 hours ago

Our HTTP edge overwrites X-Forwarded-Proto, X-Real-IP, X-Forwarded-For and X-Forwarded-Host on every request it forwards. Copies supplied by the client are replaced, not kept or appended to, and the specs describe X-Forwarded-Proto as https. TLS ends at the edge, and your app receives HTTP/1.1, with HTTP/2 converted when needed. We have not confirmed how Forwarded is handled, and the specs note that these details can change.

This only covers the public edge, including .up.railway.app. Direct *.railway.internal requests skip the edge, so any service in the same environment can reach your port and send its own headers. There is no edge-only listener, signed header or app-visible mTLS identity, and no stable proxy address range to allowlist. Trusting proxy headers from any peer therefore means trusting every private caller in that environment, plus any TCP proxy you add later, since TCP proxy traffic passes through unchanged.

Healthchecks use the hostname healthcheck.railway.app, run only at deploy time, and don't go through the edge (docs). Your allowed hosts and redirect rules need to let that request return a 2xx.

Edge Rules can filter public-domain traffic by IPv4, host, path or header. On Hobby they require turning on the feature flag at feature flags. They don't affect private callers and don't validate client certificates.


Status changed to Awaiting User Response Railway • about 9 hours ago


jak0w122
HOBBYOP

9 hours ago

Thank you. Synthetic validation remains paused, and we have made no configuration changes.

Could a Railway engineer please confirm the two outstanding points?

  1. Does the edge remove all client-supplied X-Forwarded-Proto values—including duplicate headers, case variants and comma-separated lists—and deliver exactly one https value?
  2. What concrete Django 5.2/Gunicorn 26.2 configuration do you recommend for determining HTTPS without static proxy addresses, assuming every private caller is trusted and no TCP proxy exists?

Our application already has narrowly scoped readiness handling. We are not seeking to broaden allowed hosts or enable Edge Rules.

We understand that environment-wide trust would replace exact-peer authentication. Please confirm that this is the supported approach and identify any remaining limitations before we review a change.


Status changed to Awaiting Railway Response Railway • about 9 hours ago


Railway
BOT

9 hours ago

Our edge overwrites X-Forwarded-Proto on every request it forwards, so copies the client sends are replaced, not kept or appended to, and the specs describe the value as https (technical specifications). We have not separately confirmed how each malformed shape (duplicate lines, case variants, comma lists, HTTP/1.1 vs HTTP/2 clients) is normalized, and the specs note these details can change, so this is not a per-case guarantee.

Environment-wide trust is the model that fits our platform. There is no stable proxy address range to allowlist, no edge-only listener, and no signed header or app-visible mTLS identity, so treating the forwarded scheme as authoritative from any peer is the only option that does not rely on static addresses. In Django that means relying on SECURE_PROXY_SSL_HEADER for HTTP_X_FORWARDED_PROTO = https, and if Gunicorn should also honor the header, setting its forwarded-allow-ips to trust all peers. Both accept the header from whoever connects.

The limitations stay the same across restarts and redeploys. Any service in the same environment can reach your port over *.railway.internal and send its own X-Forwarded-Proto, because private traffic skips the edge. A TCP proxy added later would pass client headers through untouched. Healthchecks also skip the edge and only run at deploy time, so they arrive without the edge's header. Private networks are isolated between projects and environments, which is what bounds that trust.


Status changed to Awaiting User Response Railway • about 9 hours ago


jak0w122
HOBBYOP

9 hours ago

Thank you for clarifying the limitations. Could a Railway engineer please review this exchange and confirm the proposed trust model and configuration guidance?

We understand that malformed-header normalization remains unverified. We need human technical review before approving a change to our security boundary. Please route this thread to the appropriate engineer.


Status changed to Awaiting Railway Response Railway • about 9 hours ago


Railway
BOT

3 hours ago

Yes, for this topology, accepting the forwarded scheme from every peer fits our platform. We don't publish a stable proxy address range for the socket peer your container sees, and there is no edge-only listener, signed forwarding header, or app-visible mTLS identity you could use to authenticate the edge. That trust holds only because private networks are isolated between projects and environments, so it covers every service in that environment.

We cannot provide guaranteed support from a team member on this plan; apologies.


Status changed to Awaiting User Response Railway • about 3 hours ago


Welcome!

Sign in to your Railway account to join the conversation.

Loading...