HTTPS, HSTS und HPKP auf einem Uberspace

Ich habe in letzter Zeit mal ein bisschen herum experimentiert, was Webseitenverschlüsselung angeht.

So ein Uberspace ist ja schon toll. Man kann da sogar eigene Domains aufschalten. Aber Seiten nur über HTTP aufzurufen, ist ja schon ziemlich unsicher. Jeder Angreifer, der auch nur passiv den Datenverkehr mitschneiden kann, sieht alles.

Was kann man dagegen tun?

Die Lösung (oder jedenfalls ein Teil davon) lautet:

HTTPS

Ja, genau. Laut Wiki von Uberspace funktioniert HTTPS ja standardmäßig per https://benutzername.host.uberspace.de/ - aber diese URL sieht ja schon irgendwie blöd aus.

Für alles andere braucht man ein eigenes Zertifikat. Aber das ist nicht schwierig oder kompliziert. Am einfachsten (und kostenlos!) sind Zertifikate von Let‘s Encrypt.

Zum Einrichten muss man einmal Folgendes ausführen:

ytvwld@betelgeuse ~> uberspace-letsencrypt

We have now created the following config file for you:

  /home/ytvwld/.config/letsencrypt/cli.ini

Please review especially the domain list we have generated for you
and adapt the 'domains' setting if you want to make some changes:

  asteroid.ytvwld.de
  www.ytvwld.de
  ytvwld.de
  [...]

When you're done you can use the official Let's Encrypt client by executing:

  letsencrypt certonly

You can safely ignore all 'Root (sudo) is required to run most of letsencrypt functionality' warnings.

Evtl. wichtig hierbei: Die erste Domain hier wird als Hauptdomain für das neue Zertifikat genommen. Wenn man da was ändern möchte, also einfach den entsprechenden Abschnitt ~/.config/letsencrypt/cli.ini bearbeiten:

[...]

Beware that Let's Encrypt does NOT support wildcard hostnames.
# If you're using wildcards you have to add each subdomain explicitly.
domains = asteroid.ytvwld.de,[...],www.ytvwld.de,ytvwld.de

[...]

Dann kann man auch schon das Zertifikat erstellen lassen:

ytvwld@betelgeuse ~> letsencrypt certonly
[...]
IMPORTANT NOTES:
 - If you lose your account credentials, you can recover through e-mails sent to [...].
 - Congratulations! Your certificate and chain have been saved at   /home/ytvwld/.config/letsencrypt/live/asteroid.ytvwld.de/fullchain.pem.
   Your cert will expire on 2016-05-03. To obtain a new version of the certificate in the future, simply run Let's Encrypt again.
 - Your account credentials have been saved in your Let's Encrypt configuration directory at /home/ytvwld/.config/letsencrypt.  You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained   by Let's Encrypt so making regular backups of this folder is ideal.

Dabei auftretende Fragen einfach so beantworten, wie man es gerne hätte. Nun hat man u.a. einen privaten Schlüssel und ein Zertifikat:

ytvwld@betelgeuse ~> ls -l ~/.config/letsencrypt/live/asteroid.ytvwld.de/
insgesamt 4
lrwxrwxrwx. 1 ytvwld ytvwld   42  3. Feb 20:18 cert.pem -> ../../archive/asteroid.ytvwld.de/cert5.pem
lrwxrwxrwx. 1 ytvwld ytvwld   43  3. Feb 20:18 chain.pem -> ../../archive/asteroid.ytvwld.de/chain5.pem
lrwxrwxrwx. 1 ytvwld ytvwld   47  3. Feb 20:18 fullchain.pem -> ../../archive/asteroid.ytvwld.de/fullchain5.pem
lrwxrwxrwx. 1 ytvwld ytvwld   45  3. Feb 20:18 privkey.pem -> ../../archive/asteroid.ytvwld.de/privkey5.pem

Jetzt muss man noch den Webserver so einrichten, dass der die auch benutzt:

ytvwld@betelgeuse ~> uberspace-add-certificate -k ~/.config/letsencrypt/live/asteroid.ytvwld.de/privkey.pem -c ~/.config/letsencrypt/live/asteroid.ytvwld.de/cert.pem
🔑  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 asteroid.ytvwld.de.
🌍  temporary webserver started...
✅  certificate is valid.
🌍  killed temporary webserver...
🚀  All good! Your new certificate will be live within the next five minutes.

Wenn man noch mal nachgucken möchte, ob das tatsächlich geklappt hat:

ytvwld@betelgeuse ~> uberspace-list-certificates                                     
common name: asteroid.ytvwld.de
issuer: Let's Encrypt Authority X1
valid until: 2016-05-03 20:17:00 CEST
will be removed in 89 days.
alternative name: www.ytvwld.de
alternative name: ytvwld.de
alternative name: asteroid.ytvwld.de
[...]

So, jetzt sollte die Seite auch über die eigene Domain per HTTPS aufrufen lassen.

Aber was bringt uns das jetzt eigentlich?

Wenn Leute die HTTPS-Version der Seite aufrufen, können Angreifer, die einfach nur passiv den Datenverkehr mitschneiden, nicht mehr sehen, was tatsächlich übertragen wird.

Was fehlt noch?

Die wenigsten Leute werden absichtlich die HTTPS-Version aufrufen.

HTTPS erzwingen

Dazu kann man sie aber ganz einfach zwingen. Man muss nur die .htaccess (für die entsprechende Seite - oder für alle Seiten: /var/www/virtual/benutzername/.htaccess) anpassen:

[...]

RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteCond %{ENV:HTTPS} !=on
RewriteRule .* https://%{SERVER_NAME}%{REQUEST_URI} [R=301,L]

[...]

Was hat das jetzt gebracht?

Besucher können die Seite nun nicht mehr unverschlüsselt aufrufen. Passives Mitschneiden des Traffics ist nun also zwecklos.

Was fehlt noch?

Angreifer, die aktiv die Daten verändern können, können z.B. mit SSLstrip eingreifen. Wenn ich da jetzt mal das Diagramm klaue:

Benutzer  <== HTTP ==>  Angreifer  <== HTTPS ==>  Website

Wir haben nun erzwungen, dass die rechte Seite HTTPS sein muss. Toll, aber das reicht nicht. Der Browser des Clients muss wissen, dass unsere Seite nur über HTTPS aufgerufen werden kann / soll / darf / muss.

HSTS

HTTP Strict Transport Security bietet dies teilweise.

Dazu einfach noch mal die .htaccess bearbeiten und Folgendes einfügen:

[...]

header always set Strict-Transport-Security "max-age=31556926 ; includeSubDomains"

[...]

Das bedeutet: Diese Seite (und alle ihre Subdomains) soll ein Jahr lang (356,25 Tage) nur noch über HTTPS aufgerufen werden.

Was hat das jetzt gebracht?

Wenn ein Client im letzten Jahr einmal eine erfolgreiche verschlüsselte Verbindung zu unserer Seite aufgebaut hatte, dann wird er ein Jahr lang auch nur solche akzeptieren.

Was fehlt noch?

Für den ersten Verbindungsaufbau bringt uns das nichts.

HSTS preloaden

Aber wir können unsere Seite einer Liste mit vielen HSTS-Seiten hinzufügen, die mehrere Browser verwenden.

Dazu einfach noch mal die .htaccess bearbeiten und das preload ergänzen:

[...]

header always set Strict-Transport-Security "max-age=31556926 ; includeSubDomains ; preload"

[...]

Nun einfach noch unsere Domain auf dieser Seite eingeben.

Was hat das jetzt gebracht?

Alle großen Browser wissen nun, dass unsere Seite nur via HTTPS aufrufbar ist. Dies schützt uns und Besucher vor Angreifern, die den Traffic beliebig manipulieren und mitschneiden können, aber kein gültiges Zertifikat für unsere Domain haben.

Was fehlt noch?

Angreifer, die über ein gültiges Zertifikat für unsere Domain verfügen (was eigentlich nie passieren sollte!), können sich gegenüber Besuchern als wir und gegenüber uns als Besucher ausgeben - also:

Benutzer  <== HTTPS ==>  Angreifer  <== HTTPS ==>  Website

HPKP

Mit HTTP Public Key Pinning können wir festlegen, welche Zertifikate gültig für unsere Seite sind. Eine ausführlich Anleitung findet sich hier.

Ein Problem dabei ist, dass unsere Zertifikate von Let’s Encrypt nur drei Monate gültig sind und jedesmal den privaten Schlüssel wechseln. Die zu pinnen, wird also schwierig.

