Aus Linux-Magazin 04/2021

PKI-Workshop, Teil 3: PKI-Automatisierung per Ansible-Playbook

© rawpixel / 123RF.com

Nachdem die beiden ersten Folgen dieser Serie die PKI-Grundlagen erläuterten und geeignete Open-Source-Tools vorstellten, geht es diesmal um die automatisierte Implementierung mithilfe eines Ansible-Playbooks.

Der erste Teil unseres Workshops hat den Aufbau und Zweck einer PKI erklärt. Teil 2 evaluierte, welche quelloffenen Tools für eine automatisierte PKI infrage kommen. Die hier vorliegende dritte Folge zeigt eine beispielhafte Implementierung.

Als Basis dazu stellt der Autor auf seinem Github-Account unter https://github.com/OlafRadicke im Repository »play_pki_ca_with_ansible« ein funktionstüchtiges Ansible-Playbook bereit, um dessen Funktion es im Folgenden geht.

Das Repository umfasst nicht weniger als 83 YAML-Dateien – zu viele, um sie an dieser Stelle alle im Detail zu besprechen. Der Autor konzentriert sich deshalb auf die wesentlichen Punkte und verweist bei Fragen zu Ansible auf die hervorragende Dokumentation des Projekts. Fehlt Ansible auf Ihren Systemen noch, werden Sie bei den meisten Distributionen in deren Paketquellen fündig.

Voraussetzungen

Um das Playbook auszuprobieren, benötigen Sie mindestens vier Linux-Maschinen. Das Playbook wurde mit Ubuntu 18.04 LTS auf VMs von Azure getestet; mit wenigen Anpassungen funktioniert es auch in anderen Umgebungen. Zwingend notwendig sind allerdings statische IP-Adressen, denn das Einbeziehen einer dynamischen DNS-Konfiguration hätte das Projekt zu komplex gemacht.

Das Playbook erstellt die Maschinen, die es benötigt, nicht selbst, sondern geht davon aus, dass sie bereits laufen und sich per SSH (passwortlos mit SSH-Key) erreichen lassen. Über ein weiteres Playbook könnte man auch die notwendigen VMs samt passender FQDNs erstellen. Weil das aber für jeden Cloud-Anbieter unterschiedlich funktionieren würde, lassen wir es hier außen vor.

Für das Playbook spielt es keine Rolle, ob die CAs auf physischen Maschinen, in VMs oder sogar in Containern laufen. Allerdings würde man hier von den Vorteilen eines Containers kaum profitieren. Nimmt man das Playbook in ein Docker-Image auf, startet einige Container mit SSH-Daemon und lässt diese dann vom ersten Container via SSH-Verbindung provisionieren, funktioniert das zwar – man nennt das Lift-and-Shift –, und man kann sogar ein wenig mit dem Playbook spielen. Für den produktiven Einsatz müsste man sich jedoch überlegen, wie man mit den persistenten Daten umgeht und wie man die privaten Schlüssel vor unberechtigten Zugriffen schützt. Das Playbook zielt also in erster Linie auf Bare-Metal- und VM-Installationen ab.

Die Orchestrierung

Eine der großen Herausforderungen bei einer komplexen PKI stellt ihre Orchestrierung dar: Die einzelnen Komponenten müssen in der richtigen Reihenfolge miteinander kooperieren. Abbildung 1 zeigt in einem Sequenzdiagramm, wie bei einer mehrstufigen PKI-CA-Hierarchie die einzelnen Komponenten in der richtigen Reihenfolge die passenden Schritte tun müssen, um die Chain of Trust zu schließen.

Abbildung 1: Ablaufdiagramm des Aufbaus einer Chain of Trust.

Abbildung 1: Ablaufdiagramm des Aufbaus einer Chain of Trust.

Das Diagramm vereinfacht den Aufbau stark. Es lässt den Prozess der Schlüsselerstellung aus und bildet nur ab, welche Instanz wann ein CSR erstellt haben muss, und zu welchem Zeitpunkt welche Instanz welches CSR signieren muss. Im Diagramm geht das Vertrauen von rechts aus und wird stufenweise nach links durchgereicht (Abbildung 1, (1)), bis am Ende eine End Entity (eine Person oder ein Service-Endpoint) steht. Bevor das Vertrauen in Form eines Zertifikats ausgesprochen werden kann, muss ein Subject das CSR erstellen und an den Issuer übergeben. Das zeigen die Pfeile von links nach rechts (Abbildung 1, (2)).

Die drei CAs im Diagramm könnten alle auf einer VM beziehungsweise einem physischen Rechner laufen. Aus Sicherheitsgründen wäre das jedoch nicht ratsam. Die CAs auf der rechten Seite des Diagramms sollen zudem bei Nichtgebrauch abgeschaltet werden. Das reduziert die Angriffsfläche weiter und sorgt dafür, dass nur ein kleiner Kreis von Mitarbeitern die VMs bei Bedarf starten kann. Letzteres lässt sich beispielsweise durch das Verschlüsseln der Festplatte erreichen.

