DNS-01 challenge mit dehydrated und bind9

Es gibt viele Wege zu einem gültigen SSL/-TLS-Zertifikat für die eigene Server-Landschaft. In zahlreichen Anleitungen beschrieben ist die Möglichkeit, eine HTTP-01 challenge auf dem jeweiligen Webserver durchzuführen. Dieses Verfahren ist gut und solide. Spätestens wenn Zertifikate für Server benötigt werden, die nicht über das Internet erreichbar sind/sein sollen, scheitert diese Vorgehensweise jedoch. Zudem können Wildcard-Zertifikate nur über die DNS-01 Challenge austgestellt werden.

Diese Anleitung beschreibt eine Einrichtung eines eigenen DNS-Servers mit bind9, der Einrichtung des ACME-Clients dehydrated und dem anschließenden automatischen Erzeugen von Let’s-Encrypt-Zertifikaten via DNS-01 challenge. Diese werden im Anschluss via ansible auf die Zielserver verteilt

Konzept

Let’s Encrypt unterstützt (derzeit aktiv) drei verschiedene Challenge-Arten. Bei der nachfolgend beschriebenen DNS-01 challenge erfolgt der Beweis der Domänen-Inhaberschaft über einen besonderen DNS-Eintrag, der bei der Challenge im DNS-System hinterlegt werden muss. Nur der Eigener der Domän kann dies tun (zumindest sollte das so sein ;-)). Ist der Eintrag erfolgt, prüft Let’s encrpyt (= CA: Certificate Authority), ob der Eintrag an Ort und Stelle ist. Ist das der Fall, wird das Zertifikat ausgestellt.

Das Vorgehen kann mit einem ACME-Client (certbot, dehydrated, etc.) manuell durchgeführt, aber eben auch automatisiert werden. Der Ablauf von der DNS-01 challegene bis zur Verteilung der Zertifikate (wie ich sie hier beschreibe) wird durch folgende Abbildung verdeutlicht:

Mögliche Struktur zur Erzeugung von Let's encrypt-Zertifikaten via DNS-01 challenge
Mögliche Struktur zur Erzeugung von Let's encrypt-Zertifikaten via DNS-01 challenge
  1. Im Vorfeld werden Zonen vom DNS-Registrar an eigenen DNS-Server delegiert.
  2. Auf dem DNS-Server ist ein ACME-Client installiert der die DNS-Challegene initiiert.
  3. Let’s Encrypt gibt Token vor, prüft ob dieser im DNS hinterlegt wurde und stellt Zertifikate aus.
  4. Die Zertifikate werden dann an die jeweilgen Stellen via ansible verteilt.
Zum Seitenanfang

Basisinstallation eines Debian-Servers

Bei der Erstellung dieses Beitrags, habe ich die nachfolgenden Codezeilen auf einem Server durchgeführt, der unter der IP-Adresse 128.140.114.17 lief. Dieser wurde nur kurzfristig durch einen Cloud-Provider bereit gestellt. Wer die Anleitung durcharbeitet, muss die Adresse des eigenen DNS-Servers verwenden.

Die Installation wird auf einem Debian-Server (zum Zeitpunkt dieses Beitrags Debian 12) durchgeführt. Nach der Basisinstallation sollte eine Minimalabsicherung erfolgen:

  • SSH aktivieren
  • [evtl. einen alternativen Port für SSH verwenden]
  • Public-Key-Auth einrichten
  • fail2ban installieren, so dass fehlerhafte Logins zu einem BAN führen
  • Firewall installieren und nur Port für SSH und Port 53 freigeben.

Könnte in der Kürze wie folgt aussehen:

SSH-Key von meinem Client hochladen:

ssh-copy-id -i ~/.ssh/<mein-ssh-key>.pub <user>@<server>

Auf dem Server ufw und fail2ban installieren, SSH-Port umziehen (Mach ich so, müsst Ihr nicht) und passende UFW-Regeln erstellen:

apt update
apt install ufw fail2ban
sed -i 's/#Port 22/Port 42042/' /etc/ssh/sshd_config
systemctl restart sshd.service
ufw allow 42042
ufw allow 53
ufw enable
Vorsicht bei der Port-Änderung von SSH und der anschließenden Einrichtung der Firewall. Man kann sich ganz schön aussperren, wenn man hier nur abtippt.

Bei Debian 12 muss bzgl. fail2ban noch an einer Stelle nachgearbeitet werden. Nach der Installation läuft der Service nicht korrekt. Die Anpassung erfolgt in der /etc/fail2ban/jail.conf. Ich löse das Problem in dem ich die Zeile backend = %(sshd_backend)s durch backend = systemd tausche und dann fail2ban neu starte:

apt install python3-systemd
sed -i 's/backend = %(sshd_backend)s/backend = systemd/' /etc/fail2ban/jail.conf
systemctl restart fail2ban

Siehe hierzu auch: https://github.com/fail2ban/fail2ban/issues/3292

Zum Seitenanfang

Installation und Einrichtung von bind9

Nun beginnt die eigentliche Einrichtung und wir beginnen mit der Installation eines DNS-Servers.

apt install bind9 bind9-dnsutils

Konfiguration des DNS-Servers

Die Konfigurationsdateien liegen unter /etc/bind/. Die Hauptkonfiugrationsdatei /etc/bind/named.conf lassen wir in Ruhe. Diese Datei beinhaltet nur die Includes auf die anderen Dateien die uns wichtig sind.

Die eigentliche Konfiguration (außerhalb der Zonendefinitionen) erfolgt in der /etc/bind/named.conf.options. Im Beispiel hier habe ich folgende Konfiguration hinzugefügt:

