Table des matières

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 :

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.

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 :

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 :

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

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

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