Ansible-Tutorial: Setup von HAProxy und HTTPS von Let’s Encrypt mit Ansible

Im dritten Artikel dieser Serie habe ich auf meinem Server Docker, MySQL und WordPress mit Ansible eingerichtet. In diesem vierten und letzten Artikel werde ich zeigen, wie man – wiederum mit Ansible – HAProxy einrichtet – sowie ein kostenloses HTTPS-Zertifikat von Let’s Encrypt / CertBot, um die Webseite über HTTPS erreichbar zu machen.

Den Code zum Artikel findet ihr in meinem GitLab-Repository unter https://gitlab.com/SvenWoltmann/happycoders-tutorial-server-setup.

Zusätzlich zum Artikel gibt es auch wieder ein Video-Tutorial: Setup von HAProxy und eines HTTPS-Zertifikats von Let’s Encrypt mit Ansible

Die Artikelserie ist in folgende vier Teile aufgeteilt:

Installation von HAProxy

Im ersten Schritt werde ich HAProxy ohne spezifische Konfiguration installieren, da ich die dabei angelegte Konfigurationsdatei haproxy.cfg als Ausgangsbasis für meine weitere Konfiguration verwenden möchte. Allerdings möchte ich nicht die unter Debian Stretch verfügbare Version 1.7 installieren, sondern die aktuelle Version 1.8, da diese das moderne HTTP/2-Protokoll unterstützt. Diese Version erhalte ich über das stretch-backports-Repository.

Ich erstelle eine Rolle happy1_haproxy mit der Task-Definitionsdatei roles/happy1_haproxy/tasks/main.yml und füge entsprechend in meinem Playbook happy1.yml zum Array roles den Eintrag happy1_haproxy hinzu, so dass die neue Rolle beim nächsten Ausführen des Playbooks ausgeführt wird. In die main.yml trage ich folgende zwei Tasks ein:

---
- name: Add "stretch-backports" to sources (required for HAProxy 1.8)
 apt_repository:
   repo: deb http://httpredir.debian.org/debian stretch-backports main
   state: present
   update_cache: yes

- name: Install HAProxy
apt:
name: haproxy
state: present
  default_release: stretch-backports
cache_valid_time: 3600

Mit dem Ansible-Modul apt_repository füge ich das Backports-Repository zu APT hinzu. Im apt-Modul gebe ich dann über den Parameter default_release an, dass das Backports-Repository für die Installation von HAProxy verwendet werden soll.

Ich führe das Playbook aus:

ansible-playbook --ask-vault-pass happy1.yml
Installation von HAProxy via Ansible
Installation von HAProxy via Ansible

Dass docker-compose up -d den Status changed meldet, kann ignoriert werden. Dies liegt daran, dass das Kommando über das Ansible Module „shell“ ausgeführt wird und dieses die Ausgabe des Kommandos (diese ist: „wordpress_db_1 is up-to-date, wordpress_wordpress_1 is up-to-date“) nicht als „keine Änderung“ interpretiert.

Konfiguration von HAProxy

Ich kopiere mir nun die HAProxy-Standard-Konfigurationsdatei /etc/haproxy/haproxy.cfg vom Server ins lokale Template-Verzeichnis meiner Rolle happy1_haproxy als roles/happy1_haproxy/templates/haproxy.cfg.j2. Ins Template-Verzeichnis deshalb, weil ich die Webseite zunächst mit einem Passwort schützen möchte – dies aber über eine Variable einfach abschaltbar machen möchte. Die Konfiguration erweitere ich nun um folgende drei Sektionen:

Sektion frontend – hier definiere ich, dass HAProxy an Port 80 gebunden wird und alle Anfragen an das Backend happycoders_wordpress weiterleiten soll:

frontend happycoders_80
bind *:80
default_backend happycoders_wordpress

Sektion backend – hier wird das soeben vorab referenzierte Backend happycoders_wordpress definiert, welches die Anfragen an den lokalen Port 8001, auf dem der Docker-Wordpress-Container läuft, weiterleiten soll.

backend happycoders_wordpress
mode http
server server1 localhost:8001 check
http-response del-header x-powered-by
{% if happycoders_testing|default(false) %}
acl authorized http_auth(happycoders_test_users)
acl wp-cron path_beg -i /wp-cron.php
http-request auth realm HappyCoders.eu unless authorized or wp-cron
{% endif %}

