a day ago
Hello Railway team,
We need authoritative clarification suitable for a production security design.
Our API uses Node.js, Express 4.22.1 and NestJS 10.4.22. We need a spoof-resistant per-client identity for application rate limiting without collapsing distinct users onto a shared ingress/CDN proxy IP.
Our relevant request paths include:
-
Production custom API domain:
Cloudflare → Railway HTTP ingress → API
-
A Railway-generated *.up.railway.app domain:
directly reachable Railway HTTP ingress → the same Production API
-
Staging custom API domain:
directly reachable Railway HTTP ingress → Staging API
-
Railway private-network service callers
The application currently uses:
app.set("trust proxy", true)
We are reviewing this before implementing a security fix.
Could a Railway staff member please clarify the supported security contract for the following?
-
When an Internet client sends its own:
- X-Forwarded-For
- X-Real-IP
- Forwarded
through Railway HTTP public ingress, what exact value reaches the application?
For example, what happens if the caller supplies:
X-Forwarded-For: 203.0.113.7
-
For each header, does Railway overwrite, append, preserve, normalize, de-duplicate, or reject caller-provided values?
Please include behavior for:
- repeated header lines;
- mixed casing;
- comma-separated values;
- HTTP/1.1;
- HTTP/2.
-
Which header and parsing rule, if any, does Railway guarantee contains a client identity that cannot be selected or rotated through caller-supplied HTTP headers?
-
When Cloudflare is in front of Railway, does that trusted identity represent:
- the end user;
- the Cloudflare edge;
- another peer?
If Railway recovers CF-Connecting-IP, how is trusted Cloudflare provenance established?
Can a caller connecting directly through a Railway-generated domain impersonate the Cloudflare path by supplying Cloudflare-style headers?
-
Are the client-IP guarantees identical for:
- custom domains;
- Railway-generated *.up.railway.app domains;
- Railway regions;
- Railway-managed CDN/edge routing?
-
Does Railway officially support any of the following for security-sensitive proxy trust?
- authoritative ingress proxy CIDRs;
- a stable final-hop predicate;
- an authenticated ingress header/signature;
- another recommended mechanism for Express/NestJS trust proxy and IP-based rate limiting.
We have seen public suggestions involving 100.0.0.0/8. Is that an actual supported security contract?
-
How should an application safely resolve client identity for:
- direct Railway public ingress;
- Cloudflare → Railway;
- Railway private-network service-to-service traffic?
Do private-network HTTP requests bypass public-ingress header sanitization?
We found these prior Railway staff discussions and would appreciate clarification of the supported current behavior:
https://station.railway.com/questions/security-critical-questions-on-edge-prox-8fddd775
https://station.railway.com/questions/which-header-should-i-rely-on-for-real-c-d78a6f96
For our security review, we specifically need to distinguish guaranteed behavior from incidental implementation details or historical behavior.
Our security requirement is that an arbitrary Internet caller must not be able to choose or rotate the application rate-limit identity by changing request headers, while legitimate users must not all collapse onto a shared Railway/Cloudflare proxy IP.
A Railway staff confirmation would be greatly appreciated.
Thank you.
3 Replies
a day ago
Our HTTP edge overwrites X-Real-IP, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host on every request, including copies the caller sends. Incoming values are replaced, not appended or kept, so a client-sent X-Forwarded-For: 203.0.113.7 never reaches your app. Use X-Real-IP for rate limiting. Our public networking specs document it as the client's remote IP. How Forwarded is handled is not documented, so do not rely on it.
With Cloudflare in front, X-Real-IP comes from CF-Connecting-IP only when the connection reaching us is from Cloudflare's published IPv4 ranges and that header is present. It is then the end user's IP. Direct requests to *.up.railway.app also go through our edge, and a forged CF-Connecting-IP from any other source does not become X-Real-IP. We have not confirmed this for Cloudflare over IPv6. CF-Connecting-IP reaches your container unchanged, so do not read it directly.
We do not publish proxy CIDRs or a signed ingress header. Observed 100.x peer addresses are not a stable contract. Express trust proxy builds req.ip from the socket peer and X-Forwarded-For, not X-Real-IP, and the hop count is not fixed.
Requests over *.railway.internal skip the edge and its header sanitization. Any service in the same environment can send its own X-Real-IP, so do not trust forwarded headers on that path. These specs may change.
Status changed to Awaiting User Response Railway • about 24 hours ago
Railway
Our HTTP edge overwrites `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto` and `X-Forwarded-Host` on every request, including copies the caller sends. Incoming values are replaced, not appended or kept, so a client-sent `X-Forwarded-For: 203.0.113.7` never reaches your app. Use `X-Real-IP` for rate limiting. Our [public networking specs](https://docs.railway.com/networking/public-networking/specs-and-limits#technical-specifications) document it as the client's remote IP. How `Forwarded` is handled is not documented, so do not rely on it. With Cloudflare in front, `X-Real-IP` comes from `CF-Connecting-IP` only when the connection reaching us is from Cloudflare's published IPv4 ranges and that header is present. It is then the end user's IP. Direct requests to `*.up.railway.app` also go through our edge, and a forged `CF-Connecting-IP` from any other source does not become `X-Real-IP`. We have not confirmed this for Cloudflare over IPv6. `CF-Connecting-IP` reaches your container unchanged, so do not read it directly. We do not publish proxy CIDRs or a signed ingress header. Observed `100.x` peer addresses are not a stable contract. Express `trust proxy` builds `req.ip` from the socket peer and `X-Forwarded-For`, not `X-Real-IP`, and the hop count is not fixed. Requests over `*.railway.internal` skip the edge and its header sanitization. Any service in the same environment can send its own `X-Real-IP`, so do not trust forwarded headers on that path. These specs may change.
a day ago
Thank you. This is very helpful and answers most of the security question.
Before we use this as the basis for a production security boundary, could a Railway staff member please confirm the following points?
-
Is the statement that Railway HTTP ingress overwrites caller-supplied X-Real-IP and X-Forwarded-For a supported/current platform guarantee for both custom domains and generated *.up.railway.app domains?
-
Can we safely treat X-Real-IP, after Railway public HTTP ingress, as caller-unspoofable for application rate limiting?
-
For Cloudflare → Railway traffic over IPv6, does Railway also validate Cloudflare provenance and derive the end-user IP into X-Real-IP, or can X-Real-IP become the Cloudflare edge address?
-
Please confirm that requests over *.railway.internal bypass this sanitization and that applications should therefore not trust X-Real-IP as end-user identity on private-network requests.
-
Is your recommended application design to avoid Express req.ip / trust proxy for this purpose and instead validate and consume the Railway-generated X-Real-IP only on public HTTP-ingress requests?
A staff confirmation of these guarantees would let us close our security review and implement the rate-limiting fix.
Thank you.
Status changed to Awaiting Railway Response Railway • about 24 hours ago
an hour ago
-
Yes. Our HTTP edge overwrites caller-supplied X-Real-IP and X-Forwarded-For on every request it handles. That applies the same way to custom domains and to generated *.up.railway.app domains, because both go through the edge. It is current behavior. Our public networking specs say this information can change at any time, so it is not a versioned contractual guarantee.
-
Yes, with one limit. A caller cannot set X-Real-IP by sending a forged copy through the public edge. It is still an IP address and not a persistent identity, so a client can change it by switching networks or using a VPN.
-
We have only confirmed the Cloudflare derivation for connections from Cloudflare's published IPv4 ranges. We cannot confirm what X-Real-IP contains when Cloudflare reaches us over IPv6, so do not assume it holds the end user's address on that path.
-
Confirmed. Requests over *.railway.internal do not pass through the public edge and do not get its header sanitization. Any service in the same environment can send its own X-Real-IP, so it is not end-user identity on private-network requests.
-
Yes. Express req.ip comes from the socket peer and X-Forwarded-For under trust proxy, not from X-Real-IP. The hop count is not fixed and we do not publish proxy CIDRs. For public-ingress requests, use the X-Real-IP value we set. Treat private-network requests separately. Any visitor IP a frontend relays over the private network is your application's own metadata, not something we assert. See the public networking specs.
Status changed to Awaiting User Response nico • about 1 hour ago