Simplifier la génération de certificats SSL avec Let’s Encrypt

Lorsque que nous devons assurer nous-mêmes l’hébergement de certains projets (pour des clients ou des projets internes), nous utilisons Ansible pour provisionner les serveurs et/ou déployer nos applications. Au fil des années, nous avons rassemblé notre expérience avec Ansible au sein d’un outil interne qui se veut utilisable facilement par tout le monde chez nous, y compris les personnes moins à l’aise avec l’OPS.

Dans les nombreuses tâches proposées par notre outil se trouve celle qui permet de mettre en place le HTTPS, à savoir : l’utilisation de Let’s Encrypt pour générer les certificats SSL et le renouvellement automatique des certificats.

Grâce à quelques changements récents, nous avons pu simplifier cette étape de génération de certificats sans downtime. C’est l’occasion de montrer comment nous tirons parti de Let’s Encrypt et de partager quelques astuces qui pourront être utiles – que vous utilisiez, ou pas, Ansible et nginx.

Section intitulée le-fonctionnement-de-let-s-encryptLe fonctionnement de Let’s Encrypt

Section intitulée le-protocole-acmeLe protocole ACME

Let’s Encrypt a pour vocation de démocratiser l’utilisation du HTTPS et permet donc à tout le monde de générer des certificats SSL gratuitement et d’automatiser leur renouvellement.

Pour ce faire, Let’s Encrypt se base sur le protocole ACME qui définit comment une autorité de certification (ici Let’s Encrypt) peut vérifier qu’un client est bien le propriétaire d’un (ou plusieurs) domaine(s). Les deux méthodes (ou « challenges ») les plus couramment utilisées sont HTTP-01 et DNS-01.

Elles ont chacune leurs avantages et inconvénients (n’hésitez pas à consulter la documentation pour déterminer lesquels). Votre choix se fera en fonction de si votre port 80 est ouvert, si vous avez un seul ou plusieurs serveurs ou encore de si vous avez la main sur vos DNS. Pour les infrastructures traditionnelles (avec un seul serveur), il est généralement plus simple d’utiliser le challenge HTTP-01.

Section intitulée le-challenge-httpLe challenge HTTP

Pour simplifier, lorsqu’un client ACME va demander un certificat à l’api de Let’s Encrypt avec le challenge HTTP, il va recevoir un token. Ce token devra être rendu accessible à l’URL http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>.

Note : cela implique que le DNS de votre domaine pointe déjà sur votre serveur. Dans le cas d’une migration d’un domaine existant, nous vous recommandons plutôt de transférer le dossier /etc/letsencrypt (ou à minima les certificats SSL) de l’ancien serveur vers le nouveau pour ne pas avoir de downtime le temps que le nouveau serveur soit opérationnel.

Une fois que l’URL est disponible, Let’s Encrypt va l’appeler et vérifier son contenu. Si cela correspond au token transmis au client, alors le domaine est validé et un certificat SSL peut être généré et transmis au client.

Il ne reste qu’à indiquer à notre serveur web l’emplacement du certificat SSL reçu par le client. Il s’agit généralement du dossier /etc/letsencrypt/live/<YOUR_DOMAIN>/.

Section intitulée certbotCertbot

Il existe énormément de clients et bibliothèques compatibles avec le protocole ACME.

Celui recommandé par défaut est Certbot, un client fourni par l’EFF, l’un des organismes derrière Let’s Encrypt. Il est disponible sur la plupart des plateformes et se veut simple à l’usage.

certbot prend la forme d’un binaire disponible en ligne de commande. Il permet de générer un certificat pour un ou plusieurs domaines et supporte les challenges DNS et HTTP. Pour chaque challenge, plusieurs plugins sont disponibles pour simplifier l’installation en fonction de votre infrastructure, comme nous le verrons dans la suite.

Note : nous n’aborderons pas le sujet du renouvellement des certificats. Let’s Encrypt génère des certificats qui sont valides 3 mois. Il faut donc penser à configurer la partie renewal de Certbot pour automatiser le renouvellement dès que la date d’expiration approche.

Section intitulée notre-utilisationNotre utilisation

Avec l’architecture de nos rôles Ansible, nous distinguons trois scénarios lorsque nous voulons que Certbot génère un certificat SSL.

Ainsi, Certbot doit fonctionner :

  • si aucun serveur web ne tourne actuellement (par exemple, lorsque nous sommes en train de provisionner le serveur et que nginx n’est pas encore installé) ;
  • si nginx tourne et que le domaine est déjà géré par un virtualhost nginx dédié (c’est le cas lors du renouvellement de certificats d’un domaine déjà actif) ;
  • si nginx tourne mais que le domaine n’a pas encore de virtualhost (c’est le cas lorsque nous voulons ajouter un tout nouveau domaine sur le serveur).

Voyons comment traiter ces trois cas.

Section intitulée detecter-si-un-serveur-http-est-actifDétecter si un serveur HTTP est actif

Pour savoir via Ansible si nginx tourne déjà, nous utiliserons le module wait_for. Nous allons stocker le résultat dans une variable pour le réutiliser dans la suite :

- name: Check if port 80 is listening
  ansible.builtin.wait_for:
      port: 80
      timeout: 5
      msg: "Timeout waiting for 80 to respond"
  register: port_check
  failed_when: false

Si le check met moins de 5 secondes, alors c’est que le port 80 est déjà occupé. Sinon, c’est le timeout qui est atteint et le port 80 est disponible (ou que nginx a mis plus de 5 secondes à répondre, ce qui est forcément un cas d’erreur et nécessitera une investigation).