Der Servername server1 ist beliebig gewählt, und die Option check sorgt dafür dass regelmäßige Health Checks durchgeführt werden. Mit http-response del-header entferne ich aus Sicherheitsgründen die von PHP als Response-Header zurückgegebene Versionsinformation. Die restlichen Zeilen aktivieren die Basic Authentication, wenn die Variable happycoders_testing auf true gesetzt ist. Im Einzelnen:

  • Die ACL authorized prüft, ob der User bereits autorisiert ist.
  • Mit der ACL wp-cron stelle ich sicher, dass WordPress seinen eigenen Scheduler aufrufen kann. Wichtig ist, dass diese ACL über den Pfad /wp-cron.php definiert wird, nicht über die Quell-IP-Adresse, denn diese ist beim Scheduler-Aufruf nicht die Server-Adresse, sondern die des Docker-Containers.
  • Mit der Direktive http-request auth wird letztendlich die Autorisierung angefordert, es sei denn der User ist bereits autorisiert oder die Anfrage geht an den WordPress-Scheduler.

In der letzten Sektion userlist werden der oder die User aufgelistet, die per Basic Authentication Zugriff haben sollen. Mein Passwort verschlüssele ich mit mkpasswd:

Verschlüsselung meines Passworts mit "mkpasswd"
Verschlüsselung meines Passworts mit „mkpasswd“

Die Benutzerliste trage ich wie folgt ein:

{% if happycoders_testing|default(false) %}
userlist happycoders_test_users
user sven password X/UgQ53BouggY
{% endif %}

Die fertige Datei roles/happy1_haproxy/templates/haproxy.cfg.j2 sieht nun wie folgt aus (die zwei if-Blöcke habe ich zu einem zusammengefasst):

global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon

# Default SSL material locations
ca-base /etc/ssl/certs
crt-base /etc/ssl/private

# Default ciphers to use on SSL-enabled listening sockets.
# For more information, see ciphers(1SSL). This list is from:
# https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
# An alternative list with additional directives can be obtained from
# https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=haproxy
ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
ssl-default-bind-options no-sslv3

defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http

frontend happycoders_80
bind *:80
default_backend happycoders_wordpress

backend happycoders_wordpress
mode http
server server1 localhost:8001 check
http-response del-header x-powered-by
{% if happycoders_testing|default(false) %}
acl authorized http_auth(happycoders_test_users)
acl wp-cron path_beg -i /wp-cron.php
http-request auth realm HappyCoders.eu unless authorized or wp-cron

userlist happycoders_test_users
user sven password X/UgQ53BouggY
{% endif %}

Um die Basic Authentication zu aktivieren, trage ich folgende Zeile in meine Host-Konfigurationsdatei host_vars/happy1.happycoders.eu ein:

happycoders_testing: true

Um die Konfigurationsdatei auf den Server zu kopieren, füge ich folgenden Task in die Datei roles/happy1_haproxy/tasks/main.yml ein:

- name: Configure HAProxy
template:
src: haproxy.cfg.j2
dest: /etc/haproxy/haproxy.cfg
owner: root
group: root
mode: 0644
notify:
- Restart HAProxy

Das template-Modul sowie die notify-Funktion habe ich bereits im zweiten Teil dieser Artikelserie erläutert. Dafür benötige ich noch den entsprechenden Handler in der Datei roles/happy1_haproxy/handlers/main.yml:

---
- name: Restart HAProxy
service:
name: haproxy
state: restarted

Ich führe das Playbook erneut aus:

ansible-playbook --ask-vault-pass happy1.yml
Konfiguration von HAProxy mit Ansible
Konfiguration von HAProxy mit Ansible

Die Webseite ist jetzt direkt über Port 80 über die URL http://happy1.happycoders.eu erreichbar. Port 8001 kann ich nun in der Firewall schließen, in dem ich den entsprechenden Eintrag des Arrays firewall_allowed_tcp_ports aus der Datei host_vars/happy1.happycoders.eu entferne. Ich führe das Playbook also noch einmal aus und starte danach Docker neu (sudo service docker restart), da – wie im vorangegangenen Artikel beschrieben – durch die Änderung der Firewall-Regeln die Docker-Regeln entfernt wurden.

Installation von Certbot

