# Migration vers Traefik v2 Les changements entre Traefik v1 et v2 sont assez importants car tous les concepts sont redéfinis entre les deux versions. Cette page résume les étapes de migrations effectuées chez Picasoft. Notez que ce mini-guide n'est valide que pour les versions de Traefik au dessus de `v2.2`, qui a apporté de nombreuses améliorations. Auparavant, la migration nécessitait de très nombreuses redondances (redirection HTTP vers HTTPS, utilisation de TLS, utilisation de Let's Encrypt, etc, à indiquer sur **chaque conteneur**). Un aperçu complet des fichiers est disponible à la fin pour faciliter la compréhension. ## Rappel de nos besoins Au moment de de la migration, les fonctionnalités recherchées sont : * Redirection HTTP → HTTPS * Gestion des certificats par Let's Encrypt * Ajout de headers de sécurité * Version minimale de TLS à utiliser * Compression des flux L'objectif est de minimiser la configuration à effectuer via les labels, et de mettre le maximum dans les fichiers de configuration. ## Mise à jour des fichiers de configuration Avec Traefik v1, on configurait l'essentiel de Traefik via `traefik.toml` : entrypoints (80/443), HTTP → HTTPS, Let's Encrypt. Le fichier utilisé pour la v1 [se trouve sur Gitlab](https://gitlab.utc.fr/picasoft/projets/dockerfiles/-/blob/92ba0fcce0c6ca82bb885fd3fa31c23c73f30378/pica-traefik/traefik.toml). Il s'agit d'un fichier **statique**, c'est-à-dire qu'il est chargé au démarrage de Traefik et ne change pas. La configuration **dynamique** désigne toute la configuration de routage, et elle est amenée à changer souvent : démarrage d'un nouveau conteneur, etc. La configuration dynamique se construit grâce à des *providers* (des composants existants qui donnent des informations sur où router, par exemple). Dans notre cas, elle utilisera le provider `docker` pour regarder les nouveaux conteneurs démarrés et router vers ceux-ci, et se servira du provider `file` pour lire dans un fichier. En effet, avec Traefik v1, on pouvait se contenter d'un fichier statique et de labels sur les conteneurs. Pour nos besoins, avec Traefik v2, il faudra créer un nouveau fichier (dynamique). ### Fichier dynamique On commence par créer le fichier dynamique, qui contient normalement la configuration des routeurs et des middlewares. * Un routeur est l'équivalent du `frontend` de Traefik v1 : un par conteneur. Il analyse la requête pour décider où elle doit aller en fonction de critères comme l'URL. * Un middleware est un intermédiaire le routeur et le service : il permet des opérations spécifiques, comme la compression du flux, l'ajout de *headers* de sécurité pour HTTPS, etc. Il modifie la requête avant de l'envoyer au service. Dans Traefik v2, les options TLS et les middlewares sont configurées **pour chaque routeur**. Néanmoins, depuis Traefik v2.2, une fonctionnalité permet d'appliquer les options TLS et les middlewares à utiliser **par défaut** (voir prochaine section). Dans ce fichier, on se contentera de **définir** les middlewares et les options TLS, sans les appliquer à un routeur spécifique. On ajoute les middlewares pour ajouter les *headers* de sécurité et le middleware pour compresser les flux : ```toml [http] [http.middlewares.hardening.headers] addVaryHeader = true browserXssFilter = true contentTypeNosniff = true forceSTSHeader = true frameDeny = true stsIncludeSubdomains = true stsPreload = true customFrameOptionsValue = "SAMEORIGIN" referrerPolicy = "same-origin" featurePolicy = "vibrate 'self'" stsSeconds = 315360000 [http.middlewares.compression.compress] excludedContentTypes = ["text/event-stream"] ``` On ajoute la configuration TLS : ``` [tls.options] [tls.options.tls12] minVersion = "VersionTLS12" cipherSuites = [ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256" ] curvePreferences = ["CurveP521","CurveP384"] ``` `hardening`, `compression` et `tls12` sont des noms arbitraires, qui permettent de les "importer" dans un routeur, par exemple. ### Fichier statique Un [outil](https://github.com/containous/traefik-migration-tool) développé par l'équipe de Containous permet de migrer `traefik.toml`. Néanmoins, il est vraiment peu utile dans notre cas, il ne migre que les options simples et demande de faire le reste à la main. On commence donc par les options basiques. Comme attendu, les providers permettent de construire la configuration dynamique depuis Docker et depuis le fichier construit plus haut. ```toml [global] sendAnonymousUsage = false checkNewVersion = true [providers] providersThrottleDuration = "2s" [providers.docker] watch = true endpoint = "unix:///var/run/docker.sock" exposedByDefault = false network = "proxy" [providers.file] filename = "/traefik_dynamic.toml" watch = true [log] level = "INFO" ``` Notez le réseau `proxy` qui sera celui dans lequel devront se trouver Traefik et tous les services. On configure ensuite un fournisseur de certificat. Let's Encrypt est utilisé par défaut. Cette configuration est similaire à la v1, seuls les nom des paramètres changent : ```toml [certificatesResolvers] [certificatesResolvers.letsencrypt] [certificatesResolvers.letsencrypt.acme] email = "picasoft@assos.utc.fr" storage = "/certs/acme.json" [certificatesResolvers.letsencrypt.acme.httpChallenge] entryPoint = "web" ``` Notez le nom `letsencrypt`, arbitraire, qui permettra de s'y référer. Depuis Traefik v2.2, on retrouve la possibilité de définir dans la configuration statique, pour un entrypoint : * Une redirection globale (HTTP → HTTPS) * L'activation TLS et la définition des options utilisées par défaut * Le fournisseur de certificat à utiliser par défaut Pas besoin de configurer ces options pour chaque routeur, ce qui évite de se retrouver avec 10 labels par conteneur. Il est évidemment possible d'écraser ces options sur un conteneur spécifique. ``` [entryPoints] [entryPoints.web] address = ":80" [entryPoints.web.http.redirections.entryPoint] to = "websecure" scheme = "https" [entryPoints.websecure] address = ":443" [entryPoints.websecure.http] middlewares = ["hardening@file", "compression@file"] [entryPoints.websecure.http.tls] certResolver = "letsencrypt" options = "tls12@file" ``` On retrouve : * `hardening@file` et `compression@file`, qui se réfèrent aux middlewares déclarés dans le fichier de configuration dynamique * `tls12@file`, qui se réfère aux options TLS déclarés dans le fichier de configuration dynamique * `letsencrypt`, qui se réfère au fournisseur de certificat déclaré dans la section `certificatesResolvers` de la configuration statique. *Note : ce que fait cette configuration ne déroge pas aux concepts de Traefik v2, c'est juste une astuce de configuration. Elle crée un routeur attaché à l'entrypoint `web`, et lui attache un middleware qui redirige vers l'entrypoint `websecure`. Ensuite, elle **copie** les options TLS, le fournisseur de certificat utilisé et les middlewares sur chaque routeur créé sur l'entrypoint `websecure`, ce qui évite d'écrire nous même les labels.* ## Labels Avec Traefik v1, les labels ajouté sur les conteneurs pour activer le routage vers ce conteneur ressemblaient à ceci : ```yaml labels: traefik.frontend.rule: Host:X.picasoft.net traefik.port: 80 traefik.enable: true ``` Il y a plein d'autres options, mais c'est tout ce qu'on utilise chez Picasoft. Grâce aux options par défaut de Traefik v2.2, les labels à utiliser ne sont pas beaucoup plus nombreux : ```yaml labels: # websecure est l'entrypoint HTTPS - nom arbitraire défini dans la config traefik.http.routers..entrypoints: websecure traefik.http.routers..rule: Host(`X.picasoft.net`) traefik.http.services..loadbalancer.server.port: 80 traefik.enable: true ``` Où `` est un nom unique arbitraire. Notez qu'une règle du style `Host:voice.picasoft.net;Path:/metrics` devient : ``` Host(`voice.picasoft.net`) && Path(`/metrics`) ``` ## acme.json Le format du fichier `acme.json` doit être migré pour fonctionner avec Traefik v2. On peut le faire avec [traefik-migration-tool](https://github.com/containous/traefik-migration-tool), mais cela nécessite que l'ensemble des services soient éteints. Pour notre migration, nous avons purement et simplement supprimé le fichier, car il contenait de nombreux certificats inutilisés. Si le rate-limiting de Let's Encrypt n'est pas un problème, tous les certificats seront re-générés lors du démarrage des conteneurs applicatifs. ```bash $ cd /DATA/docker/certs $ sudo rm acme.json # Valid empty JSON object $ echo '{}' | sudo tee acme.json $ sudo chmod 600 acme.json ``` ## Compose pour Traefik Il faudra simplement mettre à jour la version de Traefik et monter le fichier de configuration dynamique : ```yaml version: '3.7' # Common network for Traefik and services networks: proxy: # Same name than in traefik.toml name: 'proxy' services: traefik: image: traefik:2.3 [...] volumes: - /var/run/docker.sock:/var/run/docker.sock - ./traefik.toml:/traefik.toml - ./traefik_dynamic.toml:/traefik_dynamic.toml [...] networks: - proxy [...] ``` ## Mise en production Pour le coup, c'est difficile sans interruption de service. Pour les services critiques, on peut combiner les labels de Traefik v1 et Traefik v2 et redémarrer ces services. Ensuite, on peut essayer Traefik v2, et si ça ne fonctionne pas, revenir à Traefik v1 (nécessite des dossiers de configuration séparés pour passer facilement de l'un à l'autre). Une fois qu'on est certain que Traefik v2 fonctionne bien, on peut éteindre les conteneurs applicatifs (via Compose ou autre), puis éteindre Traefik v1 et supprimer le conteneur. On re-crée ensuite Traefik, puis les conteneurs applicatifs, et on surveille les logs. ## Pages personnalisée pour les erreurs Cette partie n'est pas encore en production. Il faut notamment créer les pages d'erreurs et les certificat wildcard avant de pouvoir le déployer Traefik v2 permet de paramétrer aisément des pages personnalisées d'erreur. Pour cela, il faut définir un middleware qui va intercepter les code de retour d'erreur et appeler un service personnalisé à la place. ### Fichier dynamique Le middleware peut être configuré au moyen de labels sur le conteneur ou via la configuration dynamique. Pour un soucis de lisibilité, on préférera le mettre dans la configuration dynamique. * On créée le middleware `errorspages` de type errors * On lui demande d'intercepter les erreurs entre 400 et 408, 500 et 505 et 418 * On lui indique de rediriger l'erreur vers le service `nginx-errors` * Il doit servir la page correspondant au status intercepté. ```toml [http.middlewares.errorspages.errors] status = ["400-408", "500-505", 418] service = "nginx-errors" query = "{status}.html" ``` ### Fichier statique Le fichier statique doit maintenant appeler le middleware supplémentaire : ```toml middlewares = ["hardening@file", "compression@file", "errorspages@file"] ``` ### Création du service d'erreur Il faut maintenant ajouter le service `nginx-errors` qui va servir les pages d'erreurs personnalisée. Pour cela, on crée un conteneur docker basé sur nginx pour servir les différentes pages. ``` FROM nginx:alpine COPY html /usr/share/nginx/html ``` le dossier `html` doit contenir les différentes pages d'erreurs. Dans un deuxième temps, on va ajouter ce conteneur en temps que service dans le `docker-compose` de traefik. ``` errors: image: nginx-errors container_name: traefik_errors build: . labels: traefik.http.routers.traefik_errors.rule: HostRegexp(`{subdomain:.*}.test.picasoft.net`) traefik.http.routers.traefik_errors.priority: 1 traefik.http.services.traefik_errors.loadbalancer.server.port: 80 traefik.enable: true networks: - proxy restart: unless-stopped ``` Afin d'éviter d'avoir des erreurs bizarres sur des sous-domaine non utilisés, on choisit de rediriger tous les domaines vers ce service. Cependant on rentre en conflit avec les autres services présents sur la machine. En effet, Traefik utilise une règle de longueur du match pour choisir la priorité d'un conteneur. Ainsi, la règle la plus longue aura la priorité la plus forte. le problème ici c'est que la règle `.*` peut matcher une chaine de n'importe quelle longueur et à donc la priorité la plus haute. Il faut donc définir une priorité de 1 à la main pour laisser la préseance aux autre conteur. Attention ! la récupération de tous les sous domaines par un même conteneur de manière sécurisé avec let's encrypt nécessite de mettre en place un certificat wildcard. (TODO) De même, il faut créer une page `index.html` dans le conteneur errors-pages pour accueillir les visiteurs. ## Synthèse ### traefik.toml ```toml [global] sendAnonymousUsage = false checkNewVersion = true [entryPoints] [entryPoints.web] address = ":80" [entryPoints.web.http.redirections.entryPoint] to = "websecure" scheme = "https" [entryPoints.websecure] address = ":443" [entryPoints.websecure.http] middlewares = ["hardening@file", "compression@file", "errorspages@file"] [entryPoints.websecure.http.tls] certResolver = "letsencrypt" options = "tls12@file" [providers] providersThrottleDuration = "2s" [providers.docker] watch = true endpoint = "unix:///var/run/docker.sock" exposedByDefault = false network = "proxy" [providers.file] filename = "/traefik_dynamic.toml" watch = true [log] level = "INFO" [certificatesResolvers] [certificatesResolvers.letsencrypt] [certificatesResolvers.letsencrypt.acme] email = "picasoft@assos.utc.fr" storage = "/certs/acme.json" [certificatesResolvers.letsencrypt.acme.httpChallenge] entryPoint = "web" ``` ### traefik_dynamic.toml ```toml [tls.options] [tls.options.tls12] minVersion = "VersionTLS12" cipherSuites = [ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256" ] curvePreferences = ["CurveP521","CurveP384"] [http] [http.middlewares.hardening.headers] addVaryHeader = true browserXssFilter = true contentTypeNosniff = true forceSTSHeader = true frameDeny = true stsIncludeSubdomains = true stsPreload = true customFrameOptionsValue = "SAMEORIGIN" referrerPolicy = "same-origin" featurePolicy = "vibrate 'self'" stsSeconds = 315360000 [http.middlewares.compression.compress] excludedContentTypes = ["text/event-stream"] [http.middlewares.errorspages.errors] status = ["400-408", "500-505", 418] service = "nginx-errors" query = "{status}.html" ``` ### docker-compose.yml ```yaml version: '3.7' networks: proxy: name: 'proxy' services: traefik: image: traefik:2.3 container_name: traefik ports: - 80:80 - 443:443 volumes: - /etc/localtime:/etc/localtime:ro - /var/run/docker.sock:/var/run/docker.sock - ./traefik.toml:/traefik.toml - ./traefik_dynamic.toml:/traefik_dynamic.toml - /DATA/docker/traefik/certs:/certs networks: - proxy restart: unless-stopped errors: image: nginx-errors container_name: traefik_errors build: . labels: traefik.http.routers.traefik_errors.rule: HostRegexp(`{subdomain:.*}.test.picasoft.net`) traefik.http.routers.traefik_errors.priority: 1 traefik.http.services.traefik_errors.loadbalancer.server.port: 80 traefik.enable: true networks: - proxy restart: unless-stopped ```