Seit vielen Jahren verwende ich Let’s Encrypt-Zertifikate für meine Webserver. Zum Ausstellen der Zertifikate habe ich in den Anfangszeiten das Kommando certbot
genutzt. Weil die Installation dieses Python-Scripts aber oft Probleme bereitete, bin ich schon vor vielen Jahren auf das Shell-Script acme.sh
umgestiegen (siehe https://github.com/acmesh-official/acme.sh).
Kürzlich bin ich auf einen Sonderfall gestoßen, bei dem acme.sh
nicht auf Anhieb funktioniert. Die Kurzfassung: Ich verwende Apache als Proxy für eine REST-API, die in einem Docker-Container läuft. Bei der Zertifikatausstellung/-erneuerung ist Apache (der auf dem Rechner auch als regulärer Webserver läuft) im Weg; die REST-API liefert wiederum keine statischen Dateien aus. Die Domain-Verifizierung scheitert. Abhilfe schafft eine etwas umständliche Apache-Konfiguration.
Ausgangspunkt
Ausgangspunkt ist also ein »gewöhnlicher« Apache-Webserver. Dieser soll nun zusätzlich eine REST-API ausliefern, die in einem Docker-Container läuft (localhost:8880). Die erste Konfiguration sah ziemlich simpel aus:
# zusätzliche Apache-Proxy-Konfigurationsdatei
# für einen Docker-Container
<VirtualHost *:443>
ServerName api.example.com
# SSL
SSLEngine on
SSLCertificateFile /etc/acme-letsencrypt/api.example.com.pem
SSLCertificateKeyFile /etc/acme-letsencrypt/api.example.com.key
# Proxy: localhost:8880 <-> https://api.example.com
ProxyPreserveHost On
ProxyPass / http://localhost:8880/
ProxyPassReverse / http://localhost:8880/
# Logging Konfiguration ...
</VirtualHost>
# HTTP -> HTTPS
<VirtualHost *:80>
ServerName api.example.com
Redirect permanent / https://api.example.com
</VirtualHost>
Das Problem
Das Problem besteht darin, dass acme.sh
zwar diverse Domain-Verifizierungsverfahren kennt, aber keines so richtig zu meiner Konfiguration passt:
acme.sh ... --webroot
scheitert, weil die API eine reine API ist und keine statischen Dateien ausliefert.acme.sh ... --standalone
scheitert, weil der bereits laufende Webserver Port 80 blockiert.acme.sh ... --apache
scheitert mit could not resolve api.example.com.well-known.
Die Lösung
Die Lösung besteht darin, die Apache-Proxy-Konfiguration dahingehend zu ändern, dass zusätzlich in einem Verzeichnis statische Dateien ausgeliefert werden dürfen. Dazu habe ich das neue Verzeichnis /var/www/acme-challenge
eingerichtet:
mkdir /var/www/acme-challenge
chown www-data:www-data /var/www/acme-challenge/
Danach habe ich die Konfigurationsdatei für Apache umgebaut, so dass Anfragen an api.example.com/.well-known/acme-challenge
mit statischen Dateien aus dem Verzeichnis /var/www/acme-challenge/.well-known/acme-challenge
bedient werden:
# Apache-Konfiguration wie bisher
<VirtualHost *:443>
ServerName api.example.com
# SSL
SSLEngine on
SSLCertificateFile /etc/acme-letsencrypt/api.example.com.pem
SSLCertificateKeyFile /etc/acme-letsencrypt/api.example.com.key
# Proxy: localhost:8880 <-> api.example.com
ProxyPreserveHost On
ProxyPass / http://localhost:8880/
ProxyPassReverse / http://localhost:8880/
# Logging Konfiguration ...
</VirtualHost>
# geändert: HTTP auf HTTPS umleiten, aber nicht
# für well-known-Verzeichnis
<VirtualHost *:80>
ServerName api.example.com
# Handle ACME challenges locally
Alias /.well-known/acme-challenge /var/www/acme-challenge/.well-known/acme-challenge
<Directory /var/www/acme-challenge/.well-known/acme-challenge>
Require all granted
</Directory>
# Redirect everything EXCEPT ACME challenges to HTTPS
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge/
RewriteRule ^(.*)$ https://api.example.com$1 [R=301,L]
</VirtualHost>
Nach diesen Vorbereitungsarbeiten und mit systemctl reload apache2
gelingt nun endlich das Zertifikaterstellen und -erneuern mit dem --webroot
-Verfahren. Dabei richtet acme.sh
vorübergehend die Datei /var/www/acme-challenge/.well-known/acme-challenge/xxx
ein und testet dann via HTTP (Port 80), ob die Datei gelesen werden kann.
# Zertifikat erstmalig erstellen
acme.sh --issue --server letsencrypt -d api.example.com --webroot /var/www/acme-challenge
# Zertifikat installieren und Renew-Prozess einrichten
acme.sh --install-cert -d api.example.com \
--key-file /etc/acme-letsencrypt/api.example.com.key \
--fullchain-file /etc/acme-letsencrypt/api.example.com.pem \
--reloadcmd "service apache2 reload"
# Renew-Prozess testen
acme.sh --renew -d api.example.com --force
Traefik
Eine noch elegantere Lösung besteht darin, den Docker-Container mit Traefik zu kombinieren (siehe https://traefik.io/traefik/). Bei korrekter Konfiguration kümmert sich Traefik um alles, nicht nur um die Proxy-Funktionen sondern sogar um das Zertifikatsmanagment. Aber diese Lösung kommt nur in Frage, wenn auf dem Host nicht schon (wie in meinem Fall) ein Webserver läuft, der die Ports 80 und 443 blockiert.
Eine weitere Option wäre, eine DNS-Challenge durchzuführen: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert#4-dns-api-mode
Damit braucht man keinen Webserver und acme.sh unterstützt über 170 DNS-Anbieter: https://github.com/acmesh-official/acme.sh/wiki/dnsapi
Durch die DNS-Challenge ist zudem ein Wildcard (z.B. *.kofler.info) Zertifikat möglich.