Nun zum spannenden Teil des Artikels: Als nächstes möchte ich, dass die Seite über HTTPS erreichbar ist. Dazu muss ich zum einen HAProxy an den HTTPS-Port 443 binden und diesen an Port 8001 weiterleiten und zum anderen ein SSL/TLS-Zertifikat installieren. Ein SSL/TLS-Zertifikat bekomme ich kostenlos bei Let’s Encrypt. Die Zertifikatsanforderung läuft automatisiert über Certbot. Certbot kann darüber hinaus die Zertifikate automatisch in Apache und nginx installieren. Für die Installation in HAProxy wird ein separates Plugin benötigt, welches von Greenhost angeboten wird: HAProxy plugin for Let’s Encrypt’s Certbot

Zunächst einmal muss Certbot installiert werden. Dazu werde ich den Instruktionen unter https://certbot.eff.org/lets-encrypt/debianstretch-haproxy folgen. Wie immer übertrage ich diese in eine Ansible-Rolle. Ich lege keine separate Rolle für Certbot an, sondern füge die Installationsschritte der happy1_haproxy-Rolle hinzu, da die Konfiguration von Certbot sehr eng mit der Konfiguration von HAProxy verknüpft ist.

Allerdings möchte ich dieses Mal gleich zu Beginn für etwas Ordnung sorgen und verschiebe die ersten drei Kommandos zur Installation und Konfiguration von HAProxy aus der roles/happy1_haproxy/tasks/main.yml in die Datei roles/happy1_haproxy/tasks/install_haproxy.yml und schreibe in die main.yml stattdessen folgendes:

---
- import_tasks: install_haproxy.yml
- import_tasks: install_certbot.yml

Die Datei roles/happy1_haproxy/tasks/install_certbot.yml erstelle ich mit zunächst folgenden zwei Tasks zur Installation von Certbot:

- name: Add "stretch-backports" to sources (required for Certbot)
apt_repository:
repo: deb http://httpredir.debian.org/debian stretch-backports main
state: present
update_cache: yes

- name: Install Certbot
apt:
name: python-certbot
state: present
default_release: stretch-backports
cache_valid_time: 3600

Laut Instruktionen für das HAProxy-Plugin für Certbot soll nun folgendes Kommando ausgeführt werden:

openssl dhparam -out /opt/certbot/dhparams.pem 2048

Hierfür gibt es praktischerweise das Ansible-Modul openssl_dhparam, das ich wie folgt in die install_certbot.yml eintrage. Zuvor muss ich allerdings das Zielverzeichnis /opt/certbot anlegen:

- name: Create Certbot directory
file:
path: /opt/certbot
state: directory
owner: root
group: root
mode: 0755

- name: Generate 2048 bit dhparams.pem file
openssl_dhparam:
owner: root
group: root
mode: 0644
path: /opt/certbot/dhparams.pem
size: 2048

Außerdem möchte ich, wie der Artikel empfiehlt, Certbot als unpreveligierter Nutzer starten und erstelle dazu einen User certbot. Da das user-Ansible-Modul auch das Home-Verzeichnis anlegt, kann ich das Create Certbot directory-Kommando direkt wieder entfernen und den User anlegen bevor ich die dhparams.pem-Datei generiere. Schließlich lege ich noch die drei geforderten Verzeichnisse an:

- name: Create Certbot group
group:
name: certbot
state: present

- name: Create Certbot user
user:
name: certbot
group: certbot
groups: haproxy
shell: /bin/bash
home: /opt/certbot
state: present

- name: Generate 2048 bit dhparams.pem file
openssl_dhparam:
owner: root
group: root
mode: 0644
path: /opt/certbot/dhparams.pem
size: 2048

- name: Create Certbot log directory
file:
path: /opt/certbot/logs
state: directory
owner: certbot
group: certbot
mode: 0755

- name: Create Certbot config directory
file:
path: /opt/certbot/config
state: directory
owner: certbot
group: certbot
mode: 0755

- name: Create Certbot .config/letsencrypt directory
file:
path: /opt/certbot/.config/letsencrypt
state: directory
owner: certbot
group: certbot
mode: 0755

Als nächstes muss ich die Konfigurationsdatei /opt/certbot/.config/letsencrypt/cli.ini auf dem Server erstellen. Dazu speichere ich diese lokal in meiner Rolle unter roles/happy1_haproxy/files/cli.ini mit folgendem Inhalt ab:

work-dir=/opt/certbot/
logs-dir=/opt/certbot/logs/
config-dir=/opt/certbot/config/

Um diese auf den Server zu kopieren, füge ich folgendes in die Tasks-Definitionsdatei ein:

- name: Copy Certbot configuration
copy:
src: cli.ini
dest: /opt/certbot/.config/letsencrypt/cli.ini
owner: root
group: root
mode: 0644