options {
        directory "/var/cache/bind";
        listen-on port 53 { any; };
        allow-query { any; };
        allow-transfer {"none";};
        allow-recursion {"none";};
        recursion no;
        rate-limit {
            responses-per-second 5;
            window 5;
        };
        // Weiterer Inhalt gekürzt

Damit darf weltweit jedes Gerät diesen DNS-Server verwenden. Allerdings ist eine rekursive Anfrage nicht erlaubt. Somit kann der DNS-Server nur für die eigenen (delegierten) Domains verwendet werden. Zusätzlich sorgt das rate-limit dafür, dass der Server nur eine gewisse Anzahl an DNS-Anfragen pro Zeiteinheit zulässt. Somit wird ein Missbrauch vermieden wie z. B. hier beschrieben: BSI-Hinweise zu offenen DNS-Resolvern.

Zonen definieren

In der /etc/bind/named.conf.local werden die einzelnen Zonen definiert:

include "/etc/bind/nsupdate.key";

zone "lab.toheine.net" IN {
  type master;
  allow-update { key "nsupdate-key"; };
  file "db.lab.toheine.net";
  notify yes;
};

zone "_acme-challenge.toheine.net" IN {
  type master;
  allow-update { key "nsupdate-key"; };
  file "db._acme-challenge.toheine.net";
  notify yes;
};

Im Beispiel wurden zwei Zonen definiert. Die erste Zone ist für die Subdomain lab.toheine.net komplett zuständig. Diese Form verwende ich, wenn ich neben der DNS-01 challenge auch weitere Records erstellen will. Die zweite Zone _acme-challenge.toheine.net wird später ausschließlich für die Zertifikats-Erstellung verwendet. Alle anderen Records die unter der toheine.net Domain laufen, werden direkt bei meinem Registrar eingegeben.

Innerhalb der Zone verraten die Zuweisungen für file, wo die einzelnen Records tatsächlich liegen und allow-update ermöglicht das dynamische Eintragen von Resource Records, sofern der korrekte Key verwendet wird. Über dieses Verfahren trägt der ACME-Client ein Token als TXT-Record ein. Den Key brauchen wir erst später … aber weil wir gerade dabei sind, erzeugen wir diesen gleich und verpassen der Datei die minimalen Rechte:

tsig-keygen -a sha512 nsupdate-key > /etc/bind/nsupdate.key
chmod 600 /etc/bind/nsupdate.key
chown bind: /etc/bind/nsupdate.key

Zoneneinträge erzeugen/vorbereiten

Wenn wir bind9 konfiguriert haben, werden die einzelnen Zonen-Dateien unter /var/cache/bind/ erwartet. Für jede Zone erstellen wir eine eigene Datei, die wir im Anschluss mit Inhalten füllen:

Die Zonendatei /var/cache/bind/db.lab.toheine.net hat nachfolgenden exemplarischen Aufbau:

$TTL 300	  ; 5 minutes 

@   IN SOA	dns.lab.toheine.net. mail.toheine.net. (
       2024041701 ; serial
       86400      ; refresh (1 day)
       7200       ; retry (2 hours)
       604800     ; expire (1 week)
       172800     ; minimum (2 days)
)

@      NS    dns             
dns    A     128.140.114.17  ; produktive IP meines DNS-Servers
www    A     192.0.2.1       ; Test-Net-IP
test   CNAME www             ; Alias auf www
cloud  CNAME www             ; Alias auf www

Das Kommentar-Zeichen der Zonendatei ist das Semikolon. Gerade wenn man neu in der DNS-Welt unterwegs ist, hilft das für die Zuordnung der vielen Werte oder DNS-Typen. Wichtig ist der SOA-Eintrag (Start of Authority), der einige Parameter enthält, die für die Eröffnung der Zone notwendig sind. Der Aufbau der Datei ist auf Wikipedia gut erklärt.

Die weiteren Einträge dienen erstmal nur dazu, um später zu sehen, ob der DNS-Server tatsächlich die Anfragen korrekt auflöst. Hier können beliebige, gültige Einträge vorgenommen werden. Analog dazu erstelle ich eine leere Zonen-Datei unter /var/cache/bind/db._acme-challenge.toheine.net die allerdings neben dem SOA- und NS-Eintrag keine weiteren Records enthält:

$TTL 300	; 5 minutes
@  IN   SOA dns.lab.toheine.net. mail.toheine.net. (
       2024041701 ; serial
       86400      ; refresh (1 day)
       7200       ; retry (2 hours)
       604800     ; expire (1 week)
       172800     ; minimum (2 days)
)
@      NS   dns.lab.toheine.net.

Nun sind wir fertig und wir können bind9 neu starten, damit die geänderten Konfigurationen neu eingelesen werden. Vorab ist es allerdings absolut empfehlenswert, die Konfiguration mit folgendem Befehl zu überprüfen:

named-checkconf -z

Die Ausgabe sollte wie folgt aussehen:

zone lab.toheine.net/IN: loaded serial 2024041701
zone _acme-challenge.toheine.net/IN: loaded serial 2024041701
zone localhost/IN: loaded serial 2
zone 127.in-addr.arpa/IN: loaded serial 1
zone 0.in-addr.arpa/IN: loaded serial 1
zone 255.in-addr.arpa/IN: loaded serial 1

Wenn hier Fehler enthalten sind, müssen diese vor einem Nameserver-Neustart bereinigt werden. Ansonsten wird bind9 nicht starten. Ist alles korrekt, kann der named-service neu gestartet werden:

systemctl restart named.service

Damit ist die DNS-Konfiguration fertig. Um von einem lokalen Client zu testen, ob der DNS-Server seine Arbeit korrekt erledigt, können wir mit dem Kommando dig @128.140.114.17 test.lab.toheine.net einen DNS-Lookup durchführen. Wichtig ist zum jetzigen Zeitpunkt, dass wir den DNS-Server (hinter dem @-Zeichen) angeben. Die Ausgabe sollte wie folgt aussehen:

; <<>> DiG 9.18.24-1-Debian <<>> @128.140.114.17 test.lab.toheine.net
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45885
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: def4386d4f9e1e0f010000006620039ac691052a8e61c70f (good)
;; QUESTION SECTION:
;test.lab.toheine.net.		IN	A

;; ANSWER SECTION:
test.lab.toheine.net.	300	IN	CNAME	www.lab.toheine.net.
www.lab.toheine.net.	300	IN	A	192.0.2.1

;; Query time: 4 msec
;; SERVER: 128.140.114.17#53(128.140.114.17) (UDP)
;; WHEN: Wed Apr 17 19:15:06 CEST 2024
;; MSG SIZE  rcvd: 111

Bei DNS-Registrar die Zonen-Delegation eintragen

Im letzten Schritt der DNS-Konfiguration, müssen wir bei unserem DNS-Registrar noch die Zonen-Delegation hinterlegen. Hier sind die Einträge je nach Anbieter recht ähnlich und kann wie folgt aussehen:

Einträge beim DNS-Registrar
Einträge beim DNS-Registrar
  1. IPv4- und IPv6- Eintrag, um den eingerichteten DNS-Server mit Namen aufzulösen
  2. Zonen-Delegation ausschließlich für die DNS-01 challenge.
  3. Zonen-Delegation für eine komplette Suddomain (DNS-01 challengenge und sonstige Records)
Zum Seitenanfang

Dynamisches Eintragen von DNS-Records via nsupdate

Sobald man eigene Konfigurations-Dateien auf einem eigenen Server hat, kann man grundsätzlich mit Hilfe von Skripten (bash-Skripte, python, etc.) die Dateien anpassen. Für das dynamische Einspielen von DNS-Records sind aber solche Skripte nicht wirklich geeignet, da u. a. der DNS-Server nach einem Resource-Eintrag seine Konfiguration jedes Mal neu laden müsste.

Für dynamische DNS-Updates ist das Tool nsupdate vorgesehen, das wir im Rahmen von bind9-dnsutils oben installiert haben. Unser ACME-Client soll später über dieses Tool die DNS-Einträge vornehmen. Sofern man nsupdate ohne Angabe von Optionen oder Argumenten aufruft, befindet man sich in einem interaktiven nsupdate-Modus von wo aus verschiedene nsupdate-Befehle übergeben werden können. Mit help sieht man die Möglichkeiten vor Ort und mit quit beendet man den interaktiven Modus wieder.

Sehr komfortabel ist die Verwendung einer Textdatei um die Funktion von nsupdate zu prüfen. Diese enthält die Befehlsfolge, die wir bei Aufruf von nsupdate übergeben. Im Beispiel schreibe ich folgenden Inhalt in eine ~/nsupdate.txt-Datei:

server 127.0.0.1
zone lab.lfb-linux.de
update add botschaft.lfb-linux.de. 300 in TXT "HeyYoda"
show
send

Sinn und Zweck ist das Testen des dynamischen Eintrags. Später brauchen wir die Datei nicht mehr. Das übernimmt dann der ACME-Client. Daher ist der RRecord (Resource Record) im Beispiel eben, mit HeyYoda relativ sinnfrei. Wenn wir den Eintrag vornehmen wollen, rufen wir nsupdate mit folgendem Befehl auf:

nsupdate -k /etc/bind/nsupdate.key -v nsupdate.txt

Es erfolgt folgende Ausgabe:

Outgoing update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id:      0
;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
;; ZONE SECTION:
;lab.toheine.net.		IN	SOA

;; UPDATE SECTION:
botschaft.lab.toheine.net. 300	IN	TXT	"HeyYoda"

Um zu prüfen, ob der dynamische Eintrag funktioniert hat, führen wir auf dem Client ein DNS-Lookup durch:

dig -t TXT botschaft.lab.toheine.net

Die Ausgabe sollte dann (gekürzt wie folgt aussehen):

;; ANSWER SECTION:
botschaft.lab.toheine.net. 300	IN	TXT	"HeyYoda"

;; AUTHORITY SECTION:
lab.toheine.net.	300	IN	NS	dns.lab.toheine.net.

;; ADDITIONAL SECTION:
dns.lab.toheine.net.	300	IN	A	128.140.114.17
Zum Seitenanfang

Einrichtung von dehydrated

Die Installation von dehydrated auf dem DNS-Server kann man diskutieren. Die DNS-01 Challenge und damit verbundenen dynamischen Updates können auch von jedem anderen Host aus angestoßen werden, sofern dieser über den oben erzeugten nsupdate-key verfügt.

Nun kommen wir zur Einrichtung von dehydrated, das wir via apt install dehydrated installieren. Die Konfiguration erfolgt im Verzeichnis /etc/dehydrated. Eine gute Doku mit Beispielen findet sich bei Debian unter /usr/share/doc/dehydrated/.

Die Hauptkonfigurationsdatei ist die /etc/dehydrated/config, die folgenden Aufbau besitzt:

CONFIG_D=/etc/dehydrated/conf.d
BASEDIR=/etc/dehydrated
DOMAINS_TXT="${BASEDIR}/domains.txt"
CERTDIR="${BASEDIR}/certs"
CHALLENGETYPE="dns-01"
CA="https://acme-staging-v02.api.letsencrypt.org/directory"
#CA="letsencrypt"
HOOK="${BASEDIR}/hook.sh"
AUTO_CLEANUP="yes"
CONTACT_EMAIL=mail@toheine.net

Vieles erklärt sich von selbst. Bei Ersteinrichtung von dehydrated, nehme ich vorab die Staging-CA, die verhindert, dass ich bei Konfigurationsfehlern gegen das Rate Limit von Let’s encrypt laufe (siehe dazu: staging-environment). Produktiv verwendet wird später die korrekte CA (CA="letsencrypt").

Die einzelnen Domains, für die ein Zertifikat ausgestellt werden sollen, liegen in einer eigenen Datei unter /etc/dehydrated/domains.txt:

lab.toheine.net *.lab.toheine.net > lab.toheine.net
toheine.net                       > toheine.net

Jede Domain, die hier pro Zeile eingetragen wird, enthält ein eigenes Zertifikat. Möchte man ein Zertifikat für mehrere Domains haben, werden diese Leerzeichen-getrennt in eine Zeile geschrieben. Hinter dem >-Zeichen ist dann der für dehydrated hinterlegte Alias eingetragen. Unter diesem Namen werden die Zertifikate dann unter /etc/dehydrated/certs abgelegt.

Im Anschluss brauchen wir das Hook-Skript, das während der Challenge aufgerufen wird und den dynamischen Eintrag erzeugt. Darin enthalten:

#!/usr/bin/env bash

#
# Example how to deploy a DNS challenge using nsupdate
# https://github.com/dehydrated-io/dehydrated/wiki/example-dns-01-nsupdate-script
#

set -e
set -u
set -o pipefail

NSUPDATE="nsupdate -k /etc/dehydrated/nsupdate.key"
DNSSERVER="127.0.0.1"
TTL=300

case "$1" in
    "deploy_challenge")
        printf "server %s\nupdate add _acme-challenge.%s. %d in TXT \"%s\"\nsend\n" "${DNSSERVER}" "${2}" "${TTL}" "${4}" | $NSUPDATE
        ;;
    "clean_challenge")
        printf "server %s\nupdate delete _acme-challenge.%s. %d in TXT \"%s\"\nsend\n" "${DNSSERVER}" "${2}" "${TTL}" "${4}" | $NSUPDATE
        ;;
    "deploy_cert")
        # optional:
        # /path/to/deploy_cert.sh "$@"
        ;;
    "unchanged_cert")
        # do nothing for now
        ;;
    "startup_hook")
        # do nothing for now
        ;;
    "exit_hook")
        # do nothing for now
        ;;
