SSL Test Result

HAProxy, Nginx, LXD und Let’s Encrypt

In meinem letzten Beitrag habe ich beschrieben, wie man verschiedene Webserver, die in einem LXD Container laufen, von außen über einen Reverse-Proxy (in unserem Fall HAProxy) erreichbar machen kann. Diese Setup läuft aber nur über HTTP (Port 80) und damit über einen unverschlüsselte Verbindung. Heutzutage ist es unabdingbar, dass man seine Website auch verschlüsselt. Deswegen möchte ich heute das Setup erweitern, sodass die Webserver über eine verschlüsselte Verbindung erreichbar sind. HAProxy wird dabei SSL/TLS Termination Proxy agieren, d.h. wir müssen nur an einer Stelle alle unsere Zertifikate verwalten und nicht auf jedem einzelnen Webserver selbst. Der Vorteil ist, dass den Webservern Arbeit durch den Proxy abgenommen wird, allerdings muss man wissen, dass die Kommunikation zwischen HAProxy und den Webservern unverschlüsselt erfolgt. Dieses private Netz sollte als sicher angesehen werden. In unserem Fall ist das das Subnet von LXD, in dem sich die Container befinden. Folgende Grafik veranschaulicht den Prozess:

TLS / SSL Termination

Container überprüfen

Zuerst überprüfen wir, ob alle unsere Container laufen und eine IP haben. Das können wir mit dem bekannten lxc list Befehl machen:

$ lxc list
+---------+---------+-----------------------+------+------------+-----------+
|  NAME   |  STATE  |         IPV4          | IPV6 |    TYPE    | SNAPSHOTS |
+---------+---------+-----------------------+------+------------+-----------+
| haproxy | RUNNING | 10.10.10.10 (eth0)    |      | PERSISTENT | 0         |
+---------+---------+-----------------------+------+------------+-----------+
| web1    | RUNNING | 10.10.10.100 (eth0)   |      | PERSISTENT | 0         |
+---------+---------+-----------------------+------+------------+-----------+
| web2    | RUNNING | 10.10.10.200 (eth0)   |      | PERSISTENT | 0         |
+---------+---------+-----------------------+------+------------+-----------+

Alle unsere Container sind online und haben eine IP-Adresse bekommen. Weiterhin sollten die DNS-Einträge unserer beiden Webserver auf die öffentliche IP des Servers zeigen. Dies können wir z.B. mit dem host-Befehl überprüfen:

$ host web2.example.com
web1.example.com has address 1.2.3.4

Weiterleitung für HTTPS einrichten

Bisher wird nur Traffic, der auf Port 80 auf unserem Server ankommt an den HAProxy Container weitergeleitet. Für HTTPS brauchen wir aber noch eine Weiterleitung für Port 443. Diese können wir mit einer ähnlichen iptables Regel einrichten:

$ sudo iptables -t nat -I PREROUTING -i eth0 -p TCP -d server_ip/32 --dport 443 -j DNAT --to-destination haproxy_ip:443

Diese Regel ist nach einem Neustart wieder weg. Um sie dauerhaft einzurichten, kann man z.B. das Paket iptables-persistent installieren.

HAProxy anpassen

Als nächsten müssen wir den HAProxy Konfiguration anpassen. Dazu loggen wir uns als root in dem Container ein:

$ lxc exec haproxy bash

Nun können wir die Konfigurationsdatei (/etc/haproxy/haproxy.cfg) öffnen (im HAProxy-Container) und anpassen.

$ nano /etc/haproxy/haproxy.cfg

Damit unser SSL Setup möglichst sicher ist, fügen wir folgende Zeilen im global Abschnitt hinzu. Ich habe Sie mit dem Mozilla SSL Configuration Generator erzeugt:

global
    ...
    # set default parameters to the modern configuration
    ssl-default-bind-ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
    ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
    ssl-default-server-ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
    ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets

Im defaults Block fügen wir folgende Zeilen hinzu:

defaults
     ...
     option forwardfor
     option http-server-close
     ...

Nun sind einige Anpassungen im frontend www_frontend Abschnitt notwendig:

frontend www_frontend
    # Http
    bind *:80
    # Https mit Ordner für Certifikate
    bind *:443 ssl crt /etc/haproxy/certs/
    # allen Traffic nach https weiterleiten
    redirect scheme https if !{ ssl_fc }
    # TLS Termination. HAProxy informiert hier unseren Webserver darüber. 
    reqadd X-Forwarded-Proto:\ https

    # Unterscheide zwischen sicheren und unsicheren Requests (wird in den nächsten zwei Zeilen verwendet).
    acl secure dst_port eq 443
    
    # Markiere alle Cookies als sicher, wenn sie über SSL gesendet werden.
    rsprep ^Set-Cookie:\ (.*) Set-Cookie:\ \1;\ Secure if secure
    # Füge den HSTS-Header mit einem maximalen Alter von 1 Jahr hinzu.
    rspadd Strict-Transport-Security:\ max-age=31536000 if secure

In in den Backend Abschnitten müssen wir keine Änderungen vornehmen.

