Automatisierter Zertifikatsimport

Ihr könnt (und sollt!) bei uns ja nun schon lange eigene TLS-Zertifikate für den Webserver benutzen, jedoch war das bisher immer mit einer Mail an den Support verbunden, nach der wir das Zertifikat dann einbinden. Wir haben das bisher nicht etwa so gehandhabt, weil wir zu faul waren, uns etwas anderes zu überlegen oder weil wir euch ärgern wollten, das hatte schlicht sicherheitstechnische Hintergründe: Die Zertifikate werden in unser HTTPS-Frontend Pound eingepflegt und das betrachten wir als außerordentlich kritische Infrastruktur, bei der wir so wenig Angriffsvektoren wie möglich öffnen wollen. Da es ja nun aber doch so aussieht, als ob die freie Verfügbarkeit von TLS-Zertifikaten für jeden direkt vor der Tür steht und wir dies soweit wie möglich automatisieren wollen, haben wir uns mal ein paar Gedanken gemacht, wie das mit dem Zertifikatsimport (so weit wie möglich) ohne unser Zutun ablaufen kann.

Wenn ihr diesen Artikel lest, ist die Wahrscheinlichkeit, dass ihr bereits eigene TLS-Zertifikate benutzt, recht hoch und ihr habt sicherlich schon mal mit unserem uberspace-prepare-certificate-Skript zu tun gehabt:

[wiebke@amnesia ~]$ uberspace-prepare-certificate -k /home/wiebke/tls/unverschluesselter-key.pem -c /home/wiebke/tls/certificate.pem 
🔑  Key seems valid, moving on...
📝  Certificate seems valid, moving on... (step by step)
🔐  Certificate matches key, moving on... (we are getting there!)
📜  Magically getting intermediate certificate(s) if there are any needed... (hold on tight)
🔐  The certificate validates against our ca-bundle, awesome! Moving on...
🚀  All good!

(Das sah bis vor kurzem noch etwas anders aus, hat unterm Strich aber das gleiche gemacht)

Da TLS-Zertifikate (und OpenSSL im allgemeinen) für viele Menschen (verständlicherweise) pures Voodoo sind, haben wir da schon vor längerer Zeit maximal viele Checks eingebaut, damit so wenig wie möglich schief gehen kann. Im speziellen sind das:

  1. Wir prüfen, ob der Key verschlüsselt ist und entschlüsseln ihn ggf.
  2. Wir prüfen, ob der Key und Zertifikate im PEM-Format vorliegt.
  3. Wir prüfen, ob Key und Zertifikat überhaupt zusammenpassen.
  4. Neuerdings kümmern wir uns um die Zwischenzertifikate, in dem wir die Authority Information Access extension nutzen und die Zertifikate einfach herunterladen und einbetten.
  5. Wir prüfen, ob das Zertifikate gegen das Certificate Authority bundle von CentOS validiert, also, ob das Zertifikat von einer vertrauenswürdigen Certificate Authority stammt (hierüber lässt sich natürlich streiten, das soll aber nicht Inhalt dieses Artikels sein).

Das Problem Nummer 1

Diese Überprüfung haben wir bisher mit direkt per openssl verify erledigt, was aber nicht dazu in der Lage ist, zu meckern, wenn beispielsweise zu viele Zertifikate im vorher gebauten Bundle vorhanden sind. (Außerdem lässt sich OpenSSL auf der Kommandozeile wirklich wahnsinnig schlecht bedienen, gibt vollkommen wirre Exitcodes aus und ich kenne nicht wenig Menschen, die einen ausdauernden Zahnarztbesuch oder eine Operation der Nasenscheidewand der Arbeit mit OpenSSL bevorzugen würden.) GnuTLS bringt das hübsche certtool mit, das aber das gleiche Problem hat. Es ist nicht pingelig genug, was die Validierung der Zertifikate angeht, auch hier kann man Zertifikate so bauen, dass das certtool zwar nicht meckert und alles für gut befindet, ein Browser aber durchaus meckern würde. Das passierte zwar nur selten, wenn wir den Import nun aber vollkommen automatisieren wollen, geht das so nicht.

Die Lösung für Problem Nummer 1

