Letsencrypt ohne rootrechte und (halb) automatisiert

Seit einiger Zeit bietet die Initiative letsencrypt.org nun kostenlose SSL-Zertifikate an und nachdem sich die anfängliche Verwirrung über die benötigten Rechte des ACME-Clients gelegt haben, sieht das ganze sogar halbwegs brauchbar aus. Was liegt also näher, das ganze mal auszuprobieren – eigentlich nicht viel.

Was ist letsencrypt?

Bei letsencrypt handelt es sich um den Versuch, die Transportverschlüsselung beim Abruf von Webseiten stärker zu verbreiten, also den Abruf von Webseiten statt über unverschlüsselte HTTP-Verbindungen über das verschlüsselte HTTPS-Protokoll abzuwickeln. Damit können weder der eigene Internetprovider noch der Anbieter und andere Nutzer offener WLANs mitlesen, wenn man die so gesicherten Webseiten aufruft.

Randnotiz: solange man kein VPN benutzt werden die DNS Anfragen weiterhin unverschlüsselt übertragen, auch bei DNSSEC, und man kann zumindest die Namen der Webseiten doch wieder mitlesen. Immerhin bleiben aber die Inhalte und die konkreten Unterseiten privat, so dass man zwar erkennt dass die Wikipedia angesurft wurde, aber nicht welche Artikel konkret gelesen wurden.

Self-signed Zertifikate versus Hochsicherheitszertifizierungsstellenzertifizierung

Das Problem dabei ist nun, dass sich zwar jede(r) die zum Verschlüsseln benötigten Schlüsselpaare selbst erzeugen kann, aber eine ‘Beglaubigung’ dieser Schlüssel durch eine Zertifizierungsstelle notwendig ist. Beglaubigt man die Schlüssel selbst und erzeugt ein self-signed Zertifikat, kommt es beim Aufruf der Webseite zu unterschiedlich drastischen Warnmeldungen des Browsers, da dieser kein Zertifikat einer ihm bekannten Zertifizierungsstelle vorfindet – obwohl die Daten an sich verschlüsselt übertragen werden.

Der Hintergrund dabei ist, dass ein Angreifer sich nicht selbst die passenden Zertifikate für die Webseite einer Bank ausstellen können soll, um sich in die Verbindung zwischen einem Bankkunden und seine Bank dazwischenzuschalten und die Zugangsdaten zum Onlinebanking trotz Verschlüsselung abgreifen kann (Man-in-the-Middle Angriff).

Die Zertifizierungsstellen lassen sich diesen Dienst allerdings gut bezahlen, außerdem ist zu beobachten, dass viele dieser Certification Autorities, oder kurs CAs, ihren Prüfpflichten nicht nachkommen oder gar gehackt wurden.

In der Praxis beschränkte sich die Prüfung ohnehin oft auf das Zusenden eines Fax mit einem Firmenkopf und einem Rückruf bei der im Briefkopf genannten Telefonnummer.

Vor einigen Jahren wurde dann CaCert gegründet, eine CA die kostenlose Zertifikate ausstellt und bei der Freiwillige die Identität der Antragssteller prüfen. Das Problem dabei ist jedoch, dass aktuell kein gängiger Browser dieser CA vertraut, da auch die Browserhersteller mitverdienen wollen und sich die Aufnahme in die Liste der vertrauenswürdigen CAs ebenfalls bezahlen lassen.

Bei letsencrypt hat man sich einfach gesagt nun dafür entschieden, dieses Geld durch Sponsoren zusammenzubekommen und in einem automatisierten Prozess kostenlos Zertifikate auszustellen. Diese sind nur knapp 3 Monate gültig, lassen sich aber sehr einfach erneuern und automatisch aktualisieren.

Alles sudo oder was?

Kurz nachdem letsencrypt die ersten öffentlichen Betatests seiner Software durchgeführt hat, kam es jedoch zu einiger Verwirrung und starken Kommentaren gegen den Einsatz der von letsencrypt vorgeschlagenen Lösung.

Hintergrund ist, dass letsencrypt es ermöglichen möchte, ohne jegliche tiefere Sachkenntnis Zertifikate für die eigene Domain auszustellen. Die Zielgruppe scheinen hier insbesondere die Kunden von Hostern zu sein, die dort einen rootserver mieten und gerne ihre Webseite(n) per HTTPS anbieten wollen.

