Eine der diversen Möglichkeiten, sich beim Betrieb eines Webservers gnadenlos ins Knie zu schießen, heißt HTTP Public Key Pinning oder kurz HPKP. Diese Erweiterung erlaubt es, für jede Domain einer Webseite exakt festzulegen, welche Public Keys in einem für diese Seite verwendeten Zertifikat verwendet werden dürfen, und welche nicht. Soweit, so langweilig!
Denn so schön einfach diese Idee klingt, so viel Seil liefert einem der definierende Standard, um sich gemütlich damit zu erhängen. Zwar zwingt einen RFC 7469 ein Backup für seine Schlüssel vorzuhalten, was bei ausreichender Verpeilung und dank ungetesteter Restore-Prozeduren aber reichlich Spielraum für Fehler und Chaos aller Art lässt. Vergisst man also seine Ersatz-Schlüssel, oder kann wegen Bugs in der Schlüssel-Generierung auf einen Schlag keinen seiner Ersatzschlüssel nutzen, bleibt einem lediglich der Katastrophe ihren Lauf zu lassen. Fail-Safe ist was für Weicheier!
Und damit das Chaos auf dem Server auch garantiert gut dokumentiert werden kann, ist die Nutzung verschiedener Zertifizierungspfade Grundvoraussetzung. Das erschwert einem zwar die Selbstverstümmelung beim In-Den-Fuß-Schießen, forciert aber bei der Gelegenheit, dass man schon bei der zweiten Aktualisierung versehentlich den Live-Pin aus dem Header löscht und seine Website mit Leichtigkeit unzugreifbar macht.
Doch, leichter geht dies mit Automatisierung, was sich bei nginx erschreckend unkompliziert konfigurieren lässt. Schmeißen wir hierzu in die globale SSL-Konfiguration eine simple Aufforderung zum Hinzufügen eines Headers:
add_header Public-Key-Pins $sslcfg_cert_hpkp;
Die Variable $sslcfg_cert_hpkp ist hierbei der Name einer Map, die auf dem $ssl_server_name, also dem Wert der SNI-Extension basiert, und den angeforderten Hostname enthält. Doch wer jetzt denkt, diese Map lässt sich in wenigen Augenblicken per Hand generieren, sollte seine LPIC-1-Zertifizierung überprüfen und ein kleines Script in die Konsole hacken:
#!/bin/bash
CFG=/etc/nginx/conf.d/sslcfg_cert_hpkp
function list_domains() {
for a in /etc/ssl/public/*.crt; do
openssl x509 -in "$a" -text -noout -certopt no_subject,no_header,no_version,no_serial,no_signame,no_validity,no_subject,no_issuer,no_pubkey,no_sigdump,no_aux | \
awk '/X509v3 Subject Alternative Name:/','/X509v3 Basic Constraints:/' | \
grep -A1 "X509v3 Subject Alternative Name:" | \
grep -v "X509v3 Subject Alternative Name:"
done | \
tr , "\n" | \
grep DNS: | \
cut -d: -f2 | \
grep -v '\*\.' | \
sort -u
}
function list_hpkp() {
for a in /etc/ssl/public/*.crt; do
openssl x509 -in "$a" -text -noout -certopt no_subject,no_header,no_version,no_serial,no_signame,no_validity,no_subject,no_issuer,no_pubkey,no_sigdump,no_aux | \
awk '/X509v3 Subject Alternative Name:/','/X509v3 Basic Constraints:/' | \
grep -A1 "X509v3 Subject Alternative Name:" | \
grep -v "X509v3 Subject Alternative Name:" | \
tr ", " "\n" | \
grep -q "^DNS:$1\$" && echo "$a"
done | \
sort -u
}
function gen_hpkp() {
for a in $(list_hpkp "$1"); do
[ $(date +%s "--date=$(openssl x509 -in "$a" -noout -dates|cut -d= -f2|head -1)") -lt $(date +%s) ] && \
[ $(date +%s "--date=$(openssl x509 -in "$a" -noout -dates|cut -d= -f2|tail -1)") -gt $(date +%s) ] && \
openssl x509 -pubkey -noout -in "$a" | \
openssl rsa -pubin -inform pem -outform der | \
openssl dgst -sha256 -binary | \
base64
done |
xargs -i printf '; pin-sha256=\\"%s\\"' {}
}
echo 'map "$ssl_server_name" $sslcfg_cert_hpkp {' > $CFG
echo ' default "";' >> $CFG
for b in $(list_domains 2>/dev/null); do
printf " \"%s\" \"max-age=3600%s\";\n" "$b" "$(gen_hpkp "$b" 2>/dev/null)"
done >> $CFG
echo '}' >> $CFG
Einmal Execute-Rechte setzen; einmal ausführen und einmal nginx restarten. Fertig.
Nunja, nicht ganz: Damit das funktioniert, müssen ALLE Zertifikate unter /etc/ssl/public liegen – inklusive derer, die aktuell nicht verwendet werden, sondern als Backup dienen. Aber das sollte für einen geübten Admin ja machbar sein.
P.S.: Jede Domain muss zu jedem Zeitpunkt mindestens 2 gültige Zertifikate haben. Mehr schaden in der Regel nicht …