It’s what the world needs.

I caved in and decided to use HAProxy in front of Bozohttpd so I could:

  1. Redirect http to https
  2. Redirect www to just the domain (only just fully finished this bit)

The fiddly bit with Let’s Encrypt and HAProxy is handling the renewal of the cert. All the posts I’ve found either do the simple, but reliable, approach of stopping a web-server, running a renewal using --standalone and then re-starting a web-server, or the slightly more advanced approach of using --standalone on a non-standard port with a HAProxy rule that passes through to it as needed. But why not use --webroot instead?

Since I used --webroot originally I just have a couple of cron entries that run:

/usr/pkg/bin/certbot renew --renew-hook /usr/local/bin/

The --renew-hook only gets called if the certificate is actually renewed. Where that script is:

#! /bin/sh
# TODO: Really, this should be a /etc/rc.d reload script
cat /usr/pkg/etc/letsencrypt/live/ /usr/pkg/etc/letsencrypt/live/ > /usr/pkg/etc/haproxy.crt
export conf_file=/usr/pkg/etc/haproxy.cfg
export pid_file=/usr/pkg/etc/
haproxy -f $conf_file -sf $(cat $pid_file) -p $pid_file -D

Which does the necessary bits of combining the two parts of the cert (I do like that bozohttpd doesn’t require this) and then doing a hot reload of the HAProxy configuration so that the HAProxy is serving the new cert.

Then in my haproxy.cfg I have a http frontend with these rules:

frontend http
	bind :::80 v4v6
	acl letsencrypt path_beg /.well-known/acme-challenge/
	acl http      ssl_fc,not
	http-request redirect scheme https if http !letsencrypt
	reqadd X-Forwarded-Proto:\ http
	use_backend bozohttpd if letsencrypt

Which re-directs all http requests to a https frontend unless they match the Let’s Encrypt path, in that case they pass through to the backend as http - this is important as the webroot plugin can’t work over https (which seems a bit counter-intuitive for Let’s Encrypt).

Then I have a frontend for the https stuff as follows:

frontend https
	bind :::443 v4v6 ssl crt /usr/pkg/etc/haproxy.crt no-sslv3
	http-request redirect prefix https://%[hdr(host),regsub(^www\.,,i)] code 301 if { hdr_beg(host) -i www }
	reqadd X-Forwarded-Proto:\ https
	default_backend bozohttpd

Which serves the actual Let’s Encrypt cert and also redirects the www prefix to the main domain. The backend is nothing fancy at all:

backend bozohttpd
	mode http
	# Since the check doesn't pass a domain it will 404
	option httpchk
	http-check expect status 404
	server bozo check

And this works lovely.

However, one VERY IMPORTANT thing to be aware of if you are using IPv6 and you starting seeing timeouts reported during renewals or dry-runs of renewals it is GUARANTEED to be due to your site not resolving over IPv6. There are many posts about this on the Let’s Encrypt forums and all of them start with that “definitely not being the problem” and end with “Oh, actually it was”. I too ran into this issue, but hadn’t realised as my IPv6 access had broken at home (and I hadn’t realised) and had also been broken on my server for months (and I hadn’t realised) even though I appeared to have an IPv6 address.

I’d originally only generated a certificate for, but of course if I want to redirect www to my plain domain I also actually need the certificate to be valid for www as well. It took me a little bit to figure out how to do this. I basically relied on Bozohttpd’s virtual host support and created a /var/www/vroot/ directory (rather than to try to do further clever redirection in the HAProxy) for the sole purpose of serving up the acme-challenge stuff. Then with the above HAProxy setup this worked:

sudo certbot certonly --webroot -w /var/www/vroot/ -d -w /var/www/vroot/ -d --cert-name

[EDIT: 2018-11-23] See: Adding a reloadcert command to /etc/rc.d/haproxy.