ASP.NET Core trusted ingress boundary and authoritative client IP
sulemathar
HOBBYOP

22 days ago

Hello Railway team,

Could a Railway team member confirm the supported behavior below, or point us to authoritative documentation? We need a documented trust boundary before enabling forwarded headers.

We need clarification of Railway’s supported HTTP ingress trust contract before enabling forwarded headers in our ASP.NET Core 8 Gateway.

Our immediate use case is staging, accessed directly through a Railway-generated *.up.railway.app HTTPS domain. We need the effective scheme, host and client IP for application security and rate limiting.

Observed behavior

With application forwarding disabled, two HTTPS requests—one normal and one containing synthetic forwarding headers—reached our application with:

  • Transport peer: 100.64.0.1 and 100.64.0.2, respectively.
  • Effective application scheme: http.
  • Effective Host: the expected Railway public domain.
  • X-Forwarded-For: two IP values.
  • X-Forwarded-Proto: one value, https.
  • X-Forwarded-Host: one value, the expected public domain.

For the synthetic request, the supplied spoofed X-Forwarded-* values did not appear in the received headers. However, the supplied RFC Forwarded header was preserved. We did not collect X-Real-IP.

These are observations of two requests, not assumptions about a platform-wide guarantee.

Questions

  1. Trusted transport peers

    What supported mechanism identifies Railway’s HTTP ingress as an authorized forwarding peer? Is there a documented, maintained IP/CIDR allowlist suitable for ASP.NET Core KnownProxies or KnownNetworks?

    Are the observed 100.64.0.1 and 100.64.0.2 addresses stable and exclusive to trusted ingress? How should applications distinguish ingress from private-network callers, health checks or other direct callers?

  2. Header normalization and spoof resistance

    For X-Real-IP, X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host and Forwarded, does Railway overwrite, append, preserve or reject caller-supplied values?

    Does the contract also cover duplicate header lines and comma-separated values over HTTP/1.1 and HTTP/2?

  3. Authoritative client IP

    Which header and parsing rule does Railway recommend for obtaining a client IP that cannot be selected by the caller through supplied HTTP headers?

    In our observed two-value X-Forwarded-For chain, what does each position represent? Is that topology guaranteed, or can the number of hops vary?

    If X-Real-IP is the supported source, is it always replaced by Railway on public ingress, and what boundary prevents a private or direct caller from impersonating that header?

  4. Recommended ASP.NET Core configuration

    Can you provide a supported configuration covering trusted peers, header names and forwarding limits?

    Our current implementation requires header symmetry and limits processing to one hop. The observed XFF/proto/host value counts are 2/1/1, so enabling it unchanged would not establish the client-IP behavior we need.

  5. Other access paths

    Please distinguish any guarantees for Railway-generated domains, custom domains, external CDNs and private-network requests. If a spoof-resistant client IP is unavailable on a particular path, please state that limitation explicitly.

We currently keep forwarding disabled and maintain an explicit host allowlist. We do not want to trust arbitrary proxies, allow the entire 100.64.0.0/10 range based on observations, or select the leftmost XFF value without a documented trust contract.

A documentation reference or written confirmation of the supported boundary and its limitations would let us implement and verify a bounded configuration.

Thank you.

Solved

5 Replies

Railway
BOT

22 days ago

Our HTTP edge overwrites X-Real-IP, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host on every request it forwards. Copies supplied by the caller are replaced, not appended to or kept. This applies to Railway-generated .up.railway.app domains and custom domains. For a client IP the caller can't pick through the edge, use X-Real-IP. X-Forwarded-For is rewritten too, but we don't guarantee how many entries it has or a fixed hop topology, so don't depend on the position of a value or a one-hop limit. Don't treat the RFC Forwarded header as something the edge sets or cleans.

We don't publish a range for the socket peer your container sees. The 100.64.0.0/10 addresses you observed aren't stable or limited to the edge, and they can change, so they aren't suitable for KnownProxies or KnownNetworks. There's no signed header or edge-only identity your app could check either.

Requests over private networking (*.railway.internal) skip the edge and its header cleanup. Any service in the same project environment can reach your listening port and send its own forwarding headers, including X-Real-IP. Traffic through a TCP proxy also skips HTTP header handling. If you put Cloudflare in front, we set X-Real-IP from CF-Connecting-IP only when the connection comes from Cloudflare's published IPv4 ranges.

In short, X-Real-IP is trustworthy only on the public edge path, and only if nothing else in that environment can reach the listener. The public networking specs list the header meanings and note they can change.


Status changed to Awaiting User Response Railway • 22 days ago


Railway

