Traefik v2, un vrai reverse proxy

Une approche différente du reverse proxy traefik

Je suis très longtemps resté sur mon bon vieux nginx pour faire mon reverse proxy, ça fonctionne parfaitement.
J'avais déjà testé traefik en version 1.5, et je l'avais trouvé plutôt lent et limité, mais récemment, je me suis dit que j'allais retesté tout ça, à l'heure ou j'écris ces lignes, nous sommes à la version 2.2, nommée chevrotin.

J'ai eu beaucoup de mal à bien comprendre le fonctionnement de traefik, car la quasi totalité des tutoriels sont identiques niveau configuration, ce qui changeait c'est l'application mis derrière. Mais aucun réel tutoriel qui en explique le fonctionnement. Nous allons ici tenter de réellement comprendre ce que nous faisons, avec une approche totalement différente.

Je ne reviendrais sur ce qu'est traefik, si vous êtes tombé sur ce tutoriel, c'est que vous savez ce que c'est.

Vocabulaire

Pour comprendre le fonctionnement de traefik, nous aurons besoin d'un peu de vocabulaire :

les entrypoints

Les entrypoints sont comme le nom l'indique les points d'entrées, ce sont les adresses et ports exposés par traefik, qui permettrons d'exposer nos applications. Bien souvent nous utilisons les ports 443 et 80.

les services

Les services sont nos applications, par exemple nextcloud. Dans la version 1.X de traefik, c'était nommé les backends.
Nous avons 3 types de services :

  • http
  • tcp
  • udp

Nous ne sommes donc pas limité au web, nous pouvons redirigé du ssh, du ftp , what else ?!!!

les routers

Les routers sont les règles de redirection du reverse, nous pointons un router vers un service. A ce router nous lui donnons des URLs, des entrypoints.

les providers

Les providers sont les sources de génération de la configuration. C'est ce qui fait la force de traefik, il en existe plusieurs, de plusieurs type :

  • Moteur de conteneur :
    • docker : La plus connu, on donne accès au socket de docker à traefik, et lui va gérer la création des services et routers en lisant celui-ci, en fonction des labels utilisés.
    • kubernetes : Fonctionne de manière similaire à docker.
    • rancher : Je n'ai pas encore regardé le fonctionnement, mais je suppose que le fonctionnement est identique à kubernetes
    • marathon : Pareil que pour rancher
  • Base de clé/valeur : Je n'ai pas regardé exactement le fonctionnement de ce type de provider
    • consul
    • consulcatalog
    • etcd
    • redis
    • zookeeper
  • file : Permets de lire un fichier ou un répertoire contenant des fichiers de configuration à la volée.

les middlewares

Les middlewares seront des étapes intermédiaires entre le router et le service, ça peux être un peu tout et n'importe quoi, comme de la compression, de la sécurisation, de la configuration, ou même de l'authentification (cela fera l'objet d'un autre article).

Configuration

Pour configurer traefik, nous avons deux méthodes, la méthode interactive, et la méthode déclarative.

Interactive

Cette méthode permets de passer la configuration directement en paramètre de l'exécutable, par exemple nous pourrions avoir ceci :

$ traefik --accesslog=true \
          --api=true \
          --api.insecure=true \
          --api.dashboard=true \
          --api.debug=true \
          --log.level=INFO \
          --providers.docker.endpoint=unix:///var/run/docker.sock \
          --providers.docker.exposedbydefault=false \
          --providers.docker.watch=true \
          --providers.docker.swarmmode=true \
          --providers.file.filename=/etc/traefik/traefik_dynamic.yml \
          --providers.file.watch=true \
          --entrypoints.web.address=:80 \
          --entrypoints.websecure.address=:443 \
          --entrypoints.web.http.redirections.entrypoint.scheme=https \
          --entrypoints.web.http.redirections.entrypoint.to=websecure \
          [email protected] \
          --certificatesresolvers.letsencrypt.acme.caserver=https://acme-v02.api.letsencrypt.org/directory \
          --certificatesresolvers.letsencrypt.acme.storage=/acme.json \
          --certificatesresolvers.letsencrypt.acme.keytype=EC384 \
          --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \
          --certificatesresolvers.letsencrypt.acme.tlschallenge=true

C'est personnellement la méthode que j'utilise.
Cette méthode devient déclarative si on utilise docker et docker-compose, ou même kubernetes.

Déclarative

Nous pouvons également passer un fichier de paramètre à traefik, au format toml ou yaml, ce qui donnerais pour la même configuration :

TOML

