Need an all in one, do everything, all singing, all dancing reverse proxy? Look no further! I’ve been building a dense HAProxy system which will take care of SSL passthrough, SSL termination, Let’s Encrypt certificate generation, conversion and autorenewal and the other benefits that come with HAProxy, like caching and load balancing.

Lets get straight into the config file – I’ll give a brief summary of how it works later.

haproxy.cfg

#//////////////////////////////////////// G E N E R A L  S H I T  \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\  

global
        maxconn 20480
        user haproxy
        group haproxy

defaults
        mode http
        timeout client 60s
        timeout connect 5s
        timeout server 60s

#/////////////////////////////////////////// F R O N T E N D \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

frontend http-letsencrypt
	# sends certbot traffic to certbot and everything else to everything else
	mode http
	bind *:80
	# Test URI to see if its a letsencrypt request and assign an ACL
	acl letsencrypt-acl path_beg /.well-known/acme-challenge
	#redirect if the acl is for letsencrypt
	use_backend letsencrypt-backend if letsencrypt-acl
	#passthrogh everything else
	default_backend http-in-redirect

frontend ssl_switchtrack 
	# passthrough encrypted SSL for termination elsewhere if match, otherwise terminate locally
	mode tcp
	bind *:443
	inspect hostname
	tcp-request inspect-delay 5s
	tcp-request content accept if { req_ssl_hello_type 1 }
	# matching hostnames
	use_backend some-backend if { req_ssl_sni -i url.for.passthrough }
	# redirect everything else for ssl termination
	default_backend https-in

frontend http-redirect
        # upgrade http connections on port 80 to https on port 443
        mode http
        bind *:81
        redirect scheme https if !{ ssl_fc }

frontend https-in
        # frontend for SSL termination
        mode http
        #bind and look for matching certs 
        bind *:444 ssl crt /usr/local/etc/haproxy/pems/
        # tell servers what protocol was used
        http-request set-header X-Forwarded-Proto https if { ssl_fc }
        # forward client IPs to backend servers
        option forwardfor header X-Real-IP
        http-request set-header X-Real-IP %[src]
        # backends for terminated SSL traffic based on hostname
        use_backend nextcloud if { hdr(host) -i nextcloud.homelab.com }
        use_backend backend-x if { hdr(host) -i url-x.homelab.com }

#//////////////////////////////////////////// B A C K E N D \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

backend letsencrypt-backend
        # redirect letsencrypt traffic defined in the http frontend to port 8888
	mode http
	server letsencrypt localhost:8888

backend http-in-redirect
        # redirect non letsencrypt traffic (for https redirection in this case)
	mode http
	server http-redirect localhost:81

backend https-in
        # redirect https traffic that hasn't been passed through for termination on this haproxy
        mode tcp
        server https-frontend localhost:444

backend nextcloud
        server nextcloud 69.69.4.20:80 check
        # needed so some apps (apple, cough cough) can sync properly
        acl url_discovery path /.well-known/caldav /.well-known/carddav
        http-request redirect location /remote.php/dav/ code 301 if url_discovery    
        # so security scan doesn't shit itself
        http-response set-header Strict-Transport-Security max-age=15552000;\ includeSubDomains;\ preload;

backend backend-x
        server backend-server 69.69.4.20:443 check

How it works:

Lets start with HTTP/80:

HAProxy listens for connections on port 80 – which is all well and good, but certbot also wants to listen on port 80 so it can request SSL certificates. Two services can’t use the same port so HAProxy inspects the URL and then decides where to send it. If it’s letsencrypt traffic, HAProxy redirects it to wherever we’ve got certbot running, except on a different port (on the same machine, in this case, port 8888). We can then tell certbot to use port 8888 when requesting certificates and everything will work.

Everything else which isn’t letsencrypt traffic gets sent to a default backend, which redirects it to a frontend, which in this case send a HTTP->HTTPS redirect request. If you had web services that communicate over HTTP you would send them to the backend servers instead.

And HTTPS/443?

Sometimes, your backend application wants to terminate SSL itself – in my case it was Stunnel (encapsulates TCP traffic in TLS). The HTTPS frontend listens on port 443 and does the same thing as the HTTP frontend – it inspects the URL to see if it matches, and directs it accordingly

If the URL doesn’t match those defined for passthrough, it’s redirected through a backend to a frontend listening on port 444, where SSL termination happens. HAProxy inspects the URL once again to a) terminate it with the correct certificate and b) send to the correct backend.

Creating Let’s Encrypt Certificates

Now HAProxy is sending letsencrypt traffic to port 8888, we need to tell certbot to use port 8888 when we want to create a certificate. I made a handy little bash script which makes things easier.

#!/bin/bash
echo -n "enter domain name for certificate: "
read;
certbot certonly --standalone -d ${REPLY} --non-interactive --agree-tos --email someemail@email.com --http-01-port=8888

Converting the Certificates for use with HAProxy

Unfortunately, HAProxy can’t use the .oem files that Let’s Encrypt produces. The fullchain.pem and privkey.pem files have to be merged into one .pem file for HAProxy. Another handy bash script will do this for us and replace any existing duplicate certificates in the case that you’re renewing expired certificates.

#!/bin/bash
touch domains.txt
ls /etc/letsencrypt/live/ > domains.txt
echo "domains detected for conversion:"
cat domains.txt
FILENAME="domains.txt"
LINES=$(cat $FILENAME)
for LINE in $LINES
do
	if  [[ $LINE != README ]];
	then
		rm /usr/local/etc/haproxy/pems/$LINE.pem
		cat /etc/letsencrypt/live/$LINE/fullchain.pem \
		/etc/letsencrypt/live/$LINE/privkey.pem \
		| tee /usr/local/etc/haproxy/pems/$LINE.pem &> /dev/null
	fi
done
echo ".pem files active:"
ls /usr/local/etc/haproxy/pems/
rm domains.txt

This is written for use in a HAProxy Docker container, so change the paths if needed.

I think that’s pretty much all I wanted to say. I was going to go into a full tutorial with making Dockerfiles and even provide my ready made Docker image, but I kinda ran out of energy. I hope this helps someone in any case, it took me a long time to work this shit out.

Thanks for reading, catch you later!

Leave a Reply

Your email address will not be published. Required fields are marked *