Howto Let's Encrypt
- Documentation : https://letsencrypt.org/docs/
- Documentation Certbot : https://certbot.eff.org/docs/
- Rôle Ansible : https://gitea.evolix.org/evolix/ansible-roles/src/branch/stable/certbot/
- Statut de cette page : prod / bookworm
Let’s Encrypt est une autorité de certification fournissant des certificats SSL valables 90 jours gratuitement et automatiquement. L’obtention d’un certificat signé s’effectue via le protocole ACME (Automated Certificate Management Environment), ce qui nécessite d’avoir temporairement un serveur HTTP sur le port TCP/80 de l’adresse IP correspondant au Common Name du certificat à signer. Le certificat intermédiaire de Let’s Encrypt est également signé par l’autorité de certification IdenTrust préinstallée dans la plupart des navigateurs.
Installation
On installe certbot et ses dépendances :
# apt install certbot
# mkdir -p /var/lib/letsencrypt/
# chmod 755 /var/lib/letsencrypt/
$ certbot --version
certbot 2.1.0
Et l’on place les « renewal hooks » nécessaires (à
trouver sur notre Ansible.) dans
/etc/letsencrypt/renewal-hooks/deploy/
avec les bons
droits, par exemple :
# ls -l /etc/letsencrypt/renewal-hooks/deploy/
-rwxr-x--- 1 root root 1043 Aug 17 2023 apache.sh
-rwxr-x--- 1 root root 1058 Aug 17 2023 postfix.sh
-rwxr-x--- 1 root root 1375 Aug 17 2023 z-commit-etc.sh
Ils seront exécutés lors d’un renouvellement de certificat, par exemple pour redémarrer Apache ou Postfix.
Note : le script
manual-deploy.sh
permet de relancer l’ensemble des « renewal hooks » et particulièrement si l’on a un hook pour déployer sur d’autres serveurs : cela permet de déployer des certificats non générés par Let’s Encrypt, ou alors de déployer lors de la création d’un nouveau certificat (il faudra alors utiliser l’option--deploy-hook /etc/letsencrypt/renewal-hooks/manual-deploy.sh
)
Configuration du Serveur Web
Pour vérifier que la demande est légitime, Let’s Encrypt doit accéder à un fichier temporaire via HTTP sur le port TCP/80 de l’adresse IP correspondant au Common Name du certificat à signer, du type http://www.example.com/.well-known/acme-challenge/hNgN_ygEFf-XiHJd6VErwNbfRcpP2CbJmIN3qpJXZOQ
Apache
Il faut inclure la configuration globale, par exemple via
/etc/apache2/conf-available/letsencrypt.conf
:
<IfModule jk_module>
SetEnvIf Request_URI "/.well-known/acme-challenge/*" no-jk
</IfModule>
<IfModule proxy_module>
ProxyPass /.well-known/acme-challenge/ !
</IfModule>
Alias /.well-known/acme-challenge /var/lib/letsencrypt/.well-known/acme-challenge
<Directory "/var/lib/letsencrypt/.well-known/acme-challenge">
Options -Indexes
Require all granted
</Directory>
Nginx
Il faut créer la configuration
/etc/nginx/snippets/letsencrypt.conf
ainsi :
location ~ /.well-known/acme-challenge {
alias /var/lib/letsencrypt/;
try_files $uri =404;
allow all;
}
puis inclure cette configuration dans chaque « VirtualHost » :
include /etc/nginx/snippets/letsencrypt.conf;
Utilisation
Génération du certificat
# certbot certonly --webroot --webroot-path /var/lib/letsencrypt/ -d example.com,www.example.com --cert-name example.com --register-unsafely-without-email
Options intéressantes :
--key-type rsa
: pour avoir une clé privée de type RSA au lieu de ECDSA (permettant d’avoir des ciphers différents comme expliqué sur ce blog)
Génération en test
Pour interroger les serveurs de “staging” de Let’s Encrypt – afin de
tester la validation d’une demande sans risque – on peut ajouter
l’option --test-cert
. Tout le processus est identique à
l’appel normal, mais le certificat généré sera non-signé par une
autorité reconnue.
Pour faire les actions normalement sauf la génération du certificat
et la modification locale des fichiers, il est possible d’utiliser
l’option --dry-run
.
En combinant --test-cert
et --dry-run
on
peut donc faire un test de génération de certificat sans modification
locale et sans risque de pénalisation en cas d’échecs répétés.
Renouvellement du certificat
Les certificats Let’s Encrypt sont valables 90 jours. Un timer systemd roule automatiquement à chaque jour pour revalider le certificat.
Pour effectuer des actions post-renouvellement (recharger le service web, par exemple), on repose sur les hooks. Ils sont automatiquement exécutés s’ils sont détectés et exécutables.
Pour Apache
(/etc/letsencrypt/renewal-hooks/deploy/apache.sh
):
#!/bin/sh
error() {
>&2 echo "${PROGNAME}: $1"
exit 1
}
debug() {
if [ "${VERBOSE}" = "1" ] && [ "${QUIET}" != "1" ]; then
>&2 echo "${PROGNAME}: $1"
fi
}
daemon_found_and_running() {
test -n "$(pidof apache2)" && test -n "${apache2ctl_bin}"
}
config_check() {
${apache2ctl_bin} configtest > /dev/null 2>&1
}
letsencrypt_used() {
grep -q -r -E "letsencrypt" /etc/apache2/
}
main() {
if daemon_found_and_running; then
if letsencrypt_used; then
if config_check; then
debug "Apache detected... reloading"
systemctl reload apache2
else
error "Apache config is broken, you must fix it !"
fi
else
debug "Apache doesn't use Let's Encrypt certificate. Skip."
fi
else
debug "Apache is not running or missing. Skip."
fi
}
readonly PROGNAME=$(basename "$0")
readonly VERBOSE=${VERBOSE:-"0"}
readonly QUIET=${QUIET:-"0"}
readonly apache2ctl_bin=$(command -v apache2ctl)
main
Pour Nginx
(/etc/letsencrypt/renewal-hooks/deploy/nginx.sh
) :
#!/bin/sh
error() {
>&2 echo "${PROGNAME}: $1"
exit 1
}Ticket #59956: Renouvelement certificat *.stprovence.fr
debug() {
if [ "${VERBOSE}" = "1" ] && [ "${QUIET}" != "1" ]; then
>&2 echo "${PROGNAME}: $1"
fi
}
daemon_found_and_running() {
test -n "$(pidof nginx)" && test -n "${nginx_bin}"
}
config_check() {
${nginx_bin} -t > /dev/null 2>&1
}
letsencrypt_used() {
grep -q --dereference-recursive -E "letsencrypt" /etc/nginx/sites-enabled
}
main() {
if daemon_found_and_running; thenTicket #59956: Renouvelement certificat *.stprovence.fr
if letsencrypt_used; then
if config_check; then
debug "Nginx detected... reloading"
systemctl reload nginx
else
error "Nginx config is broken, you must fix it !"
fi
else
debug "Nginx doesn't use Let's Encrypt certificate. Skip."
fi
else
debug "Nginx is not running or missing. Skip."
fi
}
readonly PROGNAME=$(basename "$0")
readonly VERBOSE=${VERBOSE:-"0"}
readonly QUIET=${QUIET:-"0"}
readonly nginx_bin=$(command -v nginx)
main
D’autres hooks sont disponibles dans le dépôt Ansible.
Pour exécuter un hook automatiquement dès la création du certificat :
$ certbot certonly --webroot --webroot-path /var/lib/letsencrypt/ --deploy-hook /etc/letsencrypt/renewal-hooks/deploy/<hook>.sh -d example.com,www.example.com --cert-name example.com
Pour exécuter un hook manuellement, par exemple après la création du certificat :
$ VERBOSE=1 RENEWED_LINEAGE=/etc/letsencrypt/live/<cert-name> /etc/letsencrypt/renewal-hooks/deploy/<hook>.sh
Ajouter des domaines au certificats
Il y a besoin de préciser le nom du certificat avec les anciens et nouveaux domaines à certifier :
certbot certonly --webroot --webroot-path /var/lib/letsencrypt/ --cert-name domaine.net --expand -d nouveau-domaine.net,ancien-domaine.net --test-cert --dry-run
Liste des certificats
# certbot certificates
Suppression d’un certificat
# certbot delete --cert-name www.example.com
Automatisation
Pour automatiser l’installation de Certbot et la génération/renouvellement de certificats, nous utilisons des scripts : Evoacme.
Challenge DNS
Si notre serveur web n’est pas accessible de l’extérieur, on peut utiliser un challenge par DNS plutôt que via une page HTTP.
$ certbot -d domain.example.com --manual --preferred-challenges dns certonly
Il suffira alors de créer l’entrée DNS que certbot affichera et continuer le processus interactif.
Mises-à-jour
Mise-à-jour de certbot vers Debian 8 (suite a l’arrêt du protocole ACMEv1)
On désinstalle le paquet certbot :
# apt remove certbot
# mv /usr/local/bin/certbot /usr/local/bin/certbot.bak
On s’assure que les unités systemd sont supprimés :
# rm /etc/systemd/system/certbot.service
# rm /etc/systemd/system/certbot.service.d
# rm /etc/systemd/system/certbot.timer
# systemctl daemon-reload
Il faut remplacer certbot par le script letsencrypt-auto comme ceci :
# wget https://raw.githubusercontent.com/certbot/certbot/master/letsencrypt-auto-source/letsencrypt-auto /usr/local/bin/
# chmod 755 /usr/local/bin/letsencrypt-auto
# cat > /usr/local/bin/certbot <<EOF
#!/bin/sh
letsencrypt-auto --no-self-upgrade \$@
EOF
# chmod 755 /usr/local/bin/certbot
Mise-à-jour de certbot vers Debian 9
Si /usr/local/bin/certbot
est présent :
mv /usr/local/bin/certbot /usr/local/bin/certbot.bak
hash -d certbot # renouvelle le cache des exécutables de bash
Dans /etc/letsencrypt/cli.ini
, si la ligne
no-self-upgrade = 0
, commentez-la.
Réinstallez ou mettez-à-jour la version de certbot présent dans les dépôts Debian :
A présent, testez votre installation avec la commande
certbot certificates
.
En l’abence ou en cas d’oubli des modifications précédentes, on rencontre les erreurs :
Skipping bootstrap because certbot-auto is deprecated on this system.
Your system is not supported by certbot-auto anymore.
Certbot cannot be installed.
Please visit https://certbot.eff.org/ to check for other alternatives.
FAQ
Certificat Wildcard ou EV
Let’s Encrypt permet de générer un certificat Wildcard (via challenge DNS) mais pas EV (Extended Validation).
Je n’ai pas de port TCP/80 accessible
Let’s Encrypt vérifie la légitimité de la demande en faisant une requête DNS sur l’enregistrement DNS correspondant au Common Name du certificat à signer (si il n’y a pas d’enregistrement DNS propagé mondialement, c’est donc impossible d’utiliser Let’s Encrypt) et effectue une requête HTTP vers le port TCP/80 de l’adresse IP : il faut donc obligatoirement avoir temporairement un service HTTP sur le port TCP/80… au moins temporairement.
Rate-limits
Doc officielle : https://letsencrypt.org/docs/rate-limits/
L’API de Let’s Encrypt dispose un « rate limiting » afin d’éviter les abus.
- 20 certificats pour 1 domaine maximum par semaine. 1 domaine est la partie juste avant le TLD. www.example.com, le domaine est example.com. blog.exemple.fr, le domaine est exemple.fr ; Si vous faites 1 certificat par sous-domaines, vous êtes donc limités à 20 par semaine. Il est donc préférable de regrouper plusieurs sous-domaines dans un seul certificat (SAN). La limite du SAN est de 100 sous-domaines. La limite est donc de 2000 certificats pour 1 domaine par semaine si vous mettez 100 sous-domaines par certificats.
- La limite de renouvellement d’un certificat est de 5 par semaine. Si votre procédure de renouvellement automatique échoue, au bout de 5 jours vous allez être banni pendant 1 semaine.
- La limite d’échec de validation du challenge est de 5 par heures (par adresse IP et utilisateur enregistré par email) ;
Si vous échouez 5 fois à valider le challenge DNS, on peut penser qu’il suffit d’attendre 1h, sauf qu’en fait non. Le « bannissement » est pour 1 semaine !
Erreur de renouvellement liée à webroot_path
Il arrive que la configuration de renouvellement d’un certificat “déconne” et perde une partie de la configuration, entrainant une impossibilité de renouvellement.
Ça se matérialise par une erreur de ce type :
Attempting to renew cert (example.com) from /etc/letsencrypt/renewal/example.com.conf produced an unexpected error: Missing command line flag or config entry for this setting:
Input the webroot for example.com:. Skipping.
Il est fort probable que la configuration du
webroot_path
ait disparu du fichier
/etc/letsencrypt/renewal/example.com.conf
.
Voici le correctif :
authenticator = webroot
-webroot_path = /var/lib/letsencrypt,
server = https://acme-v02.api.letsencrypt.org/directory
[[webroot_map]]
+www.example.com = /var/lib/letsencrypt
+example.com = /var/lib/letsencrypt
Incident avec le certificat DST X3 du 30 septembre 2021
Le 30 septembre 2021, le certificat DST X3 d’IdenTrust a expiré, provoquant de nombreux effets de bord. Plus d’explications sur https://blog.evolix.com/expiration-du-certificat-identrust-dst-x3-et-lets-encrypt/
Concrètement, pour les clients HTTPS on conseille de bien mettre à
jour openssl
(qui est moins strict dans les versions
récentes ce qui corrige certains problèmes) et de bannir le certificat
X3 du keystore local :
# vim /etc/ca-certificates.conf
!mozilla/DST_Root_CA_X3.crt
# update-ca-certificates
Pour les serveurs HTTPS, par défaut Let’s Encrypt ajoute un certificat un peu foireux dans la chaîne de certification, mais cela peut poser des problèmes à des vieux clients HTTPS notamment clients d’API, callback de paiement, anciennes versions de NodeJS ou PHP Guzzle, etc.
On peut contourner le problème est retirant le certificat
problèmatique dans fullchain.pem
et chain.pem
(puis rechargeant les démons qui utilisent ces certificats) :
-----BEGIN CERTIFICATE-----
MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
...
he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC
Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5
-----END CERTIFICATE-----
Attention, au prochain renouvellement Let’s Encrypt celui-ci va revenir !
Pour le bannir définitivement, il faut avoir un certbot récent et
utiliser l’option –preferred-chain
.
Erreur : Unexpected value for no-self-upgrade
Si vous rencontrez l’erreur suivante alors que vous n’avez pas
fournit l’option --no-self-upgrade
en argument :
certbot: error: Unexpected value for no-self-upgrade: '0'. Expecting 'true', 'false', 'yes', or 'no'
Dans /etc/letsencrypt/cli.ini
, si elle est présente,
commentez la ligne no-self-upgrade = 0
.
Clé privée en RSA ou ECDSA ?
Par défaut, la clé privée est de type ECDSA. On peut vouloir une clé
privée de type RSA (--key-type rsa
à la création) notamment
pour pouvoir proposer des ciphers spécifiques comme expliqué
sur ce blog.