HAProxy mit SSL von Let’s Encrypt

HAProxy mit SSL Absichern

Vielleicht hast du ja schon ein wenig mit Let’s Encrypt getestet oder meinen Artikel über Nginx mit Let’s Encrypt absichern gefunden. Dass ich großer Fan von HAProxy bin, sollte spätestens in den Artikeln Loadbalancing mit HAProxy und HAProxy Tutorial: Installation unter Debian/Linux klar geworden sein 🙂

Worüber ich noch nichts geschrieben habe: HAProxy mit SSL Absichern. Das hole ich jetzt mit diesem Artikel nach. Let’s Encrypt ist eine neue Zertifizierungsstelle, die einfache und kostenfreie SSL Zertifikate zur Verfügung stellt. Die CA ist in allen relevanten Browsern eingebettet, so dass du mit Let’s Encrypt deine Webseiten absichern kann. Und alles ohne Kosten.

Was mir persönlich noch wichtiger ist: Bislang habe ich mein Icinga immer auf jedes SSL-Zertifikat angesetzt. So konnte ich sicherstellen, immer rechtzeitig darüber informiert zu werden, wenn ein SSL-Zertifikat drohte abzulaufen.

Bei einem Monitoring ging dann immer die Prozedur los:

  • Schlüssellänge checken (ob die noch ausreichend ist)
  • Ggf. Renewal des SSL Zertifikats anfordern oder neuen SSL Key und neues SSL CRT erstellen
  • Dann Kreditkarte durch Thawte oder Comodo ziehen, mich währenddessen mit schlechten Interfaces herumärgern
  • Dann warten, warten, warten…. und warten
  • Dann in einem nächsten Schritt die SSL Zertifikate auf den Servern austauschen
  • Dienste neustarten und prüfen, dass alles läuft

Wenn du auch etwas mehr als eine Handvoll SSL Zertifikate für deine Server und Services hast, dann ist das echt anstrengend. Ich habe also vor einiger Zeit angefangen, die SSL Zertifikate nach- und nach durch Let’s Encrypt Zertifikate auszutauschen. Zunächst habe ich mit meinen Webserver angefangen, aber inzwischen habe ich auch schon ein paar Mailserver umgestellt. Dank der Software HAProxy kann ich ein- und dieselbe Konfiguration für nahezu alles verwenden, dass sich per Layer-4 loadbalancen lässt.

Dieser Artikel baut auf meinen Artikel Installation von HAProxy unter Debian auf. Wenn du es eilig hast und meinen Artikel nicht lesen willst, bau dir einfach schnell das abgebildete HAProxy-Setup auf.

Vorbereitungen

Wenn ich mit SSL auf Webservern arbeite und vor den Webservern einen HAProxy (oder auch einen F5 oder Broacade Loadbalancer) einsetze, dann verzichte ich meist auf die Verschlüsselung auf den Webservern selber. Ich lasse SSL auf dem Loadbalancer terminieren und arbeite im Backend mit einer unverschlüsselten Verbindung. Insbesondere bei Hardware-Loadbalancern und Seiten mit einer großen Menge an HTTPS Sitzungen spart das unfassbar viel CPU-Load ein. Die Loadbalancer haben meist speziell für SSL designte CPU-Karten eingebaut, die um ein vielfaches schneller sind.

Da die Verbindungen zwischen HAProxy und den Webservern über ein private Netzwerk zustande kommt, ist die Gefahr einer Man-in-the-Middle (MitM) Attacke überschaubar. Erst einmal müsste man mein internes Netzwerk kompromittieren um an die unverschlüsselten Datenpakete zu gelangen. Gelingt das, hätte ich eh ein ganz anderes Problem.

Erstelle zunächst ein selbst signiertes SSL-Zertifikat

Für die ersten Tests erstelle dir zunächst einmal ein selbst signiertes SSL-Zertifikat. In meiner Beispielumgebung ist bislang noch kein OpenSSL installiert, hol das ggf. kurz nach:

root@haproxy:~# apt-get -y install openssl

Mit dem folgenden Befehl erstellst du dein selbst signiertes SSL-Zertifikat und legst es nach ‚/etc/ssl/private‘ ab.

root@haproxy:~# openssl req -nodes -x509 -newkey rsa:2048 -keyout /etc/ssl/private/test.key -out /etc/ssl/private/test.crt -days 30
Generating a 2048 bit RSA private key
..........................................+++
......................................+++
writing new private key to '/etc/ssl/private/test.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:DE
State or Province Name (full name) [Some-State]:NRW
Locality Name (eg, city) []:Cologne
Organization Name (eg, company) [Internet Widgits Pty Ltd]:gridscale Cloud Computing
Organizational Unit Name (eg, section) []:gridscale Beta Labs
Common Name (e.g. server FQDN or YOUR name) []:*
Email Address []:

Du kannst die ganzen Eigenschaften für das Zertifikat mit deinen persönlichen Daten füllen oder einfach leer lassen. Wir ersetzen das Zertifikat nachher ja eh durch ein Let’s Encrypt Zertifikat.

Nun erstelle noch eine pem-Datei, indem du Key und Zertifikat in eine Datei kopierst. Das geht mit:

root@haproxy:~# cat /etc/ssl/private/test.key /etc/ssl/private/test.crt > /etc/ssl/private/test.pem

Konfiguration von HAProxy mit SSL Listener für HTTPS

Auf dem HAProxy kannst du mit Hilfe der folgenden Konfiguration einen Listener auf Port 443 (HTTPS) erstellen und HAProxy anweisen, sowohl SSL zu terminieren als auch auf Layer-7 zu arbeiten.

frontend ssl_443
 bind *:443 ssl crt /etc/ssl/private/test.pem
 mode http
 http-request set-header X-Forwarded-For %[src]
 reqadd X-Forwarded-Proto:\ https
 option http-server-close
 default_backend ssl_443

backend ssl_443
 mode http
 balance leastconn
 server web1 10.0.0.2:80 check
 server web2 10.0.0.3:80 check

Kurzer Parameter-Check:

  • bind *:443 ssl crt /etc/ssl/private/test.pem erstellt einen Listener auf Port 443 mit einem SSL Zertifikat, dass im entsprechenden Pfad liegt.
  • mode http sowie die ganzen Header-Manipulationen hatte ich im vorherigen Artikel schon erklärt
  • option http-server-close ist neu. Dieser Parameter ist für’s erste unwichtig, allerdings erlaubt er dem Webserver keepalive zu aktivieren. Das macht das Surferlebnis auf deinen Webseiten etwas schneller.

Nach einem Neustart des HAProxy sollten deine Webserver auf dem Port 443 erreichbar sein. Wenn du direkt die info.php aufrufst, siehst du auch gleich welche zusätzlichen Header jetzt zur Verfügung stehen. Vergleiche doch mal die info.php über Port 80 und über Port 443.

Bist du bereit zu starten?

Oder hast du noch Fragen? Erstelle dir jetzt dein kostenloses Konto oder lass dich in einem persönlichen Gespräch beraten.

Installation von Let’s Encrypt auf HAProxy

Sofern du meinen Artikel Nginx mit Let’s Encrypt absichern gelesen hast, weiß du in etwa was jetzt kommt. Auf dem HAProxy-System muss die Let’s Encrypt Suite installiert werden, damit du SSL Zertifikate anfordern kannst. Da Let’s Encrypt Domain Validierte Zertifikate ausstellt, brauchst du zunächst einen DNS Eintrag der auf die IP-Adresse deines HAProxy zeigt. Ich nutze im Folgenden den DNS ‚haproxy.kmsg.cc‘.

Installiere dir zunächst ein paar Tools auf dem HAProxy System, die später von Let’s Encrypt benötigt werden.

root@haproxy:~# apt-get -y install bc git

Dann clone dir das Repository von Let’s Encrypt:

root@haproxy:~# git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt

Nun hast du erst einmal alles auf dem System, dass für Let’s Encrypt benötigt wird.

Schalte vorübergehend deinen HAProxy ab, es sind nämlich noch ein paar Konfigurationen notwendig:

root@haproxy:~# /etc/init.d/haproxy stop

Funktionsweise von Let’s Encrypt Suite

Mal ganz grob zum Ablauf der Let’s Encrypt Suite. Die geclonte Suite arbeitet mit sogenannten Modulen für diverse Dienste und Programme. Diese Module übernehmen normalerweise die ganze Arbeit. Sie erstellen die Zertifikate, führen die Validierung durch und konfigurieren letztlich auch noch deinen Dienst.

Für die Implementierung in HAProxy gibt es kein solches Modul. Daher ist es hilfreich zu verstehen, wie Let’s Encrypt funktioniert. Mit diesem Wissen kannst du dir dann einen Workaround bauen, der mit einem HAProxy funktioniert.

Das Auto-Modul von Let’s Encrypt stellt eine Verbindung zu den Let’s Encrypt Servern her und tauscht dort einen Token aus. Dieser Token wird in einem Webserver-Verzeichnis abgelegt und im Anschluss von den Let’s Encrypt Servern aufgerufen. Stimmt der Token überein, signieren die Let’s Encrypt Server dein Zertifikat.

Anpassung von HAproxy für Let’s Encrypt