Wenn ein Zertifikat erneuert wird, muss danach HAProxy neugestartet werden. Dies dem certbot-User zu erlauben geschieht mit einem Eintrag in der Datei /etc/sudoers und lässt sich sehr einfach mit dem Ansible-Modul lineinfile realisieren, wobei ich festlege, dass eine ggf. vorhandene, mit %certbot beginnende Zeile überschrieben wird. Ich füge also folgenden Task hinzu:

- name: Allow the certbot user to restart HAProxy
lineinfile:
dest: /etc/sudoers
state: present
regexp: '^%certbot'
line
: '%certbot ALL=NOPASSWD: /bin/systemctl restart haproxy'

Als nächstes muss das certbox-haproxy-Modul installiert werden. Hierfür bediene ich mich der Ansible-Module git, easy_install und pip und trage dazu folgendes in die Tasks-Definitionsdatei ein:

- name: Clone certbot-haproxy repository
git:
repo: https://code.greenhost.net/open/certbot-haproxy.git
dest: /opt/certbot/certbot-haproxy

- name: Install python-setuptools
apt:
name: python-setuptools
state: present

- name: Install pip
easy_install:
name: pip

- name: Install certbot-haproxy
pip:
name: /opt/certbot/certbot-haproxy/

Der erste Task clont das Git-Repository des certbot-haproxy-Moduls; der zweite Task installiert die python-setuptools, welche das Programm easy_install enthalten, mit dem wiederum im dritten Task pip installiert wird, ein Paketverwaltungsprogramm für Python-Pakete; der vierte Tasks letztendlich ruft pip auf, um das certbot-haproxy-Modul zu kompilieren und zu installieren.

Nun müssen noch Änderungen an der haproxy.cfg.j2 vorgenommen werden. Zunächst muss die durch Ansible erstellte Datei dhparams.pem eingetragen werden. Dazu wird im global-Abschnitt eine Zeile angehängt, so dass der Abschnitt wie folgt endet:

global
[...]
ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
ssl-default-bind-options no-sslv3
ssl-dh-param-file /opt/certbot/dhparams.pem

Außerdem müssen die Anfragen des Let’s Encrypt-Servers (deren Pfad beginnt mit /.well-known/acme-challenge) auf das certbot-haproxy-Modul umgeleitet werden, welches auf Port 8000 gebunden ist. Dafür erweitere ich den frontend-Abschnitt wie folgt:

frontend happycoders_80
bind *:80

# Forward Certbot verification requests to the certbot-haproxy plugin
acl is_certbot path_beg -i /.well-known/acme-challenge
use_backend certbot if is_certbot

default_backend happycoders_wordpress

Und am Ende der haproxy.cfg.j2 trage ich das Backend certbot ein:

backend certbot
log global
mode http
server certbot 127.0.0.1:8000

Da die Passwort-Abfrage im happycoders_wordpress-backend definiert ist, sind die Challenge-Anfragen des Let’s Encrypt-Servers über den Pfad /.well-known/acme-challenge davon nicht betroffen. Für diejenigen, die den Überblick verloren haben: Hier ist noch einmal der komplette Inhalt der Datei roles/happy1_haproxy/templates/haproxy.cfg.j2:

global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon

# Default SSL material locations
ca-base /etc/ssl/certs
crt-base /etc/ssl/private

# Default ciphers to use on SSL-enabled listening sockets.
# For more information, see ciphers(1SSL). This list is from:
# https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
# An alternative list with additional directives can be obtained from
# https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=haproxy
ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
ssl-default-bind-options no-sslv3
ssl-dh-param-file /opt/certbot/dhparams.pem

defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http

frontend happycoders_80
bind *:80

# Forward Certbot verification requests to the certbot-haproxy plugin
acl is_certbot path_beg -i /.well-known/acme-challenge
use_backend certbot if is_certbot

default_backend happycoders_wordpress

backend happycoders_wordpress
mode http
server server1 localhost:8001 check
http-response del-header x-powered-by
{% if happycoders_testing|default(false) %}
acl authorized http_auth(happycoders_test_users)
acl wp-cron path_beg -i /wp-cron.php
http-request auth realm HappyCoders.eu unless authorized or wp-cron

userlist happycoders_test_users
user sven password X/UgQ53BouggY
{% endif %}

backend certbot
log global
mode http
server certbot 127.0.0.1:8000