Aber wir können ja einfach Let’s Encrypt als CA pinnen. Deren Hash ist YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=. (Dies kann man hier herausfinden oder, indem man das Zertifikat speichert und dann openssl x509 -in Let\'sEncryptAuthorityX1.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 ausführt.)

Zusätzlich brauchen wir noch Backups. Hierzu bietet es sich an, auf mehreren Rechnern (mindestens zwei) Folgendes zu tun:

  1. Einen privaten Schlüssel erstellen: openssl genrsa -out backup.key 4096
  2. Einen CSR erstellen: openssl req -new -key backup.key -config openssl.conf -batch -out backup.csr
  3. Hashen: openssl req -pubkey < backup.csr | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64

Meine verwendete openssl.conf ist diese (sie ist allerdings nicht für Let’s Encrypt geeignet wegen den Wildcards):

#Quellen: https://svn.cacert.org/CAcert/Software/CSRGenerator/csr und http://apetec.com/support/GenerateSAN-CSR.htm

[ req ]
distinguished_name      = req_distinguished_name
encrypt_key             = no
req_extensions = v3_req # Extensions to add to certificate request

[ req_distinguished_name ]
commonName = Common Name (eg, YOUR name)
commonName_default = asteroid.ytvwld.de
commonName_max          = 64

[ v3_req ]
subjectAltName=@alt_names

[ alt_names ]
DNS.1 = *.asteroid.ytvwld.de
DNS.2 = *.ytvwld.de
DNS.3 = ytvwld.de

Jetzt haben wir drei (oder mehr) Hashes.

pin-sha256="YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=";
pin-sha256="3UrfkYs2Fysa5tb+I/pe6cjOqPphiKM2G0U0GaXNcYY=";
pin-sha256="0Iq1O1lr5s01CpdFH9DNucq5U3KUpa3L5j3x+3CahgU=";

HPKP soll auch für Subdomains gelten:

includeSubdomains;

Wie viele Sekunden soll das alles gelten?

max-age=31536000;

Es wäre auch schön, wenn es Benachrichtigungen gäbe, wenn die Verbindung manipuliert wird - report-uri.io bietet so etwas an:

report-uri="https://ytvwld.report-uri.io/r/default/hpkp/enforce"

Und damit haben wir unseren HPKP-Header auch schon fertig:

pin-sha256="YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=";
pin-sha256="3UrfkYs2Fysa5tb+I/pe6cjOqPphiKM2G0U0GaXNcYY=";
pin-sha256="0Iq1O1lr5s01CpdFH9DNucq5U3KUpa3L5j3x+3CahgU=";
includeSubdomains;
max-age=31536000;
report-uri="https://ytvwld.report-uri.io/r/default/hpkp/enforce"

Den müssen wir jetzt noch Apache beibringen. Also die .htaccess bearbeiten und Folgendes einfügen:

[...]

header always set Public-Key-Pins 'pin-sha256="YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg="; pin-sha256="3UrfkYs2Fysa5tb+I/pe6cjOqPphiKM2G0U0GaXNcYY="; pin-sha256="0Iq1O1lr5s01CpdFH9DNucq5U3KUpa3L5j3x+3CahgU="; includeSubdomains; max-age=31536000; report-uri="https://ytvwld.report-uri.io/r/default/hpkp/enforce"'

[...]

Was hat das jetzt gebracht?

Nur Zertifikate von Let’s Encrypt oder mit unseren Backup-Keys sind für die Domain noch gültig.

Was fehlt noch?

Automatische Verlängerung.

Automatische Verlängerung

Das folgende Skript sollte das regeln:

#!/usr/bin/env python3

from time import sleep
from random import randint
from subprocess import Popen, PIPE
import os
import sys

#sleep for a random time
sleep(randint(0, 6000))

#get the configuration folder
letsencrypt_folder = os.path.join(os.environ["HOME"], os.path.join(".config", "letsencrypt"))

hostname = "asteroid.ytvwld.de"
print ("Frage neues Zertifikat an...")
Popen(["letsencrypt", "--verbose", "--non-interactive", "--renew-by-default", "--agree-tos",  "--expand", "certonly"]).wait()
print ("Tausche Zertifikat aus...")
folder = os.path.join(os.path.join(letsencrypt_folder, "live"), hostname)
print ("(importiere aus {}...)".format(folder))
Popen(["uberspace-add-certificate", "-k", os.path.join(folder, "privkey.pem"), "-c", os.path.join(folder, "cert.pem")]).wait()

Dies kann man als ~/bin/certificate-renewal.py speichern, ausführbar machen und monatlich in cron einstellen.

Was hat das jetzt gebracht?

Unsere Zertifikate und Schlüssel werden regelmäßig erneuert.

Was fehlt noch?

Natürlich hilft das alles nichts, wenn jemand Zugriff auf den Server bekommt.

Das Ergebnis

… findet man im SSL-Test.


Kommentare

Die eingegebenen Daten und der Anfang der IP-Adresse werden gespeichert. Die E-Mail-Adresse wird für Gravatar und Benachrichtungen genutzt, Letzteres nur falls gewünscht. - Fragen oder Bitte um Löschung? E-Mail an (mein Vorname)@ytvwld.de.