Eigentlich ganz einfach, wenn da nicht ein kleiner Haken wäre. Dein HAProxy liefert standardmäßig keinerlei Inhalte aus, sondern leitet die Anfragen in das Backend um. Das Backend wiederum kann weder SSL sprechen (also auch keine Zertifikat-Requests erstellen), noch weiß es irgendwas von dem, was du auf dem HAProxy gerade machst.

Sprich der Token kann nicht ausgetauscht werden und damit bekommst du kein signiertes Zertifikat.

Ich möchte dieses Problem gerne umgehen, indem ich HAProxy anweise, alle Anfragen an das Token-Verzeichnis von Let’s Encrypt anders zu behandeln, als sämtliche anderen Requests. Dazu brauchst du zunächst den Namen der Verzeichnisse, die Let’s Encrypt auf deinem Webserver anfragt. Passe nun die HAProxy Konfiguration entsprechend an.

frontend port_80
 bind *:80
 mode http
 acl lets_encrypt path_beg /.well-known/acme-challenge/
 use_backend lets_encrypt if lets_encrypt 
 default_backend port_80

backend port_80
 mode http
 http-request set-header X-Forwarded-For %[src]
 balance roundrobin
 option httpchk HEAD / HTTP/1.0
 server web1 10.0.0.2:80 check
 server web2 10.0.0.3:80 check

Achte auf die eingefügte ACL. Sollte eine Anfrage an den HAProxy gestellt werden, die mit ‚.well-known‘ in der URI beginnt, wird diese an das Backend ‚lets_encrypt‘ umgeleitet.

Nun fehlt noch eine Definition für das Backend ‚lets_encrypt‘. Dafür reicht der vermutlich einfachste Backend Eintrag für HAProxy:

backend lets_encrypt
 mode http
 server local localhost:60001

Damit ist für HAProxy klar, dass alle Anfragen an das Backend über den localhost zugestellt werden sollen. Was nun fehlt ist noch ein Webserver, der auch auf localhost Port 60001 antwortet. Diesen Webserver bringt letsencrypt direkt mit.

Nun starte deinen HAProxy und schau in das Logfile /var/log/haproxy.log ob dort irgendwelche Fehler protokolliert werden. Wenn du die „admin stats“ aktiviert hast (mehr dazu in einem Artikel über ein paar erweiterte Konfigurationen von HAProxy), solltest du jetzt auf der Admin Seite von HAProxy die neue Konfiguration sehen.

Nun teste mal, ob die Ausnahme-Regel im HAProxy für die URL .well-known funktioniert. Ruf die URL im Browser auf, beispielsweise: ‚http://haproxy.kmsg.cc/.well-known/acme-challenge‘. Hast du alles richtig gemacht, bekommst du einen Fehler (503 Service Unavailable vom HAProxy). Denn aktuell ist auf dem Localhost-Port 60001 ja noch kein Dienst gestartet, in sofern kann HAProxy keinen Request weiterleiten.

Let’s Encrypt Zertifikat anfordern

Jetzt kommt der vorletzte Schritt, das Anfordern des Let’s Encrypt Zertifikats. Erstelle zunächst (sofern noch nicht vorhanden) das Verzeichnis /etc/letsencrypt/ und lege dort eine Konfigurationsdatei „cli.ini“ an.

rsa-key-size = 4096
email = email@domain.tld
authenticator = standalone
standalone-supported-challenges = http-01

Du kannst die Parameter auch an das CLI Tool übergeben, wenn du lieber ohne Konfigurationsdatei arbeiten möchtest. Nun gehe in das Verzeichnis /opt/letsencrypt (Reminder: Hierhin hattest du die Let’s Encrypt Suite geclont) und führe folgendes Kommando aus. Ersetze dabei –domains durch deinen DNS, für den du ein Zertifikat anfordern möchtest.

root@haproxy:/opt/letsencrypt# ./letsencrypt-auto certonly --domains haproxy.kmsg.cc --renew-by-default --http-01-port 60001 --agree-tos

Voilà! Du findest nun in /etc/letsencrypt/live/ ein Verzeichnis mit deinem DNS Namen und darin sind Key, Zertifikat, Chain usw…

Jetzt musst das neue Zertifikat nur noch in die haproxy.cnf eingetragen werden. Leider kannst du nicht direkt die von Let’s Encrypt erstellten Dateien verwenden, da HAProxy alles in einer Datei braucht.

Ich lege alle Zertifikate für HAProxy nach /etc/haproxy/cert. Das finde ich übersichtlich, aber über die richtige Struktur bricht regelmäßig unter DevOps Streit aus. Also überleg dir einfach, wie du es machen willst 🙂

