Forwarding incoming HTTP/HTTPS connections by hostname with HAProxy

Sun 24th Nov 2019 / Tags: #HAProxy

This is how I set up HAProxy to forward HTTP/HTTPS connections to our single office IP address to several backend servers based on hostname.

The key requirement was each site/application on the backend servers should be unaware of the proxy server. That means they needed to see the external client IP address not the proxy server IP address, and use HTTP/HTTPS connections that match the incoming ones.

All instructions are for Ubuntu, and assumes the backend servers are Apache.

Install HAProxy

Install HAProxy and (optionally) the Vim syntax highlighting:

sudo apt install haproxy vim-haproxy

Copy the backend server SSL certificates

If the backend server SSL certificates are self-signed (e.g. /etc/ssl/certs/ssl-cert-snakeoil.pem), copy them to /etc/ssl/certs/ on the HAProxy server (e.g. /etc/ssl/certs/alpha.pem).

If they're publicly trusted, this isn't required.

Generate an SSL certificate with Let's Encrypt

For example, this generates a single certificate that covers all subdomains. That requires DNS validation - I use Cloudflare.

sudo apt install certbot certbot-dns-cloudflare

sudo tee /srv/letsencrypt-credentials.ini <<END
dns_cloudflare_email = dave@example.com
dns_cloudflare_api_key = XXXXXXXXX
END

sudo chmod 600 /srv/letsencrypt-credentials.ini

sudo certbot certonly \
    --noninteractive \
    --server https://acme-v02.api.letsencrypt.org/directory \
    --dns-cloudflare \
    --dns-cloudflare-credentials /srv/letsencrypt-credentials.ini \
    --dns-cloudflare-propagation-seconds 20 \
    --cert-name haproxy \
    -d alpha.example.com,*.alpha.example.com,bravo.example.com,*.bravo.example.com \
    -m dave@example.com \
    --agree-tos

(See here if you need to use multiple certificates.)

Configure HAProxy

In /etc/haproxy/haproxy.cfg:

#---------------------------------------
# Global
#---------------------------------------

global
    log /dev/log local0
    log /dev/log local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

    # Default SSL material locations
    ca-base /etc/ssl/certs
    crt-base /etc/ssl/private

    # Default ciphers to use on SSL-enabled listening sockets.
    # For more information, see ciphers(1SSL). This list is from:
    #  https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
    ssl-default-bind-ciphers ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS:!AESCCM
    ssl-default-bind-options no-sslv3

    ssl-default-server-ciphers ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS:!AESCCM
    ssl-default-server-options no-sslv3

    # Added per warning in the log file
    tune.ssl.default-dh-param 2048

#---------------------------------------
# Defaults
#---------------------------------------

defaults
    log global
    mode http
    option httplog
    option dontlognull
    timeout connect 5s
    # Timeout increased to 1 hour due to long-running scripts
    timeout client 3600s
    timeout server 3600s
    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

    # Enable stats
    stats enable
    stats uri /haproxy-stats
    stats realm HAProxy\ Statistics
    stats auth haproxy:MySuperSecretHAProxyStatsPassword

    # Add X-Forwarded-For header
    option forwardfor

#---------------------------------------
# Frontends
#---------------------------------------

# Listen for HTTP/HTTPS requests
# Note: Can't use TCP/SNI for HTTPS because then we can't add the X-Forwarded-For header
frontend http
    bind *:80
    bind *:443 ssl crt /etc/letsencrypt/live/haproxy/privkey.pem

    acl is_ssl dst_port 443

    # Match requests for *.alpha.example.com
    acl is_alpha hdr_dom(host) -i alpha.example.com
    use_backend alpha-https if is_alpha is_ssl
    use_backend alpha-http  if is_alpha

    # Match requests for *.bravo.example.com
    acl is_bravo hdr_dom(host) -i bravo.example.com
    use_backend bravo-https if is_bravo is_ssl
    use_backend bravo-http  if is_bravo

#---------------------------------------
# Backends
#---------------------------------------

# Send requests to alpha server
backend alpha-http
    server alpha 192.168.1.11:80

backend alpha-https
    server alpha 192.168.1.11:443 ssl ca-file alpha.pem

# Send requests to bravo server
backend bravo-http
    server bravo 192.168.1.12:80

backend bravo-https
    server bravo 192.168.1.12:443 ssl ca-file bravo.pem

Alternatively, use publicly trusted SSL certificates for the backend servers and the public CA certificates file:

server alpha 192.168.1.11:443 ssl ca-file ca-certificates.crt

Or you can disable backend SSL validation (useful when debugging issues):

# DO NOT do this in production - "verify none" disables certificate checking
    server alpha 192.168.1.11:443 ssl ca-file ca-certificates.crt verify none

Then restart HAProxy and check the status:

sudo systemctl restart haproxy
sudo systemctl status haproxy

Configure the backend servers

We then use the Apache mod_remoteip module to treat the X-Forwarded-For header from HAProxy as the client's IP address.

On each of the backend servers, add this to /etc/apache2/conf-available/haproxy.conf:

# Use the client IP provided by the HAProxy server
RemoteIPInternalProxy 192.168.1.10
RemoteIPHeader X-Forwarded-For

Replace the IP address with the HAProxy server internal IP address.

Then enable it and reload the Apache configuration:

sudo a2enmod remoteip
sudo a2enconf haproxy
sudo systemctl reload apache2
sudo systemctl status apache2

Forward incoming requests

If the HAProxy server is not connected directly to the internet, set up port forwarding on the router:

Service Port Destination
HTTP 80 192.168.1.10:80
HTTPS 443 192.168.1.10:443

Replace the IP address with the HAProxy server internal IP address.