Apache Reverse Proxy: mod_proxy Design and Trust Boundaries
Apache reverse proxy design using mod_proxy: disable open proxying, forward to trusted backend, preserve client IP, and validate with apachectl configtest and curl.
19 Jul 2026, 14:50 UTC

The Problem: Exposing an Internal Application Through Apache
You run an application server on localhost:3000 that should not be reachable directly from the internet. Users connect to Apache on port 80 or 443, and Apache must forward requests to the backend, return responses, and preserve enough request context for the application to work correctly.
Smallest Suitable Design: Reverse Proxy with mod_proxy
A reverse proxy accepts client connections and forwards them to an upstream server. In Apache, this behavior comes from mod_proxy and mod_proxy_http. The smallest production design uses server configuration rather than .htaccess, disables forward proxying, and maps a URL prefix to the backend.
Example VirtualHost Configuration
# Run on the Apache host. Requires root or sudo to edit server config.
<VirtualHost *:443>
ServerName app.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/certs/app.example.com.pem
SSLCertificateKeyFile /etc/ssl/private/app.example.com.key
# Prevent Apache from acting as an open proxy for arbitrary targets.
ProxyRequests Off
ProxyPreserveHost On
# Forward /app and /app/ to the backend application.
ProxyPass /app http://127.0.0.1:3000/app
ProxyPassReverse /app http://127.0.0.1:3000/app
# Pass the original client IP so the backend sees real addresses.
RemoteIPHeader X-Forwarded-For
RemoteIPInternalProxy 127.0.0.1
</VirtualHost>
mod_remoteip replaces the remote client IP with the value in X-Forwarded-For only for trusted internal proxies, preventing clients from spoofing their own address.
Trust and Data Boundaries
- Always set
ProxyRequests Offunless you explicitly need a forward proxy. - Limit
ProxyPassto known backend paths instead of catching all traffic. - Trust only loopback or known load-balancer IPs in
RemoteIPInternalProxy. - Strip or sanitize backend headers that reveal internal hostnames before sending them to clients.
Operational Checks
- Validate syntax before applying. Run
sudo apachectl configteston the Apache host. Expected output: Syntax OK. Risk: none, but invalid config prevents reload. - Reload Apache. Run
sudo systemctl reload apache2on Debian/Ubuntu orsudo systemctl reload httpdon RHEL/CentOS. Risk: reload fails if configtest was skipped and the file is invalid. - Verify proxying. Run
curl -I http://127.0.0.1/appfrom the Apache host. Expect a response from the backend, not a direct Apache directory listing or 404. - Inspect headers. Run
curl -v http://127.0.0.1/appand confirmViaappears and backend responses do not leak internal hostnames. - Watch errors. Run
sudo tail -f /var/log/apache2/error.logwhile testing. Look for connection refused to 127.0.0.1:3000 or permission denied on SSL certificates.
Failure Modes and Conditions That Change the Design
If the backend is not on the same host, replace 127.0.0.1 with the backend IP or hostname and ensure network policy allows Apache to reach it. If the application uses WebSockets, ProxyPass alone is insufficient; add ProxyPassReverse and consider RewriteRule with the [P] flag or mod_proxy_wstunnel. If clients send very large headers, increase LimitRequestFieldSize or Apache returns 431.
When Not to Use Apache as the Reverse Proxy
- The backend already terminates TLS and you do not need Apache-level SSL offloading.
- You need application-aware routing based on cookies or JWT claims; use the application or an ingress controller instead.
- Traffic volume is high enough that Apache's process model becomes more expensive than a dedicated proxy such as nginx or Envoy.
Limitations
mod_proxy adds a hop and small latency per request. It is not a web application firewall; it forwards valid HTTP requests to the backend without validating application logic. Buffering large uploads through Apache can consume memory on the proxy host.
Practical Way to Check the Result
After reload, run curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/app. A 2xx or 3xx code means Apache reached the backend. A 502 means Apache could not connect to 127.0.0.1:3000. Confirm the backend access log shows the request with the original client IP, not 127.0.0.1.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.