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 concatenation 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 and privkey.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 or docker 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.

Greetings everyone

This is my main website, where I’m gathering various posts I wrote here and there, along with some documentation I wrote too.

Kernel code snippets are licensed under GPLv2.
The rest is licensed under MIT.
The whole blog content is available on Gitlab.

The Hugo theme that I’m using, Myynimalist, is made by myself and available on Gitlab.
This theme is still not very mobile-friendly. Notably the header.
Still, if you have trouble navigation this site (but can still see this), file an issue on Gitlab.

I’m still gathering my content, so some old posts might be added.