Certificats Letsencrypt via DNS

La création de certificats Letsencrypt se fait traditionnellement par authentification HTTP en utilisant le client certbot de l'EFF et ça permet de couvrir la plupart des besoins. Mais il est également possible de créer des certificats Letsencrypt par authentification DNS pour des cas d'usages moins simples et surtout c'est le seul moyen actuellement d'obtenir des certificats Wildcard.

Pour les besoins du service XMPP c'est un certificat Wildcard qui a été créé en utilisant le client dehydrated.

Ici j'explique ce qui a été fait pour mettre cela en place la première fois afin de pouvoir reproduire la démarche ailleurs. Les fichiers de configuration et autres scripts présentés ici ne correspondent plus forcément à la réalité au moment où vous lisez ces lignes, et c'est pas grave hein.

Principe de fonctionnement

Le principe du fonctionnement de la validation est similaire que l’on passe par validation DNS-01 ou validation HTTP-01 :

Il faut donc pouvoir publier un enregistrement DNS de type TXT sur nos serveurs Bind (en fait sur un seul ça suffit).

Configurer Bind pour les mises à jour dynamiques

Techniquement il est possible de configurer directement la zone parinux.org pour accepter les changements dynamiques, avec la commande nsupdate par exemple. Mais quand on active cette option alors Bind réorganise complètement le fichier zone à sa façon illisible. Et ça c'est pénible pour les adminsys. Et puis en plus ce fichier zone ne doit plus être édité manuellement comme l'indique la page man nsupdate :

Zones that are under dynamic control via nsupdate or a DHCP server should not be edited by hand. Manual edits could conflict with dynamic updates and cause data to be lost.

Donc le plus simple c'est créer une délégation DNS uniquement pour _acme-challenge.parinux.org. C'est ce qui est suggéré dans la doc de dehydrated.

Donc pour cela dans le fichier de zone de parinux.org ajouter l'enregistrement suivant (et penser à modifier le serial dans le foulée) :

_acme-challenge NS ns1.parinux.org.

Créer une clef secrète qui ne sera partagée qu'entre le serveur Bind et les machines ayant besoin de publier des enregistrements DNS dynamiquement.

cd /tmp
dnssec-keygen -r /dev/urandom -a hmac-sha512 -b 128 -n HOST rndc-acme

La clef secrète se trouve dans un fichier nommé /tmp/Krndc-acme.xxxx et ressemble à quelque chose comme ça : cZh3zV8Uf7aIOxjv8ZA8iA==. Ensuite, créer un fichier /etc/bind/rndc-acme.key :

key "rndc-acme" {
        algorithm hmac-sha512;
        secret "cZh3zV8Uf7aIOxjv8ZA8iA==";
};

Dans le fichier /etc/bind/named.conf.local déclarer cette nouvelle zone et inclure le fichier de la clef secrète :

include "/etc/bind/rndc-acme.key";
zone "_acme-challenge.parinux.org" {
  file "/var/cache/bind/_acme-challenge.parinux.org.zone";
  type master;
  masterfile-format text;
  allow-update { key "rndc-acme"; };
};

Il faut maintenant peupler un minimum cette zone dynamique (un SOA et un NS). Créer le fichier /var/cache/bind/_acme-challenge.parinux.org.zone comme ceci :

_acme-challenge.parinux.org. IN SOA troll4.parinux.org. admin.parinux.org. (
       2018112200
       86400
       300
       1814400
       86400
)
  NS   troll4.parinux.org.

Permettre à Bind de modifier ce fichier :

sudo chown bind: /var/cache/bind/_acme-challenge.parinux.org.zone

Puis sudo systemctl restart bind9.

Maintenant on peut tester la publication dynamique. On peut déjà vérifier qu'on peut manuellement ajouter/modifier/supprimer l'enregistrement DNS depuis troll4 elle-même (par la suite il faudra pouvoir faire le même chose depuis un autre machine ayant la clef secrète).

sudo nsupdate -k /etc/bind/rndc.key <<EOF
server 127.0.0.1
update delete _acme-challenge.parinux.org
update add _acme-challenge.parinux.org 8000 IN TXT "coucou tout va bieng ?"
send
EOF

Puis

dig +short _acme-challenge.parinux.org
Voilà. Ça, c'est fait.

Certificat Wildcard avec dehydrated

Sur la machine ayant besoin d'un certificat Wildcard (ici la machine xmpp-1 hébergeant ejabberd) créer le fichier /etc/parinux/rndc-acme.key contenant la même clef secrète que sur le serveur Bind :