esac

exit 0

Dieses Skript muss zur Ausführung berechtigt sein chmod +x hook.sh. Zuletzt kopieren wir noch den nsupdate.key an Ort und Stelle:

cp ../bind/nsupdate.key .

Im Anschluss an die Einrichtung von dehydrated, muss man sich (pro CA) einmal regisitrieren …

dehydrated --register --accept-terms

… und kann im Anschluss dehydrated manuell ausführen:

dehydrated -c

Hat alles geklappt sollte die Ausgabe wie folgt aussehen:

# INFO: Using main config file /etc/dehydrated/config
 + Creating chain cache directory /etc/dehydrated/chains
Processing lab.toheine.net with alternative names: *.lab.toheine.net 
 + Creating new directory /etc/dehydrated/certs/lab.toheine.net ...
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting new certificate order from CA...
 + Received 2 authorizations URLs from the CA
 + Handling authorization for lab.toheine.net
 + Handling authorization for lab.toheine.net
 + 2 pending challenge(s)
 + Deploying challenge tokens...
 + Responding to challenge for lab.toheine.net authorization...
 + Challenge is valid!
 + Responding to challenge for lab.toheine.net authorization...
 + Challenge is valid!
 + Cleaning challenge tokens...
 + Requesting certificate...
 + Order is processing...
 + Checking certificate...
 + Done!
 + Creating fullchain.pem...
 + Done!
