Preserve host header with SSL termination

Hello,

I have a java application in Tomcat which does a redirect based on the host header. To get to this application we must go through nginx and then haproxy. Nginx sets a host request header to match the service name, and then sends the request off to haproxy. Haproxy uses that host request header to route the request to the correct service. For example:

  1. User requests https://example.com/app

  2. Nginx has a location rule for /app and sets a Host header of app.internal.lan so we can find the app. It then passes the request off to http://app.internal.lan/app. Something like

     location /app/ {
         proxy_pass         http://app.internal.lan/app/;
         proxy_set_header   Host app.internal.lan;
         proxy_intercept_errors on;
     }
    
  3. HAProxy sees the Host header is set to app.internal.lan and routes to the correct backend rule (which goes to Tomcat)

I notice when I go through haproxy without SSL everything works fine. That is, after the user logs in they are directed to https://example.com/app/welcome.do as I would expect.

However, when I enable SSL in HAProxy, my redirect suddenly wants to take me to https://app.internal.lan/app/welcome.do.

I have confirmed that HAProxy is not passing the header the same way it does when using HTTP. If I add http-request set-header Host example.com into my backend rule it works as expected.

My question is: is there any way to preserve the Host header when using SSL termination on HAProxy? Why does the redirect work via HTTP, but not HTTPS? Or maybe the question is, why does the Host header get lost when using SSL.

Thanks.

Haproxy doesn’t strip the Host header, something else must be going on here. Can you post the haproxy configuration, as complete as possible?

If you enable SSL on haproxy, do you still connect via nginx? How does that work, because nginx either forwards only TCP there, so proxy_set_header won’t work, or if the browser connects directly to haproxy, than of course proxy_set_header doesn’t work either because nginx is not even involved.

Hi @lukastribus. Yes, if I use SSL on haproxy I still connect via nginx. I have several applications that work fine this way, it’s only this one particular web app which does the redirect after login.

Here’s my working haproxy config (via http):

global
    daemon
    maxconn 16384
    log 127.0.0.1 local0
    log 127.0.0.1 local1 notice
    log-send-hostname

defaults
    mode http
    log global
    unique-id-format %{+X}o\ %ci:%cp_%fi:%fp_%Ts_%rt:%pid
    unique-id-header X-Unique-ID
    timeout connect   5s
    timeout client   60s
    timeout server   60s
    timeout tunnel 3600s
    option dontlognull
    option http-server-close
    option redispatch
    errorfile 400 /etc/haproxy/errors/400.http
    errorfile 403 /etc/haproxy/errors/403.http
    errorfile 408 /etc/haproxy/errors/408.http
    errorfile 500 /etc/haproxy/errors/500.http
    errorfile 502 /etc/haproxy/errors/502.http
    errorfile 503 /etc/haproxy/errors/503.http
    errorfile 504 /etc/haproxy/errors/504.http

listen stats 0.0.0.0:8551
    mode http
    stats enable
    stats hide-version
    stats realm Haproxy\ Statistics
    stats uri /

frontend http-in
    bind 0.0.0.0:8550
    option httplog
    option forwardfor
    log-format %ci:%cp\ [%t]\ %ft\ %b/%s\ %Tq/%Tw/%Tc/%Tr/%Tt\ %ST\ %B\ %CC\ %CS\ %tsc\ %ac/%fc/%bc/%sc/%rc\ %sq/%bq\ %hr\ %hs\ %{+Q}r\ %ID

    # HTTP service: app
    acl acl_app hdr(host) -i app.internal.lan
    acl acl_app hdr(host) -i app.internal.lan

    use_backend backend_app if acl_app


# HTTP services: app
backend backend_app
    balance roundrobin
    server  c01app1.domain.corp_31545 10.7.15.30:31545 maxconn 128  weight 1

…and my working nginx config to accompany it (for example.com):

location /app/ {
    proxy_pass         http://app.internal.lan/app/;
    proxy_set_header   Host app.internal.lan;
    proxy_intercept_errors on;
}

And here’s my non-working haproxy config. You’ll notice the only difference is ssl crt /etc/haproxy/haproxy.pem is added.

global
    daemon
    maxconn 16384
    log 127.0.0.1 local0
    log 127.0.0.1 local1 notice
    log-send-hostname