Die von letsencrypt zunächst hauptsächlich beschriebene Lösung sah dabei vor, dass eine Reihe von scripten, die unter dem root-user ausgeführt werden müssen, auf einem Standardsystem die Webserverkonfiguration anpassen, die API von letsencrypt bedienen, SSL-Keys erzeugen und alles auf magische Art und voll automatisch erledigen.

Dies kommt aus mehreren Gründen für mich und viele andere nicht in Frage, da es letztlich das Vertrauen auf eine Blackbox ist und man die Kontrolle verliert, was da auf dem eigenen Server eigentlich vorgeht. Von Sicherheitslücken in den Scripten abgesehen ist das ein mulmiges Gefühl. Hinzu kommt, dass für die Aufgabe ein SSL-Zertifikat zu beantragen eigentlich keinerlei Rootrechte notwendig sein sollten.

Für mich ergibt sich noch ein weiteres Problem, bei meinen Systemen handelt es sich schlicht nicht um default-Installationen.

Meine hier vorgestellte Lösung kommt im laufenden Betrieb komplett ohne erweiterte Rechte aus, alle Scripte laufen unter einem getrennten Benutzer und man benötigt nur beim initialen Konfigurieren eines Vhosts für letsencrypt Rootrechte.

Voraussetzungen

Wir kurz angedeutet laufen meine Systeme in der Regel nicht in der default Konfiguration. Ich benutze als Webserver den Apache – die weiter unten vorgestellten Scripte lassen sich aber einfach anpassen, da die Zertifikate lediglich im letzten Schritt in die Apache Konfigurationsverzeichnisse kopiert werden.

Zur Konfiguration des Apache benutze ich eine Reihe von Makros, die mir das Konfigurieren neuer Domains doch erheblich vereinfachen, diese werden im folgenden Abschnitt kurz erklärt, danach folgt die Erklärung meines letsencrypt Setups unter Verwendung des acme-tiny clients (https://github.com/diafygi/acme-tiny).

Die im folgenden vorgestellte Apache Konfiguration ermöglicht nicht nur das schnelle Aufsetzen neuer VHosts, sie ermöglicht auch das einfache automatisierte Erneuern der Zertifikate.

Apache konfigurieren

Da die meisten der bei mir gehosteten Domains in eine von sehr wenige Kategorien fallen, z.B. statische Seite, WordPress Instanz oder Proxy-Frontend, habe ich diese Fälle durch Makros in der Apache-Konfiguration abgebildet. Dadurch lassen sich derartige Seiten mit nur sehr wenigen Einträgen in der Apache-Konfiguration aufsetzen. Dazu benötigt wird mod_macro, unter Debian im Paket libapache2-mod-macro zu finden. Danke an Seba an dieser Stelle, der mich irgendwann mal mit mod_macro bekannt gemacht hatte.

Um die einzelnen Seiten voneinander zu trennen wird zudem für jeden Kunden ein eigener Systemnutzer angelegt und mittels itk, aus dem debian-Paket apache2-mpm-itk, dafür gesorgt, dass alle Seiten eines Kunden unter diesem Nutzer laufen. Auch hier gilt mein Dank wieder Seba.

Damit die verschiedenen Virtual Hosts sich trotz unterschiedlicher Zertifikate eine IP-Adresse teilen, kommt zudem SNI zum Einsatz, bei dem der Server je nach Hostnamen dem Client unterschiedliche Zertifikate liefern kann. Dies wird seit einiger Zeit gut unterstützt, nur einige eher historische Browser kommen damit nicht zurecht, bisher habe ich aber keine Beschwerden gehört und meine Antwort wäre ein Hinweis auf Grund gravierender Sicherheitslücken den Browser mal zu erneuern.

Im Folgenden ein  Auszug aus der Apache Konfiguration, zusammengetragen aus mehreren Dateien in /etc/apache2/conf.d/

##########
# Logging
##########
<Macro log $host>
 ErrorLog /var/log/apache2/error_$host.log
 CustomLog /var/log/apache2/access_$host.log combined
</Macro>

<Macro logssl $host>
 ErrorLog /var/log/apache2/error_ssl_$host.log
 CustomLog /var/log/apache2/access_ssl_$host.log combined

 <IfModule log_config_module>
 CustomLog /var/log/apache2/ssl_request_$host.log \
 "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
 </IfModule>
</Macro>
##########
# SSL
##########
<Macro ssl_defaults>
 SSLEngine on
 <FilesMatch "\.(cgi|shtml|phtml|php)$">
 SSLOptions +StdEnvVars
 </FilesMatch>
 <IfModule setenvif_module>
 BrowserMatch ".*MSIE.*" \
 nokeepalive ssl-unclean-shutdown \
 downgrade-1.0 force-response-1.0
 </IfModule>
</Macro>

<Macro ssl-sni $certname>
 # Zertifikate basierend auf dem certname Parameter laden:
 use ssl_defaults
 SSLCertificateFile /etc/apache2/sni/$certname/certificate.pem
 SSLCertificateKeyFile /etc/apache2/sni/$certname/key.pem
 SSLCertificateChainFile /etc/apache2/sni/$certname/chain.pem
</Macro>
##########
# WordPress
##########
<Macro wordpress_site_itk $domain $webmaster $customer>
 ServerName $domain
 ServerAdmin $webmaster

 ServerSignature Off
 HostnameLookups Off

 php_value memory_limit 64M
 php_value upload_max_filesize 20M
 php_admin_value allow_url_fopen Off
 php_admin_value expose_php Off
 php_admin_flag safe_mode Off
 php_admin_value open_basedir /srv/www/$customer/$domain:/usr/share/pear:/usr/share/php5:/usr/share/php

 DocumentRoot /srv/www/$customer/$domain/htdocs

 #itk:
 AssignUserID $customer $customer
 MaxClientsVHost 128
 NiceValue 12

 <Directory />
 Options -FollowSymLinks -Indexes
 AllowOverride None
 </Directory>

 <Directory /srv/www/$customer/$domain/>
 Options -Indexes +FollowSymLinks
 AllowOverride All
 Order allow,deny
 allow from all
 </Directory>

 <Location "/xmlrpc.php">
 # super Einfallstor um an den Captchas vorbeizukommen,
 # die rpc nutzt lokal hier niemand, also kann sie weg
 order allow,deny
 deny from all
 allow from none
 ErrorDocument 403 "Access denied."
 ErrorDocument 404 "Access denied."
 </Location>

 Use common_$domain

</Macro>

<Macro wp_itk_sni $domain $customer $certname>
 <VirtualHost *:80>
 Use log $domain
 Use wordpress_site_itk $domain webmaster@$domain $customer

 RedirectMatch permanent ^/wp-login(.*)$ https://$domain/wp-login$1
 RedirectMatch permanent ^/wp-admin$ https://$domain/wp-admin/
 RedirectMatch permanent ^/wp-admin/(.*)$ https://$domain/wp-admin/$1
 </VirtualHost>

 <VirtualHost *:443>
 Use ssl-sni $certname
 Use logssl $domain
 Use wordpress_site_itk $domain webmaster@$domain $customer
 </VirtualHost>
</Macro>

Wie sieht nun eine typische Webseiten-Konfiguration für WordPress aus?

Hier eine exemplarische sites-enabled/streibelt.de:

<Macro common_streibelt.de>
    ServerAlias www.streibelt.de
    RewriteEngine on
    RewriteCond %{HTTPS} !=on
    RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]
</Macro>

Use wp_itk_sni streibelt.de fls_web letsencrypt/streibelt.de

Das ist doch recht übersichtlich…

Der Trick besteht darin, dass die globalen Makros dynamisch ein Seiten-spezifisches Makro aufrufen, in dem man dann wieder Spezialfälle konfigurieren kann, wie hier die Umleitung auf https.

Zertifikate erstellen

Im Gegensatz zu anderen Zertifizierungsstellen setzt letsencrypt darauf, dass die Zertifikate über eine API beantragt und heruntergeladen werden – optimiert also darauf dass sich der Vorgang automatisieren lässt und ohne menschliche Interaktion auskommt. Man muss also nicht Certification Signing Requests (CSR) manuell in ein Webformular pasten und bekommt das Ergebnis nicht per mail sondern man muss eine Software benutzen, die das sigenannte ACME-Protokoll von letsencrypt spricht.

Ein Problem dabei stellte wie erwähnt jedoch der zuerst veröffentlichte Client dar, der zuviel automatisch und vor allem mit Rootrechten machen wollte.

acme-tiny

Glücklicherweise gab es recht schnell einen Client der sich auf die absolut minimale Funktionalität beschränkt: Signing-Request hochladen, Challenge-Response Verfahren zur Verifikation durchführen, Zertifikat runterladen. Zu finden ist er auf github.com/diafygi/acme-tiny und eine Beschreibung mit einem minimalen Beispiel findet sich hier: www.metachris.com/2015/12/comparison-of-10-acme-lets-encrypt-clients/

Zusammen mit einem kleinen Shellscript das weiter unten zu finden ist und der oben vorgestellten Apache-Konfiguration lassen sich so schnell und einfach Zertifikate beantragen und erneuern.

Verifikation der Domainherrschaft

Um sicherzustellen, dass nur eine Person mit Kontrolle über die betreffende Webseite ein SSL-Zertifikat beantragen kann, sieht letsencrypt vor, dass eine Datei mit einem von letsencrypt vorgegebenen Inhalt unter einer bestimmten URL abrufbar sein muss. Diese wird vom acme-client automatisch angelegt und mit Inhalt befüllt.

Zunächst habe ich einen eigenen Benutzer für letsencrypt angelegt und diesem ein schreibbares Verzeichnis in /srv/www/acme erzeugt, wo das Script die für die Verifikation der Domain durch letsencrypt notwendigen Dateien ablegen kann. Das Verzeichnis gehört diesem Benutzer und der Gruppe und ist world readable (mode 755) , so dass auch der Webbrowser unabhängig vom itk-user die Daten lesen kann. Da die Daten nicht vertraulich sind stellt dies kein Problem dar.

Dieses Verzeichnis muss nun unter der jeweiligen Webseite abrufbar sein, dazu bediene ich mich der Alias Anweisung der Apachekonfiguration und dem speziellen common Makro aus der oben vorgestellten Konfiguration. Die Webseite wird nun also wie folgt konfiguriert:

<Macro common_streibelt.de>
 ServerAlias www.streibelt.de
 RewriteEngine on
 RewriteCond %{HTTPS} !=on
 RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]

 # Falls frontend-proxy, pfad lokal behandeln:
 # ProxyPass /.well-known/acme-challenge/ !

 Alias /.well-known/acme-challenge/ /srv/www/acme/streibelt.de/
</Macro>

Use wp_itk_sni streibelt.de fls_web letsencrypt/streibelt.de

Achtung: Existiert das Verzeichnis nicht, wird der Apache beim restart oder reload zu Recht ein wenig beleidigt sein.

Der Vollständigkeit ist (auskommentiert) auch die Anweisung aufgeführt um den Pfad nicht an den Backend-Server weiterzuleiten, sollte der Apache als Reverse-Proxy zum Einsatz kommen.

Ich empfehle, das wp_itk_sni Makro zu einem wp_itk_sni_le zu erweitern, welches diesen Alias von sich aus mitbringt.

Zertifikate beantragen

Das folgende Script kann nun verwendet werden um für einen Virtual Host mit einem oder mehreren Aliasen Zertifikate zu erstellen und, sollten sie nur noch 30 Tage gültig sein, automatisch zu erneuern. Das Script ist ein erster Anfang und enthält sicher noch den ein oder anderen Haken – hat heute aber schon seinen Dienst gut erfüllt.

#!/bin/bash
set -e

if [ "$(whoami)" != "leuser" ]; then
 echo "error: only call me as leuser"
 exit 3
fi

accountkey="/home/leuser/certs/account.key"

# muss initial von der Webseite heruntergeladen werden und
# gegebenenfalls aktualisiert werden:
# wget -O - https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.pem  > /home/leuser/certs/chain.pem
certchain="/home/leuser/certs/chain.pem"

# Basisconfig - default reicht in der Regel
sslconfig="/etc/ssl/openssl.cnf"