Processing toheine.net
 + Creating new directory /etc/dehydrated/certs/toheine.net ...
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting new certificate order from CA...
 + Received 1 authorizations URLs from the CA
 + Handling authorization for toheine.net
 + 1 pending challenge(s)
 + Deploying challenge tokens...
 + Responding to challenge for toheine.net authorization...
 + Challenge is valid!
 + Cleaning challenge tokens...
 + Requesting certificate...
 + Order is processing...
 + Checking certificate...
 + Done!
 + Creating fullchain.pem...
 + Done!
 + Running automatic cleanup

Die notwendigen Zertifikate liegen nun in unter /etc/dehydrated/certs.

Haben wir mit der Staging-CA begonnen, muss in der /etc/dehydrated/config noch die korrekte CA hinterlegt werden und wir müssen uns für diese ebenfalls einmalig mit dehydrated --register --accept-terms regisitrieren.

Damit die Zertifikate in regelmäßigen Abständen neu gezogen werden, richten wir cron mit dem Aufruf crontab -e ein und hinterlegen folgende Zeile:

25 10 * * 1 /usr/bin/dehydrated -c >> /var/log/dehydrated.log 2>&1

Die Challenge wird nun 1x pro Woche initiiert. Sofern sich das Zertifikat dem Verfallsdatum nährt, wird ein neues ausgestellt. Ob ich das loggen muss, mir eine Mail zu sende oder die Ausgabe nach /dev/null schiebe, ist Geschmackssache.