Our HTTP edge overwrites `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto` and `X-Forwarded-Host` on every request it forwards. Copies supplied by the caller are replaced, not appended to or kept. This applies to Railway-generated `.up.railway.app` domains and custom domains. For a client IP the caller can't pick through the edge, use `X-Real-IP`. `X-Forwarded-For` is rewritten too, but we don't guarantee how many entries it has or a fixed hop topology, so don't depend on the position of a value or a one-hop limit. Don't treat the RFC `Forwarded` header as something the edge sets or cleans. We don't publish a range for the socket peer your container sees. The `100.64.0.0/10` addresses you observed aren't stable or limited to the edge, and they can change, so they aren't suitable for `KnownProxies` or `KnownNetworks`. There's no signed header or edge-only identity your app could check either. Requests over private networking (`*.railway.internal`) skip the edge and its header cleanup. Any service in the same project environment can reach your listening port and send its own forwarding headers, including `X-Real-IP`. Traffic through a TCP proxy also skips HTTP header handling. If you put Cloudflare in front, we set `X-Real-IP` from `CF-Connecting-IP` only when the connection comes from Cloudflare's published IPv4 ranges. In short, `X-Real-IP` is trustworthy only on the public edge path, and only if nothing else in that environment can reach the listener. The [public networking specs](https://docs.railway.com/networking/public-networking/specs-and-limits#technical-specifications) list the header meanings and note they can change.

sulemathar
HOBBYOP

22 days ago

Thank you. This explains the remaining blocker: private-network callers can reach the same listener and supply X-Real-IP, while the application has no stable ingress peer allowlist or authenticated edge identity.

Could a Railway team member confirm these limitations and identify a supported way to restrict the application listener to public HTTP edge traffic, while preserving Railway health checks?

If no such mechanism exists, please confirm that header configuration alone cannot establish a spoof-resistant client-IP trust boundary when private callers can reach the listener. We need this distinction before enabling forwarding.


Status changed to Awaiting Railway Response Railway • 22 days ago


Railway
BOT

22 days ago

We can confirm these limitations. There's no supported setting that limits your listener to traffic from our public HTTP edge. The container gets no authenticated edge identity, such as a signed header or mTLS. The socket peer addresses it sees aren't a published or stable range. Edge Rules only filter requests arriving on your public domains, so they don't control private-network callers.

So header configuration alone can't create a spoof-resistant client-IP trust boundary while other services can reach the listener over private networking. A private caller can send its own X-Real-IP, and nothing in the request shows whether it passed through the edge.

Private networks are isolated between projects and environments. The effective trust boundary is therefore every service in that project environment, plus any TCP proxy you expose. Trusting X-Real-IP is sound only when every path to the listener is trusted.


Status changed to Awaiting User Response Railway • 22 days ago


Railway

We can confirm these limitations. There's no supported setting that limits your listener to traffic from our public HTTP edge. The container gets no authenticated edge identity, such as a signed header or mTLS. The socket peer addresses it sees aren't a published or stable range. Edge Rules only filter requests arriving on your public domains, so they don't control private-network callers. So header configuration alone can't create a spoof-resistant client-IP trust boundary while other services can reach the listener over private networking. A private caller can send its own X-Real-IP, and nothing in the request shows whether it passed through the edge. Private networks are isolated between projects and environments. The effective trust boundary is therefore every service in that project environment, plus any TCP proxy you expose. Trusting X-Real-IP is sound only when every path to the listener is trusted.

sulemathar
HOBBYOP

22 days ago

Thank you. This clarifies the platform limitation. We will keep application forwarding disabled until we explicitly establish and verify trust across every path to the listener. Please provide an authoritative documentation reference or Railway team confirmation so we can cite this limitation in our security decision record.


Status changed to Awaiting Railway Response Railway • 22 days ago


Railway
BOT

22 days ago

We confirm the limitations in our earlier replies. There is no supported way to restrict a listener to traffic from our public HTTP edge. The container gets no authenticated edge identity, and the socket peer addresses it sees are not a published or stable range. Requests over private networking do not pass through the edge or its header overwriting, so any service in the same project environment can reach the listener and send its own X-Real-IP. Header configuration alone therefore cannot create a spoof-resistant client-IP trust boundary while other callers can reach the listener.

For your record, the public networking technical specifications list the edge-set headers and their meanings, and note that this information can change. The private networking architecture page describes how services within an environment share one private network that is isolated from other projects and environments. You can cite this thread as our written confirmation of the listener-restriction and socket-peer limitations.


Status changed to Awaiting User Response Railway • 22 days ago


Railway
BOT

14 days ago

This thread has been marked as solved automatically due to a lack of recent activity. Please re-open this thread or create a new one if you require further assistance. Thank you!

Status changed to Solved Railway • 14 days ago


Welcome!

Sign in to your Railway account to join the conversation.

Loading...