Verteilt man allerdings die CAs auf mehrere VMs (oder Rechner), erschwert das die Verwaltung extrem. Nähme man alle notwendigen Schritte händisch vor, würde das zudem die Betriebsabläufe stark verlangsamen. Deswegen muss man aber nicht auf DevOps verzichten, sondern erweitert das Konzept zu DevSecOps, dehnt also die Automatisierung auf den Sicherheitsbereich aus.

Flexibilität

Wie bereits erwähnt, bietet das Playbook die Flexibilität, mehrere CAs auf einer unterschiedlichen Anzahl von Maschinen zu installieren. Für eine Testinstanz empfiehlt sich eine ressourcensparende Variante, wie sie Abbildung 2 zeigt. Fünf CAs verteilen sich auf drei virtuelle Maschinen: drei auf »vm_01« und jeweils eine CA auf »vm_02« und »vm_03«. Eine vierte VM dient als Test-Service für den Integrationstest. Von diesem Setup gehen wir im Weiteren aus. Die grau unterlegten VMs haben ein Zertifikat mit einer sehr langen Laufzeit. Das sind die VMs, die man gegebenfalls temporär offline nehmen würde.

Abbildung 2: Die Verteilung der CAs für eine Testumgebung.

Abbildung 2: Die Verteilung der CAs für eine Testumgebung.

Unterschiedliche Setups lassen sich mit Ansible leicht in Form unterschiedlicher Inventories abbilden. Abbildung 3 zeigt das Inventory für das Setup aus Abbildung 2.

Abbildung 3: Das Inventory f&uuml;r das Test-Setup aus <a href="#artRef-f2">Abbildung&nbsp;2</a>.

Abbildung 3: Das Inventory für das Test-Setup aus Abbildung 2.

Konfiguriert man in Ansible mehrere Hosts mit derselben IP-Adresse, betrachtet das Tool sie immer noch als unterschiedliche Systeme, auch wenn es nach dem SSH-Login auf derselben VM operiert. Es kann zu Problemen führen, wenn die Tasks konkurrierende Dinge erledigen, wie etwa Software zu installieren.

Der Paketmanager Dnf prüft vor dem Start, ob bereits ein Dnf-Prozess läuft. Falls ja, verweigert er die Ausführung um zu verhindern, dass konkurrierende Zugriffe die Paketdatenbank korrumpieren. In diesem Fall muss man auf den Serial-Modus zurückgreifen, und Ansible arbeitet nicht mehr parallel an den Hosts. Davon macht im Playbook die Datei »pki_bootstrap.yml« Gebrauch (Abbildung 4). Es ist auch der Grund, warum dieser Abschnitt im Playbook sehr lange dauert.

Abbildung 4: Auszug aus der Datei &raquo;pki_bootstrap.yml&laquo;.

Abbildung 4: Auszug aus der Datei »pki_bootstrap.yml«.

In Abbildung 4 sorgt die Zeile 12 dafür, dass die Rolle immer nur auf einem Host gleichzeitig ausgeführt wird. Im Playbook gibt es ein Verzeichnis »inventories/«, das die Ordner »production/« und »staging/« enthält. Die Konfiguration aus »staging/« bildet das Test-Setup aus Abbildung 2 ab.

Die ersten Zeilen in Abbildung 3 definieren die Anzahl zu startender VMs sowie deren IP-Adressen. Die darauffolgenden Zeilen verteilen die CAs auf die VMs. Ab Zeile 15 sind die End Entities auf die VMs verteilt. Im Playbook übernehmen sie die Rolle eines Integrationstests. So wird geprüft, ob die eingerichtete PKI korrekte Zertifikate ausstellt. Als Automatisierungsenthusiasten testen wir das freilich nicht händisch, sondern lassen Ansible das automatisch übernehmen.

Nachdem die IP-Adressen der Testsysteme in das Inventory unterhalb von »inventories/staging/group_vars/pki.yml« eingetragen wurden, lässt sich das Playbook starten; Listing 1 zeigt den Aufruf. Die in der ersten Zeile gesetzte Umgebungsvariable sorgt dafür, dass Ansible beim SSH-Login nicht nach einer Bestätigung fragt, wenn es den Fingerprint des Servers noch nicht kennt. Das passiert, wenn man die VMs in der Cloud geskriptet aufsetzt und sich darauf noch nicht angemeldet hat. Diese Interaktion stört bei einer Automatisierung natürlich.

