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.
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 :
- snippet.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 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.
- snippet.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 :
- snippet.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
etcompression@file
, qui se réfèrent aux middlewares déclarés dans le fichier de configuration dynamiquetls12@file
, qui se réfère aux options TLS déclarés dans le fichier de configuration dynamiqueletsencrypt
, qui se réfère au fournisseur de certificat déclaré dans la sectioncertificatesResolvers
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 :
- snippet.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 :
- snippet.yaml
labels: # websecure est l'entrypoint HTTPS - nom arbitraire défini dans la config traefik.http.routers.<service>.entrypoints: websecure traefik.http.routers.<service>.rule: Host(`X.picasoft.net`) traefik.http.services.<service>.loadbalancer.server.port: 80 traefik.enable: true
Où <service>
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, 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.
- snippet.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 :
- snippet.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
Important:
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é.
- snippet.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 :
- snippet.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
Attention:
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.
Important:
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
- snippet.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
- snippet.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
- snippet.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