Damit sind die Tasks zur Installation von Certbot vollständig, und es ist an der Zeit das Ansible-Playbook auszuführen:

ansible-playbook --ask-vault-pass happy1.yml
Installation von Certbot und certbot-haproxy mit Ansible
Installation von Certbot und certbot-haproxy mit Ansible

Erstellung des Certbot-Skripts

Nachdem nun Certbot und das certbot-haproxy-Modul installiert sind, wird es an der Zeit Certbot auszuführen. Ich habe mehrere HappyCoders-Domains, die ich in einem späteren Schritt auf happycoders.eu umleiten möchte. Ich benötige daher für alle meine Domains ein SSL/TLS-Zertifikat. Certbot kann mit einem einzigen Aufruf die Zertifikate für alle Domains erstellen. Dazu lege ich zunächst folgendes Array in der Host-Konfigurationsdatei host_vars/happy1.happycoders.eu an:

happycoders_domains:
- happycoders.eu
- happycoders.at
- happycoders.biz
- happycoders.ch
- happycoders.club
- happycoders.de
- happycoders.info
- happycoders.it
- happycoders.me - happycoders.net
- happycoders.uk

Ich habe bewusst happycoders.eu an den Anfang geschrieben, da die erste Domain im Zertifikat als Hauptdomain und alle weiteren als alternative Domains eingetragen werden. Die DNS-Einträge für all diese Domains müssen auf meinen Server zeigen, damit die Challenge-Anfragen des Let’s Encrypt-Servers ankommen. Für den Aufruf von Certbot erstelle ich folgendes Skript in der Datei roles/happy1_haproxy/templates/run-certbot.j2:

runuser -l certbot -c 'certbot run \
--authenticator certbot-haproxy:haproxy-authenticator \
--installer certbot-haproxy:haproxy-installer \
--non-interactive \
--domains {% for domain in happycoders_domains %}{% if loop.index > 1 %},{% endif %}www.{{ domain }},{{ domain }}{% endfor %} \
--expand \
--agree-tos \
--email [email protected]'

Mit runuser -l certbot -c '<Kommando>' lasse ich das in Apostrophen angegebene Kommando durch den certbot-User aufrufen. Die Parameter des certbot-Kommandos bedeuten im Einzelnen:

  • --authenticator gibt das Authenticator-Plugin an, in diesem Fall das des zuvor installierten certbot-haproxy-Moduls. Das Authenticator-Plugin prüft, ob die Domains, für ich die Zertifikate erstellen will, unter meiner Kontrolle liegen, indem es eine Datei über HTTP verfügbar macht, welche dann von Let’s Encrypt überprüft wird.
  • --installer definiert das Installer-Plugin; auch hier verwende ich das des certbot-haproxy-Moduls.
  • --non-interactive legt fest, dass Certbot ohne User-Interaktion durchläuft.
  • --domains listet die Domains auf, für die ich Zertifikate erstellen möchte. Ich gebe hier jede Domain zunächst mit und dann ohne www an.
  • --expand bestimmt, dass das Zertifikat erweitert werden darf, wenn die Liste der Domains erweitert wird. Wird die Liste erweitert ohne diesen Parameter anzugeben, bricht das Kommando mit einer Fehlermeldung ab.
  • --agree-tos bedeutet, dass ich den Nutzungsbedingungen („terms of service“) zustimme.
  • --email legt die E-Mail-Adresse fest, unter der ich z. B. über auslaufende Zertifikate informiert werde.

Um das Skript auf den Server zu kopieren lege ich die Task-Datei roles/happy1_haproxy/tasks/setup_certbot_script.yml an mit folgendem Inhalt:

---
- name: Create Certbot scripts directory
file:
path: /opt/certbot/scripts/
state: directory
owner: root
group: root
mode: 0755

- name: Copy Certbot script
template:
src: run-certbot.j2
dest: /opt/certbot/scripts/run-certbot
owner: root
group: root
mode: 0755

- name: Link cronjob to renew Certbot certificates
file:
src: /opt/certbot/scripts/run-certbot
dest: /etc/cron.daily/run-certbot
owner: root
group: root
state: link

Mit den bereits bekannten Ansible-Modulen erstelle ich ein Verzeichnis, kopiere dorthin das Skript und verlinke dieses schließlich nach /etc/cron.daily/, so dass es täglich ausgeführt wird. Der Grund dafür ist, dass das Skript nicht nur die Zertifikate erstmals erstellt, sondern diese auch automatisch verlängert, sobald sie kurz vor dem Ablaufdatum stehen. Die Datei setup_certbot_script.yml importiere ich nun noch in die Haupt-Task-Definitionsdatei roles/happy1_haproxy/tasks/main.yml, welche nun wie folgt aussieht:

---
- import_tasks: install_haproxy.yml
- import_tasks: install_certbot.yml
- import_tasks: setup_certbot_script.yml

Ich führe das Playbook aus, um das Certbot-Skript auf dem Server zu installieren:

ansible-playbook --ask-vault-pass happy1.yml
Installation des Certbot-Skripts mit Ansible
Installation des Certbot-Skripts mit Ansible

Ich prüfe auf dem Server, ob das Skript korrekt erstellt wurde:

Das generierte Certbot-Skript auf dem Server
Das generierte Certbot-Skript auf dem Server

Ausführen des Certbot-Skripts

Das Skript sieht gut aus, so dass ich es auch gleich ausführe. Dies muss ich als root-User tun, da nur dieser das Kommando runuser kennt.

Ausführen von Certbot
Ausführen von Certbot

Die Zertifikate wurden erfolgreich generiert – meine Seite ist jedoch noch nicht über HTTPS erreichbar, da ich noch keinen entsprechenden frontend-Abschnitt in der HAProxy-Konfiguration angelegt habe.

Bevor ich das tue, möchte ich meine Neugier befriedigen und ausprobieren, was passiert, wenn ich das Skript erneut aufrufe. Dieses Mal bekomme ich eine knappe, zufriedenstellende Antwort:

Cert not yet due for renewal
Keeping the existing certificate

Aktivierung von HTTPS in HAProxy

Nun werde ich das HTTPS-Frontend erstellen. Da dieses die Zertifikatsdatei benötigt, die von Certbot erstellt wird, wäre HAProxy auf einem frischen Server nicht lauffähig bevor Certbot ausgeführt wurde. Ohne HAProxy wiederum könnte Certbot nicht aufgerufen werden – ein Henne-Ei-Problem. Die Lösung ist, Ansible prüfen zu lassen, ob das Zertifikat existiert und das HTTPS-Frontend nur dann zu erstellen, wenn dies der Fall ist. Die Prüfung erfolgt durch folgenden Task in der Datei roles/happy1_haproxy/tasks/install_haproxy.yml, welcher vor dem Task Configure HAProxy eingefügt wird:

- name: Check if the Certbot certificates exist
stat:
path: /opt/certbot/haproxy_fullchains/__fallback.pem
register: certbot_cert_check_result

Das Ansible-Modul stat habe ich im ersten Artikel der Serie erläutert. Und folgendes wird in die haproxy.cfg.j2 eingefügt, vor backend happycoders_wordpress:

{% if certbot_cert_check_result.stat.exists %}
frontend happycoders_443
bind *:443 ssl crt /opt/certbot/haproxy_fullchains/__fallback.pem crt /opt/certbot/haproxy_fullchains alpn h2,http/1.1

# Important, so that WordPress doesn't send a redirect to HTTPS ("https" must be in lowercase letters!)
http-request set-header X-Forwarded-Proto https

default_backend happycoders_wordpress
{% else %}
# SSL not yet activated because certificates do not exist yet.
# To create the certificates, run:
# sudo /opt/certbot/scripts/run-certbot
# Then execute the playbook again.
{% endif %}

Wenn die Zertifikatsdatei existiert, wird das HTTPS-Frontend eingebunden, ansonsten wird ein Kommentar eingetragen, mit dem ich mich quasi selbst darauf hinweise, dass das Certbot-Skript noch ausgeführt werden muss. Wichtig ist die Zeile http-request set-header X-Forwarded-Proto https – diese fügt einen Header in den HTTP-Request ein, der WordPress darüber informiert, dass die Seite über HTTPS aufgerufen wurde. Ansonsten würde WordPress immer wieder mit einem Redirect auf HTTPS antworten, sofern die in WordPress konfigurierte Seiten-URL mit https:// beginnt.

Ich führe das Ansible-Playbook erneut aus, um die aktualisierte HAProxy-Konfiguration auf den Server zu laden. Da sich an der Ausgabe nichts wesentliches geändert hat, verzichte ich hier auf einen Screenshot. Nach Ausführung des Playbooks ist die Seite über HTTPS erreichbar:

WordPress ist über HTTPS erreichbar
WordPress ist über HTTPS erreichbar

