technique:old:migration-traefik-v2

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.

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.

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).

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.

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 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.

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

<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`)

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

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
    [...]

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.

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.

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"

Le fichier statique doit maintenant appeler le middleware supplémentaire :

snippet.toml
middlewares = ["hardening@file", "compression@file", "errorspages@file"]

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.

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"
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"
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
  • technique/old/migration-traefik-v2.txt
  • de qduchemi