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 \
--certificatesresolvers.letsencrypt.acme.email=xataz@monmail.net \
--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 = "mail@nomdedomaine.org"
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: "mail@nomdedomaine.org"
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.