Hier mein Weg:

root@haproxy:~# mkdir -p /etc/haproxy/cert
root@haproxy:~# cat /etc/letsencrypt/live/haproxy.kmsg.cc/fullchain.pem /etc/letsencrypt/live/haproxy.kmsg.cc/privkey.pem > /etc/haproxy/cert/haproxy.kmsg.cc.pem

Nun editiere noch den Listener für Port 443 in deiner haproxy.cnf:

frontend ssl_443
# bind *:443 ssl crt /etc/ssl/private/test.pem
  bind *:443 ssl crt /etc/haproxy/cert/
  mode http
  http-request set-header X-Forwarded-For %[src]
  reqadd X-Forwarded-Proto:\ https
  option http-server-close
  default_backend ssl_443

und starte HAProxy neu. Anschließend teste, ob du auf deinem DNS nun per SSL zugreifen kannst.

Letzter Schritt, automatische Verlängerung der Zertifikate

Let’s Encrypt stellt Zertifikate immer nur für einen Zeitraum von 90 Tagen aus. Das heißt, du musst innerhalb dieser Zeit das Zertifikat erneuern.

Der einfachste Weg dafür ist, das oben stehende Kommando in den Cronjob einzutragen. Sofern du aber für eine Domain sehr viele Zertifikate erstellen möchtest, eine Warnung vorab: Let’s Encrypt hat ein sogenanntes Rate Limit eingebaut. Dieses Rate Limit zählt die Zertifikatsanfragen auf Basis der Domain (Achtung, nicht Subdomain!). Jede Anfrage für eine Subdomain zählt in den Zähler der Hauptdomain hinein. Pass also auf, dass du die Server von Let’s Encrypt nicht zu oft nach einem Renewal fragst (sonst hast du Pech), aber auch nicht zu selten (sonst ist dein Zertifikat plötzlich ungültig).

Ich nutze für die Automatisierung ein kleines Shellskript. Du findest es auf unserem BitBucket Repository und kannst es mit dieser URL direkt herunterladen. Lade es einfach nach /opt herunter.

root@haproxy:~# wget https://bitbucket.org/gridscale/letsencrypt/raw/HEAD/le-autorenew.sh -O /opt/le-renew-webroot

Editiere in dem Skript den Parameter „exp_limit“ und setze ihn z.B. auf irgendeinen Wert zwischen 10 und 30. Kurzer Überblick, was ich in dem Skript mache:

  • Ich lese die vorhandenen Verzeichnisse in /etc/letsencrypt/live aus
  • Für jedes Verzeichnis prüfe ich die Key und CA Datei
  • In der CA schaue ich nach, wie lange das Zertifikat noch gültig ist. Wenn es länger als exp_limit gültig ist, dann springe zu dem nächsten Zertifikat.
  • Wenn das Zertifikat weniger Tage gültig ist als in exp_limit angegeben, dann lese ich die Zertifikatsnamen aus (du kannst ja mehr als einen DNS pro Zertifikat hinterlegen)
  • Dann generiere ich das Let’s Encrypt Kommando und versuche das Zertifikat verlängern zu lassen
  • Ist das geglückt, tausche ich das Zertifikat im HAProxy aus und lade den Dienst neu. Schlägt der Vorgang fehl, versuche ich das vorherige Zertifikat wiederherzustellen.

Das Skript ist nur schnell zusammen gehackt und soll dir eher als Beispiel dienen. Es wäre sicherlich gut, noch ein wenig mehr Fehlerhandling zu bauen. Folgende Szenarien fange ich nicht ab:

  • Wenn der DNS für eine Domain nicht mehr gültig ist, kann Let’s Encrypt die Domain Validierung nicht durchführen. Let’s Encrypt quittiert dann mit einem Fehler. Besser wäre es, wenn die hinterlegten Domain-Namen auf den Registry-Status geprüft werden (also in der Root-Zone der jeweiligen NIC) und anschließend die zuständigen Nameserver nach den eingetragenen DNS gefragt würden.
  • Es gibt kein Fehlerhandling für HAProxy in dem Skript.
  • Kein Fehler- und Info-Reporting in dem Skript

Kurz um: Nutze das Skript vorsichtig oder baue es ggf. nach. Wenn ich mal eine ruhige Stunde habe, hacke ich das Bashskript vielleicht noch einmal in ein Python Skript und bessere die Mängel nach. Falls du dir die Arbeit machen möchtet, freue ich mich über deine Einsendung die ich hier gerne veröffentlichen werde.

Wenn du trotzdem auf mein Skript vertrauen möchtest, erstelle einfach einen Cronjob damit es beispielsweise einmal pro Tag ausgeführt wird.