Trusted Proxies
When OxPHP runs behind a reverse proxy (Kubernetes Ingress, Cloudflare, AWS ALB, nginx), every request arrives from the proxy's IP address. Without trusted proxy configuration, rate limiting, access logging, and $_SERVER['REMOTE_ADDR'] all see the proxy IP instead of the real client.
Configuration
# Comma-separated CIDR list
TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
# Shorthand: all RFC-1918 + loopback + link-local (IPv4 and IPv6)
TRUSTED_PROXIES="private"
# Keywords and CIDRs combine in one list
TRUSTED_PROXIES="private,cloudflare,100.64.0.0/10"Each comma-separated entry is a CIDR, a bare IP address (a single host), or one of the keywords private and cloudflare, in any letter case. An entry that is none of these is a startup error.
When unset, OxPHP ignores all forwarding headers. That is the safe default.
How it works
When a request arrives from a trusted IP, OxPHP inspects forwarding headers in priority order:
Forwarded(RFC 7239), the standardized headerX-Forwarded-For/X-Forwarded-Proto/X-Forwarded-Host/X-Forwarded-Port, the de-facto fallback
If the Forwarded header is present, X-Forwarded-* headers are ignored.
Client IP extraction
OxPHP uses the rightmost-non-trusted algorithm, the same approach nginx (real_ip_recursive on), Caddy, Traefik, and Apache use:
X-Forwarded-For: 203.0.113.50, 172.16.1.1, 10.0.0.5
TCP peer: 10.0.0.1 (trusted)
Walk right-to-left:
10.0.0.5 → trusted → skip
172.16.1.1 → trusted → skip
203.0.113.50 → NOT trusted → client IPThis prevents spoofing via prepended values. An attacker can add fake IPs to the left, but the rightmost untrusted IP was set by the last trusted proxy in the chain.
An entry may carry a port: 203.0.113.50:4711 or [2001:db8::1]:4711, the form some load balancers write into X-Forwarded-For (Azure Application Gateway among them). When the client address comes from such an entry, its port becomes REMOTE_PORT.
The walk stops at an entry with no address or one that does not parse as an IP address (an empty element, unknown, an obfuscated RFC 7239 identifier), and at a header line OxPHP cannot read as text (one with a non-ASCII byte, for example). Nothing left of that point is used, since that part of the chain may be only what the client sent. The client address is the rightmost untrusted entry right of that point. If every entry there is trusted, it is the leftmost of them; if there is no entry there at all, the address is not rewritten and stays the connecting peer, port included.
X-Forwarded-For: 198.51.100.9, unknown, 10.0.0.5
TCP peer: 10.0.0.1 (trusted)
Walk right-to-left:
10.0.0.5 → trusted → skip
unknown → does not parse → stop
198.51.100.9 → never reached
Client IP: 10.0.0.5 (every entry right of the stop is trusted → the leftmost of them)With the cloudflare keyword, a request whose forwarding chain shows it arrived from a Cloudflare edge takes its client address from the CF-Connecting-IP header instead. See Behind a CDN and a platform load balancer.
What changes
When TRUSTED_PROXIES is configured and the connecting IP is trusted:
| Component | Without trusted proxies | With trusted proxies |
|---|---|---|
$_SERVER['REMOTE_ADDR'] |
Proxy IP | Real client IP; the proxy IP when the forwarding headers yield no address |
$_SERVER['REMOTE_PORT'] |
Proxy source port | Client port from an ip:port entry in Forwarded or X-Forwarded-For, otherwise 0; the peer's port when the address is not rewritten |
$_SERVER['HTTPS'] |
Based on OxPHP's TLS config | From Forwarded: proto= or X-Forwarded-Proto |
$_SERVER['REQUEST_SCHEME'] |
http or https from TLS |
From forwarded protocol |
$_SERVER['SERVER_NAME'] |
From Host header |
From Forwarded: host= or X-Forwarded-Host |
$_SERVER['SERVER_PORT'] |
From Host header |
From X-Forwarded-Port, else port of X-Forwarded-Host / Forwarded: host=, else 443/80 |
| Rate limiting | Per-proxy IP | Per-client IP |
| Access log | Proxy IP | Real client IP |
When the client address is rewritten, REMOTE_PORT is the client's source port if the entry the address comes from carries one (an RFC 7239 Forwarded: for="ip:port" node, or an ip:port entry in X-Forwarded-For), and 0 otherwise. The port cannot be reconstructed, so it is zeroed instead of guessed. An address taken from CF-Connecting-IP has no port either, so REMOTE_PORT is 0 then too. When the headers yield no client address and the address is not rewritten, as described under Client IP extraction, REMOTE_PORT stays the connecting peer's port.
private networks
The private shorthand includes:
| Network | Description |
|---|---|
10.0.0.0/8 |
Class A private |
172.16.0.0/12 |
Class B private |
192.168.0.0/16 |
Class C private |
127.0.0.0/8 |
Loopback |
169.254.0.0/16 |
Link-local |
::1/128 |
IPv6 loopback |
fc00::/7 |
IPv6 unique local |
fe80::/10 |
IPv6 link-local |
Shared address space (100.64.0.0/10, RFC 6598) is not part of private. If your load balancer connects from it, list it next to the keyword: TRUSTED_PROXIES="private,100.64.0.0/10".
cloudflare networks
The cloudflare keyword adds the edge ranges Cloudflare publishes at cloudflare.com/ips: 15 IPv4 and 7 IPv6 ranges, as published on 2026-10-05. The list is compiled into OxPHP and changes only with an OxPHP release. When Cloudflare's list changes first:
- A new range can be listed next to the keyword:
TRUSTED_PROXIES="private,cloudflare,<new range>". A listed range counts as one of your own proxies rather than as a Cloudflare edge, so requests through it take their address by the rightmost-non-trusted rule, not fromCF-Connecting-IP, until a release adds the range to the keyword. - A withdrawn range cannot be subtracted from the keyword. To drop one before an OxPHP release does, list the ranges you still trust as CIDRs instead of using the keyword. That also gives up reading
CF-Connecting-IP, which is tied to the keyword.
Behind a CDN and a platform load balancer
A common deployment puts a CDN in front of a hosting platform's load balancer:
client → Cloudflare → load balancer → OxPHPWith TRUSTED_PROXIES=private, only the load balancer is trusted. The rightmost untrusted address in X-Forwarded-For is then the Cloudflare edge that connected to the load balancer, and every request appears to come from Cloudflare in REMOTE_ADDR, the access log, rate limiting and the client.address span attribute. Trust both hops:
TRUSTED_PROXIES="private,cloudflare"If the load balancer connects from an address outside the private ranges, list its range too: TRUSTED_PROXIES="private,100.64.0.0/10,cloudflare".
With the cloudflare keyword, OxPHP resolves the client address in three steps:
- 1Walk past your own proxies. Starting at the connecting peer, OxPHP walks left through the forwarding chain (
Forwardedif present, otherwiseX-Forwarded-For), past your own proxies: theprivateranges and the CIDRs you listed. An address inside Cloudflare's ranges counts as Cloudflare even when you listed it. - 2Look for a Cloudflare hop. If the first hop that is not one of your own proxies is a Cloudflare address, the request came through Cloudflare, and the client address is the
CF-Connecting-IPheader. The header must appear exactly once and hold a valid IP address. - 3Otherwise, rightmost-non-trusted. If the request did not come through Cloudflare, or
CF-Connecting-IPis missing, repeated or malformed, the rightmost-non-trusted rule applies, with the Cloudflare ranges counted as trusted.
The hop where the walk stops is the connecting peer or an address one of your own proxies recorded. A client that reaches the load balancer directly therefore cannot make OxPHP read a CF-Connecting-IP it sent itself: the load balancer records the client's own address there, and a Cloudflare address the client wrote further left is never reached. Like the rightmost-non-trusted rule, this relies on your proxies maintaining the header OxPHP reads: they have to append the address that connected to them instead of passing a client's value through unchanged.
Your proxies have to maintain Forwarded too. OxPHP reads it in preference to X-Forwarded-For, and Cloudflare passes it on as the visitor sent it, since it is not one of the headers Cloudflare sets. A load balancer that maintains only X-Forwarded-For therefore has to remove Forwarded from the request. If it passes the header through, any client, through Cloudflare or not, chooses the address OxPHP sees, with or without the cloudflare keyword.
CF-Connecting-IP is not read either when the walk stops before it reaches a Cloudflare address, which happens at an entry with no address or one that does not parse, or at a line OxPHP cannot read as text, as described under Client IP extraction. A record one of your proxies puts on a line of its own is not affected by an unreadable line the client sent before it. A record a proxy writes onto the same line as text the client sent is lost with that line: the walk stops there, and the address comes from what your proxies recorded right of that line, or stays the connecting peer. It never comes from anything the client sent.
Cloudflare appends the address of whoever connected to it to X-Forwarded-For, so for an ordinary visitor both headers name the same client. They can differ for requests that Cloudflare Workers send to your origin, which also leave from Cloudflare's addresses, whichever account owns the Worker. Cloudflare's HTTP headers reference does not say what such a request may carry in X-Forwarded-For. For CF-Connecting-IP it does: a Worker can choose the value only for a request to a hostname in its own zone; for a hostname in another Cloudflare zone the header is the fixed address 2a06:98c0:3600::103, and for a hostname not on Cloudflare it is the client's address, which the Worker cannot alter.
Trusting the Cloudflare ranges still means trusting everything that leaves Cloudflare's network, not only your own zone. If the client address feeds a security decision, make the origin accept traffic from your zone only, by whatever means your platform offers.
Security
- The default is safe: without
TRUSTED_PROXIES, no forwarding headers are processed. - Entries in
TRUSTED_PROXIESare validated, and an invalid value causes a startup error. - The rightmost-non-trusted algorithm resists spoofing by ignoring attacker-prepended values.
- Requests from untrusted IPs have their forwarding headers ignored entirely.
Prompts for your agent
These prompts assume an agent with the OxPHP documentation MCP server connected. The agent reads the pages it needs itself.
Configure trusted proxies for my OxPHP deployment. Read the `security/trusted-proxies` page through the OxPHP documentation MCP server.
Work out from my infrastructure files what sits in front of the application, then give me the exact configuration and tell me what each value admits and what it would let a client forge if I got it wrong.See also
- Rate Limiting: per-IP rate limiting uses the resolved client IP
- Access Logging: the
remote_ipfield shows the resolved client IP - Configuration Reference: all environment variables