Let’s Encrypt Zertifikate einrichten

Nun brauchen wir für die verschlüsselte Kommunikation noch ein Zertifikat für unsere Webserver. Let’s Encrypt bietet kostenfreie Zertifikate schon seit einigen Jahren an und ab diesem Jahr sogar Wildcard-Zertifikate.

Damit wir die Zertifikate einrichten können, brauchen wir den Let’s Encrypt Client, den wir wie folgt installieren (im HAProxy Container):

$ sudo apt install letsencrypt

Wir generieren ein Zertifikat, welches für alle unsere Domains gültig ist:

$ letsencrypt certonly -d example.com -d web2.example.com

Wenn alles geklappt hat, haben wir nun ein gültiges Zertifikat für unsere beiden Webserver. Jetzt müssen wir nur noch HAProxy die richtigen Zertifikate zur Verfügung stellen. Dazu müssen wir die Certificate Chain und den Private Key zusammenfügen, damit HAProxy etwas damit anfangen kann. Wir legen im ersten Schritt das Zielverzeichnis an und fügen dann die Zertifikate und den Key zusammen:

$ mkdir -p /etc/haproxy/certs/
$ DOMAIN='example.com' sudo -E bash -c 'cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem /etc/letsencrypt/live/$DOMAIN/privkey.pem > /etc/haproxy/certs/$DOMAIN.pem'

Zum Schluss starten wir den HAProxy neu:

$ systemctl restart haproxy

Testen

Wir sind fast am Ziel und möchten unsere Domains noch testen, ob auch alles richtig konfiguriert wurde.  Zuerst rufen wir die Seiten in einem Webbrowser auf. Es sollte neben der Adresse ein grünes Schloss erscheinen (im Firefox). Weiterhin sollten wir noch einen SSL Server Test durchführen. Das Ergebnis lässt sich sehen:

SSL Test Result

Fazit

Mit wenigen Schritten haben wir ein Setup geschaffen, in dem wir mehrere Webseiten nach außen hin erreichbar machen können. Das Ganze lässt sich leicht erweitern und skalieren. Besonders angenehm finde ich die Verwaltung der Zertifikate an einer Stelle und nicht auf jedem Webserver einzeln. LXD und Let’s Encrypt, sowie HAProxy sind für mich eine der großen persönlichen Entdeckungen der letzten zwei Jahre.

Was sind für dich die größten Herausforderung bei der Bereitstellung von Webservern?

6 Comments:

  1. Hallo,

    gern gehe ich auf die Frage am Ende deines lesenswerten Artikels ein.

    Die größte Herausforderung besteht für mich darin, mir bereits bei der Bereitstellung eines Webservers, die notwendigen Gedanken für den folgenden Betrieb zu machen. Denn die meisten Probleme treten meiner Erfahrung nach nicht während der Bereitstellung sondern erst später im Betrieb auf.

    Hier gilt es Betriebsunterbrechungen aufgrund von Updates z.B. des PHP-Interpreters oder der Webanwendungen zu vermeiden und Probleme bei der automatischen Zertifikatsverlängerung rechtzeitig zu erkennen.

    Gruß
    Tronde

  2. zefanja

    Hi Tronde,

    das stimmt. Im Schulumfeld haben wir dieses Problem nicht so stark, dass die Dienste hauptsächlich außerhalb der Ferien genutzt werden 🙂

    Theoretisch kann man das aber auch mit LXD auffangen. Man macht einen Snapshot und kopiert diesen Snapshot als in einen neuen Container und führt die Updates durch. Wenn alles passt nimmt man die Kopie als neuen „Master“. So verringert man die Down-Time.

    Wir setzen weiterhin Check_mk auf allen unseren Servern ein. Dort kann man sich auch einen Check für die Zertifikate einbauen und bestimmen wie viel Tage man vorher informiert werden möchte. Das schöne an Let’s Encrypt ist ja auch, dass man die Erneuerung automatisieren kann.

    Gruß,
    Stephan

  3. Hallo Stephan,

    > Das schöne an Let’s Encrypt ist ja auch, dass man die Erneuerung automatisieren kann.

    Und hierbei muss man dafür sorgen, im Fehlerfall rechtzeitig informiert zu werden. In der Vergangenheit verwendete ich den Client acme-tiny, welcher plötzlich nicht mehr funktionierte und mit Ausnahmefehlern abstürzte. Dank Monitoring hatte ich noch genügend Zeit, um zu reagieren.

    Gruß
    Tronde

  4. Thomas

    da fehlt eventuell noch ein cronjob der alle X Tage das Zertifikat wieder für den HAProxy zusammenfügt.

  5. Sebastian Heye

    Hallo zefanja,

    ich habe etwas rum gegoogelt so richtig schlau wurde ich nicht. Da dachte ich fragen wir dich einfach, wir möchten, dass default alle requests zu lxc „xyz“ gehen und nur für die, wo wir eine Domain extra configurieren, die sollen auf eine andere lxcs laufen. Ist sowas möglich?

    Beste Grüße Sebastian

Leave a Reply:

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert