/ Let's Encrypt

Let's Encrypt!

Update: Ich habe die hook.sh und die config.sh verschlankt und an die neue version von letsencrypt.sh namens dehydrated angepasst. Die Details findet ihr in einem eigenen Blogpost.


Nachdem Let's Encrypt auch von Uberspace unterstützt zu werden beginnt, und ich ja immer nach Wegen suche, mir das Teilzeitadmin-Leben zu vereinfachen, wollte ich mir die automatische Domain-Verifikation von Let's Encrypt zunutze machen. Klingt ja fantastisch: Einfach per Konfiguration einstellen, für welche Domains man Zertifikate haben möchte, und der Let's-Encrypt-Client kümmert sich um alles. Nie wieder per E-Mail nachweisen, dass mir die Domain macfrog.de immer noch gehört - juhu!

Welchen Client hätten's denn gern?

Allerdings habe ich mit dem Standard-Client so meine Probleme. Das fängt damit an, dass das Teil root-Rechte haben möchte. Ich stimme Jonas da voll zu: Es gibt exakt überhaupt keinen Grund, warum das so sein müsste. Muss es ja auch nicht, wie man an Uberspace sieht. Da funktioniert der Standard-Client auch so.

Trotzdem ist mir das Programm nicht ganz geheuer. Erstens ist mein Python ein wenig... ähm... eingerostet, so dass ich nicht wirklich nachvollziehen kann, was es tut. Und das wüsste ich schon gerne - immerhin geht es hier unter anderem um die privaten Schlüssel zu meinen Website-Zertifikaten. Und außerdem ist mir das Programm für einen Code Review auch ein wenig zu lang.

Ich habe mich also auf die Suche nach einer Alternative gemacht, und bin bei letsencrypt.sh fündig geworden. Das ist ein Shell-Skript, das die wesentlichen Funktionen des Original-Clients, nämlich

  • Let's-Encrypt-Account anlegen
  • Challenge für Domain-Nachweis abwickeln
  • Zertifikat generieren und unterschreiben lassen

genau so gut abwickelt, dabei keine root-Rechte braucht und noch dazu so übersichtlich ist, dass ich mir einigermaßen sicher darüber bin, was es tut.

Allerdings möchte ich nicht verschweigen, dass letsencrypt.sh noch ziemlich in der Entwicklung steckt. Aber wie's aussieht, ist zumindest die Version vom 08.01.2016 ganz schön stabil. Mir ist zumindest noch kein Fehler aufgefallen. Und Let's Encrypt selber ist ja auch noch in der Beta-Phase.

Installation

Also nichts wie los, eine Kopie der Sourcen geholt und die wichtigsten Dateien nach ~/opt/letsencrypt/ kopiert. Das Zielverzeichnis könnt Ihr selbstverständlich frei wählen - ich fand den Platz ganz passend:

cd ~/src
git clone https://github.com/lukas2511/letsencrypt.sh.git
mkdir ~/opt/letsencrypt
cd ~/opt/letsencrypt
cp ~/src/letsencrypt/letsencrypt.sh .
cp ~/src/letsencrypt/config.sh.example config.sh
cp ~/src/letsencrypt/domains.txt.example domains.txt

Jetzt müssen natürlich noch die Konfigurationsdateien angepasst werden:

domains.txt

Hier kommen alle Domainnamen rein, für die Zertifikate generiert werden sollen. Falls eine Domain mit mehreren Unterdomains vertreten ist, bietet es sich an, diese als SAN (Subject Alternate Name) anzugeben. Diese Alternativnamen kommen in eine Zeile hinter den ursprünglichen Domainnamen. Für mich sieht das dann so aus:

macfrog.de blog.macfrog.de www.macfrog.de
mcfrog.de blog.mcfrog.de www.mcfrog.de

Bei Euch sollten selbstverständlich Eure eigenen Domainnamen stehen.

config.sh

Hier müsst Ihr auf jeden Fall die Variable WELLKNOWN anpassen und könnt eine CONTACT_EMAIL-Adresse setzen, wenn Ihr das möchtet:

WELLKNOWN="/var/www/virtual/<username>/html/.well-known/acme-challenge"
CONTACT_EMAIL="<username>@<hostname>.uberspace.de"

Vergesst nicht, die #-Kommentarzeichen zu entfernen, wenn Ihr Werte setzt, sonst werden die Standardwerte verwendet, und das ist ja gerade nicht das, was Ihr wollt.

<username> und <hostname> ersetzt ihr natürlich durch Euren Uberspace-Benutzernamen und den zugehörigen Host. Zumindest was die E-Mail-Adresse angeht, könnt Ihr Euch aber offensichtlich eine beliebige Adresse aussuchen. Das oben ist nur der Wert, den der Standard-Client setzt.

Auch wenn es sich hier so einfach liest, muss ich zugeben, dass mir WELLKNOWN ein bisschen Kopfzerbrechen bereitet hat: Eigentlich wollte ich analog der Anleitung von auf GitHub den Apache von Uberspace per .htaccess dazu bringen, den Pfad http://macfrog.de/.well-known/acme-challenge/ auf ein passendes Verzeichnis umzubiegen. Aber egal, was ich mit .htaccess anstellte - ich konnte keine Inhalte ausspielen. Bis mir klar wurde, dass Uberspace anscheinend tatsächlich wie von Jonas - zunächst nur als Konzept - beschrieben die Zugriffe für jede eingetragene Domain eines Benutzers vor dem Apache abfängt und grundsätzlich aus /var/www/virtual/<username>/html/.well-known/acme-challenge bedient.

Das ist einfach und elegant, weil der Client nur ein konstantes Verzeichnis für die ACME-Challenge bedienen muss, egal welche Domain oder Subdomain überprüft werden soll. Kein Hantieren mit unterschiedlichen Konfigurationen für Subdomains. Hab' ich schon geschrieben, dass es einfach Spaß macht, bei Uberspace zu sein? :-)

Automatische Zertifikatsinstallation

Bis hierhin funktioniert alles ähnlich wie der Standard-Client. letsencrypt.sh weist aber noch zwei Besonderheiten auf, die es für einen Einsatz auf Uberspace geradezu prädestinieren: Es überprüft, ob Zertifikate in den nächsten 30 Tagen ablaufen und fordert nur diese neu an. Soweit ich sehe, kann das der Standard-Client (noch) nicht. Selbstverständlich ist die Zeit bis zum Verfallsdatum einstellbar. Und es kann ein Skript aufrufen, sobald ein Zertifikat erfolgreich generiert und unterschrieben wurde.

Das schreit doch geradezu danach, an dieser Stelle einen Umweg über uberspace-prepare-certificate zu machen. Zumal die Zertifikatsinstallation bei Uberspace seit neuestem vollautomatisch funktioniert und keine E-Mail an den Support mehr benötigt. Und genau diesen kleinen Umweg nehmen wir jetzt auch.

Wie Ihr vielleicht schon in der config.sh gesehen habt, gibt es den Parameter HOOK:

# Program or function called in certain situations
#
# After generating the challenge-response, or after failed challenge (in this case altname is empty)
# Given arguments: clean_challenge|deploy_challenge altname token-filename token-content
#
# After successfully signing certificate
# Given arguments: deploy_cert domain path/to/privkey.pem path/to/cert.pem path/to/fullchain.pem
#
# BASEDIR and WELLKNOWN variables are exported and can be used in an external program
# default: <unset>
HOOK="${BASEDIR}/hook.sh"

Die ersten zwei Fälle sind uns relativ egal, aber der dritte Fall ist spannend. Wie Ihr seht, bekommt das HOOK-Script ein Argument übergeben, in welcher Situation es aufgerufen wurde. Uns interessiert hier der Fall deploy_cert, bei dem dann unter anderem die Pfade zum privaten Schlüssel und zum Zertifikat als weitere Parameter angegeben werden. Also genau das, was uberspace-prepare-certificate braucht.

hook.sh

Wir legen daher noch folgendes Skript unter ~/opt/letsencrypt/ (bzw. Eurem Installationsverzeichnis) ab:

#!/bin/env bash
#============================================================================
#
# DESCRIPTION
#    This script hooks into letsencrypt.sh and deploys generated
#    certificates to Uberspace.
#
# COMMANDS
#    deploy_challenge  Deploys challenge to system for ACME CA to query
#                      Currently not implemented. Might be used in future
#                      for dns-01 challenge type.
#
#    clean_challenge   Removes previously deployed challenge
#                      Currently not implemented.
#
#    deploy_cert       Deploys generated certificate to Uberspace
#
# EXAMPLES
#    hook.sh deploy_cert example.com privkey.pem cert.pem fullchain.pem
#
# IMPLEMENTATION
#    version         0.0.1
#    author          Thorsten Köster
#    copyright       Copyright (C) Thorsten Köster
#    license         GNU General Public License v3.0
#
#============================================================================
#
#  HISTORY
#     2016/01/09 : tkoester : Script creation
#
#============================================================================

if [[ ! $# -ge 1 ]] ; then
  echo "No arguments given." >&2
  exit 2
fi

case "${1}" in
  deploy_challenge)
    exit 0
    ;;
  clean_challenge)
    exit 0
    ;;
  deploy_cert)
    shift 1
    if [[ ! $# -ge 3 ]] ; then
      echo "Insufficient number of arguments." >&2
      exit 2
    fi
    uberspace-prepare-certificate -k ${2} -c ${3}
    if [[ $? = "0" ]] ; then
      exit 0
    else
      exit 1
    fi
    ;;
  *)
    printf "Unknown command: ${1}" >&2
    exit 2
    ;;
esac

Das Skript unterscheidet zunächst mal anhand des ersten Arguments, in welcher Phase es gerade aufgerufen wurde. deploy_challenge und clean_challenge ignorieren wir vorerst und tun so, als wäre die Ausführung erfolgreich gewesen. Bei deploy_cert prüfen wir, ob wir mindestens drei weitere Parameter übergeben bekommen haben und rufen dann entsprechend uberspace-prepare-certificate auf. Der Rest ist dann nur noch Fehlerbehandlung.

Natürlich müsst Ihr das Skript noch ausführbar machen:

chmod u+x hook.sh

Und vergesst nicht, die config.sh azupassen, falls Ihr das nicht schon oben getan habt:

HOOK="${BASEDIR}/hook.sh"

Voller Service

Jetzt können wir natürlich auch noch das Zertifikats-Renewal voll automatisieren, entweder per Cronjob oder per runwhen. Da ich schon bisher alle sich wiederholenden Tätigkeiten auf Uberspace per runwhen erledige, habe ich mir einen entsprechenden Service angelegt:

runwhen-conf ~/etc/run-letsencrypt ~/opt/letsencrypt/letsencrypt.sh

Die Datei ~/etc/run-letsencrypt/run müssen wir noch ein wenig anpassen. Zum einen müssen wir natürlich noch festlegen, wann der Job laufen soll. Ich habe mich für einmal wöchentlich am Freitag, pünktlich zum Feierabend entschieden. Dann kann ich übers Wochenende versuchen, die Fehler zu beheben, falls was schief ging:

RUNWHEN=",w=5,H=16"

Zum anderen benötigt letsencrypt.sh noch den Parameter -c für Cron, um alle Zertifikate auf den neuesten Stand zu bringen. Änderungen an der domains.txt sollten damit ebenso abgearbeitet werden wie neue Zertifikate 30 Tage vor Ablauf der alten angefordert werden. Diese Zeitspanne lässt sich übrigens auch in der config.sh mit dem Parameter RENEW_DAYS einstellen.

Vollständig sieht meine ~/etc/run-letsencrypt/run nun so aus:

#!/bin/sh -e

RUNWHEN=",w=5,H=16"

# many tools and programming languages need these e.g. to find user-installed modules
export USER=macfrog
export HOME=/home/macfrog

exec 2>&1 \
rw-add n d1S now1s \
rw-match \$now1s $RUNWHEN wake \
sh -c '
  echo "@$wake" | tai64nlocal | sed "s/^/next run time: /"
  exec "$@"' arg0 \
rw-sleep \$wake \
/home/macfrog/opt/letsencrypt/letsencrypt.sh -c

Dann noch der übliche Symlink nach ~/service und den Job einmal starten:

ln -s ~/etc/run-letsencrypt ~/service/letsencrypt
svc -a ~/service/letsencrypt

Et voilà - die Zertifikate mitsamt Schlüssel für Eure Domains aus domains.txt sollten sich jetzt in ~/opt/letsencrypt/certs/<domainname>/ befinden und außerdem bereits bei Uberspace eingetragen sein. Und wenn die Automatik funktioniert, müsst Ihr Euch nie wieder um eine Erneuerung kümmern.

tl;dr

Wenn Ihr das hier auch in drei Monaten noch lesen könnt, dann hat das, was ich oben beschrieben habe, höchstwahrscheinlich funktioniert.