Der Aufruf in der zweiten Zeile startet Ansible; der Parameter »-i« übergibt das zu nutzende Inventory. Danach folgt die Datei mit dem eigentlichen Playbook – einen Ausschnitt daraus zeigte schon Abbildung 4.

Listing 1

Aufruf des Playbooks

$ export ANSIBLE_HOST_KEY_CHECKING=False
$ ansible-playbook -i inventories/staging/hosts.yml ./pki_bootstrap.yml

Beim Ausführen des Playbooks erscheinen die Fehlermeldungen TASK [intermediate_ca_signing : Generate and sign immediate CA certificate] und Segmentation fault (core dumped). Es ließ sich nicht klären, woraus diese Meldungen resultieren. Man kann den Fehler jedoch ignorieren, da OpenSSL noch ein valides Ergebnis liefert, bevor es abstürzt.

Interaktionen der CAs

Damit das Playbook auf einem Host mehr als eine CA installieren kann, muss jede davon in ihrem eigenen Pfad arbeiten. Dazu setzt die Rolle »pki« im Verzeichnis »roles/set_special_host_facts/« die Facts (Ansible-Variablen) für jede CA individuell. Diese Rolle wird im Playbook »pki_bootstrap.yml« als Erstes aufgerufen, und zwar für die Hostgroup »hosts«.

Abbildung 5 zeigt einen Ausschnitt aus der Datei »inventories/staging/hosts.yml«. Wie man sieht, stehen alle CAs unterhalb von »pki«, sodass diese Rolle auf allen Hosts greift. Nutzt mehr als ein Host (beziehungsweise eine CA) eine VM, dann wird die Rolle auf dieser virtuellen Maschine mehrmals ausgeführt. Die sehr einfach aufgebaute Rolle besteht nur aus dem Task-File »roles/set_special_host_facts/tasks/main.yml«, das Sie auszugsweise in Abbildung 6 sehen.

Abbildung 5: Die Rolle &raquo;pki&laquo; wird auf allen Hosts ausgef&uuml;hrt.

Abbildung 5: Die Rolle »pki« wird auf allen Hosts ausgeführt.

Abbildung 6: Ein Auszug aus dem Task, zu dem die Facts gesetzt werden.

Abbildung 6: Ein Auszug aus dem Task, zu dem die Facts gesetzt werden.

In der Datei kann man erkennen, dass die Pfade der CAs (Zeilen 6 und 25) ihren Common Name als Prefix haben. Der Inhalt der beiden Variablen wird in den Host-Konfigurationen im Verzeichnis »inventories/staging/host_vars/« gesetzt. Die Conditions (Bedingungen) in den Zeilen 17 und 38 tragen der Tatsache Rechnung, dass eine Root- und eine Intermediate-CA zum Teil unterschiedliche Facts haben. So verwendet etwa die Root-CA keine CSR-Datei und braucht dafür auch keinen Pfad.

Das Playbook »pki_bootstrap.yml« (Abbildung 7) ruft dann zwei Rollen auf, die die Hosts für ihre Aufgaben vorbereiten: »pre_update_and_install« richtet notwendige Software ein, »pre_config« generiert Host-abhängige OpenSSL-Konfigurationen. In den Zeilen 21 bis 64 lässt sich wieder der Ablauf aus Abbildung 1 erkennen.

Abbildung 7: Das Playbook &raquo;pki_bootstrap.yml&laquo; spiegelt im Mittelteil den Ablauf aus <a href="#artRef-f1">Abbildung&nbsp;1</a> wider.

Abbildung 7: Das Playbook »pki_bootstrap.yml« spiegelt im Mittelteil den Ablauf aus Abbildung 1 wider.

In den Zeilen 21 bis 39 werden die CSR-Files erstellt und per Nginx zum Download bereitgestellt. Die farbigen Rahmen zeigen die logische Verbindung in beiden Abbildungen. Zwischen Zeile 36 und 39 überschneiden sich die beiden Rahmen. Das visualisiert, dass die Root-CA eine Sonderrolle spielt – sie stellt sich selbst keinen CSR aus.

In Abbildung 7 erkennt man auch, dass die einzelnen Abschnitte in ihrer »hosts«-Section unterschiedliche Host-Groups aufrufen. Welche Hosts jeweils dazu gehören, legt das Inventory-File fest.

Der Integrationstest

Bei den beiden Rollen »roles/fake_service« und »roles/fake_client« (Abbildung 7 ab Zeile 56) handelt es sich um zwei End Entities für einen Integrationstest. Die Rolle »fake_client« ruft per Curl-Request die von der Rolle »fake_service« erstellten Web-Services auf.

