====== Certificats Letsencrypt via DNS ====== La création de certificats Letsencrypt se fait traditionnellement par authentification HTTP en utilisant le client [[https://certbot.eff.org/|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 [[https://dehydrated.io/|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 : * votre client ACME (certbot, dehydrated, autre) contacte le serveur CA ACME * le serveur ACME lui retourne une chaine de caractères générée pour l’occasion * le client ACME rend alors cette chaine de caractères publique : * soit à une URL http://ledomaine.net/.well-known/acme-challenge dans le cas d’une validation HTTP-01 * soit dans un enregistrement DNS de type TXT *_acme-challenge.ledomaine.net* dans le cas d’une validation par DNS-01 * le client ACME prévient le serveur ACME que ça a été publié * le serveur ACME est désormais sûr qu’il avait affaire à quelqu’un de légitime valide alors le requête, génère le certificat demandé initialement et le transmet au client ACME 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 [[https://github.com/lukas2511/dehydrated/wiki/example-dns-01-nsupdate-script|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 < 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 [[https://certbot-dns-rfc2136.readthedocs.io/en/stable/|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