Zum Seitenanfang

Verteilung der Zertifikate auf die Server via ansible

Nun werden die notwendigen Zertifikate automatisch und regelmäßig erstellt. Die Verteilung läuft via ansible. Klar … auch das kann man automatisieren. Dieses letzte Stück manuelle Ausführung behalte ich mir jedoch vor, da ich dann auch wirklich weiß, das die Zertifikate an Ort und Stelle sind und die notwendigen Dienste diese auch tatsächlich geladen haben.

Ich stelle hier nur kurz die entsprechende Rolle und ein exemplarisches Playbook vor. Wer mit ansible arbeitet, wird vorab einen ansible-Host einrichten, von wo aus die Playbooks auf die zu orchestrierenden Server losgelassen werden: ansible installieren, ansible.cfg anlegen, gewisse Ordnerstruktur erstellen. Einfache Beispiele dazu sind hier zu finden: https://codeberg.org/toheine/linadv_infra.

Wir benötigen auf dem ansible-host eine eigene Rolle update-certs. Diese liegt unter {{ roles_path }}/update-certs/tasks/main.yml und hat in meinem Fall folgenden Aufbau:

---
- name: Copy fullchain.pem to local
  ansible.builtin.fetch:
    src: /etc/dehydrated/certs/{{ certname }}/fullchain.pem
    dest: /tmp/{{ certname }}/fullchain.pem
    flat: yes
    mode: 0644
  run_once: true
  delegate_to: "{{ acmehost }}"

- name: Copy privkey.pem to local
  ansible.builtin.fetch:
    src: /etc/dehydrated/certs/{{ certname }}/privkey.pem
    dest: /tmp/{{ certname }}/privkey.pem
    flat: yes
    mode: 0640
  run_once: true
  delegate_to: "{{ acmehost }}"

- name: Create /etc/ssl/{{ certname }} directory
  ansible.builtin.file:
      path: /etc/ssl/{{ certname }}
      state: directory
      recurse: yes