Über die Chrome-Konsole verifiziere ich, dass das HTTP/2-Protokoll verwendet wird:

HappyCoders.eu auf HTTP/2-Protokoll
HappyCoders.eu auf HTTP/2-Protokoll

Umleitung auf https://www.happycoders.eu

Nun möchte ich noch HTTP auf HTTPS umleiten – und alle alternativen HappyCoders-Domains auf www.happycoders.eu. Für die Umleitung von HTTP nach HTTPS reicht eine Zeile Code – ich trage folgendes in der haproxy.cfg.j2 im Frontend happycoders_80 direkt vor default_backend ein:

    # Redirect HTTP to HTTPS for all other cases
redirect scheme https code 301 if !is_certbot

Mit if !is_certbot stelle ich sicher, dass die Challenge-Anfragen des Let’s Encrypt-Servers weiterhin über HTTP erfolgen können. Die Umleitung auf www.happycoders.eu kann ansich ebenfalls über eine Zeile Code erfolgen:

    # Redirect to https://www.happycoders.eu if another host name was specified
redirect prefix https://www.happycoders.eu code 301 if !{ hdr(host) -i www.happycoders.eu }

Allerdings wird es komplizierter, wenn man dabei auch den HTTP Strict Transport Security-Header an den Client zurückliefern möchte. Warum das nicht funktioniert, wenn man die entsprechende Direktive mit in das Frontend einträgt und was man stattdessen machen muss, wird im HAProxy-Blog-Artikel HAProxy and HTTP Strict Transport Security (HSTS) Header in HTTP Redirects erklärt. Zusammengefasst: Das Frontend trägt Response Header nur dann ein, wenn die Antwort von einem Backend kommt – daher wird ein Dummy-Backend benötigt.

Ich trage im Frontend happycoders_443 folgendes ebenfalls direkt vor default_backend ein:

    http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

# Redirect to https://www.happycoders.eu if another host name was specified.
# Need to do this via a dummy backend + frontend as otherwise the HSTS response header would not be sent.
# (see https://www.haproxy.com/de/blog/haproxy-and-http-strict-transport-security-hsts-header-in-http-redirects/)
use_backend redirect_to_www_happycoders_eu if !{ hdr(host) -i www.happycoders.eu }

Mit der ersten Direktive wird der HSTS-Header eingefügt, so dass der Browser in Zukunft per „internal redirect“ auf die HTTPS-Seite umleiten wird, wenn http:// eingegeben wird – ohne überhaupt erst die HTTP-Seite aufzurufen. Die use_backend-Direktive leitet alle Anfragen, die nicht an den Host www.happycoders.eu gehen, an das Dummy-Backend redirect_to_www_happycoders_eu um. Dieses füge ich zusammen mit dem Dummy-Frontend wie folgt vor dem Backend happycoders_wordpress ein:

# This dummy backend is needed so that the HSTS response header is also being sent with a redirect (see comment above).
backend redirect_to_www_happycoders_eu
server dummy_redirect_server 127.0.0.1:8002

# This dummy frontend is needed so that the HSTS response header is also being sent with a redirect (see comment above).
frontend redirect_to_www_happycoders_eu
bind 127.0.0.1:8002
redirect prefix https://www.happycoders.eu code 301

Das Dummy-Backend leitet also auf das Dummy-Frontend um, welches schließlich einen „permanent redirect“ auf den Host www.happycoders.eu zurückliefert. Der dabei entstehende Overhead kann getrost ignoriert werden, da dieser Redirect pro User maximal einmal pro Jahr stattfindet.

Die Datei roles/happy1_haproxy/templates/haproxy.cfg.j2 sieht abschließend wie folgt aus:

global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon

# Default SSL material locations
ca-base /etc/ssl/certs
crt-base /etc/ssl/private

# Default ciphers to use on SSL-enabled listening sockets.
# For more information, see ciphers(1SSL). This list is from:
# https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
# An alternative list with additional directives can be obtained from
# https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=haproxy
ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
ssl-default-bind-options no-sslv3
ssl-dh-param-file /opt/certbot/dhparams.pem

defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http

frontend happycoders_80
bind *:80

# Forward Certbot verification requests to the certbot-haproxy plugin
acl is_certbot path_beg -i /.well-known/acme-challenge
use_backend certbot if is_certbot

# Redirect HTTP to HTTPS for all other cases
redirect scheme https code 301 if !is_certbot

default_backend happycoders_wordpress