Section intitulée utiliser-le-bon-authenticatorUtiliser le bon authenticator

Certbot propose plusieurs modes pour effectuer le challenge HTTP. Les modes sont appelés « authenticator » et correspondent aux plugins à utiliser pour certifier que nous sommes propriétaires du domaine. Ici, nous sommes intéressés par 2 plugins en particulier, webroot et standalone.

Nous souhaitons utiliser standalone (certbot --authenticator standalone ou certbot --standalone) lorsqu’aucun serveur HTTP ne tourne encore. Dans ce mode, Certbot va démarrer son propre serveur qui écoute sur le port 80 et traiter les requêtes du challenge HTTP (aka les requêtes sur les URLs /.well-known/acme-challenge/xxx).

Mais si nginx est actif, alors nous utiliserons le mode webroot (certbot --authenticator webroot ou certbot --webroot). Cette fois, on demande à Certbot d’écrire les fichiers à servir sous l’URL du challenge (toujours /.well-known/acme-challenge/xxx) directement dans le webroot du serveur web en question. C’est donc nginx qui répondra au challenge par la suite avec les fichiers qu’il a à disposition dans ce dossier.

Avec Ansible, cela nous donne le code suivant :

- name: Attempt to get the certificate using the standalone authenticator, in case eg the webserver isn't running yet
  ansible.builtin.command: >-
      certbot
      --agree-tos
      -d {{ domain }}
      --email {{ email }}
      --expand
      --authenticator standalone
  args:
      creates: "/etc/letsencrypt/live/{{ domain }}"
  failed_when: false
  when:
      - port_check.elapsed >= 5

- name: Attempt to get the certificate using the webroot authenticator
  ansible.builtin.command: >-
      certbot
      --agree-tos
      -d {{ domain }}
      --email {{ email }}
      --expand
      --authenticator webroot
      --webroot-path {{ webroot_nginx }}
      certonly
  args:
      creates: "/etc/letsencrypt/live/{{ domain }}"
  failed_when: false
  when:
      - port_check.elapsed < 5

Nous avons traité le cas où un serveur web tourne déjà ou non. Mais si nous nous arrêtons là, nous ne sommes pas capables de demander un certificat pour un domaine qui ne serait pas encore configuré avec un virtualhost (et donc, l’url du challenge HTTP finirait avec une erreur 404 de nginx). Pour éviter ça, il faudrait couper nginx sur le serveur, et relancer la tâche Ansible pour que certbot et son mode standalone puisse prendre la main et traiter lui-même les requêtes entrantes. Mais cela implique que toutes les applications hébergées actuellement sur le serveur ne seront plus accessibles. Nous allons maintenant faire une dernière modification pour éviter ça.

Section intitulée un-webroot-unique-pour-les-controler-tousUn webroot unique pour les contrôler tous 💍

Dans notre infrastructure, nous mettons toujours un virtualhost par défaut. Celui-ci permet d’afficher une page minimaliste expliquant qu’aucun site web n’est disponible à cette adresse plutôt qu’une page d’erreur brute générée par nginx. Elle est utilisée, par exemple, lorsque l’on accède à l’IP du serveur ou à un nom de domaine qui pointe sur le serveur mais pas encore géré par nginx.

server {
  listen 80 default;
  server_name _;

  root /var/www/default;

  charset utf-8;
  index index.html;
}

L’astuce ici consiste à mutualiser le dossier où seront déposés les fichiers du challenges HTTP. Et ce pour tous les noms de domaines, mais aussi pour le virtualhost par défaut. Nous allons donc ajouter le bloc location suivant dans tous nos virtualhosts :

location ^~ /.well-known/acme-challenge/ {
    default_type "text/plain";
    root {{ acme_challenge_path }};
}

# Hide /acme-challenge subdirectory and return 404 on all requests.
# Ending slash is important!
location = /.well-known/acme-challenge/ {
    return 404;
}

Puis on modifie notre commande Certbot pour adapter le chemin du webroot :

- name: Attempt to get the certificate using the webroot authenticator
  ansible.builtin.command: >-
      certbot
      # …
      --authenticator webroot
      --webroot-path {{ acme_challenge_path }}
      # …

Une fois que nginx sera rechargé avec cette config, nous aurons le comportement souhaité.

Imaginons que nous voulions générer un certificat pour un nom de domaine mon-domaine.com. Lorsque que nous allons lancer Certbot pour ce nom de domaine, alors Certbot écrira les fichiers requis pour le challenge HTTP dans le dossier {{ acme_challenge_path }}.

Puis, quand Let’s Encrypt appelera l’URL http://mon-domaine.com/.well-known/acme-challenge/<TOKEN>, alors :

  • si le domaine mon-domaine.com a déjà un virtualhost de configuré dans nginx, alors c’est ce virtualhost qui sera utilisé et nginx servira les fichiers présents dans le dossier mutualisé ;
  • si le domaine n’est pas encore configuré, alors c’est le virtualhost par défaut qui sera utilisé par nginx, et là aussi, ce sont les fichiers présents dans le dossier mutualisé qui seront servis.

Section intitulée pour-finirPour finir

Avec cet article, nous avons vu comment fonctionne la génération de certificats SSL avec Let’s Encrypt. Avec quelques astuces simples, il est possible d’obtenir des certificats dans tous les scénarios, que votre serveur web tourne déjà ou non, et que le nom de domaine soit déjà géré ou non. Même si cet article donne des exemples pour Ansible et nginx, il devrait être facilement adaptable dans d’autres situations.

Commentaires et discussions