Génération de certificats TLS LetsEncrypt sur plusieurs instances Traefik en DNS Round Robin (FR)
lun. 24 janvier 2022 Dan LousquiGénération de certificats TLS LetsEncrypt sur plusieurs instances Traefik en DNS Round Robin
Présentation des acteurs
Lorsque l'on souhaite déployer un projet en utilisant Docker, une question à se poser est de comment rendre l'application disponible sur Internet. Traefik est un outil qui permet de répondre à cette problématique en déployant un applicatif frontal (exposant les ports HTTP et HTTPS) jouant un rôle de reverse-proxy entre les applications à publier et Internet.
Lorsque l'on souhaite sécuriser un minimum son application, il convient de fournir un moyen permettant d'assurer la confidentialité entre les clients et le serveur. Cela se fait généralement par SSL/TLS en utilisant HTTPS. Traefik permet d'automatiser cette sécurisation via l'utilisation de LetsEncrypt, assurant la génération et la rotation de certificats.
Lorsque l'on souhaite assurer un service fiable sur son application, on parle de haute disponibilité. Pour ce faire, on va généralement monter plusieurs instances de son application sur plusieurs serveurs différents, afin que si l'un des serveurs tombe en panne, d'autres peuvent prendre le relai. Une façon d'implémenter cela est le DNS Round Robin, qui consiste à faire en sorte qu'en requetant le serveur, le serveur DNS, en charge de donner l'adresse IP du serveur, renverra arbitrairement l'adresse IP d'une des instances du service permettant ainsi d'équilibrer la charge sur les serveurs et de couper l'accès aux serveurs en panne.
LetsEncrypt rentre sur scène
Dans ce schéma, un élément important est le HTTPS. Le fonctionnement de HTTPS passe par la génération de certificats qui doivent être signés par une autorité de certification. Le plus simple (et le plus économe) aujourd'hui, est de passer par des mécanismes de type LetsEncrypt.
Ainsi, lorsque le serveur souhaite générer ou renouveler un certificat, le serveur notifie une instance LetsEncrypt. S'ensuit alors un mécanisme de challenge afin de prouver que la génération est bien légitime, et que la personne qui fait la demande de certificat maitrise bien le domaine et/ou le serveur sous-jacent.
Nous avons un problème ici. Lorsque l'une des instances de notre application souhaite initier un challenge avec LetsEncrypt, lorsque LetsEncrypt souhaitera se connecter au serveur, rien ne garantie que la connexion se fera sur la bonne instance de notre application.
En effet, lorsque LetsEncrypt contactera le serveur, il effectuera préalablement une requête DNS, ayant une possibilité de renvoyer le bon serveur (premier schéma), ou un autre (deuxième schéma).
Différents mécanismes de challenges
LetsEncrypt propose plusieurs mécanismes de challenge, tous implémentés dans Traefik :
- Le challenge DNS-01, qui nécessite de donner à Traefik des droits d'écritures sur le DNS, ce qui n'est pas acceptable dans beaucoup de contexte ;
- Le challenge TLS-ALPN-01, qui s'intègre directement dans le protocole TLS, et nécessite une connexion directe entre le serveur qui fait la demande de certificat et LetsEncrypt, ce qui est très complexe à faire dans un contexte de DNS Round Robin ;
- Le challenge HTTP-01, qui se fait sur le port
HTTP/80, où LetsEncrypt effectuera une requête vers
http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>
, où la réponse doit être<TOKEN>.<KEY_THUMBPRINT>
, où<KEY_THUMBPRINT>
est un condensat de la clé d'authentification à LetsEncrypt (JSON Web Key Thumbprints). À noter que cette clé n'est pas la clé utilisée dans les certificats qui seront générées.
L'idée est donc :
- de partager la clé d'authentification à LetsEncrypt entre toutes les instances ;
- d'utiliser le challenge HTTP-01 ;
- d'ajouter un service qui répond au challenge LetsEncrypt à la place de Traefik (sinon le Traefik d'une instance ne répondra pas au challenges d'une autre instance).
Let's go !
Note : ce qui suit part du principe que la configuration LetsEncrypt avec un Traefik est déjà fonctionnelle, nous allons faire en sorte que les générations de certificats fonctionnent dans un cadre de DNS Round Robin. Si ce n'est pas le cas, il suffit de se référer à la documentation de Traefik.
Partager la clé d'authentification à LetsEncrypt entre toutes les instances
Dans le fichier de configuration de Traefik (généralement traefik.toml
) doit se trouver une ligne avec le fichier
contenant l'ensemble des clés et certificats de l'instance :
[certificatesResolvers.sample.acme]
# ...
storage = "/etc/traefik/acme.json"
# ...
Afin de partager la clé d'authentification, il suffit de partager ce fichier JSON entre toutes les instances de l'application.
Utiliser le challenge HTTP-01
Normalement, pour activer le challenge HTTP-01 sur LetsEncrypt, il suffit d'ajouter l'option
certificatesResolvers.sample.acme.httpChallenge
avec un entrypoint
joignable depuis Internet. Sauf qu'ici, nous
allons créer notre propre service de réponse au challenge. Nous devons donc avoir un entrypoint
qui n'est pas
joignable ! Pour cela, nous allons créer un entrypoint
appelé devnull
, qui ne servira à rien. Si Traefik est
utilisé via Docker (ce qui est recommandé), il ne faudra pas exposer le port 8000
.
Les éléments suivants sont à mettre dans la configuration de Traefik :
[entryPoints]
# ...
[entryPoints.devnull]
address = ":8000"
# ...
[certificatesResolvers]
[certificatesResolvers.sample]
[certificatesResolvers.sample.acme]
# ...
[certificatesResolvers.sample.acme.httpChallenge]
entryPoint = "devnull"
Ajouter un service qui répond au challenge LetsEncrypt
A ce stade :
- tous les noeuds sont en HTTPS sur Traefik ;
- tous les noeuds utilisent les mêmes authentifiants sur LetEncrypt ;
- tous les noeuds tentent une génération de certificat via le challenge HTTP-01 ;
- tous les challenges échouent... car on a désactivé les réponses de challenge sur Traefik.
Comme vu précédemment, pour répondre à un challenge, LetsEncrypt ira à l'adresse
http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>
et s'attendra à avoir la réponse <TOKEN>.<KEY_THUMBPRINT>
.
Dans un premier temps, déterminons la valeur de <KEY_THUMBPRINT>
. Pour cela, nous avons besoin du fichier acme.
json
précédemment partagé. Le script python suivant permet de determiner la valeur de <KEY_THUMBPRINT>
:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes
import json
import base64
from acme import jose
acme_conf = json.load(open("acme.json"))
priv_key = base64.b64decode(acme_conf["sample"]["Account"]["PrivateKey"])
der_key = serialization.load_der_private_key(priv_key, password=None)
thumb_print = jose.JWKRSA(key=der_key).thumbprint(hash_function=hashes.SHA256)
print(f"KEY_THUMBPRINT: {jose.b64encode(thumb_print).decode()}")
(Ne pas oublier d'adapter le code, notamment avec le nom du fichier et sample
, qui est le nom de la configuration
LetsEncrypt)
Une fois la valeur de <KEY_THUMBPRINT>
, un simple nginx avec une configuration minimale suffit à monter le service.
L'extrait du fichier nginx.conf
suivant peut être utilisé comme inspiration :
[...]
http {
[...]
server {
[...]
location ~ ^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)$ {
default_type text/plain;
return 200 "$1.<KEY_THUMBPRINT>";
}
}
}
(Ne pas oublier de remplacer <KEY_THUMBPRINT>
par la valeur précédemment calculée)
Configuration docker-compose.yml
:
version: '3'
services:
http:
image: nginx:latest
labels:
- "traefik.http.routers.shared_cert.rule=PathPrefix(`/.well-known/acme-challenge/`)"
- "traefik.http.routers.shared_cert.priority=300"
- "traefik.enable=true"
volumes:
- "nginx.conf:/etc/nginx/nginx.conf:ro"
networks:
- web
- default
networks:
web:
external: true
(Ne pas oublier de remplacer le network web
par celui sur la port HTTP/80 de Traefik, et le fichier nginx.conf
par celui à rédiger)