DEV Community

StarkMan
StarkMan

Posted on

Auditing Nginx client-IP trust when PROXY protocol meets RealIP

Auditing Nginx client-IP trust when PROXY protocol meets RealIP

A reverse proxy can make an application see the real client address, but the same feature can also make it believe an address that never connected. Nginx exposes this distinction through the HTTP RealIP module. The module changes the client address and optional port from a value supplied by another system. Its safety therefore depends on who is allowed to supply that value.

What the directives actually change

set_real_ip_from defines addresses that Nginx treats as trusted senders of replacement addresses. real_ip_header selects the source: X-Real-IP, X-Forwarded-For, or proxy_protocol. PROXY protocol is available only after it is enabled on the listening socket with the proxy_protocol parameter. Nginx's documentation also notes that the HTTP RealIP module is not built by default and must be enabled at compile time.

real_ip_recursive controls how a header chain is interpreted. With recursion off, a request from a trusted address has its client address replaced by the last address in the selected header. With recursion on, Nginx walks past trusted addresses and chooses the last non-trusted one. That behavior is useful behind a controlled chain of proxies. It is risky when the header can arrive from a network that includes clients, because a client can then become the author of its own apparent address.

The direct-listener mistake

A common design places Nginx behind a load balancer and trusts the balancer's subnet. The public listener on the load balancer accepts PROXY protocol or injects forwarding headers. The Nginx listener is supposed to accept traffic only from the balancer. If that listener is also reachable through a forgotten public interface, a direct client may be able to supply the same syntax.

Firewall rules must enforce the network assumption. Binding the listener to a private address is better than relying on a host firewall alone. A container deployment needs the check at the container network, the host network, and the upstream load balancer. A trusted source range should be as narrow as the actual proxy addresses or an operationally controlled subnet. Trusting an entire private range can include systems that should not set client identity.

Production checks

Start with the rendered configuration, not the template. Confirm which listen lines include proxy_protocol, which set_real_ip_from ranges apply in each server and location block, and whether real_ip_recursive is on. Then test from an untrusted host. Send a forged X-Forwarded-For header to the public edge and observe the address Nginx writes to logs. A forged value must not become the trusted client address.

Test a direct request to the backend listener as well. If the connection succeeds, treat it as a bypass even when the application itself checks a header. The application cannot recover a trustworthy address from a value that was never authenticated. Restrict the listener and confirm that the rejection comes from the network boundary, not from an application error page.

Logging also needs review. $remote_addr reflects the module's replacement, while $realip_remote_addr keeps the original sender address. Retaining both can separate the client address from the proxy address during an investigation. If logs feed access controls, alerting, or rate limits, document which variable each system uses.

Where IP trust is the wrong control

An IP address can support network policy, but it cannot replace user authentication. VPN egress addresses change, mobile networks share addresses, and proxy chains can alter what is observed. Administrative endpoints should still use strong authentication and server-side authorization. Rate limiting can use an address as a signal, but a shared address should not turn one client's actions into a blanket block or grant. Use account and device identity where the decision needs that identity.

A defensible Nginx deployment makes the trust boundary explicit: only approved proxies can connect, only approved fields can carry client identity, and direct access to the backend is blocked. The configuration should make that design visible to the next operator.

References

Top comments (0)