if [ "$#" -lt 1 ]; then
 echo "usage: $0 domain [subdomain] [subdomain] ..."
 exit 1
fi

# Ist der Hostname im DNS?
for fqdn in $@; do
 echo "resolving $fqdn"
 result="$(dig +short "$fqdn")"
 if [ "$result" == "" ]; then
 echo "error resolving $fqdn"
 exit 2
 fi
done


domain="$1"
shift 1
subdomains="$@"

acmedir="/srv/www/acme/$domain"
certdir="/home/leuser/certs/$domain"
outdir="/etc/apache2/sni/letsencrypt/$domain"

ts="$( date +'%Y%m%d.%H%M%S' )"

domainkey="$certdir/key.pem"
domaincsr="$certdir/certrequest.$ts.pem"
signedcrt="$certdir/certificate.$ts.pem"
crtlatest="$certdir/certificate.pem"

echo " Domain: $domain"

sanconfig=""

if [ "$subdomains" != "" ]; then
 # we need to create a openssl config
 sanconfig=`mktemp`
 trap "rm -f $sanconfig" EXIT
 echo " sub: $subdomains"
 echo " config: $sanconfig"

 cat "$sslconfig" > "$sanconfig"
 echo >> "$sanconfig"

 echo "[SAN]" >> "$sanconfig"
 echo -n "subjectAltName=DNS:$domain" >> "$sanconfig"

 while [ "$1" != "" ]; do
 echo -n ",DNS:$1" >> "$sanconfig"
 shift 1
 done
 echo >> "$sanconfig"
 echo >> "$sanconfig"

 cp $sanconfig /tmp/san
 
fi

echo "SSL Key: $domainkey"
echo " CSR: $domaincsr"
echo " CRT: $signedcrt"
echo " CRT: $crtlatest"

delta=-1

if [ -f "$crtlatest" ]; then

 expire="$(openssl x509 -enddate -noout -in "$crtlatest" | cut -d '=' -f 2 )"
 expiresec="$(date -d "$expire" +%s)"
 now="$(date +%s)"
 
 ((delta=($expiresec-$now)/60/60/24))

 echo "existing certificate expires in $delta days"
 
fi

if [ "$delta" -gt 30 ]; then
 echo "not renewing certificate, nothing to do here"
 exit 0
fi


if [ ! -d "$certdir" ]; then
 echo "creating directory for certificates"
 mkdir -v -p "$certdir" || exit 3
fi

if [ ! -d "$outdir" ]; then
 echo "creating directory for webserver certificates"
 mkdir -v -p "$outdir" || exit 3
fi

if [ ! -e "$accountkey" ]; then
 # Generate a private key
 echo "generating new account key for letsencrypt"
 openssl genrsa 4096 > "$accountkey"
fi


if [ ! -d "$acmedir" ]; then
 # Create the challenge folder in the webroot
 echo "creating the challenge folder in the webroot"
 mkdir -v -p "$acmedir" || exit 3
 echo "please configure your webserver to serve that dir now"
cat<<EOF
 ProxyPass /.well-known/acme-challenge/ !
 Alias /.well-known/acme-challenge/ /srv/www/acme/$domain/
EOF
 exit 1
fi



if [ ! -e "$domainkey" ]; then
 # Generate a domain private key (if you haven't already)
 echo "creating new key for webserver"
 openssl genrsa 4096 > "$domainkey"
else
 echo "re-using existing key for webserver"
fi


if [ "$subdomains" != "" ]; then
 # Create a CSR for domain and www.domain
 openssl req -new -sha256 -key "$domainkey" -subj "/" -reqexts SAN -config "$sanconfig" > "$domaincsr"
else
 # Create a simple CSR 
 openssl req -new -sha256 -key "$domainkey" -subj "/CN=$domain" > "$domaincsr"
fi

# Get a signed certificate with acme-tiny
python /home/leuser/acme-tiny/acme_tiny.py --account-key "$accountkey" --csr "$domaincsr" --acme-dir "$acmedir/" > "$signedcrt"

# Sicherstellen, dass das Zertifikat sich als solches parsen lässt,
# Leider kann das pythonscript das Zertifikat nur auf stdout ausgeben!

openssl x509 -in "$signedcrt" -text | grep "Not After :"

