====== Certificat Let's Encrypt wildcard avec challenge DNS ====== L'article décrit comment obtenir un certificat Let's Encrypt * Indépendant d'un serveur web (nginx, apache, ...): Afin de pouvoir réutiliser les certificats librement et sur plusieurs services * Wildcard (*.mondomain.tld) et domaine de base: Ainsi pas besoin de rejouer la certification à chaque nouveau service * Challenge par DNS intermédiaire supportant API: Pour 2 raisons : * Que notre serveur de base fournisse une API ou pas, on ne s'en préoccupe pas * La clé API utilisé sur notre DNS intermédiaire ne couvre QUE le l'entrée DNS de certification. En cas de fuite c'est limité à celui-ci Le programme fournira en sortie les certificats *.mondomain.tld et mondomain.tld. On pourra faire pointer nos services directement dessus ou les fournir aux différents serveurs. Note: Un certificat Let's Encrypt a une durée de vie de 90 jours. Il est généralement renouvellé 30 jours avant son expiration. Dans mon cas mes services redémarrent au moins une fois par semaine, je ne me préoccupe donc pas de redémarrer les services au moment du changement de certificat, ceux-ci chargerons les nouveaux certificats de manière asynchrone au moment du redémarrage. ===== Préparation du système ===== apt install python3 python3-venv On va créer un compte dédié à l'usage de certbot : adduser --system --group --no-create-home --shell /usr/sbin/nologin certbot addgroup certssl-user ''--system'' : Compte système, possède un UID/GID bas, afin de séparer des utilisateurs interactif \\ ''--group'' : Crée un groupe éponyme \\ ''--no-create-home'' : N'aura pas de home directory, inutile ici \\ ''--shell /usr/sbin/nologin'' : Le binaire nologin empêche d'ouvrir un shell si on s'y connect L'utilisateur n'aura pas de mot de passe non plus, car inaccessible. Le groupe ''certssl-user'' permettra d'autoriser qui peut consulter les certificats Le dossier de travail sera ''/opt/certbot'', on va créer le nécessaire à cet endroit. En l'occurence un environnement virtuel python avec le package certbot installé # Création du répertoire de travail mkdir /opt/certbot cd /opt/certbot # Création de l'environnement virtuel python3 -m venv . bin/pip install --upgrade pip # Installation de certbot bin/pip install certbot # certbot travaillera dans le dossier letsencrypt, on lui crée ses dossiers mkdir -p letsencrypt/{conf,work,log} La structure est maintenant faite ===== Configuration et automatisation des entrées DNS ===== Le service DNS de [[https://console.hetzner.com/|Hetzner]] servira de DNS intermédiaire pour nous fournir un sous-domaine avec une API DNS. Sur le site vous devrez : * Créer un projet (Nom libre) * Dans la partie DNS du projet, vous ajoutez votre nom de domaine * **Note**: Il indiquera "It looks like your domain is pointing to different nameservers.", ceci est normal et nous lui déléguerons uniquement le sous-domaine _acme-challenge utile à Let's Encrypt * Créer une zone type *TXT* nommé "_acme-challenge", avec une valeur libre et un TTL de 60 secondes * Dans la partie Security > API tokens, vous aurez besoin d'une clé API avec accès en écriture (Read/Write) ==== Fichier de configuration ==== On va réunir la configuration utilisé dans les scripts dans un fichier **.env** DOMAIN="mondomain.tld" TOKEN="MON-TOKEN-A-REMPLACER" ==== Mise à jour automatique du sous-domaine chez Hetzner ==== Ce script sera appelé par Certbot pour mettre à jour le challenge chez Hetzner. #!/usr/bin/env bash curl "https://api.hetzner.cloud/v1/zones/${DOMAIN}/rrsets/_acme-challenge/TXT/actions/add_records" \ --request POST \ --header "Content-Type: application/json" \ --header "Authorization: Bearer ${TOKEN}" \ --data "{ \"ttl\": 300, \"records\": [ { \"value\": \"\\\"${CERTBOT_VALIDATION}\\\"\", \"comment\": \"Certbot Script - $(date)\" } ] }" sleep 30 On n'oublie pas de le rendre exécutable chmod +x hook-duckdns.sh A noter que la variable ''CERTBOT_VALIDATION'' sera une variable d'environnement passé par certbot. Pour valider le bon fonctionnement : export CERTBOT_VALIDATION="Exemple de challenge de Certbot" ./hook-hetzner.sh unset CERTBOT_VALIDATION Doit retourner : { "action": { "id": XXXXXXX, "status": "running", "command": "set_rrset_records", "progress": 0, "started": "2026-04-24T15:41:15Z", "finished": null, "error": null, "resources": [ { "id": XXXXXXX, "type": "zone" } ] } } ==== Nettoyage du sous-domaine chez Hetzner ==== Il est recommandé (mais pas obligatoire) de nettoyer l'enregistrement DNS après validation par Let's Encrypt. Tout comme pour le script précédent, Certbot propose d'appeler un script pour le nettoyage, que voici : #!/usr/bin/env bash curl "https://api.hetzner.cloud/v1/zones/${DOMAIN}/rrsets/_acme-challenge/TXT" \ --request DELETE \ --header "Authorization: Bearer ${TOKEN}" ==== Configuration du domaine principal ==== Le domaine principal doit pouvoir déléguer ''_acme-challenger.mondomain.tld'' vers Hetzner, ce qui est le rôle des enregistrement de type **NS**. Sur votre console Hetner, dans la partie DNS > mondomain.tld > Name servers, Hetner indique les serveurs DNS qui contient notre enregistrement, dans mon cas : ''hydrogen.ns.hetzner.com.'', ''helium.ns.hetzner.de.'', ''oxygen.ns.hetzner.com.'' (Potentiellement différent chez vous) Chez votre registrar, vous devrez ajouter ces serveurs comme record de type **NS** pour le sous-domaine _acme-challenge : _acme-challenge IN NS hydrogen.ns.hetzner.com. _acme-challenge IN NS helium.ns.hetzner.de. _acme-challenge IN NS oxygen.ns.hetzner.com. Pour valider le bon fonctionnement : dig _acme-challenge.mondomain.tld TXT ;; ANSWER SECTION: _acme-challenge.gh3.be. 268 IN TXT "Exemple de challenge de Certbot" ===== Automatisation de Certbot ===== Tout est en place pour que Certbot puisse travailler. Le certificat n'étant valable que 90 jours, il est obligatoire d'automatiser son renouvellement. On créer un script également pour ceci : #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(dirname "$0")" CONF_DIR="letsencrypt/conf/" WORK_DIR="letsencrypt/work/" LOGS_DIR="letsencrypt/log/" echo \> "$(date)": Starting renew script cd "${SCRIPT_DIR}" if [[ ! -f ".env" ]]; then echo "Fichier .env manquant" exit 1 fi set -a source .env set +a echo \> Renew \[*.\]${DOMAIN} bin/certbot --config-dir "${CONF_DIR}" --work-dir "${WORK_DIR}" --logs-dir "${LOGS_DIR}" \ certonly --verbose --non-interactive --agree-tos --manual \ --preferred-challenges dns --domains "${DOMAIN}" --domains "*.${DOMAIN}" \ --manual-auth-hook ./hook-hetzner.sh \ --manual-cleanup-hook ./hook-hetzner-cleanup.sh echo \> Publish new *.pem files to directory cert/${DOMAIN} if [ ! -d "cert/${DOMAIN}" ]; then mkdir -p "cert/${DOMAIN}" fi cp -rfL "${CONF_DIR}/live/${DOMAIN}/*.pem" "cert/${DOMAIN}/" chmod 0640 "cert/${DOMAIN}/*" Après l'avoir rendu exécutable, vous pouvez le lancer. chmod +x certbot-renew.sh ./certbot-renew.sh ==== Sécurisation du répertoire ==== Mes tests ont été fait avec un utilisateur interactif par facilité. Mettons en place la sécurité nécessaire # L'utilisateur certbot devient propriétaire du dossier (récursif) chown -R certbot: /opt/certbot # Droit restreint sur les fichiers et dossiers cd /opt/certbot find . -type d -exec chmod 750 {} \; find . -type f -exec chmod 640 {} \; chmod 700 *.sh chmod 600 .env # Le groupe certssl-user est autorisé sur le dossier cert chown -R certbot:certssl-user cert chown certbot:certssl-user . ==== Crontab ==== Je trouve qu'une fois semaine est un bon compromis. Dès qu'il restera 30 jours au certificat, il sera au maximum dans les 7 prochains jours. Certbot ne tentera rien si le certificat est trop récent. crontab -e -u certbot # Renouvellement certificat Let's Encrypt # Chaque dimanche à 2h14 14 2 * * 0 /opt/certbot/certbot-renew.sh > /opt/certbot/cron.log En cas de problème, le log sera disponible dans cron.log. ===== Conclusion ===== Le script n'est pas parfait, n'est pas hyper sécurisé et il y a forcément moyen de faire mieux/optimisé... MAIS, je l'utilise depuis maintenant des années (un peu amélioré pour l'article et initialement avec DuckDNS), et il ne m'a jamais fait défaut. CEPENDANT ! Il ne dispense pas d'un monitoring de l'état du certificat SSL sur le site web où il sera utilisé avec une alerte s'il expire prochainement (- de 15 jours), signe d'un problème.