[accesslog]
[api]
  insecure=true
  dashboard=true
  debug=true

[log]
  level="INFO"

[providers]
  [providers.docker]
    endpoint="unix:///var/run/docker.sock"
    exposedbydefault=false
    watch=true
    swarmmode=true
    
  [providers.file]
    filename=/etc/traefik/traefik_dynamic.yml
    watch=true

[entryPoints]
  [entryPoints.web]
    address=":80"
    [entryPoints.web.http.redirections.entrypoint]
      scheme="https"
      to="websecure"
  [entryPoints.websecure]
    address=":443"

[certificatesResolvers]
  [certificatesResolvers.letsencrypt]
    [certificatesResolvers.letsencrypt.acme]
      email = "[email protected]"
      caServer = "https://acme-v02.api.letsencrypt.org/directory"
      storage = "acme.json"
      keyType = "EC384"

      [certificatesResolvers.letsencrypt.acme.httpChallenge]
        entryPoint = "web"

Perso je n'aime pas du tout ce format, c'est ce qui m'avait fait fuire traefik à l'époque.

YAML

accesslog: {}
api:
  insecure: true
  dashboard: true
  debug: true

log:
  level: "INFO"

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedbydefault: false
    watch: true
    swarmmode: true
    
  file:
    filename: /etc/traefik/traefik_dynamic.yml
    watch: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entrypoint:
          scheme: "https"
          to: "websecure"
  websecure:
    address: ":443"

certificatesResolvers:
  letsencrypt:
    acme:
      email: "[email protected]"
      caServer: "https://acme-v02.api.letsencrypt.org/directory"
      storage: "acme.json"
      keyType: "EC384"
      httpChallenge:
        entryPoint: "web"

Je trouve le yaml beaucoup plus clair et simple.

Cas pratique

Rien de mieux qu'un exemple pour comprendre, nous allons faire une petite stack avec traefik, nextcloud et postgres.
Dans cette exemple, nous utiliserons docker pour installer les services, mais pas pour la configuration automatique de traefik.

Pouquoi donc ?!

Déjà je n'aime pas le fait de mettre mon socket docker dans un conteneur, même en lecture seul, le socket à toutes les informations des conteneurs. Autrement, je n'aime pas trop alourdir mes docker-compose, j'aime quand c'est clair, net et précis.
Selon moi, l'utilisation des labels va être utile seulement si on installe régulièrement (plusieurs fois par jours) des conteneurs, qu'on en supprime, en gros quand on est un cloud publique. Dans une utilisation personnelle, la configuration d'un reverse ne change pas souvent.

De plus, on a souvent tendance dès lors qu'on parle de traefik, de le lier directement à docker ou kubernetes, hors traefik est avant tout un reverse proxy, il est utilisable sans docker, sans conteneur, en baremetal.

Notre stack de base (docker-compose)

Nous partirons donc de cette configuration de docker-compose :

version: "3.8"

networks:
  traefik:

services:
  traefik:
    image: traefik:chevrotin
    volumes:
      - /srv/docker/traefik/acme.json:/etc/traefik/acme.json
      - /srv/docker/traefik/certs:/etc/traefik/certs
      - /var/run/docker.sock:/var/run/docker.sock
      - /srv/docker/traefik/conf.d:/etc/traefik/conf.d
    ports:
      - 80:80
      - 443:443
      - 8080:8080 # le temps de tester
    networks:
      - traefik
    command:
      - "--global.sendanonymoususage=false" # désactivation de l'envoi de donnée
      - "--global.checknewversion=false" # puisque dockerisé, on désactive le check de mise à jour
      - "--accesslog=true" # Pour avoir les logs d'accès
      - "--api=true" # Pour activer l'api
      - "--api.insecure=true" # Activer pour exposer l'api sur 8080
      - "--api.dashboard=true" # Pour activer le dashboard
      - "--log.level=INFO"
      - "--providers.file.directory=/etc/traefik/conf.d/" # Permets de charger les configurations dans le répertoire (tout les yaml et toml)
      - "--providers.file.watch=true" # Permets de surveiller le répertoire précédent pour charger dynamiquement les configurations
      - "--entrypoints.web.address=:80" # Création de l'entrypoint nommé web sur le port 80
      - "--entrypoints.websecure.address=:443" # Création de l'entrypoint nommé websecure sur le port 443
      #- "--entrypoints.web.http.redirections.entrypoint.scheme=https" # Pour créer une redirection vers https
      #- "--entrypoints.web.http.redirections.entrypoint.to=websecure" # Pour rediriger vers l'entrypoint websecure (port 443)
       - "--certificatesresolvers.letsencrypt-ecdsa.acme.email=xataz@monmail.net"
       - "--certificatesresolvers.letsencrypt-ecdsa.acme.caserver=https://acme-v02.api.letsencrypt.org/directory"
       - "--certificatesresolvers.letsencrypt-ecdsa.acme.storage=/acme.json"
       - "--certificatesresolvers.letsencrypt-ecdsa.acme.keytype=EC384"
       - "--certificatesresolvers.letsencrypt-ecdsa.acme.httpchallenge.entrypoint=web"
       - "--certificatesresolvers.letsencrypt-ecdsa.acme.tlschallenge=true"
       - "--certificatesresolvers.letsencrypt-rsa2048.acme.email=xataz@monmail.net"
       - "--certificatesresolvers.letsencrypt-rsa2048.acme.caserver=https://acme-v02.api.letsencrypt.org/directory"
       - "--certificatesresolvers.letsencrypt-rsa2048.acme.storage=/acme.json"
       - "--certificatesresolvers.letsencrypt-rsa2048.acme.keytype=RSA2048"
       - "--certificatesresolvers.letsencrypt-rsa2048.acme.httpchallenge.entrypoint=web"
       - "--certificatesresolvers.letsencrypt-rsa2048.acme.tlschallenge=true"


  db_nextcloud:
    image: postgres:12
    networks:
      - traefik
    volumes:
      - /srv/docker/db_nextcloud/:/var/lib/postgresql/
    environment:
      - POSTGRES_PASSWORD=nextcloud
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=nextcloud

  nextcloud:
    image: nextcloud:19
    networks:
      - traefik
    environment:
      - POSTGRES_HOST=db_nextcloud
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=nextcloud
      - POSTGRES_PASSWORD=nextcloud
      - NEXTCLOUD_ADMIN_USER=admin
      - NEXTCLOUD_ADMIN_PASSWORD=admin
    volumes:
      - /srv/docker/nextcloud:/var/www/html

Traefik utilise lego pour la génération des certificats ssl via acme, il est donc compatible avec les challenges DNS, et avec les API de différent provider de DNS, pour ceci vous pouvez regarder ce tableau

Nous créons 2 resolvers, un pour générer du ecdsa 384, et l'autre du RSA2048, car par exemple certains client n'aime pas trop le ECDSA.
Malheureusement traefik ne supporte pas la configuration dynamic des resolvers, nous sommes donc obligé de l'ajouter manuellement.

Maintenant que nous avons notre docker-compose, nous pouvons nous attaquer à la configuration de traefik. Nous pouvons dès à présent lancer notre stack avec docker-compose.

Configuration de traefik