Die erste Idee, die wir dazu hatten, schien etwas over-the-top: Einen temporären Webserver mit einer minimalen Konfiguration auf deinem Unix-Socket starten, das Zertifikat dort einbinden, mit curl einen Request auf den Socket starten und schauen, ob der Rückgabewert in Ordnung ist.

Die Sache ist... Wir hatten keine andere Idee und hinzu kommt, dass wir damit ein real life-Szenario abbilden können. Also, was soll's, so ist es dann eben ab jetzt und da wir das ganze über einen nginx-Webserver testen, ist das komplette Skript, was den Test erledigt, inklusive der Serverkonfiguration gerade ein mal 100 Zeilen lang.

Und so sieht's auf der Shell aus:

🌍  temporary webserver started...
✅  certificate is valid.
🌍  killed temporary webserver...

Damit hätten wir das Problem der Certificate Chain also erledigt, zumindest bei Zertifikaten, die gegen das Certificate Authority bundle von CentOS validieren. Alles andere können (und wollen) wir auch nicht automatisieren, hier werdet ihr uns also auch in Zukunft eine Mail schicken müssen, damit wir einen zweiten Blick auf die Angelegenheit werfen und das Zertifikat dann von Hand importieren. Dies gilt insbesondere für selbst signierte Zertifikate und Zertifikate von CAcert.

Problem Nummer 2: Die Sicherheit

Es ist heikel, Dateien als root aus Userverzeichnissen zu lesen. Man kann über Symlinks stolpern, race conditions können auftreten, ..., kurz gesagt: Das wollen wir nicht, das muss ja auch irgendwie anders gehen.

Die Lösung für Problem Nummer 2

Intern haben wir den automatisieren Import also nun in drei Schritte aufgeteilt und nur der erste, der im Kontext des Users - also euch - gestartet wird, liest Dateien direkt aus dem Dateisystem ein.

  1. /usr/local/bin/uberspace-prepare-certificate macht die ersten Checks und übergibt - wenn alles glatt geht - das Zertifikat per Pipe an ...
  2. ... /usr/local/bin/uberspace-check-certificate, was per sudo als User certtest ausgeführt wird und den temporären Webserver startet und das Zertifikat via curl mit einem Request auf den Socket validiert. Und nur wenn das glatt geht, gehts (wieder per Pipe) weiter an ...
  3. ... /usr/local/sbin/uberspace-import-certificate, welches nur vom User certtest aufgerufen werden kann, was letztendlich das Zertifikat importiert und dem HTTPS-Frontend, welches wir benutzen, Bescheid gibt, doch bitte mal innerhalb der nächsten 5 Minuten die Konfiguration neu zu lesen.

Und so sieht's aus, wenn man alle Puzzleteile zusammen setzt:

[wiebke@amnesia ~]$ uberspace-prepare-certificate -k key.key -c cert.crt 
🔑  Found key...
📝  Found certificate...
🔑  Key seems valid, moving on...
📝  Certificate seems valid, moving on... (step by step)
🔐  Certificate matches key, moving on... (we're getting there!)
📜  Magically getting intermediate certificate(s) if there are any needed... (hold on tight)
🔐  Checking for wiebke.org.
🌍  temporary webserver started...
✅  certificate is valid.
🌍  killed temporary webserver...
🚀  All good! Your new certificate will be live within the next five minutes.

Problem Nummer 3: CentOS 5

Wir müssen nun leider Zöpfe abschneiden und sagen: Das klappt nicht auf alten Uberspaces unter CentOS 5. Die Gründe sind vielfältig, einerseits nutzen wir dort kein einheitliches HTTPS-Frontend, anderseits kompilieren aktuelle GnuTLS-Versionen nicht (was wir nach wie für für den Import nutzen um z.B. Debuginformationen auszugeben, wenn ein Zertifikat nicht validiert).

Die Lösung für Problem Nummer 3

... gibt es nicht. Hier müsst ihr uns also leider nach wie vor eine Mail schreiben (oder am besten gleich auf einen aktuellen Host wechseln).

... und jetzt?

Und jetzt warten wir mal ab, wie oft der offizielle Start von Let's Encrypt noch verschoben wird (scnr), unser Handwerkszeug liegt auf jeden Fall bereit.

Frohes Importieren eurer Zertifikate!