Let’s Encrypt und Puppet

lets-encrypt-logoIch habe früher fast alle meine Zertifikate von CAcert bezogen. Ich habe sie regelmäßig aktualisiert und für das deployen habe ich ein Puppet Modul, dass die Zertifikate verteilt.

Nach und nach bin ich auf Let’s Encrypt Zertifikate umgestiegen. Da ein hoher Grad an Automatisierung bei Let’s Encrypt immer wieder propagiert wird dachte ich mir, dass sollte auch mit Puppet dann kein zu großes Problem sein. Hier die Lösung, die bei mir nun in Betrieb ist:

Als erstes habe ich das Puppet Modul bzed/letsencrypt installiert. Das Modul benötigt eine PuppetDB. Über exported resources  transportiert es die CSRs auf den puppetmaster, macht dort die gesamte Abwicklung und transferiert dann die Ergebnisse wieder zurück auf den Node. Das funktioniert auch sehr gut.

Auf dem Puppetmaster habe ich die Klasse eingebunden und einige wenige Einstellungen gesetzt (hiera). Bei dem Hook handelt sich m übrigen um einen letsencrypt.sh Hook:

---
classes:
  - letsencrypt

letsencrypt::challengetype: 'http-01'
letsencrypt::hook_source: 'puppet:///modules/helper/%{::fqdn}/le_hook.sh'

Auf den Nodes habe ich die Klasse eingebunden und die Domains definiert für die ein Zertifikat bezogen werden soll:

---
classes:
- letsencrypt

letsencrypt::domains:
- 'foo.example.net'
- 'bar.example.net'
- 'baz.example.net'

PL_logo_vertical_RGB_lgIn meinem recht simplen Szenario ist es so, dass ich einen Gate-Server habe, hinter dem sich alle anderen Maschinen „verstecken“. Dort läuft ein Apache Server als Proxy, der Anfragen über HTTP nach hinten weiterreicht. Die Konfiguration von Apache erfolgt ebenfalls über Puppet. Eine Authentifizierung über DNS für ACME klappt bei mir leider nicht, weswegen ich HTTP nehmen muss. Das habe ich so gelöst, dass ich für die Apache vhosts einfach über ProxyPassMatch den .well-known/acme-challenge/ auf einen Apache Vhost auf dem Puppetmaster weiterreiche. Auf dem Gate-Server sieht das für einen Vhost wie folgt aus:

apache::vhost:
  'proxy-foo.example.net':
    servername: 'foo.example.net'
    serveradmin: 'webmaster@example.net'
    port: '80'
    docroot: '/var/www/empty'
    proxy_dest: 'http://10.20.30.1'
    proxy_pass_match:
      -
        path: '^/.well-known/acme-challenge/(.*)$'
        url: 'http://10.20.30.2/$1'
        params:
          retry: '0'
    headers:
      - 'unset X-Powered-By'
    proxy_preserve_host: true

Auf dem Puppetmaster wiederum läuft ein Vhost, der einfach alle Domains als ServerAlias eingetragen hat.

Nun kommt noch der Hook für letsencrypt.sh ins Spiel den ich oben bereits erwähnt habe. Das Skript schreibt die ACME-Challenge in eine Textdatei in den DocRoot und löscht sie nach dem Erfolg wieder:

#!/bin/bash
 
#
# http-01 hook
#
 
CHALLENGEDIR="/var/www/example.net/letsencrypt"
 
done="no"
if [[ "$1" = "deploy_challenge" ]]; then
    echo "${4}" > "${CHALLENGEDIR}/${3}"
    chmod 644 "${CHALLENGEDIR}/${3}"
    done="yes"
fi
 
if [[ "$1" = "clean_challenge" ]]; then
    rm "${CHALLENGEDIR}/${3}"
    done="yes"
fi
 
if [[ "${1}" = "deploy_cert" ]]; then
    # do nothing for now
    done="yes"
fi
 
if [[ ! "${done}" = "yes" ]]; then
    echo Unkown hook "${1}"
    exit 1
fi
 
exit 0

Voila! Es braucht ein paar Durchläufe bis alles über die Puppetdb jeweils transportiert wurde, aber alles läuft vollautomatisch ab. Sehr cool!

Puppet Snippets

  • Wenn man den Katalog von einem Node anschauen möchte, kann man sich diesen auf dem Master mit folgendem Befehl kompilieren:
    puppet master --compile nodename
  • In Hiera kann man Werte wie folgt auslesen:
    hiera -c /etc/puppet/hiera.yaml apt::sources ::fqdn="puppetnode.example.net"

    Weitere Variablen kann man einfach so wie den fqdn auch übergeben:

    hiera -c /etc/puppet/hiera.yaml apt::sources ::fqdn="puppetnode.example.net"  ::provider="foobar"

 

Puppet: Hiera und Defined Types

puppetlabs apache vhost hiera howto