{% if certbot_cert_check_result.stat.exists %}
frontend happycoders_443
bind *:443 ssl crt /opt/certbot/haproxy_fullchains/__fallback.pem crt /opt/certbot/haproxy_fullchains alpn h2,http/1.1

# Important, so that WordPress doesn't send a redirect to HTTPS ("https" must be in lowercase letters!)
http-request set-header X-Forwarded-Proto https

http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

# Redirect to https://www.happycoders.eu if another host name was specified.
# Need to do this via a dummy backend + frontend as otherwise the HSTS response header would not be sent.
# (see https://www.haproxy.com/de/blog/haproxy-and-http-strict-transport-security-hsts-header-in-http-redirects/)
use_backend redirect_to_www_happycoders_eu if !{ hdr(host) -i www.happycoders.eu }

default_backend happycoders_wordpress
{% else %}
# SSL not yet activated because certificates do not exist yet.
# To create the certificates, run:
# sudo /opt/certbot/scripts/run-certbot
# Then execute the playbook again.
{% endif %}

# This dummy backend is needed so that the HSTS response header is also being sent with a redirect (see comment above).
backend redirect_to_www_happycoders_eu
server dummy_redirect_server 127.0.0.1:8002

# This dummy frontend is needed so that the HSTS response header is also being sent with a redirect (see comment above).
frontend redirect_to_www_happycoders_eu
bind 127.0.0.1:8002
redirect prefix https://www.happycoders.eu code 301

backend happycoders_wordpress
mode http
server server1 localhost:8001 check
http-response del-header x-powered-by
{% if happycoders_testing|default(false) %}
acl authorized http_auth(happycoders_test_users)
acl wp-cron path_beg -i /wp-cron.php
http-request auth realm HappyCoders.eu unless authorized or wp-cron

userlist happycoders_test_users
user sven password X/UgQ53BouggY
{% endif %}

backend certbot
log global
mode http
server certbot 127.0.0.1:8000

Ich führe das Ansible-Playbook ein letztes Mal aus, um die HAProxy-Konfiguration auf dem Server zu aktualisieren. Auch dieses Mal verzichte ich auf einen Screenshot.

Zum Test rufe ich http://happycoders.de auf und stelle zu meiner Zufriedenheit fest, dass dieser Aufruf letztendlich auf https://www.happycoders.eu umgeleitet wird, d. h. dass sowohl die Umleitung auf HTTPS als auch die auf www.happycoders.eu wie gewünscht funktionieren. Zuletzt prüfe ich noch, ob der HSTS-Header gesetzt und vom Browser korrekt ausgewertet wird.

curl -I https://happycoders.de
Überprüfung des Redirects und des HSTS-Headers mit curl
Überprüfung des Redirects und des HSTS-Headers mit curl
Überprüfung des "internal Redirects" in Chrome
Überprüfung des „internal Redirects“ in Chrome

Man sieht sehr gut, dass der Aufruf von http://www.happycoders.eu vom Browser durch einen „internal redirect“ (Status 307) auf https://www.happycoders.eu und dann vom Server durch einen „temporary redirect“ (Status 302) auf die Seite install.php umgeleitet wurde.

Zusammenfassung und Ausblick

In diesem letzten Teil der Serie habe ich euch gezeigt, wie ich meinen WordPress-Blog mit HAProxy über HTTP und HTTPS erreichbar gemacht habe und wie ich mit Let’s Encrypt/Certbot kostenlose SSL-Zertifikate angefordert und installiert habe. Damit endet diese Artikelserie zur Einrichtung meines Servers.

Ich hoffe euch haben diese Artikelserie und die dazugehörigen Tutorial-Videos gefallen. Ich freue mich sehr, wenn ihr mir Kommentare hinterlasst. Schreibt mir auch gerne, wenn ihr mittendrin irgendwo festhängt. Auch bei mir ist nicht alles so glatt gelaufen wie man meinen könnte, wenn man dieses Tutorial liest. Auch ich musste vieles ausprobieren, Schritte wiederholen, zu vorherigen Abschnitten zurückspringen und sogar einige Male ganz von vorne bei der Provisionierung des Servers anfangen. Dank Ansible war das überhaupt nicht schlimm.

Vielen Dank fürs Dabeibleiben und Happy Coding!

Wenn euch die Artikelserie gefallen habt, würde ich mich auch sehr freuen, wenn ihr diesen Artikel über einen der folgenden Share-Buttons teilt.

Kommentar verfassen

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