In diesem Beitrag beschreibe ich, wie man verschlüsselt Zugangsdaten und andere Geheimnisse sicher durch Puppet verwenden lassen und in git einchecken kann. Dazu werden sie (mit PKCS#7) asymetrisch verschlüsselt.

Das wird jetzt deswegen interessant, weil es in meiner Artikelserie zum Aufsetzen meines Servers langsam aber sicher auch um Zugangsdaten gehen wird, etwa für CloudFlare, wenn man dort seine DNS-Einträge vornehmen will, für externe Mailserver, die als Smarthost dienen, etc.

Grundlagen der verwendeten Verschlüsselung

Hier kommt ein asymetrisches Verschlüsselungsverfahren zum Einsatz. Das heißt: Alle, die den öffentlichen Schlüssel haben, können Daten abspeichern und verschlüsseln, aber nur wer den privaten Schlüssel hat, kann Daten entschlüsseln und damit nutzen.

Im praktischen Fall heißt das, dass die normale Workstation nicht über den privaten Schlüssel verfügen muss, um mit dem Code zu arbeiten. Dazu genügt der öffentliche Schlüssel. Nur der Puppet Server (und hoffentlich das hoffentlich gut verschlüsselte Backup) verfügen über den privaten Schlüssel. Teilt man sich die den Puppet-Code mit mehreren Personen in einem Team, muss dadurch auch nicht jedes Mitglied über den privaten Schlüssel und so über die Zugangsdaten externer Dienste oder private SSL-Schlüsssel etc verfügen. Im Grunde muss kein Mitglied den privaten Schlüssel haben. Selten ändert man Zugangsdaten oder private SSL-Schlüssel, meistens ersetzt man sie. Der Unterschied mag klein sein aber ausschlaggebend: Man muss den alten Wert nicht kennen, um den neuen zu setzen. Um ein neues Kennwort eines Dienstes zu setzen genügt der öffentliche Schlüssel.

Ist selbst dieses Sicherheitsmodell ein Problem, muss früher angesetzt werden: Wer Schreib- und Deploy-Rechte am Puppet-Repository hat, braucht gar keine Passwörter. Wer Puppet in der Hand hat, beherrscht sowieso alle davon konfigurierten Rechner. Es geht hier um Schutz anderswo verwendbarer Daten, etwa eigener Zugangsdaten bei externen Diensten.

Voraussetzungen

Wir erweitern unsere Puppet-Installation um eYAML - der folgende Befehl ausgeführt auf dem Puppet Server bewirkt das:

sudo /opt/puppetlabs/bin/puppetmaster gem install hiera-eyaml

Um mit verschlüsselten YAML-Dateien arbeiten zu können, brauchen wir das gleiche Paket auch auf der Workstation:

sudo apt-get install hiera-eyaml  # wer Systempakete bevorzugt
# oder
gem install hiera-eyaml  # wer rbenv, rvm o.Ä. nutzt
# oder
sudo gem install hiera-eyaml  # wehe!

Keys erzeugen

Grundlage jeder guten Verschlüsselung sind Keys, also machen wir uns welche, auf der Workstation:

eyaml createkeys

Es sind keine weiteren Argumente notwendig. Eine Passphrase zu setzen wäre schwachsinnig, weil der Puppet Server mit Hilfe des privaten Schlüssels automatisch die Entschlüsselung vornehmen soll und der hat ohnehin root auf allen angeschlossenen Hosts.

Die Keys liegen in ./keys, da gehören sie natürlich nicht hin. Den privaten Schlüssel kopieren wir auf den Puppet server:

scp ./keys/*.pem puppet.domain.tld:
ssh puppet.domain.tld
# jetzt auf dem Puppet Server:
sudo mkdir /etc/puppetlabs/eyaml/
sudo mv *.pem /etc/puppetlabs/eyaml/
sudo chown -R puppet:puppet /etc/puppetlabs/eyaml/
sudo chmod 0400 /etc/puppetlabs/eyaml/*.pem
sudo chmod 0500 /etc/puppetlabs/eyaml

Den privaten Schlüssel auf der Workstation kann man jetzt in ein verschlüsseltes Backup packen und dann von der Workstation löschen. Wer sich jetzt fragt, wieso, liest bitte die Grundlagen der verwendeten Verschlüsselung diesmal ganz durch.

Den öffentlichen Schlüssel kann man noch woanders hinkopieren, wo er nicht stört:

mkdir -p ~/.eyaml/keys/
mv keys/public_key.pkcs7.pem ~/.eyaml/keys/
echo <<EOF >>~/.eyaml/keys/config.yaml
---
pkcs7_public_key: '/home/florian/.eyaml/keys/public_key.pkcs7.pem'
EOF

Wer nicht Florian heißt, passt oben bitte den Pfad an. An dieser Stelle will ich auch kurz darüber schimpfen, dass eYAML sich nicht an XDG-Standards hält und nicht in ~/.config/eyaml nach seiner Konfiguration sucht.

Soweit, so gut: Wir können jetzt beliebige Daten für Verwendung durch den Puppet Server verschlüsseln - für Hiera, um genau zu sein. Hiera nimmt seine Daten gern als YAML und wir verschlüsseln mit Hilfe von eYAML, also lassen wir uns das als Syntax ausgeben:

eyaml encrypt -l 'geheimnis' -s 'irgend ein string'
eyaml encrypt -l 'geheimnis' -p  # fragt nach einem Passwort auf der Eingabezeile
eyaml encrypt -l 'geheimnis' -f dateiname  # verschlüsselt Dateiinhalt

Das ergibt dann in etwa folgende Ausgabe:

geheimnis: >
  ENC[PKCS7,MIIBeQYJKoZIhvcNA...Viel Text...Mehr Text]

Das >-Zeichen ist eines der zahlreichen Methoden, Multiline-Strings in YAML zu schreiben. Wer sich da unsicher ist, kann die http://yaml-multiline.info/ anschauen. Serioysly, WTF.

Umsetzung in Puppet

Bisher funktioniert unser eYAML nur auf der Workstation, aber wir wollen damit ja auch Daten im Bestand von Hiera verschlüsseln. Dazu müssen wir die Hierarchie selbst anpassen. Die sieht bei allen Leuten anders aus, daher hier das grundsätzliche Beispiel, was entsprechend angepasst werden muss:

---
version: 5
defaults:
  datadir: data
  # data_hash: yaml_data kommt weg und erscheint weiter unten wieder.

hierarchy:
  - name: "Encrypted data"
    lookup_key: eyaml_lookup_key
    paths:
      - 'secrets/nodes/%{trusted.certname}.eyaml'
      - 'secrets/common.eyaml'
    options:
      pkcs7_private_key: '/etc/puppetlabs/eyaml/private_key.pkcs7.pem'
      pkcs7_public_key: '/etc/puppetlabs/eyaml/public_key.pkcs7.pem'
  - name: "Normal configuration data"
    paths:
      - 'nodes/%{trusted.certname}.yaml'
      - 'users.yaml'
      - 'common.yaml'
    data_hash: yaml_data

Man kann eine eigene kleine Hierarchie mit verschlüsselten Daten aufbauen und die gleiche Interpolation verwenden wie für unverschlüsseltes Hiera. Man könnte auch alles verschlüsseln, aber das wäre dämlich und würde nur alles umständlich machen.

Verwendung

Um etwa das Kennwort für den Smarthost zu verschlüsseln, gibt man erst ein:

eyaml encrypt -l 'profiles::mail::smarthost_password' -p

und bekommt dann einen Block YAML, den man in eine der eYAML-Dateien einfügen kann. Auf dem Puppet Server kann diese Information dann transparent wie jede andere verwendet werden, um Entschlüsselung kümmert sich Hiera automatisch.

Kleine Verbesserung: Pointer Pattern

Nun funktioniert zwar alles, in der eigentlichen YAML-Datei ist aber ein Loch: Neben profiles::mail::smarthost_address und profiles::mail::smarthost_username fehlt smarthost_password. Im besten Fall denkt man beim Bearbeiten implizit daran, unter Druck bei blinkendem Monitoring, mit Kunden am Telefon und vier Litern Kaffee intus ohne Toilettenpause ist man aber oft über etwas mehr explizite Informationen froh.

Ich verwende daher gern ein Pointer pattern, bei dem die Daten etwas anders hinterlegt werden:

eyaml encrypt -l 'profiles::mail::smarthost_password_eyaml' -p

und der Block kommt wieder in eine inkludierte eYAML-Datei. In der eigentlichen YAML-Datei wird auch ein Eintrag für das Kennwort hinterlegt, der auf das verschlüsselte Passwort zeigt:

profiles::mail::smarthost_address: mail.example.com
profiles::mail::smarthost_username: karl_ranseier
profiles::mail::smarthost_password: "%{lookup('profiles::mail::smarthost_password_eyaml')}"

Dieses Pattern macht die Sache ein wenig mehr ausdrücklich und offensichtlich.

Warum nicht GnuPG?

Man kann hierfür statt pkcs7 auch GnuPG verwenden und dann für mehrere Empfänger verschlüsseln lassen: Für den Puppet Server und die anderen Teammitglieder etwa. Wenn der eigene Anwendungsfall das erfordert, steht dem da nichts im Wege. Meiner erfordert es nicht; ich muss einfach nie selbst lesend auf Passwörter o.Ä. zugreifen. Also verzichte ich auf ein weiteres Plugin.