Das waren in etwa die Stichworte in der Suche als ich versucht habe meine mit dem puppetlabs/apache Modul definierten Apache vhosts aus der site.pp nach hiera zu migrieren. Es wollte nämlich einfach nicht klappen. Nach einiger Zeit bin ich dann darauf gestoßen, dass es sich bei dem apache::vhost um einen sogenannten Defined Type handelt. Diese sind sehr ähnlich zu Klassen, lassen sich aber im Gegensatz zu diesen mehrfach auf einem Node definieren.

Das automatische Parameter-Lookup wie bei den Klassen funktioniert bei Defined Types hingegen nicht. Lösung ist die create_resources() Funktion. Aber damit ich das später auch noch verstehe wenn ich hier in den Blogeintrag hineingucke schreibe ich ein praktisches Beispiel auf.

Ein Apache vhost in der site.pp:

node 'www.example.org' inherits default {
apache::vhost {'www.example.org':
  servername    => 'www.example.org',
  serveraliases => [ 'example.org' ],
  serveradmin   => 'webmaster@example.org',
  port          => '80',
  docroot       => '/var/www/example.org/www/',
}

wird in hiera zu:

---
apache::vhost:
  'www.example.org':
    servername: 'www.example.org'
    serveraliases:
      - 'example.org'
    serveradmin: 'webmaster@example.org'
    port: '80'
    docroot: '/var/www/example.org/www/'

und damit dieser Eintrag auch ausgewertet und der vhost erzeugt wird sieht die site.pp danach wie folgt aus:

node 'www.example.org' inherits default {
  $myApacheVhosts = hiera('apache::vhost', {})
  create_resources('apache::vhost', $myApacheVhosts)
}

Ich empfehle hier wärmstens das Chapter 3 – Using Hiera von Puppet Lunch. Ich habe lange im Netz gesucht und bin schließlich dort fündig geworden.

Puppet: Klassen mit Parameteruebergabe in Hiera

Der dritte Teil beschaeft sich damit, wie man pro Node in Hiera eine eigene Konfigurationsdatei anlegen kann sowie mit Klassen denen Parameter uebergeben werden. Er baut auf den beiden vorherigen Hiera-Blogeintraegen (1 und 2) auf. Grundsaetzlich ist vorneweg zu sagen, dass das ganze mit Puppet 3 fuer eigene Module bedeutend einfacher ist, da dort Hiera und vor allem das autolookup bereits integriert ist. In Puppet 2.7 muss man fuer die gleiche Funktionalitaet eine Kruecke bauen, doch dazu spaeter mehr.

Zuerst wird die Hiera Konfiguration erweitert, so das pro Node eine eigene Konfigurationsdatei moeglich ist. Diese sollen unter hieradata/nodes/ liegen und dafuer in der die hiera.yaml die hierarchy Sektion wie folgt erweitern:

:hierarchy:
    - common
    - nodes/%{fqdn}

Danach den neuen Ordner anlegen:

 mkdir /etc/puppet/hieradata/nodes/

In Puppet 2.7 und Puppet 3 gibt es nun Unterschiede. Das haengt mit dem automatischen Parameterlookup zusammen der in Puppet 3 integriert ist, in Puppet 2.7 aber nicht. An der Hiera Konfiguration aendert sich nichts, aber die Module die ich bisher vorgestellt hatte muessen angepasst werden. Damit sich diese unter Puppet 2.7 genauso verhalten wie unter 3, muss man wie in der verlinkten Dokumentation die Parameter mit einer Hiera-Suche und Defaultparametern modifizieren.

Wie die Anpassung geht und wie man die das ganze in Hiera abbildet moechte ich im folgenden Anhand des apt-Moduls, das ich im Blogeintrag Puppet: ein Modul mit Template, Variablen und Bedingung (/etc/apt/sources.list) vorgestellt habe., zeigen.

Die init.pp wurde wie folgt geaendert:

class apt (
  $mirror  = hiera('apt::mirror', 'http://ftp.de.debian.org'),
  $release = hiera('apt::release', 'wheezy'),
  $source  = hiera('apt::source', false),
  ) {
 
  file { '/etc/apt/sources.list':
    ensure  => 'present',
    owner   => 'root',
    group   => 'root',
    mode    => '0644',
    content => template('apt/sources.list.erb'),
    }
}

Das Modul hatte ich wie folgt im Node eingebunden:

node 'client02.example.org' inherits default {
  class { 'apt':
    mirror => 'http://debianmirror.example.org/pub/linux/debian/debian/',
    source => true
  }
}

Die Einbindung erfolgt nun ueber die Datei hieradata/nodes/client02.example.org.yaml:

---
classes: apt
apt::mirror: http://debianmirror.example.org/pub/linux/debian/debian/
apt::source: true

Danach kann die komplette Nodekonfiguration aus der site.pp geloescht werden.