defaults
    mode http
    log global
    unique-id-format %{+X}o\ %ci:%cp_%fi:%fp_%Ts_%rt:%pid
    unique-id-header X-Unique-ID
    timeout connect   5s
    timeout client   60s
    timeout server   60s
    timeout tunnel 3600s
    option dontlognull
    option http-server-close
    option redispatch
    errorfile 400 /etc/haproxy/errors/400.http
    errorfile 403 /etc/haproxy/errors/403.http
    errorfile 408 /etc/haproxy/errors/408.http
    errorfile 500 /etc/haproxy/errors/500.http
    errorfile 502 /etc/haproxy/errors/502.http
    errorfile 503 /etc/haproxy/errors/503.http
    errorfile 504 /etc/haproxy/errors/504.http

listen stats 0.0.0.0:8551
    mode http
    stats enable
    stats hide-version
    stats realm Haproxy\ Statistics
    stats uri /

frontend http-in
    bind 0.0.0.0:8550 ssl crt /etc/haproxy/haproxy.pem
    option httplog
    option forwardfor
    log-format %ci:%cp\ [%t]\ %ft\ %b/%s\ %Tq/%Tw/%Tc/%Tr/%Tt\ %ST\ %B\ %CC\ %CS\ %tsc\ %ac/%fc/%bc/%sc/%rc\ %sq/%bq\ %hr\ %hs\ %{+Q}r\ %ID

    # HTTP service: app
    acl acl_app hdr(host) -i app.internal.lan
    acl acl_app hdr(host) -i app.internal.lan

    use_backend backend_app if acl_app



# HTTP services: app
backend backend_app
    balance roundrobin
    server  c01app1.domain.corp_31545 10.7.15.30:31545 maxconn 128  weight 1

And here’s the nginx config (example.com). The only thing that changes here is I use https instead of http for the proxy_pass.

location /app/ {
    proxy_pass         https://app.internal.lan/app/;
    proxy_set_header   Host app.internal.lan;
    proxy_intercept_errors on;
}

If I add http-request set-header Host example.com to the backend as shown below, the app redirect works as expected and I get redirected to https://example.com/app/login.do. If I don’t add this, I get redirected to https://app.internal.lan/app/login.do.

# HTTP services: app
backend backend_app
    balance roundrobin
    http-request set-header Host example.com
    server  c01app1.domain.corp_31545 10.7.15.30:31545 maxconn 128  weight 1

Like I said, haproxy doesn’t strip the Host header. Most likely nginx does not send it, which is why it only works when you manually set it in haproxy.

Test it with curl against haproxy directly or verify the request in debug mode.

But if nginx was the problem, it would happen when using haproxy via http. The only variable here is that haproxy has been changed to add ssl crt /etc/haproxy/haproxy.pem to the frontend. I don’t have the add the set-header option when I use haproxy on http. The redirect works as expected.

You are also changing the nginx configuration, by configuring proxy_pass from http:// to https://. Its not only haproxy that your are reconfiguring.

You will have to actually test this, like I said above.

Another thing that doesn’t make sense: nginx sets the Host header to app.internal.lan. You than expect that haproxy maintains that Host header, but your “fix” is to reset the Host header in haproxy to example.com, instead of app.internal.lan.

If you believe haproxy strips the host header, you configuration workaround would have to look like this:

backend backend_app
    http-request set-header Host app.internal.lan

Anyway, I just confirmed that both haproxy 1.5.0 and latest development build maintain the Host header correctly.

I tend to agree with this statement, and am also wondering how the host header stays as example.com when using HTTP (because even over HTTP nginx is changing the header to app.internal.lan). As you can see in my logging statement above, I should be seeing headers in my logs ( %hr\ %hs\), however, I don’t see them.

It is really strange that I don’t have to set http-request set-header Host app.internal.lan when using HTTP in haproxy, and the redirect still works fine (even though nginx has changed the header).

Can you please advise the best way to test this using curl? I’d like to see exactly where the header is getting change. I appreciate all of the help.

I suggest you enable debug mode in haproxy (start it from the command line be specifying -d), so you can see what nginx actually sends towards haproxy, in both http and https mode. You should also be able to see how the tomcat backend reacts.

Since the redirect happens after the login, its probably difficult to simulate using curl.