if [ ! "$?" -eq 0 ]; then
 echo "Certificate is not valid!"
 exit 5
fi

# updating local copy

cp "$signedcrt" "$crtlatest"


echo "copying files to webserver"
set -x
cp "$crtlatest" "$outdir/certificate.pem"
cp "$domainkey" "$outdir/key.pem"
cp "$certchain" "$outdir/chain.pem"
set +x

echo "done"

Warnung

Eine Warnung sei noch zu den Beispielen für den Aufruf von acme_tiny.py ausgesprochen. Wie auch im obigen Script zu sehen, wird das Zertifikat auf der Standardausgabe ausgegeben und nicht in eine Datei geschrieben. Es kann dadurch der Fall eintreten, dass statt eines Zertifikats dort Fehlermeldungen zu finden sind. Zu allem Überfluss lenken die von letsencrypt vorgegebenen Beispiele die Ausgabe direkt in die von den Webservern genutzten Zertifikatsdateien um und überschreiben diese potentiell mit ungültigen Daten – zum Beispiel wenn das Limit der an einem Tag erstellbaren Zertifikate überschritten oder die Internetverbindung gestört ist.

Ein Problem ergab sich, als die acme-url von einem CMS gefangen wurde und der Client beim Prüfen der Konfiguration statt der Verifizierungsdaten eine Webseite mit Umlauten erhielt – dabei stürzte er nämlich ab und schrieb statt eines Zertifikats einen Stacktrace in die Datei.

Das hier vorgestellte Script prüft daher zunächst ob die Datei ein valides Zertifikat enthält und überschreibt das alte Zertifikat nur wenn das neue Zertifikat verständlich aussieht.

Anwendung

Obiges Script kann sowohl genutzt werden um initial die Zertifikate zu beantragen als auch um sie regelmäßig zu erneuern, da die Zertifikate nur etwa 90 Tage gültig sind. Damit wird erreicht, dass keine Zertifikate in fremden Händen verbleiben, wenn die Kontrolle über eine Domain übertragen wird, z.B. durch Verkauf oder Kündigung.

Der Aufruf ist dabei jeweils derselbe und kann so auch in einem cronjob verwendet werden:

/home/leuser/bin/makecert.sh streibelt.de www.streibelt.de

Ich empfehle den Aufruf des scripts – der erste Parameter ist später der Name des Zertifikats – sofort in einem weiteren script zu sichern, ich beabsichtige einfach alle diese Aufrufe untereinander in einem Script namens update.sh zu sammeln und dann regelmäßig auszuführen. Die Zeit wird zeigen, ob das so funktionieren wird oder die Updates besser über den Zeitraum verteilt werden sollten.

Aktuell bleibt bei einem Fehlschlag noch eine Frist von 30 Tagen um das Zertifikat doch noch erneuert zu bekommen.

Fazit

Bis auf ein paar Kinderkrankheiten im Client macht letsencrypt mittlerweile einen recht brauchbaren Eindruck – und dank des minimalen Clients kann man es auch ohne Bauchschmerzen benutzen. Ob die regelmäßige Erneuerung der Zertifikate so auf Dauer funktioniert wird sich zeigen, aber es ist endlich wirklich einfach möglich Webseiten mit kostenlosen SSL-Zertifikaten auszustatten ohne Besucher durch Warnmeldungen zu verwirren oder eine eigene CA zu betreiben.

Nachtrag

Ich wurde darauf angesprochen, dass das insgesamt doch noch recht komplex anmutet. Ich hätte den Artikel vielleicht aufteilen sollen – einmal die von letsencrypt unabhängige Konfiguration des Apachen und dann die Änderungen  – Für mein laufendes System beschränkte sich die Umstellung auf letsencrypt darauf ein neues Makro zu definieren, einen Alias in die Definition des VHosts einzutragen sowie das shellscript aufzurufen mit dem die Zertifikate beantragt werden. Und das ist im Gegensatz zu den unübersichtlichen Webseiten kommerzieller CAs ein riesen Fortschritt – keine Copy&Paste Orgien mehr und es lässt sich bei der Einrichtung einer neuen Webseite komplett automatisieren.

Posted in apache, de, Linux, server, Software, Tipps