Abbildung 8 zeigt einen Ausschnitt der Datei »roles/fake_client/tasks/http_request_test.yml«. Zeile 67 überprüft anhand des Root-Zertifikats das Zertifikat des Services. Mit dem Zertifikat-Bundle aus Zeile 69 identifiziert sich Curl als Client gegenüber dem Service. In dieser Datei befindet sich nicht nur das eigene Zertifikat des Curl-Clients, sondern die komplette Kette der CAs bis zurück zur Root-CA.

Abbildung 8: Der Aufruf zum Testen eines HTTPS-Requests.

Abbildung 8: Der Aufruf zum Testen eines HTTPS-Requests.

Der Eigentümer des Zertifikats steht also in der Pflicht, die lückenlose Kette bis zur Root-CA nachzuweisen (oder bis zu einer gemeinsam bekannten und akzeptierten Sub-CA wie zum Beispiel einer Policy-CA). Um die Eigentümerschaft des Client-Zertifikats zu beweisen, benötigt der Client noch den privaten Schlüssel (Zeile 68). Curl übermittelt ihn nicht direkt an den Service, sondern löst damit eine sogenannte Challenge. Das genügt dem Service als Beweis, dass der Client den privaten Schlüssel kennt.

Noch eine Anmerkung: Die Rolle »fake_client« ruft Curl auf, statt das Ansible-Modul »uri« zu verwenden. Letzteres ist sehr mächtig und ließe sich leichter in ein Playbook integrieren. Allerdings bietet der Aufruf von Curl den Vorteil, dass man bei der Ansible-Fehlerausgabe den ausgeführten Befehl kopieren und händisch wiederholen kann.

Auf der Gegenseite bietet die Rolle »fake_service« mehrere End Points mit unterschiedlichen Regeln an. Abbildung 9 zeigt einen Ausschnitt der Datei »roles/fake_service/templates/fake.conf«, des Templating-Files für die Service-Konfiguration mit Nginx. In Zeile 26 verwendet Nginx ein Bundle-File mit seinem Zertifikat und der kompletten Kette bis zur Root-CA.

Abbildung 9: Ein Ausschnitt aus dem Nginx-Templating.

Abbildung 9: Ein Ausschnitt aus dem Nginx-Templating.

Auch Nginx benötigt einen privaten Schlüssel, um seine Eigentümerschaft in einer Challenge gegenüber dem Client zu beweisen (Zeile 27). Damit Nginx ein Bundle-File akzeptiert, müssen darin die Zertifikate in der richtigen Reihenfolge auftauchen. Das End-Entity-Zertifikat hat seinen Platz ganz oben, das Root-CA-Zertifikat ganz am Ende.

Zeile 31 übergibt erneut das Bundle-File. Das erscheint zunächst irritierend, weil hier eigentlich der Trust hinterlegt wird, mit dem sich die Gültigkeit des Client-Zertifikats prüfen lässt. Da sich das als Trust zu akzeptierende Root-CA-Zertifikat auch in dem Bundle von Nginx findet, lässt es sich an dieser Stelle einfach noch einmal verwenden. Dass es auch CAs enthält, die der Client weder kennt noch nutzt, stört nicht weiter.

Es wäre möglich, in dieser Konfiguration nur CAs aufzunehmen, die tatsächlich Client-Zertifikate ausstellen dürfen. Dann würde aber auch ein Client-Zertifikat akzeptiert werden, das von einer CA stammt, die nur Zertifikate für Services ausstellen sollte. Will man das verhindern, darf man an dieser Stelle nur die Policy-CA für Clients als Trust hinterlegen. Das setzt wiederum voraus, dass der Service nicht nur seine eigene Chain of Trust kennt, sondern auch mindestens einen Teil der CA-Baumstruktur des Clients. Zur Erinnerung: Dabei handelt es sich um den Teil, den Abbildung 2 links darstellt.

Zeile 48 in Abbildung 9 konfiguriert den End Point, der in Abbildung 8 aufgerufen wird. In Zeile 49 gibt es noch eine zusätzliche Bedingung: Sie prüft, ob das Client-Zertifikat für eine bekannte Organisation ausgestellt wurde, und weist alle anderen Zertifikate mit einem dem Fehlercode 403 ab.

Ausblick

War der Integrationstest erfolgreich, so beweist das, dass das Playbook den Kreis aus Abbildung 2 geschlossen hat und die Chain of Trust vollständig ist.

Ein vierter und letzter Teil dieser Artikelreihe widmet sich in der nächsten Ausgabe der Integration des Hardware-Security-Moduls YubiHSM 2 sowie der Arbeit mit Containern. (jcb/jlu)

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
1 Kommentar
Älteste
Neuste Beste Bewertung
Martin Hauptmann
5 Jahre her

Hallo, ich vermisse den letzten Teil der PKI-Serie, die für die Ausgabe 5/2021 angekündigte Yubi HSM2-Integration. Wurde das verschoben?

Nach oben