• BLOG

  • Génération de certificats TLS LetsEncrypt sur plusieurs instances Traefik en DNS Round Robin (FR)

    lun. 24 janvier 2022 Dan Lousqui

    Share on: Twitter - Facebook - Google+

    Génération de certificats TLS LetsEncrypt sur plusieurs instances Traefik en DNS Round Robin

    ban

    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.

    schéma

    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.

    schéma

    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)

    Profit

  • Comments