key "rndc-acme" {
        algorithm hmac-sha512;
        secret "cZh3zV8Uf7aIOxjv8ZA8iA==";
};

sudo apt install dehydrated dnsutils ntp

Temporairement, le temps de tester la configuration on va utiliser le serveur ACME de staging.

echo 'CA="https://acme-staging-v02.api.letsencrypt.org/directory"' | sudo tee /etc/dehydrated/conf.d/staging.sh

On configure dehydrated pour faire de la validation DNS-01 dans le fichier /etc/dehydrated/conf.d/auth-dns.sh :

CHALLENGETYPE="dns-01"
HOOK=/etc/dehydrated/hook-custom.sh

On créé le script hook /etc/dehydrated/hook-custom.sh :

#!/usr/bin/env bash

set 
set -u
set -o pipefail

NSUPDATE="nsupdate -k /etc/parinux/rndc-acme.key"
DNSSERVER="192.168.2.55"  # l'IP de troll4, le serveur DNS master
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")
        # on concatène le certificat et la clef pour ejabberd. C'est pas obligatoire mais c'est plus simple.
        cat /var/lib/dehydrated/certs/parinux.org/fullchain.pem /var/lib/dehydrated/certs/parinux.org/privkey.pem > /etc/ejabberd/parinux.org.ejabberd.pem
        sudo -u ejabberd ejabberdctl reload_config

        ;;
    "unchanged_cert")
        # do nothing for now
        ;;
    "startup_hook")
        # do nothing for now
        ;;
    "exit_hook")
        # do nothing for now
        ;;
esac

exit 0

Il faut rendre ce script exécutable :

sudo chmod +x /etc/dehydrated/hook-custom.sh

On indique le ou les domaines qu’on souhaite dans /etc/dehydrated/domains.txt :

parinux.org *.parinux.org

La première fois :

dehydrated --register --accept-terms

Puis on génère ou régénère les certificats :

dehydrated -c

Si tout s'est bien passé, alors pour peut générer un vrai certificat cette fois en supprimant le fichier /etc/dehydrated/conf.d/staging.sh :

rm /etc/dehydrated/conf.d/staging.sh
dehydrated --register --accept-terms
dehydrated -c --force # --force pour écraser le précédent certificat staging

Il faut maintenant créer une tâche cron pour renouveler le certificat automatiquement. On créé le fichier /etc/cron.weekly/acme-renew suivant :

#! /bin/bash
/usr/bin/dehydrated -c 

Et on le rend exécutable :

chmod +x /etc/cron.weekly/acme-renew

Créer le certificat avec Certbot

Cette méthode nécessite le plugin certbot-dns-rfc2136.

Installer les paquets nécessaires :

sudo apt install certbot python3-certbot-dns-rfc2136

Définir les variables qui seront utilisées par la suite :

export DOMAIN=mondomaine.net
export DNSSERVER=x.x.x.x

Créer le fichier /etc/letsencrypt/nsupdate-credentials-${DOMAIN}.ini :

cat << EOF | sudo tee /etc/letsencrypt/nsupdate-credentials-${DOMAIN}.ini
dns_rfc2136_server = ${DNSSERVER}
dns_rfc2136_port = 53
dns_rfc2136_name = rndc-acme-challenge.${DOMAIN}-key
dns_rfc2136_secret = xxxxxxxxxxxxxxxx
dns_rfc2136_algorithm = HMAC-SHA256
EOF

Avec des permissions restreintes :

sudo chmod 600 /etc/letsencrypt/nsupdate-credentials-${DOMAIN}.ini

Créer le script hook a exécuter après chaque renouvellement de certificat :

cat << EOF | sudo tee /etc/letsencrypt/renewal-hooks/post/reload-services.sh
#! /bin/sh
systemctl reload nginx
EOF

Et le rendre exécutable :

sudo chmod u+x /etc/letsencrypt/renewal-hooks/post/reload-services.sh

Puis lancer certbot certonly :

certbot certonly \
 --expand --non-interactive --agree-tos --email root@${DOMAIN} \
 --dns-rfc2136 \
 --dns-rfc2136-propagation-seconds 3 \
 --dns-rfc2136-credentials /etc/letsencrypt/nsupdate-credentials-${DOMAIN}.ini \
 -d "*.${DOMAIN}" -d ${DOMAIN} \
 --post-hook /etc/letsencrypt/renewal-hooks/post/reload-services.sh