Virtualisierter Server - SSL überall mit Let's Encrypt, verteilt durch Puppet
In diesem Beitrag beschreibe ich, wie ich die Kommunikation vieler Dienste untereinander und nach außen mit SSL-Zertifikaten abzusichern plane. Die Zertifikate sollen von Let’s Encrypt stammen und durch Puppet auf die Hosts verteilt werden. Die Verifikation der Domain wird dabei nicht über http gemacht, man muss also keine Ports umleiten oder Webserver installieren. Wir verwenden dazu eine DNS-Challenge.
Bisherige Beiträge in der Reihe:
- Puppet Server aufsetzen
- pfsense-Firewall zur Einteilung des Netzwerks mit ipv4 und ipv6
- IPv6-Vorüberlegungen
- Hardware-Setup und Proxmox
Voraussetzungen
Bisher waren die meisten Beiträge in der Reihe relativ unabhängig von Voraussetzungen. Mit steigender Komplexität erwachsen jetzt aber einige Dinge, die gegeben sein sollten:
- Funktionierendes Setup von Puppet Server und Puppet DB.
- Existierende Domain - ich habe mir für meine interne Infrastruktur eine Domain genommen. Die muss gültig sein und nicht frei erfunden, damit man dafür valide SSL-Zertifikate von Let’s Encrypt bekommt.
- Korrekt verteilte FQDNs auf den Hosts. Jede VM sollte also über
eine URL wie
puppet.domain.tld
erreichbar sein. - Möglichkeit, DNS-Einträge über eine API vorzunehmen. Bei mir (HostEurope) geht das nicht, also wird ein Workaround beschrieben.
Kurze Klarstellung noch: Was an DNS-Einträgen global im Internet gepflegt wird und was intern in der Firewall gepflegt wird, muss dabei nicht das gleiche sein. Die Hosts müssen also nicht von außen erreichbar sein - nicht einmal ihre DNS-Einträge müssen sich von außen auflösen. Die Verifikation der DNS-Einträge läuft über andere Records.
DNS braucht eine API
Um die DNS-Verifikation automatisiert ablaufen lassen zu können, müssen Änderungen am DNS der verwendeten Domain über eine API irgendeiner Art möglich sein. Bei meiner Domain, die ich bei HostEurope registriert habe, ist das nicht möglich. Ich tausche daher die Nameserver meiner Domain gegen die von CloudFlare aus.
Einrichtung bei CloudFlare
Der Betrieb einer Domain bei CloudFlare bietet verschiedene Features, darunter auch Dinge wie Ausfallsicherheit und DDoS-Schutz. Ich brauche allerdings nur die DNS-API davon. In seiner Grundfassung ist das Angebot von CloudFlare obendrein auch noch kostenfrei, was die Sache sehr viel einfacher aufzusetzen macht.
Wie genau man die Domain bei CloudFlare anlegt und die NameServer-Einträge auf die von CloudFlare umleitet, kann an dieser Stelle nicht beschrieben werden; die Methode unterscheidet sich bei jedem Anbieter. Wirklich schwierig ist es jedoch nicht: Man nimmt in den Einstellungen der Domain einfach die Änderung vor, dass die Nameserver von CloudFlare, die man aus der Doku dort erhält, verwendet werden. Zeigt einem CloudFlare im Account dann an, dass alles in Ordnung ist, kann es weiter gehen.
Verwendete Software
Wir verwenden einige neue Softwarekomponenten für das Setup:
- dehydrated als Client für Let’s Encrypt
- Das Puppet-Modul bzed-letsencrypt in Form eines Forks von mir
- Als Hook für dehydrated ein Python-Script, was bei CloudFlare die DNS-Verifikation durchführt
Die muss man jetzt nicht alle herunterladen, das machen wir im Laufe des Artikels dann.
Noch zum Fork: Das verwendete Puppet-Modul hatte einige Schwierigkeiten mit einem Update von dehydrated und erlaubte keine Angabe zusätzlicher Einstellungen - in meinem Fall keine Übergabe von Zugangsdaten für CloudFlare. Ich habe das Modul also geforked und die notwendigen Änderungen vorgenommen. Die habe ich als pull requests auch wieder upstream geschickt, bisher wurde es aber noch nicht upstream aufgenommen.
Implementierung
Wir erstellen hier ein Setup, das sowohl auf den einzelnen Hosts die Einbindung des Puppet-Moduls erfordert als auch auf dem Puppet Server. Die einzelnen Hosts erstellen ihre privaten Schlüssel und CSRs (Zertifikatsanfragen) lokal und exportieren letztere als exported resources an den Puppet Server (deswegen brauchen wir auch unbedingt die Puppet DB!). Dieser kümmert sich um die Verifikation der angefragten Domains und die Beantragung der Zertifikate bei Let’s Encrypt. Als jeweilige Domain wird einfach der FQDN des einzelnen Hosts genommen. Das Setup taugt also für Infrastruktur-Server und -Clients
- möchte man etwa noch eine Menge vHosts auf einem Webserver betreiben o.Ä., muss man das von diesem Setup getrennt machen.
Auf dem Puppet Server
Ich gehe hier davon aus, dass der Puppet Server den FQDN
puppet.domain.tld
hat und in der
hiera.yaml
eine Direktive enthalten ist, anhand
derer nodes/%{trusted.certname}.yaml
eingebunden
wird.
Installieren der Voraussetzungen
Wer r10k o.Ä. verwendet, kann hier natürlich auch eine Puppetfile schrieben, wir machen es in der Kommandozeile. Auf dem Puppet Server geben wir ein:
sudo /opt/puppetlabs/bin/puppet module install camptocamp-openssl
sudo /opt/puppetlabs/bin/puppet module install puppetlabs-stdlib
sudo /opt/puppetlabs/bin/puppet module install puppetlabs-concat
sudo /opt/puppetlabs/bin/puppet module install puppetlabs-vcsrepo
sudo git clone https://github.com/very-emmazing/bzed-letsencrypt /etc/puppetlabs/code/modules/bzed-letsencrypt
Konfiguration des Puppet Servers
Wer schon ein Modul profiles
pflegt (um das
Roles/Profiles
Pattern
umzusetzen), kann einfach ein neues Manifest anlegen, ansonsten legen
wir jetzt ein neues Modul profiles
an. Ich gebe
die Befehle an, um das ganze live umzusetzen, was natürlich eine total
dumme Idee ist. Weil ich aber nicht weiß, wie genau Ihr Euren
Puppet-Code auf den Puppet Server bringt (hoffentlich verwendet Ihr
irgendwo git), müsst Ihr diese allgemeinen Anweisungen halt auf Euer
eigenes Setup übersetzen.
sudo mkdir -p /etc/puppetlabs/code/modules/profiles/{files,manifests,data}
In die Datei hiera.yaml
des Moduls schreiben wir
nur was kurzes:
# Datei /etc/puppetlabs/code/modules/profiles/hiera.yaml
---
version: 5
defaults:
datadir: data
data_hash: yaml_data
hierarchy:
- name: Common
path: 'common.yaml
Dort können wir Standardeinstellungen für unser neues Modul hinterlegen. Das ist insofern sinnvoll, als dass wir erst auf die production-API von Let’s Encrypt umsteigen wollen, wenn wir alles auf staging getestet haben.
Nun schreiben wir das Profil sslmaster in Puppet-Code:
# Datei /etc/puppetlabs/puppet/code/modules/profiles/manifests/sslmaster.pp
# Set up cert management on puppet master
class profiles::sslmaster {
$letsencrypt_ca = lookup('profiles::sslmaster::ca')
$cloudflare_acc = lookup('profiles::sslmaster::cloudflare_acc')
$cloudflare_token = lookup('profiles::sslmaster::cloudflare_token')
$python_packages = [
['cffi' , '1.5.0'],
['cryptography' , '1.2.3'],
['dnspython' , '1.15.0'],
['enum34' , '1.1.2'],
['future' , '0.15.2'],
['idna' , '2.0'] ,
['ipaddress' , '1.0.16'],
['ndg-httpsclient' , '0.4.0'],
['pyasn1' , '0.1.9'],
['pyOpenSSL' , '17.3.0'],
['requests' , '2.9.1'],
['pycparser' , '2.14'],
['six' , '1.10.0'],
['tld' , '0.7.6'],
['urllib3' , '1.22'],
['wheel' , '0.28.0'],
]
$python_packages.each |$package| {
package { $package[0]:
ensure => $package[1],
name => $package[0],
provider=> 'pip',
}
}
class { 'letsencrypt':
letsencrypt_ca = $letsencrypt_ca,
hook_source = 'puppet:///modules/profiles/dnshook.py',
hook_env = ['CF_EMAIL=${cloudflare_acc}', 'CF_TOKEN=${cloudflare_token}'],
manage_packages = false, # auf true stellen, falls sonst nirgendwo git installiert wird!
}
}
Was passiert da? Der Reihe nach:
- Wir erstellen die neue Klasse
profiles::sslmaster
, um nachher dem Puppet Server dieses Profil zuweisen zu können. - Wir laden die Einstellungen aus Hiera. Sind noch keine
Einstellungen hinterlegt, holt es sich die Standardeinstellungen aus
profiles/data/common.yaml
. - Wir definieren die Liste von Python-Paketen, die wir als
Abhängigkeiten der DNS-Validierung installieren müssen und
installieren diese über
pip
. - Wir instanzieren die Klasse
letsencrypt
von upstream und nehmen etwas Konfiguration vor. Wir wählen die CA aus (derzeit noch auf staging), stellen ein, welches Script die Validierung vornehmen soll, übergeben zusätzliche Umgebungsvariablen mit den Zugangsdaten für CloudFlare und deaktivieren, dass das Modul das Paketgit
installiert, falls wir das schon irgendwo anders installieren. Ansonsten kann man das auftrue
stellen.
Wir brauchen noch die Standardeinstellungen in der
common.yaml
:
# Datei /etc/puppetlabs/code/modules/profiles/data/common.yaml
profiles::sslmaster::ca: 'staging'
Zugangsdaten für CloudFlare hinterlegen wir keine, das wäre auch dämlich. Das Vorhaben soll ruhig auseinanderfliegen, wenn gar keine angegeben sind. Die Fehlermeldung wird offensichtlich sein.
Als letztes fehlt der dns hook. Das Script gibt es zum Download auf
github
mit dem Dateinamen hook.py
. Ich habe es umbenannt
in dnshook.py
(wer weiß, was in
profiles/files/
noch alles für hooks auftauchen
werden) und nach
/etc/puppetlabs/code/modules/profiles/files/dnshook.py
gespeichert.
Mit dem Hinterlegen der Zugangsdaten zu CloudFlare in der Konfiguration des Setups (und nicht des Moduls) sind wir mit dem Puppet Server fertig. Dazu verwenden wir wieder Hiera:
# Datei /etc/puppetlabs/code/environments/production/nodes/puppet.domain.tld
profiles::sslmaster::cloudflare_acc: '[email protected]'
profiles::sslmaster::cloudflare_token: '123456789'
Die richtigen Zugangsdaten sind natürlich einzufügen.
Konfiguration eines Hosts
Wie genau sich die Hosts Zertifikate beantragen sollen, muss auch konfiguriert werden. Dazu legen wir das Profil ssl an:
# Datei /etc/puppetlabs/code/modules/profiles/manifests/ssl.pp
# request DNS cert from puppet master
class profiles::ssl {
$domains = lookup('profiles::ssl::domains', Array, 'first', [$facts["fqdn"],])
$python_packages = [
['dnspython' , '1.15.0'],
['future' , '0.15.2'],
['requests' , '2.9.1'],
['six' , '1.10.0'],
['tld' , '0.7.6'],
]
# dependencies for CloudFlare DNS hook
$python_packages.each |$package| {
package { $package[0]:
ensure => $package[1],
name => $package[0],
provider => 'pip',
}
}
class { 'letsencrypt':
domains => $domains,
manage_packages => false, # oder true, falls sonst git noch nicht installiert
}
}
Was passiert hier? Der Reihe Nach:
- Wir sehen nach, ob in Hiera unter
profiles::ssl::domains
ein Array mit Domains hinterlegt ist, für die ein Zertifikat gebraucht wird. Das anzugeben macht natürlich nur in Host-spezifischen Hiera-Dateien Sinn. Sollte dem nicht so sein, wird automatisch der FQDN genommen. - Es werden auch hier Abhängigkeiten installiert
- Die Klasse
letsencrypt
diesmal in der Konfiguration für einzelne Hosts. Wer sonst noch nirgendsgit
installieren lässt, kannmanage_packages => true
sagen.
Zertifikat beantragen
Nach langer Vorarbeit können wir nun die Früchte unserer Arbeit ernten
und uns über viel Automatisierung freuen. Haben wir nun einen neuen
Host, beispielsweise ssltest.domain.tld
, weisen
wir ihm im Hiera die Klasse ssl
zu und er macht
sich automatisch ein Zertifikat und einen privaten Schlüssel:
# Datei /etc/puppetlabs/code/environments/production/nodes/ssltest.domain.tld
classes:
- ssl
Das war’s schon. Jeder weitere einzelne Host wird auf diese Art
konfiguriert. Man findet das Ergebnis in
/etc/letsencrypt
. Bis das Zertifikat dort ankommt,
kann ein wenig Zeit vergehen, da der Puppet Agent bei seinem nächsten
Lauf erst die Ressource der Zertifikatsanfrage exportiert, diese in der
Puppet DB hinterlegt, der Puppet Server das irgendwann dort findet,
das Zertifikat erstellt und das Resultat beim übernächsten Lauf des
Puppet Agent auf dem einzelnen Host dort hin verteilt. Ist man
besonders ungeduldig, kann man sich mit
puppet agent -t
behelfen.
Die Konfiguration der jeweiligen Dienste, welche dann die Zertifikate
verwenden sollen, muss natürlich noch angepasst werden; als Pfade für
private Schlüssel und Zertifikate muss auf die Dateien in
/etc/letsencrypt
gezielt werden.
Production
Klappt das alles und können die ersten Server sich mit einem Zertifikate der Fake CA von Let’s Encrypt ausweisen, kann man endlich auf die production CA umstellen. Dazu genügt ein Eintrag in der Konfiguration:
# Datei
/etc/puppetlabs/code/environments/production/nodes/sslmaster.domain.tld.yaml
# [...]
profiles::sslmaster::ca: 'production
Damit Überschreiben wir die Standardeinstellung des Profils und schalten die ganze Sache scharf.
Aktualisierungen?
Die Zertifikate werden regelmäßig auf ihre Gültigkeit geprüft - wenn der Puppet Agent läuft. Erneuerte Zertifikate werden auch automatisch beantragt. Meist ist jedoch dann noch zumindest ein reload oder gar restart des jeweiligen Dienstes notwendig, um das erneuerte Zertifikat zu verwenden. Das muss getrennt und je nach Dienst spezifisch noch implementiert werden - entweder ganz stupide mit cron job einfach irgendwann nachts die Dienste neu starten lassen oder in Puppet Abhängigkeiten zwischen der Eneuerung des Zertifikats und des Neustarts des Dienstes Definieren.