Haproxy med Let's Encrypt

09 September, 2019

Det kan verka som voodoo att få vissa saker att fungera men genom att mecka, mecka och mecka lite till har jag fått fram en fungerande modell för hur man får Let’s Encrypt att fungera med Haproxy. Värt att notera är att jag baserar denna guide på Haproxy v2.0.5 kompilerad på CentOS 7.6.1810. Det finns saker jag gör i min konfigurationsfil som kommer tas bort i framtida versioner av Haproxy (i synnerhet användandet av ‘reqadd’) så det kommer jag behöva skriva om, men det är då det.

Det första du gör är, givetvis, att installera Haproxy. Hur din konfiguration ser ut varierar givetvis från fall till fall men det finns ett par saker du kan ha med som gör livet lite enklare. Som jag sa så har jag kompilerat upp Haproxy på CentOS 7 från källkoden. Jag använde följande parametrar för det för att få in stöd för SystemD och lite annat:

make -j 4 TARGET=linux-glibc USE_PCRE=1 USE_OPENSSL=1 USE_ZLIB=1 USE_CRYPT_H=1 USE_LIBCRYPT=1 USE_SYSTEMD=1

I övrigt finns det guider för hur du gör installationen på bästa sätt med CentOS 7, och det är varmt rekommenderat att göra installationen från källkod då den version som finns i CentOS 7-repot är gammal som gatan och saknar stöd för en hel del roliga saker, exempelvis stöd för http/2 vilket kom i version 1.8 av Haproxy, vilket är trevligt i kombination med SSL.

Väl att notera är också att Haproxy gärna vill använda /var/run/haproxy/ när den startar för sin socket. Det går givetvis att ändra i /etc/haproxy/haproxy.cfg om man vill det men som standard kan denna inställning finnas i konfigurationsfilen.

Under inställningarna för global kan du lägga in inställningarna för hur SSL-certen ska hanteras av Haproxy. Mina ser ut så här:

tune.ssl.default-dh-param 4096
   ssl-default-bind-ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AE
S128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-
DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS
   ssl-default-bind-options no-sslv3 no-tls-tickets
   ssl-default-server-ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-
AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDS
A-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS
   ssl-default-server-options no-sslv3 no-tls-tickets

Genom att använda ACL:er kan du göra redirects redan i Haproxy för att skicka trafiken från http till https.

Under avsnittet för frontend sätter du följande (hämtat från min egna konfiguration):

  acl is_joacimmelin            hdr_end(host) -i www.joacimmelin.se
  acl is_joacimmelin            hdr_end(host) -i joacimmelin.se

Därefter lägger du in följande i samma kodavsnitt:

  redirect scheme https if { hdr(Host) -i www.joacimmelin.se } !{ ssl_fc }
 redirect scheme https if { hdr(Host) -i joacimmelin.se } !{ ssl_fc }

Vill du ändå tillåta både http och https-trafik kan du lägga in följande i samma kodavsnitt:

use_backend  nginx if is_joacimmelin

Namnet “nginx” är namnet på min backend där alla webbservrarna ligger inlagda. Du kan givetvis döpa den till vad du vill.

Nu kommer vi till det lite knepigare avsnittet, frontend för https.

Jag börjar det så här:

frontend https_front
  compression algo gzip
  compression type text/html text/plain text/javascript application/javascript application/xml text/css
  bind *:443 ssl crt-list /etc/ssl/crt-list.txt alpn h2,http/1.1
  reqadd X-Forwarded-Proto:\ https
  acl is_joacimmelin             hdr_end(host) -i www.joacimmelin.se
  acl is_joacimmelin             hdr_end(host) -i joacimmelin.se
  use_backend             nginx if is_joacimmelin

Den kanske viktigaste raden av den alla är rad fyra där vi binder en fil som heter /etc/ssl/crt-list.txt till ssl-hanteringen i Haproxy. Där finns för övrigt också en parameter som heter h2 som slår på http/2, vilket varmt rekommenderas.

Certifikathantering i Let’s Encrypt

Filen jag nämnde ovan (crt-list.txt) ser i mitt fall ut så här:

 /etc/letsencrypt/live/joacimmelin.se/joacimmelin.se

Sökvägen till filen är väl ganska tydlig, men filnamnet (joacimmelin.se)? För att detta ska fungera måste man göra ett kombinerat cert av de filer man får från Let’s Encrypt och det gör man på detta sätt:

DOMAIN='joacimmelin.se' bash -c 'cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem /etc/letsencrypt/live/$DOMAIN/privkey.pem > /etc/letsencrypt/live/$DOMAIN/joacimmelin.se'

I filen /etc/ssl/crt-list.txt kan jag sedan lista hur många certifikat jag vill för hur många domäner eller subdomäner jag vill och Haproxy kommer kolla i filen varje gång när den https-request ska hanteras och därmed skyffla över certifikatet och ladda sidan. Eftersom jag terminerar certifikatet i Haproxy kan jag skicka webbtrafiken bakåt till backend över port 80 och förutom att Wordpress behöver lite smisk för att det ska fungera (löses enklast i form av en plugin, givetvis…) så är det riktigt smidigt att bygga sin webbplattform på detta sätt.

Eftersom man skapar sina certifikat med Let’s Encrypt och certbot på Haproxy-maskinen kan man hantera den biten på lite olika sätt. Det enklaste, men kanske inte snyggaste, är att lägga in ett script i Crontab som var tredje-femte dag eller så helt sonika stoppar Haproxy, exekvererar Certbots renew-funktion, sedan kör man sammanslagningen av certifikatfilerna till ett kombinerat cert (som jag visade tidigare) och slutligen startar man Haproxy igen. Ungefär så här kan det se ut:

 #!/bin/bash
systemctl start haproxy
/usr/bin/certbot renew --standalone
DOMAIN='joacimmelin.se' bash -c 'cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem /etc/letsencrypt/live/$DOMAIN/privkey.pem > /etc/letsencrypt/live/$DOMAIN/joacimmelin.se'
systemctl start haproxy  

Lycka till!