Reverse Proxy: Traefik

Après plusieurs année passées à utiliser nginx en tant que reverse proxy sur mes serveurs perso, j'ai découvert Traefik. Nginx avait le désavantage d'être assez lourd en terme de configuration lorsque l'on souhaite ajouter un service, récupérer un certificat let's encrypt... Avec Traefik c'est pour le coup bien plus simple, plusieurs solutions de configuration sont disponible mais les deux qui me semblent les plus intéressantes sont via des labels associés à vos containers docker (la configuration du reverse proxy reste au plus proche de chaque déclaration de service) ou avec un petit fichier de configuration yaml (une quinzaine de ligne par service).

J'ai préféré opter pour la configuration avec les fichiers yaml parce que les labels à associer aux containers docker sont assez verbeux et au moins toute ma configuration se retrouve au même endroit. En plus les fichiers de configurations sont surveillés par Traefik et rechargé à chaque changement. Plus besoin de redémarrer le container docker quand on change quelque chose.

Installation

J'ai personnellement fait le choix d'utiliser Traefik à travers son container docker, ce n'est pas une obligation mais comme tous mes services sont déployés sur docker avec compose, c'est plus simple pour moi.

Première chose à faire, créer un réseau local pour docker pour que Traefik puisse avoir accès aux autres containers.

docker network create compose_default

J'ai choisi de nommer ce réseau compose_default, vous pouvez bien évidemment le nommer comme vous le souhaitez.

Ensuite le fichier docker-compose.yml pour le déploiement de Traefik

version: '3.6'

services:
  traefik:
    container_name: traefik
    image: traefik
    volumes:
      - ./config:/etc/traefik/config
      - ./config.yml:/etc/traefik/traefik.yml
      - ./acme.json:/etc/traefik/acme/acme.json
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
    restart: always
    networks:
      - default
    ports:
      - "80:80"
      - "443:443"

networks:
  default:
    external:
      name: compose_default

Rien de bien exotique ici, on récupère l'image nommé traefik sur le docker hub, on lui monte quelques volumes :

  • ./config : c'est notre dossier qui contiendra nos fichiers de configuration
  • ./config.yml : c'est le fichier de configuration global de Traefik que l'on va voir juste après
  • ./acme.json : c'est le fichier qui servira à stocker les certificats Let's Encrypt
  • docker.sock : uniquement dans le cas où vous souhaitez vous servir des labels sur les containers docker pour passer la configuration à Traefik

On lui affecte les ports 80 et 443 afin de pouvoir écouter le trafic http et https par défaut, vous pouvez également en déclarer d'autres si besoin. Et surtout on n'oublie pas de le rattacher au réseau créé précédemment afin qu'il puisse y accéder.

Il est nécessaire de remplir le fichier de configuration config.yml pour lui donner quelques informations :

global:
  checkNewVersion: true
  sendAnonymousUsage: false

entryPoints:
  http:
    address: :80
  https:
    address: :443

certificatesResolvers:
  letsEncrypt:
    acme:
      email: votre@adresse.email
      storage: /etc/traefik/acme/acme.json
      httpChallenge:
        entryPoint: http

api:
  dashboard: true

providers:
  # Enable Docker configuration backend
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    watch: true
  # Watch changes in config/files/*
  file:
    directory: /etc/traefik/config/files
    watch: true

Les parties importantes ici, la déclarations des entry points (les ports que Traefik va écouter), la configuration de Let's Encrypt pour qu'automatiquement les certificats soient récupérés et renouvelés quand nécessaire. Les providers de configuration, ici on a activé et les labels docker, et les fichier yaml qui se trouveront dans config/files.

Dernier point, le dashboard, ce n'est pas une obligation mais ça vous permet d'avoir une vision rapide des services configuré sur Traefik via un petit site web.

Configuration

On va d'ailleurs définir le fichier de configuration associé au dashboard (et oui, il faut bien qu'on le route avec Traefik). On créé donc le fichier config/files/dashboard.yml (vous pouvez le nommer comme vous le souhaitez).

