398 lines
13 KiB
Markdown
398 lines
13 KiB
Markdown
---
|
||
categories: web security
|
||
title: 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/>
|
||
|
||
[Let's Encrypt](https://letsencrypt.org/) est une autorité de certification fournissant des certificats [SSL](HowtoSSL) valables 90 jours gratuitement et automatiquement.
|
||
L'obtention d'un certificat signé s'effectue via le protocole [ACME (Automated Certificate Management Environment)](https://en.wikipedia.org/wiki/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](https://www.identrust.com/) préinstallée dans la plupart des navigateurs.
|
||
|
||
|
||
# Installation
|
||
|
||
On installe certbot et ses dépendances :
|
||
|
||
~~~
|
||
# apt install certbot
|
||
# chmod 755 /var/lib/letsencrypt/
|
||
|
||
$ certbot --version
|
||
certbot 1.12.0
|
||
~~~
|
||
|
||
|
||
## 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 :
|
||
|
||
~~~{.apache}
|
||
<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
|
||
~~~
|
||
|
||
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`):
|
||
|
||
~~~{.bash}
|
||
#!/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`) :
|
||
|
||
~~~{.bash}
|
||
#!/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](https://gitea.evolix.org/evolix/ansible-roles/src/branch/stable/certbot/files/hooks).
|
||
|
||
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
|
||
~~~
|
||
|
||
|
||
## 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](https://forge.evolix.org/projects/ansible-roles/repository/revisions/stable/show/certbot).
|
||
|
||
|
||
## 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 :
|
||
|
||
~~~bash
|
||
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 :
|
||
|
||
~~~bash
|
||
apt install certbot
|
||
~~~
|
||
|
||
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.
|
||
~~~
|
||
|
||
|
||
# Troubleshooting
|
||
|
||
|
||
## 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 :
|
||
|
||
~~~.diff
|
||
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`.
|
||
|
||
|