- name: Copy {{ certname }}-wildcard-certificates
  ansible.builtin.copy:
      src: /tmp/{{ certname }}/fullchain.pem
      dest: /etc/ssl/{{ certname }}/fullchain.pem
      mode: 0644
  register: crt

- name: Copy {{ certname }}-wildcard-key
  ansible.builtin.copy:
      src: /tmp/{{ certname }}/privkey.pem
      dest: /etc/ssl/{{ certname }}/privkey.pem
      mode: 0600

- name: check if restart-services.sh exists
  ansible.builtin.stat:
      path: /usr/local/sbin/restart-services.sh
  register: resfile

- name: restart services
  command: /usr/local/sbin/restart-services.sh
  when: resfile.stat.exists and crt.changed

Der letzte Task aus der Rolle, führt ein Bashskript auf dem Zielsystem aus, das alle Services neu startet, die das Zertifikat verwenden. Das kann exemplarisch wie folgt aussehen:

#!/bin/bash
systemctl restart nginx.service

Pro Zielsystem muss die entsprechende Datei einmalig unter /usr/local/sbin/restart-services.sh` abgelegt werden.

Zu der Rolle gibt es pro Zertifikat ein eigenes Playbook, das dann

  • die Gruppe der Ziel-Hosts,
  • den Zeritifkats-Namen (alias der dehydrated-domains.txt),
  • den Namen des acme-hosts auf dem die Zertifikate ausgestellt werden
  • und den Rollenaufruf enthält.

Am Beispiel von lab.toheine.net sieht das playbook update-certs-lab-toheine.yml wie folgt aus:

- name: update SSL/TLS certs 
  hosts: certs_lab_toheine
  become: yes
  vars:
    certname: lab.toheine.net
    acmehost: heidns
  roles:
     - update-certs

Das dazugehörige Inventory enthält eine Zuordnung zu einer Gruppe certs_lab_toheine, der ein oder mehrere Hosts zugeordnet werden. In meinem Fall sind das zwei Host mit dem Namen web01 und web02. Ist alles an Ort und Stelle, kann das Playbook via ansible-playbook update-certs-toheine-net.yml ausgeführt werden. In meinem Fall erfolgt folgende Ausgabe:

PLAY [update SSL/TLS certs] *******************************************************

TASK [Gathering Facts] ************************************************************
ok: [web02]
ok: [web01]

TASK [update-certs : Copy fullchain.pem to local] *********************************
ok: [web01 -> heidns(128.140.114.17)]

TASK [update-certs : Copy privkey.pem to local] ***********************************
ok: [web01 -> heidns(128.140.114.17)]

TASK [update-certs : Create /etc/ssl/lab.toheine.net directory] *******************
changed: [web02]
changed: [web01]

TASK [update-certs : Copy lab.toheine.net-wildcard-certificates] ******************
changed: [web02]
changed: [web01]

TASK [update-certs : Copy lab.toheine.net-wildcard-key] ***************************
changed: [web02]
changed: [web01]

TASK [update-certs : check if restart-services.sh exists] *************************
ok: [web01]
ok: [web02]

TASK [update-certs : restart services] ********************************************
skipping: [web01]
skipping: [web02]

PLAY RECAP ************************************************************************
web01                      : ok=7    changed=3    unreachable=0    failed=0    skipped=1
web02                      : ok=5    changed=3    unreachable=0    failed=0    skipped=1

Damit sind die SSL-Zertifikate an eingestellten Ort und Stelle. Der jeweilige Service (Apache, nginx, Postifx, etc.) muss in der Folge eben noch dieses Zertifikat nutzen.

Fazit

Dieses Setting umzusetzen, ist nicht in fünf Minuten erledigt. Darüber hinaus gibt es zahlreiche andere Wege zu einem gültigen Zertifikat. Wer allerdings sowieso einen DNS-Server betreibt und eine große Farm an vielen Servern vorhält, kann über die DNS-01 Challenge saubere (Wildcard-)-Zertifikate für alle Services ausstellen.

Zum Seitenanfang

Quellen

Zum Seitenanfang