Let's Encrypt certificates and OCSP staples with HAProxy
The quick setup
Generate OCSP staples
#!/bin/bash
# To change
DOMAIN_NAME=miouyouyou.fr
# Be careful, sometimes you need to append -XXXX
# Check which letsencrypt folder is the latest one updated with `ls -td /etc/letsencrypt/live/$DOMAIN_NAME* | head -1`
LETSENCRYPT_FOLDER=/etc/letsencrypt/live/$DOMAIN_NAME
HAPROXY_SSL_FOLDER=/srv/docker-files/LoadBalancer/haproxy/ssl
# These should be good
LETSENCRYPT_FULLCHAIN_CERT=$LETSENCRYPT_FOLDER/fullchain.pem
LETSENCRYPT_PRIVKEY_CERT=$LETSENCRYPT_FOLDER/privkey.pem
LETSENCRYPT_ISSUER_CERT=$LETSENCRYPT_FOLDER/chain.pem
HAPROXY_SSL_CERT=$HAPROXY_SSL_FOLDER/$DOMAIN_NAME.fullchain.pem
HAPROXY_SSL_CERT_OCSP=$HAPROXY_SSL_CERT.ocsp
# Uncomment to regenerate the real fullchain.pem SSL Certificate
# cat $LETSENCRYPT_FULLCHAIN_CERT $LETSENCRYPT_PRIVKEY_CERT > $HAPROXY_SSL_CERT
openssl ocsp -issuer $LETSENCRYPT_ISSUER_CERT -cert $HAPROXY_SSL_CERT -respout $HAPROXY_SSL_CERT_OCSP -noverify -no_nonce -url http://ocsp.int-x3.letsencrypt.org
Part of the HAProxy configuration
frontend blogfront
bind 172.50.3.3:80
bind 172.50.3.3:443 ssl crt /etc/ssl/mine/miouyouyou.fr.fullchain.pem ssl-min-ver TLSv1.0 ssl-max-ver TLSv1.3 alpn h2,http/1.1
bind [fc00::103]:80
bind [fc00::103]:443 ssl crt /etc/ssl/mine/miouyouyou.fr.fullchain.pem ssl-min-ver TLSv1.0 ssl-max-ver TLSv1.3 alpn h2,http/1.1
mode http
# ...
default_backend blogback
backend blogback
mode http
balance roundrobin
http-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains; preload;" if { ssl_fc }
server web01 [fc00::102]:80 check
To check that OCSP staples are provided to the clients, use this command :
echo QUIT | openssl s_client -connect miouyouyou.fr:443 -status 2> /dev/null | grep -A 17 'OCSP response:' | grep -B 17 'Next Update'
Taken from : https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx
/etc/ssl/mine/miouyouyou.fr.fullchain.pem
should be replaced by a pathname leading to your SSL cert file.
This fullchain is the concat
enation of the fullchain.pem
and privkey.pem
, as stated in the OCSP script :
cat $LETSENCRYPT_FULLCHAIN_CERT $LETSENCRYPT_PRIVKEY_CERT > $HAPROXY_SSL_CERT
This is required by HAProxy.
In case you’re wondering, on my setup, the public_address:80 is NAT’ed to 172.50.3.3
and [fc00::103]
, depending on the IP protocol used.
The complete configuration is below.
[fc00::102]
is address of the HTTP server actually serving the content.
Yes you can listen on IPv4 addresses and redirect to IPv6 internal addresses, with HAProxy.
References
The long version
Greetings everyone !
It’s been a while since I had a little time for myself !
Bah, who am I kidding, trying to empty the used oil of an old useless fryer led me to spill 1 gallon of used frying oil on the floor of my living room…
Gotta love those days, where you wonder why the fuck renowned supermarkets accept to sell fryers that have a lid attached with cheap plastic brackets !
When trying to empty the oil of this rectangular fryer, I tried to take it diagonally, and so I put one of my hand on one of the handles, and lifted the fryer by the lid with the other hand… AND THE LID BRACKETS BROKE OFF INSTANTLY, DETACHING THE LID FROM THE MAIN PART ! Leading the fryer to fall over the broken side and on the ground, spilling the entire content on the floor…
This is so fucking dangerous ! If that oil was still boiling hot, I would be at the hospital ‘at best’ !
Fuck cheap plastic constructions, and fuck anyone selling that kind of shit.
These people should receive a complete business ban for selling such dangereous contraptions. It cost NOTHING to put solid brackets that don’t break off easily like this.
Now, in the first place, any big autonomous fryer should come with a detachable bowl that can be easily lifted and emptied when it’s full…
So yeah, I was able to get most of the oil out using paper towels, but the wooden floor is still greasy, so I’m trying to sponge the remaining with smectite clay powder.
Since it takes some time, I thought I’d finish this blog post about how to setup HAProxy with Let’s Encrypt SSL certificates, and provide OCSP staples, ALPN and HSTS in bonus !
So, the whole idea is that :
- You setup your domain names to point to your server.
- You setup a quick webserver (load-balancer or not… It just have to be accessible through your domain names, on port 80 and 443) for Lets Encrypt Certbot
- You invoke
certbot certonly
and set it up so that it writes the challenges files into your webserver root folder.
These challenges are basically text files that tell Lets Encrypt bots “Yes ! It’s really my domain and my server ! I’m not a fraud !” You concatenate Lets Encrypt
fullchain.pem
andprivkey.pem
, and use the new file as the SSL certificate for HAProxy.cat /etc/letsencrypt/live/yourdomainname/fullchain.pem /etc/letsencrypt/live/yourdomainname/privkey.pem > /your/haproxy/ssl/fullchain.pem
Once you got your Let’s Encrypt SSL certificates, you configure HAProxy. Here’s how I setup mine. The main part is on the
frontend
section :global daemon # 256 maximum simultaneous connections... # It's a static website so, I don't think I'll # reach that level for the moment. maxconn 256 # As stated in the documentation # --- # Sets the maximum size of the Diffie-Hellman parameters used for generating # the ephemeral/temporary Diffie-Hellman key in case of DHE key exchange. # values greater than 1024 bits are not supported by Java 7 and earlier clients. # --- # I don't care about Java 7 clients. tune.ssl.default-dh-param 4096 log /dev/log local0 # Use only decent algorithms. ssl-default-bind-options no-tls-tickets ssl-default-bind-ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK:!DSS:!SRP:!LOW defaults mode http # You got 5 seconds to connect timeout connect 5s # And 10 seconds to answer timeout client 10s timeout server 10s # This isn't used in this configuration. # Tarpit might be the worst option if # you're dealing against Slowloris attacks. timeout tarpit 1m log global frontend blogfront bind 172.50.3.3:80 # Only accept TLSv1.0 to TLSv1.3 connections. # Support ALPN and tell the client we can use HTTP/2. # This is one of the rare place where you can tell the # client to use HTTP/2 and boost the connection a little. # Fallback to HTTP 1.1 if required... bind 172.50.3.3:443 ssl crt /etc/ssl/mine/miouyouyou.fr.fullchain.pem ssl-min-ver TLSv1.0 ssl-max-ver TLSv1.3 alpn h2,http/1.1 # You REALLY want to use brackets with IPv6... # Without brackets, that would read fc00::103:80, # which is a valid address, completely different from fc00::103. # Who ever thought that ':' was a good separator for IPv6 is a fucking idiot. bind [fc00::103]:80 bind [fc00::103]:443 ssl crt /etc/ssl/mine/miouyouyou.fr.fullchain.pem ssl-min-ver TLSv1.0 ssl-max-ver TLSv1.3 alpn h2,http/1.1 mode http option httplog # Don't try stupid methods acl valid_method method GET OPTION HEAD # Don't try the IP address alone. acl valid_domains hdr_dom(Host) -i miouyouyou.fr blog.miouyouyou.fr # I don't use PHP, so don't even bother checking for PHP security holes. acl php_file path_end .php # Deny bots that don't comply http-request deny if !valid_method OR !valid_domains OR php_file OR HTTP_1.0 # It's a static website, so you don't need 'Content-Length' in request headers. acl have_payload hdr_val(content-length) gt 0 # Deny bots that don't comply http-request deny if have_payload default_backend blogback backend blogback mode http balance roundrobin # Setup HSTS : https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security http-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains; preload;" if { ssl_fc } server web01 [fc00::102]:80 check
And, then, you restart HAProxy.
For the moment, no OCSP will be sent. It’s ok, we’ll generate them now :
#!/bin/bash # To change DOMAIN_NAME=miouyouyou.fr # Be careful, sometimes you need to append -XXXX # Check which letsencrypt folder is the latest one updated with `ls -td /etc/letsencrypt/live/$DOMAIN_NAME* | head -1` LETSENCRYPT_FOLDER=/etc/letsencrypt/live/$DOMAIN_NAME HAPROXY_SSL_FOLDER=/srv/docker-files/LoadBalancer/haproxy/ssl # These should be good LETSENCRYPT_FULLCHAIN_CERT=$LETSENCRYPT_FOLDER/fullchain.pem LETSENCRYPT_PRIVKEY_CERT=$LETSENCRYPT_FOLDER/privkey.pem LETSENCRYPT_ISSUER_CERT=$LETSENCRYPT_FOLDER/chain.pem HAPROXY_SSL_CERT=$HAPROXY_SSL_FOLDER/$DOMAIN_NAME.fullchain.pem HAPROXY_SSL_CERT_OCSP=$HAPROXY_SSL_CERT.ocsp # Uncomment to regenerate the real fullchain.pem SSL Certificate # cat $LETSENCRYPT_FULLCHAIN_CERT $LETSENCRYPT_PRIVKEY_CERT > $HAPROXY_SSL_CERT openssl ocsp -issuer $LETSENCRYPT_ISSUER_CERT -cert $HAPROXY_SSL_CERT -respout $HAPROXY_SSL_CERT_OCSP -noverify -no_nonce -url http://ocsp.int-x3.letsencrypt.org
Restart HAProxy again (
killall -SIGHUP haproxy
ordocker kill -s HUP haproxy_container_id_or_name
) and check that OCSP staples are provided with :echo QUIT | openssl s_client -connect your_domain_name.ext:443 -status 2> /dev/null | grep -A 17 'OCSP response:' | grep -B 17 'Next Update'
Taken from : https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx
Additional notes
HAProxy through Docker
Now, you might wonder
“Why is he using /etc/ssl/mine/
in the HAProxy configuration,
and /srv/docker-files/LoadBalancer/haproxy/ssl
when generating the certificates and the OCSP staples ?“.
The answer is that HAProxy is running inside a Docker container, with the following docker-compose.yml
configuration :
version: '3'
services:
haproxy:
image: haproxy:latest # A container that exposes an API to show its IP address
volumes:
- "./haproxy/config:/usr/local/etc/haproxy:ro"
- "./haproxy/ssl:/etc/ssl/mine:ro"
- "/dev/log:/dev/log"
networks:
myynet:
ipv6_address: fc00::103
ipv4_address: 172.50.3.3
networks:
myynet:
external: true
Which is stored in /srv/docker-files/LoadBalancer/
and run through docker-compose up -d
.
So, /srv/docker-files/LoadBalancer/haproxy/ssl
is mounted to /etc/ssl/mine
in the Docker container.
Also, the myynet
network has the following ranges : 172.50.3.0/24
and fc00::100/120
, and is created using the docker network
commands.
So, I hope this helped you understand how to setup HAProxy with a SSL certificate from Let’s Encrypt, and provide OCSP staples.
Also, if you look at the HAProxy configuration, you’ll see how to setup ALPN and HSTS as well, which should help you get nice SSL street creds, while speeding up HTTPS connections a little.