Dans la configuration de traefik, nous avons mis un répertoire en écoute --providers.file.directory=/etc/traefik/conf.d/ (qui est monté sur l'hote dans /srv/docker/traefik/conf.d), donc toute la configuration de ce répertoire sera chargé dynamiquement --providers.file.watch=true, dès qu'on ajoutera ou modifira un fichier, il sera pris à chaud, sans avoir à redémarrer sa stack.

Nous allons ici ajouter quelques configurations, notammant sur le TLS, et quelques middleware pour la configuration.

Configuration TLS

Nous créons donc un premiers fichiers tls.yml dans notre répertoire écouté par traefik (pour rappel /srv/docker/traefik/conf.d)

tls.yml

tls:
  options:
    default:
      minVersion: "VersionTLS12"
      sniStrict: true
      cipherSuites:
        - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
        - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
        - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305"
        - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256"
        - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
        - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"
        - "TLS_AES_128_GCM_SHA256"
        - "TLS_AES_256_GCM_SHA384"
        - "TLS_CHACHA20_POLY1305_SHA256"
      curvePreferences:
        - X25519
        - CurveP521
        - CurveP384
        - CurveP256

    mintls13: # Arbitraire également, toto fonctionne aussi
      minVersion: "VersionTLS13"

Cette configuration est plutôt simple à comprendre, nous avons les ciphers autorisés dans la configuration par defaut, avec une version de TLS minimum en version 1.2, et nous créons une configuration qui forcera l'utilisation de TLSv1.3.

Quelques middlewares

Maintenant que nous avons notre configuration TLS, il faut configurer quelques middlewares, comme précédemment dis, nous pouvons créer des middlewares pour beaucoup de chose.

Ici je vais créer un fichier par middleware, pour une raison simple, c'est que si je me foire dans la configuration, il ne chargera pas que le middleware qui pose problème, et non tous.

Le format de fichier d'un middleware est :

http:
  middlewares:
    <nomdumiddleware>:
      <typedumiddleware>:
        <options>

    <nomdu2ememiddleware>:
      <typedumiddleware>:
        <options>

Middleware de compression

/srv/docker/traefik/conf.d/compression.yml :

http:
  middlewares:
    compression:
      compress:
        excludedContentTypes:
          - "text/event-stream"

Pas beaucoup d'options pour la compression, et la compression est systématiquement en GZIP, pas de brotli pour le moment.

Middleware hsts

/srv/docker/traefik/conf.d/hsts.yml

http:
  middlewares:
    hsts:
      headers:
        forceSTSHeader: true
        stsSeconds: 315360000
        stsIncludeSubdomains: true
        stsPreload: true

Ici nous activons donc le hsts, le but du tutoriel n'est pas d'expliquer son fonctionnement.

Middleware pour la redirection http vers https

/srv/docker/traefik/conf.d/redirect-to-https.yml

http:
  middlewares:
    redirect-to-https:
      redirectScheme:
        scheme: https
        permanent: true

Nous aurions pu également créer une règle global, en lançant traefik avec les options --entrypoints.web.http.redirections.entrypoint.scheme=https et --entrypoints.web.http.redirections.entrypoint.to=websecure, mais il peux être possible que certains services n'est pas besoin d'accéder en https.

Middleware pour quelques sécurités supplémentaire

/srv/docker/traefik/conf.d/security.yml

http:
  middlewares:
    security:
      headers:
        accessControlMaxAge: 100
        addVaryHeader: true
        browserXssFilter: true
        contentTypeNosniff: true
        frameDeny: true
        sslRedirect: true
        customFrameOptionsValue: "SAMEORIGIN"
        referrerPolicy: "same-origin"
        featurePolicy: "vibrate 'self'"

Middleware pour l'authentification

Pour ici, nous avons pleins de possibilités de gestion. Cette exemple permets justement de voir la puissance des middlewares.

Nous pouvons par exemple créer un middleware par groupe d'utilisateur :

http:
  middlewares:
    admin-users:
      basicAuth:
        users:
          - "xataz:$apr1$FprQWnRT$3FZXlQg0.qCkkytl4iMLc1"
          - "toto:$apr1$pbpEY6eD$DFz44UOcGC5KC5jasAhgQ/"

    dev-users:
      basicAuth:
        users:
          - "tata:$apr1$inMBbv02$C/oh3LLEfmmOyloAtqW/V/"
      

Et quand on en aura besoin, on les appelleras.

Nous pouvons également créer un middlewares par users :

http:
  middlewares:
    xataz-user:
      basicAuth:
        users:
          - "xataz:$apr1$FprQWnRT$3FZXlQg0.qCkkytl4iMLc1"
    toto-user:
      basicAuth:
        users:
          - "toto:$apr1$pbpEY6eD$DFz44UOcGC5KC5jasAhgQ/"
    tata-user:
      basicAuth:
        users:
          - "tata:$apr1$inMBbv02$C/oh3LLEfmmOyloAtqW/V/"
      

Et créer des groupes de middlewares :

http:
  middlewares:
    admin-group:
      chain:
        - "xataz-user@file"
        - "toto-user@file"
    devs-group:
      chain:
        - "toto-user@file"

Pour ce tuto, nous simplifierons avec un seul fichier avec la méthode d'un middleware par groupe : /serv/docker/traefik/conf.d/auth.yml

http:
  middlewares:
    admin-users:
      basicAuth:
        users:
          - "xataz:$apr1$FprQWnRT$3FZXlQg0.qCkkytl4iMLc1"
          - "toto:$apr1$pbpEY6eD$DFz44UOcGC5KC5jasAhgQ/"

    dev-users:
      basicauth:
        users:
          - "tata:$apr1$inMBbv02$C/oh3LLEfmmOyloAtqW/V/"

Pleins d'autres middlewares

Traefik propose pas mal de middlewares, là nous en avons vu que très peu, mais nous avons aussi la possibilité de rediriger l'authentification, de gérer les erreurs http, de limiter les IPs, etc ...., nous en verrons d'autres par la suite.

La liste complète des middlewares est disponible ici.

Les services et les routes

Personnellement j'aime bien créer les routers et les services dans le même fichier, mais un fichier par application.

Dashboard traefik

Nous allons d'abord créer notre configuration pour accéder au dashboard de traefik :

/serv/docker/traefik/conf.d/traefik.yml

http:
  services:
    traefik:
      loadBalancer:
        servers:
          - url: "http://localhost:8080"

  routers:
    traefik:
      rule: "Host(`traefik.exemple.fr`)"
      entryPoints:
        - "web"
      middlewares:
        - "redirect-to-https@file"
      service: "noop@internal"
    traefik-secure:
      rule: "Host(`traefik.exemple.fr`)"
      entryPoints:
        - "websecure"
      middlewares:
        - "hsts@file"
        - "security@file"
        - "compression@file"
        - "admins-users@file"
      service: "traefik@file"
      tls:
        certResolver: letsencrypt-ecdsa
        options: mintls13

Alors qu'avons nous dans ce fichier ?

D'abord nous avons la déclaration du service, que j'ai nommé traefik ici (très original), de type loadBalancer, qui pointe vers localhost:8080 (adresse exposé par traefik).

Puis nous avons 2 routes, traefik et traefik-secure. traefik écoute sur le port 80 (entrypoint web), réponds à l'url http://traefik.exemple.fr et utilise le middleware redirect-to-https@file, pour ce rediriger vers https. Le @file permets de définir sur qu'elle provider on tape, si c'est sur le provider docker, on utiliserais @docker.
Ensuite nous avons le service, là nous tappons sur noop@internal, alors là c'est un service particulier qui ne pointe sur rien, et puisque nous avons la redirection, je préfère tapper sur rien, pour éviter de me retrouver connecté sur le dashboard sans tls.

Puis nous avons traefik-secure, nous répondons toujours au requête de https://traefik.exemple.fr, mais sur l'entrypoint websecure. Ici nous avons plusieurs middlewares, hsts@file pour les règles hsts, security@file pour quelques sécurités, compression@file pour compresser les requêtes et admin-users@file pour limité l'accès au admins.

Et pour finir nous avons la configuration TLS, avec le certResolver qui appel letsencrypt-ecdsa pour générer automatiquement le certificat SSL ecdsa de 384 bits, et on active l'option mintls13 (du fichier tls.yml) pour n'autoriser les connexions qu'en TLS 1.3.

Si tout est bon, et sans redémarrer traefik, vous devriez pouvoir accéder à votre dashboard en TLS.

nextcloud

Pour nextcloud, nous allons faire une configuration un peu différente, histoire de voir quelques possibilité de traefik.
De base nextcloud écoute sur http://nextcloud:80/, mais nous allons ajouter un prefix /cloud.

Voici la configuration à utiliser :

http:
  services:
    nextcloud:
      loadBalancer:
        servers:
          - url: "http://nextcloud"

  routers:
    nextcloud:
      rule: "Host(`cloud.exemple.fr`) && PathPrefix(`/cloud`)"
      entryPoints:
        - "web"
      middlewares:
        - "redirect-to-https@file"
      service: "noop@internal"
    nextcloud-secure:
      rule: "Host(`cloud.exemple.fr`) && PathPrefix(`/cloud`)"
      entryPoints:
        - "websecure"
      middlewares:
        - "hsts@file"
        - "security@file"
        - "compression@file"
        - "strip-cloud@file"
      service: "nextcloud@file"
      tls:
        certResolver: letsencrypt-rsa2048

  middlewares:
    strip-cloud:
      stripPrefix:
        prefixes:
          - "/cloud"
      

Là nous avons ajouté deux choses, dans rule, nous avons ajouté le Prefix /cloud, pour que seulement cloud.exemple.fr/cloud soit redirigé, mais ensuite nous créons un middlewares nommé strip-cloud de type stripPrefix.
Pour bien comprendre, le PathPrefix se passe au niveau du front, mais de base, le reverse proxy va rediriger https://cloud.exemple.fr/cloud vers http://nextcloud:80/cloud, sauf que nextcloud ne connais pas se chemin.
Le stripPrefix va donc enlever ce prefix (/cloud), entre traefik et nextcloud, afin que nextcloud puisse retrouver son chemin.

Conclusion

Nous n'avons ici effleuré que de très prêt les possibilités de traefik. Mais nous avons pu voir que traefik est un reverse proxy à part entière, et que ce n'est pas seulement un outil pour les conteneurs.
Nous avons vu ici une méthode de configuration très différente de ce que l'on vois partout. Le but n'est pas de dénigrer la configuration par label ou autre, au contraire cette configuration à beaucoup d'avantages, mais de permettre un découpage plus propres de la configuration et surtout de montrer que traefik ­­!= docker.