When migrating from a simple nginx-to-Apache setup to a more complex HAProxy-nginx-Apache chain with SSL termination at HAProxy, the HTTP_X_FORWARDED_PROTO header behavior becomes crucial. The core issue emerges when nginx receives SSL-terminated traffic from HAProxy but interprets it as plain HTTP.
The traffic flow:
client → HAProxy (SSL termination) → nginx (reverse proxy) → Apache/PHP
Key observations:
- HAProxy 1.5-dev18 with SSL support correctly sets X-Forwarded-Proto
- nginx sees all traffic from HAProxy as HTTP (port 80)
- The protocol header gets overwritten in transit
Here's the proper way to handle this in nginx:
server {
listen 80;
server_name example.com;
# Preserve existing X-Forwarded-Proto if present
set $original_proto $http_x_forwarded_proto;
# Fallback to $scheme if header not present
if ($original_proto = "") {
set $original_proto $scheme;
}
# Forward to backend with correct protocol
location / {
proxy_set_header X-Forwarded-Proto $original_proto;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://apache_backend;
}
}
Ensure your HAProxy frontend contains:
frontend https-in
bind *:443 ssl crt /path/to/cert.pem
http-request set-header X-Forwarded-Proto https
default_backend nginx_servers
Verify the headers are propagating correctly:
curl -k -I https://yourdomain.com
# Check for X-Forwarded-Proto: https in Apache logs
# Alternative test for PHP applications:
<?php
var_dump($_SERVER['HTTP_X_FORWARDED_PROTO']);
?>
- Nginx if-is-evil considerations - use map directive for complex logic
- Header name case sensitivity (HTTP_X_FORWARDED_PROTO vs X-Forwarded-Proto)
- Multiple proxy hops overwriting headers
For more complex scenarios, consider this pattern:
map $http_x_forwarded_proto $proxy_proto {
default $http_x_forwarded_proto;
"" $scheme;
}
server {
# ...
proxy_set_header X-Forwarded-Proto $proxy_proto;
# ...
}
When transitioning from a simple nginx > apache/php
stack to a more complex haproxy > nginx > apache/php
infrastructure with SSL termination at HAProxy, header forwarding becomes crucial. The core issue arises when Nginx receives SSL-terminated traffic but interprets it as HTTP, potentially breaking protocol-sensitive applications.
In this setup:
Client → (HTTPS) → HAProxy → (HTTP) → Nginx → Apache/PHP
The X-Forwarded-Proto
header (represented as HTTP_X_FORWARDED_PROTO
in server variables) needs proper propagation through each layer.
First, ensure HAProxy is properly configured:
frontend https-in
bind *:443 ssl crt /path/to/cert.pem
http-request set-header X-Forwarded-Proto https if { ssl_fc }
default_backend nginx_servers
For Nginx, we have three approaches to handle the header:
Option 1: Conditional Header Forwarding
location / {
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Forwarded-Proto $scheme if_not($http_x_forwarded_proto);
proxy_pass http://apache_backend;
}
Option 2: Using map directive (more elegant)
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
"" $scheme;
}
server {
location / {
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_pass http://apache_backend;
}
}
Option 3: Lua script for advanced logic (requires ngx_http_lua_module)
location / {
access_by_lua_block {
if ngx.var.http_x_forwarded_proto then
ngx.req.set_header("X-Forwarded-Proto", ngx.var.http_x_forwarded_proto)
else
ngx.req.set_header("X-Forwarded-Proto", ngx.var.scheme)
end
}
proxy_pass http://apache_backend;
}
Verify the header flow using curl:
curl -vk https://yourdomain.com -H "Host: test.example.com"
Check your Apache access logs or PHP's $_SERVER
superglobal to confirm the HTTP_X_FORWARDED_PROTO
value is correctly set to "https".
When dealing with WebSocket connections in this setup:
location /ws/ {
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://ws_backend;
}
The map directive solution (Option 2) has minimal performance overhead as the mapping is evaluated once during configuration load. The Lua solution offers maximum flexibility but requires additional modules.