http:
  middlewares:
    traefik-dashboard-auth:
      basicAuth:
        users:
          - "admin:$apr1$qQzhGxNq$8emb71Ic/wW2vVLgr07dK/"
  routers:
    traefik-dashboard:
      rule: "Host(`traefik.julienmialon.com`)"
      service: api@internal
      entrypoints: https
      middlewares: 
        - traefik-dashboard-auth
      tls:
        certResolver: letsEncrypt

Comme promis, la configuration est très rapide, on défini un middleware pour avoir une couche d'authentification sur notre dashboard. Les informations de connexion sont généré avec la commande htpasswd -nb admin password. On définit ensuite notre router, nommé traefik-dashboard, on lui donne comme règle de matcher le nom d'hôte traefik.julienmialon.com, d'écouter uniquement sur le port https, d'utiliser le middleware définit juste au-dessus et de récupérer le certification SSL auprès de Let's Encrypt. La ligne service indique au router vers quel service il doit router la requête. Puisque l'on souhaite ici accéder au dashboard, il est nécessaire de lui indiquer un service interne définit par Traefik qui se nomme api@internal. Une fois le fichier enregistré, peut accéder à l'url pour accéder au dashboard après authentification.

Si par curiosité vous allez consulter le fichier acme.json, vous verrez maintenant qu'il contient les information du certificats pour le nom d'hôte que vous avez utilisé.

Ici on a utilisé un service interne à Traefik (repérable avec le @internal), mais si l'on souhaite utiliser un service qui tourne dans un autre container docker, il faut rajouter un petit peu de configuration. On va prendre ici l'exemple du fichier de configuration pour ce blog.

http:
  routers:
    blog:
      rule: "Host(`blog.julienmialon.com`)"
      service: service-blog
      entrypoints: https
      tls:
        certResolver: letsEncrypt
        
  services:
    service-blog:
      loadBalancer:
        servers:
          - url: "http://ghost-blog:4242/"

La définition du router est quasiment la même que pour le dashboard excepté qu'on mentionne le service service-blog définit plus bas dans le fichier. On a définit ici un service de type load balancer, mais avec une seule cible il ne fera qu'envoyer toujours à la même url. Le nom d'hôte de la cible est le nom du container docker associé qu'on a bien entendu relié à notre réseau compose_default et ghost tourne sur le port 4242.

Une fois le fichier enregistré, on peut aller voir sur le dashboard dans la liste des services pour vérifier qu'il apparaît bien.

Et dans la liste des routers, on a bien notre règle pour matcher sur le nom d'hôte que l'on a définit.

HTTPS / Sécurité

Dans les deux configurations, que ce soit pour le dashboard ou pour le blog, on peut voir qu'on a toujours spécifié d'écouter uniquement sur le port https. Ce qui veut dire que si quelqu'un tente d'accéder à notre site depuis une adresse http://, il n'aura aucune réponse. L'intérêt de faire ça et que l'on va pouvoir définir un router global pour le port http qui va rediriger sur la même adresse mais en https://.

On va donc rajouter un fichier de configuration pour ce router particulier.

http:
  middlewares:
    forcehttps:
      redirectScheme:
        scheme: https
        permanent: true

  routers:
    forcehttps:
      rule: PathPrefix(`/`)
      entrypoints: http
      service: api@internal
      middlewares: 
        - forcehttps

On définit un middleware nommé forcehttps qui va utiliser le composant redirectScheme en lui indiquant de rediriger sur l'adresse en https de manière permanente (le navigateur va mettre en cache cette redirection et évitera un appel inutile la prochaine fois). On définit ensuite le router qui utilise ce middleware, on lui indique de traiter les requêtes sur le port http uniquement et parce qu'au moins une règle est obligatoire on lui indique un prefix de / ce qui matchera toutes les requêtes. On lui indique le service api mais uniquement parce que c'est un paramètre obligatoire, notre requête sera de toute manière interceptée et redirigée par notre middleware avant.

Vous avez maintenant un reverse proxy simple à utiliser auquel vous pouvez rajouter tous les services que vous souhaitez :-)